美餐支付 - PHP代碼实现

前言

  • 背景
    前段时间,因接手的项目需要实现 美餐支付 的功能对接
    在此记录一下鄙人的实现步骤,方便有需要的道友参考借鉴

  • 场景描述
    我们的 “现代膳食” 售卖机,可以在屏幕上显示可配送的餐食
    用户选中商品后,点击购买
    选择 “美餐支付” 后,提示用户刷卡或扫描 美餐APP支付码
    我们的设备端,会将读取到的 卡号/⼆维码 Code 传到服务接口,随后开发人员处理支付逻辑

  • 美餐
    听客户描述,当地使用美餐卡的用户群比较普遍 …

实现步骤

以下为鄙人整理的开发过程,可根据自己的实际业务优化处理

①. 前期准备

  • 开发前,首先要沟通获取到 官方提供的 开发文档、开发者 ID、商户ID、店铺ID、开发者私钥/公钥 等信息
  • 以下为鄙人业务接口,所需要的参数要求:

②. 快速支付

  • 美餐-快速支付,核心方法如下:
    /**
     * @Notes: 快速支付
     * @param array $post_data
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-09 19:34
     * 要求 : 参数需在请求JSON传参
     */
    public function payQuick($post_data = []){
        $opFlag = false;
        $opMsg = '';
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/mode-s/pay";
        $order_sn = $post_data['order_sn']??'';
        $payer_code = $post_data['payer_code']??'';
        $quick_type = $post_data['quick_type']??1;
        $orderInfo = Order::getOrderInfoByOrderSn($order_sn,'order_id,order_amount');
        $order_id = $orderInfo['order_id']??0;

        //检验当前订单id,是否符合快速支付条件
        $check_msg = (new OrderService())->checkMeicanOrderToPay($order_id,'PAY');
        if ($check_msg){
            $opMsg = $check_msg;
        }else{
            if (!in_array($quick_type,[1,2])){
                $opMsg = '请确认美餐支付参数';
            }else{
                $sum_order_amount = Order::find()->where(['order_sn' => $order_sn])->sum('order_amount');
                //1:刷卡支付,2:美餐APP反扫码
                $type_identifier = ($quick_type==1)?'MEICAN_PHYSICAL_CARD':'MEICAN_ELECTRIC_CARD';
                $request_body = [
                    //可以考虑原订单号加随机数,避免无法付款
                    'order_id' => $order_id.'M'.$order_sn,
                    'store_id' => self::STORE_ID,//TODO 店铺ID
                    'expire_time' => $curr_time+(6*3600),
                    'description' => 'MEICAN_PAY',//⽀付单描述 售货机订单-美餐⽀付
                    'payer' => [
                        'payer_type' => 'CARD', //用户RN支付类型
                        'id_card' => [
                            'type_identifier' => $type_identifier ,//物理卡类型、美餐付款码类型
                            'code' => $payer_code,//卡内码
                        ],
                    ],
                    'total' => $sum_order_amount * 100,//⽀付⾦额(实付⾦额)分
                    'currency' => 'CNY',
                    'notification_url' => $this->curr_domain.'/meican-pay/pay_notify'
                ];
                list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);
            }
        }

        if ($opFlag){
            $this->logInfoToRuntime('actionPayQuick','订单ID【'.$order_id.'】'.json_encode($opData??[],JSON_UNESCAPED_UNICODE));
            // 判断是否支付成功
            $result_code = $opData['result_code']??'';
            $result_description = $opData['result_description']??'';
            if ($result_code == 'OK'){
                $save_data = [
                    'pay_type' => ($quick_type==1)?4:5,
                    'pay_time' => time()
                ];
                $saveTag = Order::updateOrderByOrderSn($order_sn,$save_data);
                if ($saveTag){
                    $opMsg = '支付成功';
                }else{
                    $opFlag = false;
                    $opMsg = '支付更新失败';
                }
            }else{
                $opFlag = false;
                $opMsg = '支付接口,调用失败:'.$result_description;
            }
        }
        return [$opFlag,$opMsg,$opData??''];
    }

③. 支付回调处理

对于回调接口,需要联系商家,添加到白名单

  • 根据前面配置的支付回调参数 notification_url , 回调处理如下:
    /**
     * @Notes: 快速支付,回调逻辑处理
     * 白名单接口:http://clientapi.xxxxxxxxxxxxxxxx.com/meican-pay/pay_notify
     * @User: zhanghj
     * @DateTime: 2023-08-09 11:29
     */
    public function actionPayNotify(){
        $request = new Request();
        if ($request->isPost){
            $raw_json = $request->getRawBody();
            $op_flag = (new MeicanPayService())->dealToPayNotify($raw_json);

            $data = [
                'result_code' => $op_flag?'OK':'NO',
                'result_description' => $op_flag?'Success':'Failure',
            ];
        }else{
            $data = [
                'result_code' => 'NO',
                'result_description' => 'GET请求方式不合法',
            ];
        }
        return json_encode($data,JSON_UNESCAPED_UNICODE);
    }

④. 退款申请、退款回调

具体实现,可参考文件后面的 附录代码

  • 发起退款请求,处理如下:
