前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >美餐支付 - PHP代碼实现

美餐支付 - PHP代碼实现

作者头像
泥豆芽儿 MT
发布2024-01-10 10:45:50
1310
发布2024-01-10 10:45:50
举报

前言

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

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

  • 根据前面配置的支付回调参数 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 = [
                '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);
    }
④. 退款申请、退款回调

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

  • 发起退款请求,处理如下:
代码语言: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 = [
                '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);
    }
  • 美餐支付 退款查询
代码语言:javascript
复制
    /**
     * @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. 注意,支付回调接口,一定要联系商家,添加到接口白名单
②. 美餐支付服务类(封装)
  • 整理 美餐支付服务类 ,源代码提供如下:
代码语言: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');
    }

}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 实现步骤
    • ①. 前期准备
      • ②. 快速支付
        • ③. 支付回调处理
          • ④. 退款申请、退款回调
          • 附录
            • ①. 注意事项
              • ②. 美餐支付服务类(封装)
              相关产品与服务
              云支付
              云支付(Cloud Pay,CPay)为您提供开放、可靠的聚合收款技术服务和商户管理功能。云支付支持刷卡支付、扫码支付、一码多付多种支付方式。服务商也可使用云支付提供的 SDK 和 HTTPS 接口,将云支付集成进自己的系统中,为商户提供的个性化解决方案。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档