前言
-
背景
前段时间,因接手的项目需要实现美餐支付
的功能对接
在此记录一下鄙人的实现步骤,方便有需要的道友参考借鉴 -
场景描述
我们的 “现代膳食” 售卖机,可以在屏幕上显示可配送的餐食
用户选中商品后,点击购买
选择“美餐支付”
后,提示用户刷卡或扫描 美餐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);
}
附录
①. 注意事项
-
- 注意开发私钥、公钥的存储,以我的代码实现为例,存放的私钥位置、形式如下:
- 注意开发私钥、公钥的存储,以我的代码实现为例,存放的私钥位置、形式如下:
-
- 注意,支付回调接口,一定要联系商家,添加到接口白名单
②. 美餐支付服务类(封装)
- 整理 美餐支付服务类 ,源代码提供如下:
<?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');
}
}