$order_id = $request->post('order_id',0);
 list($op_flag,$op_msg) = (new MeicanPayService)->payRefund($order_id);
  • 退款回调,处理如下:
    /**
     * @Notes: 退款申请,回调逻辑处理
     * http://clientapi.xxxxxxxxxxx.com/meican-pay/refund_notify
     * @User: zhanghj
     * @DateTime: 2023-08-09 11:29
     */
    public function actionRefundNotify(){
        $request = new Request();
        if ($request->isPost){
            $raw_json = $request->getRawBody();
            $op_flag = (new MeicanPayService())->dealToRefundNotify($raw_json);

            $data = [
                'result_code' => $op_flag?'OK':'NO',
                'result_description' => $op_flag?'Success':'Failure',
            ];
        }else{
            $data = [
                'result_code' => 'NO',
                'result_description' => 'GET请求方式不合法',
            ];
        }
        return json_encode($data,JSON_UNESCAPED_UNICODE);
    }
  • 美餐支付 退款查询
    /**
     * @Notes:美餐支付 退款查询
     * @return false|string
     * @User: zhanghj
     * @DateTime: 2023-11-06 11:27
     */
    public function actionQueryPayRefund(){
        $request = new Request();
        if ($request->isGet){
            $order_id = $request->get('order_id',0);
            list($op_flag,$op_msg,$op_data) = (new MeicanPayService)->queryPayRefund($order_id);
        }else{
            $op_flag = false;
            $op_msg = '请求方式不合法';
        }
        $op_res = [
            'code' => $op_flag?200:405,
            'msg' => $op_msg,
            'data' => $op_data??[]
        ];
        return json_encode($op_res,JSON_UNESCAPED_UNICODE);
    }

附录

①. 注意事项

    1. 注意开发私钥、公钥的存储,以我的代码实现为例,存放的私钥位置、形式如下:
    1. 注意,支付回调接口,一定要联系商家,添加到接口白名单

②. 美餐支付服务类(封装)

  • 整理 美餐支付服务类 ,源代码提供如下:
<?php

namespace clientapi\services;
use common\helper\Helper;
use common\models\Device;
use common\models\MealOrder;
use common\models\Order;
use GuzzleHttp\Client;

/**
 * Meican Pay 支付服务类
 * Class MeicanPayService
 * @package api\services
 */
class MeicanPayService
{

    const DEVELOPER_ID = '7103xxxxxxxxxxxxxxxxxxxxxxxx';         //开发者 ID(由 Meican Pay 分配)
    const MERCHANT_ID = '1013xxxxxxxxxxxx';           //商户ID
    const STORE_ID = '1011xxxxxxxxxx';              //店铺ID
    const BASE_URL = 'https://developer-api.meican.com';     //Meican Pay 接口域名
    const KEY_FILE_DIR = __DIR__.'/../web/meican_key/'; //公钥、私钥存储路径

    private $private_key;           //开发者私钥
    private $public_key;            //开发者公钥
    private $platform_public_key;   //美餐平台公钥(接收来⾃ Meican Pay 的请求应答及回调通知)

    protected $httpClient = null;
    private $curr_domain;           //当前服务器域名
    private $developerApi_domain;

    public function __construct()
    {
        $this->curr_domain = 'http://clientapi.welfare.kairende.com';
        $this->developerApi_domain = 'https://developer-api.meican.com';
        $this->httpClient = new Client([
            'base_uri' => self::BASE_URL,
            'verify' => false,
            'http_errors' => false]);
        // 加载私钥文件
        $this->private_key = openssl_pkey_get_private(file_get_contents(self::KEY_FILE_DIR.'rsa_private.pem'));
        // 加载公钥文件
        $this->public_key = openssl_pkey_get_public(file_get_contents(self::KEY_FILE_DIR.'rsa_public.pem'));
    }

    /**
     * @Notes: 获取 Header 头部配置信息
     * @param string $request_method 请求方法
     * @param string $url 请求URL
     * @param int $time_stamp 时间戳
     * @param array $request_body 请求主体
     * @return bool|array
     * @User: zhanghj
     * @DateTime: 2023-08-09 16:22
     */
    public function getHeaderConf($request_method = 'GET',
                                  $url = '',
                                  $time_stamp = 0,
                                  $request_body = []){
        $nonce_str = self::createNonceStr(); //32位的随机字符串
        list($opFlag,$opMsg,$signature_str) = $this->createSignStr($request_method,$url,$time_stamp,$nonce_str,$request_body);

        if ($opFlag){
            $header = [
                'Meican-Developer-Id' => self::DEVELOPER_ID,
                'Timestamp' => $time_stamp,
                'Nonce' => $nonce_str,
                'Sign' => $signature_str,
                //平台要求,需要 json 格式请求
                "Content-Type" => 'application/json'
            ];
        }else{
            $header = [];
            $opFlag = false;
        }
        return [$opFlag,$opMsg,$header];
    }

    /**
     * @Notes: 生成 32位的随机字符串
     * @User: zhanghj
     * @DateTime: 2023-08-09 15:11
     * @param int $length 字符串位数
     * @return string
     */
    public static function createNonceStr($length = 32){
        $nonce_str='';
        $rand_str= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $max = strlen($rand_str)-1;
        for($i = 0;$i < $length;$i++) {
            $nonce_str .= $rand_str[mt_rand(0,$max)];
        }
        return $nonce_str;
    }


