美餐支付 - PHP代碼实现

PHP技术
93
0
0
2024-03-25

前言

  • 背景 前段时间,因接手的项目需要实现 美餐支付 的功能对接 在此记录一下鄙人的实现步骤,方便有需要的道友参考借鉴
  • 场景描述 我们的 “现代膳食” 售卖机,可以在屏幕上显示可配送的餐食 用户选中商品后,点击购买 选择 “美餐支付” 后,提示用户刷卡或扫描 美餐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');
    }

}