美餐支付 - PHP代碼实现

前言

  • 背景 前段时间,因接手的项目需要实现 美餐支付 的功能对接 在此记录一下鄙人的实现步骤,方便有需要的道友参考借鉴
  • 场景描述 我们的 “现代膳食” 售卖机,可以在屏幕上显示可配送的餐食 用户选中商品后,点击购买 选择 “美餐支付” 后,提示用户刷卡或扫描 美餐APP支付码 我们的设备端,会将读取到的 卡号/⼆维码 Code 传到服务接口,随后开发人员处理支付逻辑
  • 美餐 听客户描述,当地使用美餐卡的用户群比较普遍 …

实现步骤

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

①. 前期准备
  • 开发前,首先要沟通获取到 官方提供的 开发文档、开发者 ID、商户ID、店铺ID、开发者私钥/公钥 等信息
  • 以下为鄙人业务接口,所需要的参数要求:
②. 快速支付
  • 美餐-快速支付,核心方法如下:
代码语言:javascript
复制
    /**
     * @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??''];
}</code></pre></div></div><h5 id="5hpni" name="%E2%91%A2.-%E6%94%AF%E4%BB%98%E5%9B%9E%E8%B0%83%E5%A4%84%E7%90%86"><code>③. 支付回调处理</code></h5><blockquote><p> 对于回调接口,需要联系商家,添加到白名单

  • 根据前面配置的支付回调参数 notification_url , 回调处理如下:
代码语言:javascript
复制
    /**
     * @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 = [
            &#39;result_code&#39; =&gt; $op_flag?&#39;OK&#39;:&#39;NO&#39;,
            &#39;result_description&#39; =&gt; $op_flag?&#39;Success&#39;:&#39;Failure&#39;,
        ];
    }else{
        $data = [
            &#39;result_code&#39; =&gt; &#39;NO&#39;,
            &#39;result_description&#39; =&gt; &#39;GET请求方式不合法&#39;,
        ];
    }
    return json_encode($data,JSON_UNESCAPED_UNICODE);
}</code></pre></div></div><h5 id="637t9" name="%E2%91%A3.-%E9%80%80%E6%AC%BE%E7%94%B3%E8%AF%B7%E3%80%81%E9%80%80%E6%AC%BE%E5%9B%9E%E8%B0%83"><code>④. 退款申请、退款回调</code></h5><blockquote><p> 具体实现,可参考文件后面的 附录代码

  • 发起退款请求,处理如下:
代码语言:javascript
复制
$order_id = $request->post('order_id',0);
 list($op_flag,$op_msg) = (new MeicanPayService)->payRefund($order_id);
  • 退款回调,处理如下:
代码语言:javascript
复制
    /**
     * @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 = [
            &#39;result_code&#39; =&gt; $op_flag?&#39;OK&#39;:&#39;NO&#39;,
            &#39;result_description&#39; =&gt; $op_flag?&#39;Success&#39;:&#39;Failure&#39;,
        ];
    }else{
        $data = [
            &#39;result_code&#39; =&gt; &#39;NO&#39;,
            &#39;result_description&#39; =&gt; &#39;GET请求方式不合法&#39;,
        ];
    }
    return json_encode($data,JSON_UNESCAPED_UNICODE);
}</code></pre></div></div><ul class="ul-level-0"><li>美餐支付 退款查询</li></ul><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">    /**
 * @Notes:美餐支付 退款查询
 * @return false|string
 * @User: zhanghj
 * @DateTime: 2023-11-06 11:27
 */
public function actionQueryPayRefund(){
    $request = new Request();
    if ($request-&gt;isGet){
        $order_id = $request-&gt;get(&#39;order_id&#39;,0);
        list($op_flag,$op_msg,$op_data) = (new MeicanPayService)-&gt;queryPayRefund($order_id);
    }else{
        $op_flag = false;
        $op_msg = &#39;请求方式不合法&#39;;
    }
    $op_res = [
        &#39;code&#39; =&gt; $op_flag?200:405,
        &#39;msg&#39; =&gt; $op_msg,
        &#39;data&#39; =&gt; $op_data??[]
    ];
    return json_encode($op_res,JSON_UNESCAPED_UNICODE);
}</code></pre></div></div><figure class=""><hr/></figure><h4 id="8hpib" name="%E9%99%84%E5%BD%95"><code>附录</code></h4><h5 id="fuleq" name="%E2%91%A0.-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9"><code>①. 注意事项</code></h5><ul class="ul-level-0"><li> <ol class="ol-level-1"><li>注意开发私钥、公钥的存储,以我的代码实现为例,存放的私钥位置、形式如下:
    1. 注意,支付回调接口,一定要联系商家,添加到接口白名单
  • ②. 美餐支付服务类(封装)
    • 整理 美餐支付服务类 ,源代码提供如下:
    代码语言:javascript
    复制
    <?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');
        }
    
    }