    /**
     * @Notes:生成 sha256WithRSA 签名
     * 提示:SPKI(subject public key identifier,主题公钥标识符)
     * @param null $signContent     待签名内容
     * @param string $privateKey    私钥数据(如果为单行,内容需要去掉RSA的标识符)
     * @param bool $singleRow       是否为单行私钥-标识
     * @return string               签名串
     * @User: zhanghj
     * @DateTime: 2023-09-27 9:41
     */
    public function getSHA256SignWithRSA($signContent = null, $privateKey = '', $singleRow = false){
        if ($singleRow){
            //如果传入的私钥是单行数据,且没有RSA的标识符,需做格式转化
            $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
                          wordwrap($privateKey, 64, "\n", true) .
                          "\n-----END RSA PRIVATE KEY-----";
        }
        $key = openssl_get_privatekey($privateKey);
        //开始加密
        openssl_sign($signContent, $signature, $key, OPENSSL_ALGO_SHA256);
        //进行 base64编码 加密后内容
        $encryptedData = base64_encode($signature);
        openssl_free_key($key);
        return $encryptedData;
    }

    /**
     * @Notes:验证 sha256WithRSA 签名
     * @param null $signContent     待签名内容
     * @param string $signatureStr  签名串
     * @param string $public_key    公钥数据(如果为单行,内容需要去掉RSA的标识符)
     * @param bool $singleRow       是否为单行私钥-标识
     * @return int                  1:签名成功,0:签名失败
     * @User: zhanghj
     * @DateTime: 2023-09-27 10:38
     */
    public static function verifySha256SignWithRSA($signContent = null, $signatureStr = '', $public_key = '',$singleRow = false)
    {
        if ($singleRow){
            $public_key = "-----BEGIN PUBLIC KEY-----\n" .
                wordwrap($public_key, 64, "\n", true) .
                "\n-----END PUBLIC KEY-----";
        }
        $key = openssl_get_publickey($public_key);
        $ok = openssl_verify($signContent, base64_decode($signatureStr), $key, OPENSSL_ALGO_SHA256);
        openssl_free_key($key);
        return $ok;
    }

    /**
     * @Notes: 签名生成
     * @param string $request_method 请求方法
     * @param string $url 请求URL
     * @param int $time_stamp 时间戳
     * @param string $nonce_str 32位随机字符串
     * @param array $request_body 请求主体
     * @return []
     * @User: zhanghj
     * @DateTime: 2023-08-09 15:45
     */
    public function  createSignStr($request_method = 'GET',
                                         $url = '',
                                         $time_stamp = 0,
                                         $nonce_str = '',
                                         $request_body = []){
        $op_flag = false;
        //签名串⼀共有五⾏,每⼀⾏为⼀个参数
        if ($request_body){
            $request_body_json = json_encode($request_body);
        }else{
            $request_body_json = '';
        }
        $sign_str =
            $request_method."\n".
            $url."\n".
            $time_stamp."\n".
            $nonce_str."\n".
            $request_body_json."\n";

        //使⽤开发者私钥对待签名串进⾏ SHA256 with RSA 签名,并对签名结果进⾏ Base64编码 得到签名值
        $signature_res = self::getSHA256SignWithRSA($sign_str,$this->private_key);

        // 验证签名是否正确
        //$result = self::verifySha256SignWithRSA($sign_str,$signature_res,$this->public_key);

        $result = 1;
        if ($result == 1) {
            $op_flag = true;
            $op_msg = '签名成功';
        } elseif ($result == 0) {
            $op_msg = 'Signature is invalid';
        } else {
            $op_msg = 'Verification error: ' . openssl_error_string();
        }
        return [$op_flag,$op_msg,$signature_res??''];
    }

    /**
     * @Notes: 查询退款 逻辑代码
     * @param int $order_id
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-10 21:02
     */
    public function queryPayRefund($order_id = 0){
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $currOrderInfo = Order::getOrderInfoByOrderId($order_id,'order_id,order_sn,money_paid');
        $order_sn = $currOrderInfo['order_sn']??0;
        $refund_order_id = $order_id.'M'.$order_sn.'F';
        $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/refund-orders/{$refund_order_id}";

        $request_body = [];
        list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('GET',$url,$curr_time,$request_body);
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes:全额退款
     * @param int $order_id
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-11-06 13:03
     */
    public function payFullRefund($order_id = 0){
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $currOrderInfo = Order::getOrderInfoByOrderId($order_id,'order_id,order_sn,money_paid');
        $order_sn = $currOrderInfo['order_sn']??0;
        //检验当前订单id,是否符合快速支付条件
        $check_msg = (new OrderService())->checkMeicanOrderToPay($order_id,'REFUND');
        if ($check_msg){
            $opFlag = false;
            $opMsg = $check_msg;
        }else{
            //查询 美餐支付时的 【order_id】
            $meicanMasterOrderInfo = Order::getMeicanPayMasterOrderInfoByOrderSn($order_sn,'order_id');
            $master_order_id = $meicanMasterOrderInfo['order_id']??0;
            $pay_order_id =  $master_order_id.'M'.$order_sn;
            $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/orders/{$pay_order_id}/mode-s/refund";
            $refund_order_id = $order_id.'M'.$order_sn.'F';
            $request_body = [
                'refund_order_id' => $refund_order_id,
                'full_refund' => true,
                'reason' => 'FULL_REFUND',//退款原因 售货机订单-全额退款
                'notification_url' => $this->curr_domain.'/meican-pay/refund_notify'
            ];
            list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);
        }

        if ($opFlag){
            // 判断是否退款申请成功
            $result_code = $opData['result_code']??'';
            $refund_json_str = json_encode($opData??[],JSON_UNESCAPED_UNICODE);
            $save_data = ['refund_json_str' => $refund_json_str,'order_status' => 6];
            Order::updateOrderByOrderID($order_id,$save_data);
            if ($result_code == 'OK'){
                $opMsg = '退款申请成功';
            }else{
                $opMsg = '退款接口,调用失败';
            }
        }
        return [$opFlag,$opMsg,$opData??''];
    }
    /**
     * @Notes: 发起退款 逻辑代码
     * @param int $order_id
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-10 21:02
     */
    public function payRefund($order_id = 0){
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $currOrderInfo = Order::getOrderInfoByOrderId($order_id,'order_id,order_sn,money_paid');
        $order_sn = $currOrderInfo['order_sn']??0;
        $money_paid = $currOrderInfo['money_paid']??0;
        //检验当前订单id,是否符合快速支付条件
        $check_msg = (new OrderService())->checkMeicanOrderToPay($order_id,'REFUND');
        if ($check_msg){
            $opFlag = false;
            $opMsg = $check_msg;
        }else{
            //查询 美餐支付时的 【order_id】
            $meicanMasterOrderInfo = Order::getMeicanPayMasterOrderInfoByOrderSn($order_sn,'order_id');
            $master_order_id = $meicanMasterOrderInfo['order_id']??0;
            $pay_order_id =  $master_order_id.'M'.$order_sn;
            $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/orders/{$pay_order_id}/mode-s/refund";
            $refund_order_id = $order_id.'M'.$order_sn.'F';
            $request_body = [
                'refund_order_id' => $refund_order_id,
                'full_refund' => false,
                'amount' => $money_paid*100,//⽀付⾦额(实付⾦额)分
                'reason' => '售货机订单-退款',//退款原因
                'notification_url' => $this->curr_domain.'/meican-pay/refund_notify'
            ];
            list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);
        }


        if ($opFlag){
            // 判断是否退款申请成功
            $result_code = $opData['result_code']??'';
            $refund_json_str = json_encode($opData??[],JSON_UNESCAPED_UNICODE);
            if ($result_code == 'OK'){
                $opMsg = '退款申请成功';
            }else{
                $opMsg = '退款接口,调用失败';
            }
            $save_data = ['refund_json_str' => $refund_json_str,'order_status' => 6];
            Order::updateOrderByOrderID($order_id,$save_data);
        }
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes: 发起退款 逻辑代码 (单商户版本)
     * @param int $meal_order_id
     * @param string $order_sn
     * @param int $money_paid
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-10 21:02
     */
    public function payRefundForDealer($meal_order_id = 0,$order_sn = '',$money_paid = 0){
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;

        //查询 美餐支付时的 【order_id】
        $pay_order_id =  $meal_order_id.'D'.$order_sn;
        $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/orders/{$pay_order_id}/mode-s/refund";
        $refund_order_id = $meal_order_id.'D'.$order_sn.'F';
        $request_body = [
            'refund_order_id' => $refund_order_id,
            'full_refund' => false,
            'amount' => $money_paid*100,//⽀付⾦额(实付⾦额)分
            'reason' => '售货机订单-退款',//退款原因
            'notification_url' => $this->curr_domain.'/meican-pay/refund_notify'
        ];
        list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);


        if ($opFlag){
            // 判断是否退款申请成功
            $result_code = $opData['result_code']??'';
            $refund_json_str = json_encode($opData??[],JSON_UNESCAPED_UNICODE);
            if ($result_code == 'OK'){
                $opMsg = '退款申请成功';
            }else{
                $opMsg = '退款接口,调用失败';
            }

            $save_data = [
                'refund_sn' => $refund_order_id,
                'order_status' => MealOrder::ORDER_REFUND_IN_PROGRESS,
                'refund_confirm_at' => time(),
                'update_at' => time()
                ];
            MealOrder::updateOrderInfoByOrderId($meal_order_id,$save_data);
        }
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes:光眼检测,失败进行退款
     * @param int $order_id
     * @param int $refund_fee
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-11-04 18:24
     */
    public function payRefundForLighteyeFailed($order_id = 0,$refund_fee = 0){
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $orderInfo = Order::getOrderInfoByOrderId($order_id,'order_sn,order_id,order_amount,money_paid,device_id');
        $device_id = $orderInfo['device_id']??0;
        $order_sn = $orderInfo['order_sn']??'';
        $money_paid = $orderInfo['money_paid']??0;
        //检验当前订单id,是否符合快速支付条件
        $check_msg = (new OrderService())->checkMeicanOrderToPay($order_id,'REFUND');
        if ($check_msg){
            $opFlag = false;
            $opMsg = $check_msg;
        }else{
            $meicanMasterOrder = Order::getMeicanPayMasterOrderInfoByOrderSn($order_sn,'order_id');
            $master_order_id = $meicanMasterOrder['order_id']??0;
            $pay_order_id =  $master_order_id.'M'.$order_sn;
            $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/orders/{$pay_order_id}/mode-s/refund";
            $refund_order_id = $order_id.'M'.$order_sn.'F';
            $request_body = [
                'refund_order_id' => $refund_order_id,
                'full_refund' => false,
                'amount' => $money_paid*100,//⽀付⾦额(实付⾦额)分
                'reason' => '售货机订单-退款',//退款原因
                'notification_url' => $this->curr_domain.'/meican-pay/refund_notify'
            ];
            list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);
        }

        if ($opFlag){
            // 判断是否退款申请成功
            $result_code = $opData['result_code']??'';
            $refund_json_str = json_encode($opData??[],JSON_UNESCAPED_UNICODE);

            if ($result_code == 'OK'){
                $save_data = [
                    'order_status' => 6,
                    'refund_confirm_at' => time(),
                    'light_eye_need_refund' => 2,
                    'refund_amount' => $refund_fee,
                    'refund_json_str' => $refund_json_str
                ];
                $saveTag = Order::updateOrderByOrderID($order_id,$save_data);
                if ($saveTag){
                    if(true){
                        $device = Device::find()->where(['device_id'=>$device_id])->one();
                        $device->sale_amount = $device->sale_amount - $refund_fee;
                        $device->order_amount = $device->order_amount - 1;
                        $device->save();
                    }
                    $opMsg = '退款申请成功';
                }else{
                    $opMsg = '退款更新失败';
                }
            }else{
                $save_data = [
                    'order_status' => 6,
                    'refund_json_str' => $refund_json_str,
                    'light_eye_need_refund' => 3,
                    'refund_amount' => $refund_fee
                ];

                $saveTag = Order::updateOrderByOrderID($order_id,$save_data);
                if ($saveTag){
                    $opMsg = '退款申请成功';
                }else{
                    $opMsg = '退款接口,调用失败:order_id='.$order_id;
                }
            }
        }
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes: 快速支付
     * @param array $post_data
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-09 19:34
     * 要求 : 参数需在请求JSON传参
     */
    public function payQuick($post_data = []){
        $opFlag = false;
        $opMsg = '';
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/mode-s/pay";
        $order_sn = $post_data['order_sn']??'';
        $payer_code = $post_data['payer_code']??'';
        $quick_type = $post_data['quick_type']??1;
        $orderInfo = Order::getOrderInfoByOrderSn($order_sn,'order_id,order_amount');
        $order_id = $orderInfo['order_id']??0;

        //检验当前订单id,是否符合快速支付条件
        $check_msg = (new OrderService())->checkMeicanOrderToPay($order_id,'PAY');
        if ($check_msg){
            $opMsg = $check_msg;
        }else{
            if (!in_array($quick_type,[1,2])){
                $opMsg = '请确认美餐支付参数';
            }else{
                $sum_order_amount = Order::find()->where(['order_sn' => $order_sn])->sum('order_amount');
                //1:刷卡支付,2:美餐APP反扫码
                $type_identifier = ($quick_type==1)?'MEICAN_PHYSICAL_CARD':'MEICAN_ELECTRIC_CARD';
                $request_body = [
                    //可以考虑原订单号加随机数,避免无法付款
                    'order_id' => $order_id.'M'.$order_sn,
                    'store_id' => self::STORE_ID,//TODO 店铺ID
                    'expire_time' => $curr_time+(6*3600),
                    'description' => 'MEICAN_PAY',//⽀付单描述 售货机订单-美餐⽀付
                    'payer' => [
                        'payer_type' => 'CARD', //用户RN支付类型
                        'id_card' => [
                            'type_identifier' => $type_identifier ,//物理卡类型、美餐付款码类型
                            'code' => $payer_code,//卡内码
                        ],
                    ],
                    'total' => $sum_order_amount * 100,//⽀付⾦额(实付⾦额)分
                    'currency' => 'CNY',
                    'notification_url' => $this->curr_domain.'/meican-pay/pay_notify'
                ];
                list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);
            }
        }

        if ($opFlag){
            $this->logInfoToRuntime('actionPayQuick','订单ID【'.$order_id.'】'.json_encode($opData??[],JSON_UNESCAPED_UNICODE));
            // 判断是否支付成功
            $result_code = $opData['result_code']??'';
            $result_description = $opData['result_description']??'';
            if ($result_code == 'OK'){
                $save_data = [
                    'pay_type' => ($quick_type==1)?4:5,
                    'pay_time' => time()
                ];
                $saveTag = Order::updateOrderByOrderSn($order_sn,$save_data);
                if ($saveTag){
                    $opMsg = '支付成功';
                }else{
                    $opFlag = false;
                    $opMsg = '支付更新失败';
                }
            }else{
                $opFlag = false;
                $opMsg = '支付接口,调用失败:'.$result_description;
            }
        }
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes: 快速支付
     * @param int $order_id
     * @param string $order_sn
     * @param int $sum_order_amount
     * @param string $payer_code
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-09 19:34
     * 要求 : 参数需在请求JSON传参
     */
    public function payQuickForDealer($order_id = 0,$order_sn = '',
                                      $sum_order_amount = 0,$payer_code = ''){
        $opFlag = false;
        $opMsg = '';
        $curr_time = time();
        $merchant_id = self::MERCHANT_ID;
        $url = "/meican-pay-quick/v1/merchants/{$merchant_id}/mode-s/pay";
        $quick_type = 1;

        if (!in_array($quick_type,[1,2])){
            $opMsg = '请确认美餐支付参数';
        }else{
            //1:刷卡支付,2:美餐APP反扫码
            $type_identifier = ($quick_type==1)?'MEICAN_PHYSICAL_CARD':'MEICAN_ELECTRIC_CARD';
            $request_body = [
                //可以考虑原订单号加随机数,避免无法付款
                'order_id' => $order_id.'D'.$order_sn,
                'store_id' => self::STORE_ID,//TODO 店铺ID
                'expire_time' => $curr_time+(6*3600),
                'description' => 'MEICAN_PAY',//⽀付单描述 售货机订单-美餐⽀付
                'payer' => [
                    'payer_type' => 'CARD', //用户RN支付类型
                    'id_card' => [
                        'type_identifier' => $type_identifier ,//物理卡类型、美餐付款码类型
                        'code' => $payer_code,//卡内码
                    ],
                ],
                'total' => $sum_order_amount * 100,//⽀付⾦额(实付⾦额)分
                'currency' => 'CNY',
                'notification_url' => $this->curr_domain.'/meican-pay/pay_notify'
            ];
            list($opFlag,$opMsg,$opData) = $this->httpToMeicanServer('POST',$url,$curr_time,$request_body);
        }

        if ($opFlag){
            $this->logInfoToRuntime('actionDealerMeicanImmediatePayment','订单ID【'.$order_id.'】'.json_encode($opData??[],JSON_UNESCAPED_UNICODE));
            // 判断是否支付成功
            $result_code = $opData['result_code']??'';
            $result_description = $opData['result_description']??'';
            if ($result_code == 'OK'){
                $save_data = ['pay_type' => 4];
                $saveTag = MealOrder::updateOrderInfoByOrderId($order_id,$save_data);
                if ($saveTag){
                    $opMsg = '支付成功';
                }else{
                    $opFlag = false;
                    $opMsg = '支付更新失败';
                }
            }else{
                $opFlag = false;
                $opMsg = '支付接口,调用失败:'.$result_description;
            }
        }
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes: 封装请求方法
     * @param string $request_method
     * @param string $url
     * @param int $curr_time
     * @param array $request_body
     * @return array
     * @User: zhanghj
     * @DateTime: 2023-08-09 19:46
     */
    public function httpToMeicanServer( $request_method = 'GET',
                                        $url = '',
                                        $curr_time = 0,
                                        $request_body = []){

        list($opFlag,$opMsg,$header_data) = self::getHeaderConf($request_method,$url,$curr_time,$request_body);
        if ($opFlag){
            $options = [
                'headers' => $header_data,
            ];

            if ($request_method == 'GET'){
                $options['query'] = $request_body;
            }else{
                //参数需在请求JSON传参
                //$options['form_params'] = $request_body;
                $options['json'] = $request_body;
            }

            try {
                $response  = $this->httpClient->request($request_method,$url,$options);
                $contents = $response->getBody().'';
                $opData = json_decode($contents,true);
                $opMsg = '请求成功';
            }catch (\Exception $exception){
                $opFlag = false;
                $opMsg = $exception->getMessage();
            }
        }
        return [$opFlag,$opMsg,$opData??''];
    }

    /**
     * @Notes: 处理支付回调逻辑
     * @param string $raw_json
     * @return bool
     * @User: zhanghj
     * @DateTime: 2023-08-10 15:46
     */
    public function dealToPayNotify($raw_json = ''){
        $op_flag = false;
        if ($raw_json){
            //进行日志记录
            $this->logInfoToRuntime('actionPayNotify',$raw_json);
            $raw_arr = json_decode($raw_json,true);
            if (is_array($raw_arr)){
                $return_order_id = $raw_arr['order_id']??'';//订单ID
                $isClientOrder = strrpos($return_order_id,'M');
                $isMealOrder = strrpos($return_order_id,'D');

                if ($isClientOrder){
                    //此为 设备订单,美餐支付回调
                    $orderSn = explode('M',$return_order_id)[1]??'';
                    $orderList = Order::find()
                        ->where(['order_sn'=>$orderSn])
                        ->select('order_id,order_sn,pay_type,order_status,order_amount')
                        ->asArray()->all();
                    if ($orderList){
                        foreach ($orderList as $key => $currOrder){
                            //检查是否已支付
                            if ($currOrder){
                                $pay_type = $currOrder['pay_type'];
                                $order_id = $currOrder['order_id']??0;
                                if (in_array($pay_type,[4,5]) && $currOrder['order_status']==1){
                                    $money_paid = $currOrder['order_amount']??0;
                                    $save_data = [
                                        'pay_time' => time(),
                                        'order_status' => 2,
                                        'money_paid' => $money_paid,
                                        'payment_json_str' => $raw_json
                                    ];
                                    //进行订单表更新
                                    $saveFlag = Order::updateOrderByOrderID($order_id,$save_data);
                                    if ($saveFlag){
                                        $op_flag = true;
                                    }
                                }else{
                                    //订单已不是待支付状态,无需再次请求
                                    $this->logInfoToRuntime('actionPayNotify','订单ID【'.$order_id.'】非待支付状态,无需再次请求');
                                    $op_flag = true;
                                }
                            }
                        }
                    }
                }elseif ($isMealOrder){
                    //此为 单商户外卖订单 美餐支付回调
                    $orderSn = explode('D',$return_order_id)[1]??'';
                    $order = MealOrder::findInfoByOrderSn($orderSn);
                    if ($order->order_status == MealOrder::ORDER_UNPAID) {
                        $money_paid = $raw_arr['transaction']['total']??0;//支付⾦额 (⼈⺠币 - 分)
                        $order->order_status = MealOrder::ORDER_PAID;
                        $order->money_paid   = bcdiv($money_paid, 100, 2);
                        $order->pay_time     = time();
                        $order->update_at    = time();
                        if ($order->save()) {
                            $op_flag = true;
                        }
                    }
                }
            }
        }
        return $op_flag;
    }

    /**
     * @Notes: 处理退款回调逻辑
     * @param string $raw_json
     * @return bool
     * @User: zhanghj
     * @DateTime: 2023-08-10 15:46
     */
    public function dealToRefundNotify($raw_json = ''){
        $op_flag = false;
        if ($raw_json){
            $this->logInfoToRuntime('actionRefundNotify',$raw_json);
            $raw_arr = json_decode($raw_json,true);
            if (is_array($raw_arr)){
                $refund_order_id = $raw_arr['refund_order_id']??'';//订单ID
                $isClientOrder = strrpos($refund_order_id,'M');
                $isMealOrder = strrpos($refund_order_id,'D');
                $refund_amount = $raw_arr['transaction']['amount']??0;//退款⾦额 (⼈⺠币 - 分)
                if ($isClientOrder){
                    //此为设备订单,美餐支付退款
                    $order_id = explode('M',$refund_order_id)[0]??'';
                    $save_data = [
                        'order_status' => 8,
                        'refund_json_str' => $raw_json,
                        'refund_amount' => $refund_amount/100
                    ];
                    //进行订单表更新
                    $saveFlag = Order::updateOrderByOrderID($order_id,$save_data);
                    if ($saveFlag){
                        $op_flag = true;
                    }
                }elseif ($isMealOrder){
                    //此为单商户 外卖订单美餐支付退款
                    $order_id = explode('D',$refund_order_id)[0]??'';
                    $save_data = [
                        'order_status' => MealOrder::ORDER_REFUNDED,
                        'update_at' => time(),
                    ];
                    //进行订单表更新
                    $saveFlag = MealOrder::updateOrderInfoByOrderId($order_id,$save_data);
                    if ($saveFlag){
                        $op_flag = true;
                    }
                }
            }
        }
        return $op_flag;
    }

    /**
     * @Notes: 日志整理记录
     * @param string $title
     * @param string $log_message
     * @User: zhanghj
     * @DateTime: 2023-08-11 14:49
     */
    public function logInfoToRuntime($title = '',$log_message = ''){
        $raw_arr = json_decode($log_message,true);
        if (is_array($raw_arr)){
            $log_content = json_encode($raw_arr,JSON_UNESCAPED_UNICODE);
        }else{
            $log_content = $log_message;
        }
        //进行日志记录
        $project_dir = 'clientapi';
        $file_name = 'meican_pay_'.date('Ym').'_log.txt';
        Helper::addLog($project_dir, $log_content, $title,$file_name);
        //\Yii::info("{$title}: ".$log_content,'meican_pay');
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/304585.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Jmeter 性能压测 —— 常遇问题与解决技巧!

问题1&#xff1a;如何在大并发测试下&#xff0c;让登录或者后续接口只执行一次&#xff1f; 分析&#xff1a; 这个问题网上的答案其实很多&#xff0c;但是大多不靠谱。 比如推荐使用仅一次控制器&#xff0c;但是仅一次控制器对线程组无效&#xff1b; 比如推荐跨线程组…

Sectigo的DV通配符https

Sectigo是近些年发展比较快速的CA认证机构&#xff0c;为了提升审核效率&#xff0c;在全国成立了审核机构&#xff0c;亚太审核中心的成立加快了Sectigo旗下的https证书的审核速度。Sectigo的https证书可以为网站安全提供有力支持&#xff0c;从而保护网站信息安全。今天就随S…

【REST2SQL】05 GO 操作 达梦 数据库

【REST2SQL】01RDB关系型数据库REST初设计 【REST2SQL】02 GO连接Oracle数据库 【REST2SQL】03 GO读取JSON文件 【REST2SQL】04 REST2SQL第一版Oracle版实现 信创要求用国产数据库&#xff0c;刚好有项目用的达梦&#xff0c;研究一下go如何操作达梦数据库 1 准备工作 1.1 安…

02. Eureka、Nacos注册中心及负载均衡原理

01小节中订单服务远程调用用户服务案例实现了跨服务请求&#xff0c;在微服务中一个服务可能是集群部署的&#xff0c;也就是一个服务有多个实例&#xff0c;但是我们在调用服务时需要指定具体的服务实例才能调用该服务&#xff0c;在集群模式下&#xff0c;服务地址应该写哪个…

redis 从0到1完整学习 (十六):内存回收之 key 过期处理策略

文章目录 1. 引言2. redis 源码下载3. redisDb 结构体4. Redis 过期 key 的处理策略4.1 惰性删除 (Lazy Expiration)4.2 定期删除 (Active Expire / Periodic Expiration)* 5. 参考 1. 引言 前情提要&#xff1a; 《redis 从0到1完整学习 &#xff08;一&#xff09;&#xff…

2024年如何使用WordPress构建克隆Udemy市场

您想创建像 Udemy 这样的学习管理 (LMS) 网站吗&#xff1f;最好的学习管理系统工具LifterLMS将帮助您制作像Udemy市场这样的 LMS 网站。 目录 Udemy市场是什么&#xff1f; 创建 Udemy 克隆所需的几项强制性技术&#xff1a; 步骤 1) 注册您的域名 步骤 2) 获取虚拟主…

JHipster - Spring Boot 的快速开发利器

产品介绍&#xff1a; JHipster是一个开源的、全面的应用程序生成器&#xff0c;它能够帮助开发者快速生成Spring Boot Angular/React/Vue.js的完整应用程序。它不仅提供了一个简单的界面来定义应用程序的配置&#xff0c;还提供了一组强大的代码生成器&#xff0c;可以在数分…

Taro+vue3 实现电影切换列表

1.需求 我们在做类似于猫眼电影的小程序或者H5 的时候 我们会做到那种 左右滑动的电影列表&#xff0c;这种列表一般带有电影场次 2.效果 3.说明 这种效果在淘票票 猫眼电影上 都有的 &#xff0c;一般电影类型的H5 或者小程序 这个是都有的 第一是好看 第二是客观性比较好 …

Python 工具 | conda 基本命令

Hi&#xff0c;大家好&#xff0c;我是源于花海。本文主要了解 Python 的工具的 conda 相关的基本命令。Conda 是一个开源的软件包管理系统和环境管理系统&#xff0c;用于安装多个版本的软件包及其依赖关系&#xff0c;并在它们之间轻松切换。在Windows下&#xff0c;需要安装…

杨中科 ASP.NET Core 中的依赖注入的使用

ASP.NET CORE中服务注入的地方 1、在ASP.NET Core项目中一般不需要自己创建ServiceCollection、IServiceProvider。在Program.cs的builder.Build()之前向builderServices中注入 2、在Controller中可以通过构造方法注入服 务。 3、演示 新建一个calculator类 注入 新建TestC…

使用 PHP-FFMpeg 操作视频/音频文件

做音频合成的时候找到的一个php操作ffmpeg 的类库。GitHub地址&#xff1a;https://github.com/PHP-FFMpeg/PHP-FFMpeg/。本文的例子大部分都是上面的 在使用之前请安装好 FFMpeg 。如何安装&#xff1f;请看 FFmpeg 安装教程。 使用composer快速安装 > composer require …

Arcgis像元统计数据

目录 单幅影像统计多幅影像统计 单幅影像统计 现有一幅NDVI影像&#xff0c;如何知道影像中NDVI的分布情况呢&#xff1f; 先栅格转点&#xff0c;然后在属性表中查看汇总情况 还有一种方法就是在ENVI中打开&#xff0c; -0.3-0.338占据了99% 多幅影像统计 现有多幅NDVI影…

设置flex布局的元素,其子元素宽度和超过其本身时,其宽度值未被撑起问题

如图父元素main-content设置了display:flex. 里面包含了不确定个数的子元素&#xff0c;子元素样式为&#xff1a; flex: 1; min-width: 240px;现在想获取父元素的宽度&#xff0c;发现无论子元素的个数为多少&#xff0c;父元素的宽度都是一样的大小&#xff0c;并没有被子元…

使用 CompletableFuture 分批处理任务

一、无返回值任务函数 // 数据分批 List<List<StatisticsDTO>> batches Lists.partition(statisticsList, BATCH_SIZE); List<CompletableFuture<Void>> futures new ArrayList<>(batches.size());// 数据处理 for (int i 0; i < batches…

初学者的基本 Python 面试问题和答案

文章目录 专栏导读1、什么是Python&#xff1f;列出 Python 在技术领域的一些流行应用。2、在目前场景下使用Python语言作为工具有什么好处&#xff1f;3、Python是编译型语言还是解释型语言&#xff1f;4、Python 中的“#”符号有什么作用&#xff1f;5、可变数据类型和不可变…

imgaug库指南(12):从入门到精通的【图像增强】之旅

引言 在深度学习和计算机视觉的世界里&#xff0c;数据是模型训练的基石&#xff0c;其质量与数量直接影响着模型的性能。然而&#xff0c;获取大量高质量的标注数据往往需要耗费大量的时间和资源。正因如此&#xff0c;数据增强技术应运而生&#xff0c;成为了解决这一问题的…

【PB续命06】JDBC连接Oracle数据库

JDBC(Java DataBase Connectivity) 称为Java数据库连接&#xff0c;它是一种用于数据库访问的应用程序API&#xff0c;由一组用Java语言编写的类和接口组成&#xff0c;有了JDBC就可以用同一的语法对多种关系数据库进行访问&#xff0c;而不用担心其数据库操作语言的差异。 有了…

Git分支学习

Commit 每次 Commit &#xff0c;都会多一个节点&#xff0c;C1是C2的父节点&#xff0c;在C1的基础上产生。 使用 git commit 提交代码分支。 Branch 根据逻辑分解工作到不同的分支&#xff0c;在将分支和提交记录结合起来后&#xff0c;我们会看到两者如何协作。 在 mai…

KazooClient出现【句柄无效】错误

报错信息&#xff1b; Connection dropped: socket connection error: 句柄无效。 Connection dropped: socket connection error: 句柄无效。 Connection dropped: socket connection error: 句柄无效。 Connection dropped: socket connection error: 句柄无效。 Connection …

一夜爆火,3天60亿,这泼天的富贵也轮到我们尔滨了

近日&#xff0c;哈尔滨这座北国之城突然成为全国瞩目的焦点&#xff0c;一夜之间&#xff0c;冰雪大世界、索菲亚大教堂、中央大街等老牌旅游景点在网络短视频和游客们的热切关注下&#xff0c;成为了这个冬季的新“顶流”。当地市民姚先生和胡先生异口同声表示&#xff1a;“…