背景介紹
之前對接微信支付V3接口的時候都是要藉助一些三方擴展來實現, 最近項目中需要用到微信支付分相關的api接口,一般的擴展中沒有這塊兒的功能, 通過自定義方法實現微信支付分相關的V3api接口對接
調用v3接口以及解密回調工具類
namespace fast;
use think\Log;
class WechatUtil
{
//獲取微信支付配置的參數信息
public static function getPayConfig()
{
$pay_config = \app\common\model\xilumarket\Config::getMyConfig('wxpayment');
$wxminiConfig = \app\common\model\xilumarket\Config::getMyConfig('wxmini');
$appId = $wxminiConfig['wxmini_appid'] ?? 'wx3a849c674fec66e8';
return [
'app_id' => $appId,
'apiV3Key' => '微信支付apiV3密鑰',
'service_id' => '支付分相關服務id編號', // 商户平台->支付分->我的服務->服務ID(必填)
'mch_id' => $pay_config['mch_id'] ?? '', //商户ID
'cert_client' => ROOT_PATH . 'public' . $pay_config['apiclient_cert'] ?? '', //cert證書地址//絕對路徑
'cert_key' => ROOT_PATH . 'public' . $pay_config['apiclient_key'] ?? '', //key支付證書絕對地址
];
}
/**
* @notes 微信支付v3接口簽名生成
* @param $url
* @param $http_method
* @param $data
* @param $config
* @returnstring
*/
public static function token($url, $http_method, $data, $config)
{
$timestamp = time(); //請求時間戳
$url_parts = parse_url($url); //獲取請求的絕對URL
$nonce = $timestamp . rand('10000', '99999'); //請求隨機串
$body = empty($data) ? '' : json_encode((object)$data); //請求報文主體
$stream_opts = [
"ssl" => [
"verify_peer" => false,
"verify_peer_name" => false,
]
];
$apiclient_cert_arr = openssl_x509_parse(file_get_contents($config['cert_client'], false, stream_context_create($stream_opts)));
//通過微信支付證書獲取證書序列號
$serial_no = $apiclient_cert_arr['serialNumberHex'];
//獲取微信支付證書私鑰
$mch_private_key = file_get_contents($config['cert_key'], false, stream_context_create($stream_opts));
$merchant_id = $config['mch_id']; //商户id
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$message = $http_method . "\n" .
$canonical_url . "\n" .
$timestamp . "\n" .
$nonce . "\n" .
$body . "\n";
//通過私鑰給數據生成簽名
openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign); //簽名
$schema = 'WECHATPAY2-SHA256-RSA2048';
$token = sprintf(
'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$merchant_id,
$nonce,
$timestamp,
$serial_no,
$sign
); //微信返回token
return $schema . ' ' . $token;
}
//實際發送curl請求的方法
public static function https_request($url, $data, $token)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, (string)$url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
if ($data) {
$data = is_array($data) ? json_encode($data) : $data;
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
//添加請求
$headers = [
'Authorization:' . $token,
'Accept: application/json',
'Content-Type: application/json; charset=utf-8',
'User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
];
if (!empty($headers)) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
}
$output = curl_exec($curl);
curl_close($curl);
return $output;
}
/**
* 原生解密微信支付回調數據(AES-256-GCM)
* @param string $nonce 加密隨機串
* @param string $associatedData 附加數據
* @param string $ciphertext 密文
* @return string 解密後的明文
* @throws \Exception 解密失敗時拋出異常
*/
public static function decryptAesGcm(string $nonce, string $associatedData, string $ciphertext): string
{
$config = self::getPayConfig();
// 1. 密鑰處理:APIv3 密鑰為 32 字節,直接使用
$key = $config['apiV3Key'];
// 2. 密文處理:Base64 解碼
$ciphertextDecoded = base64_decode($ciphertext);
if ($ciphertextDecoded === false) {
throw new \Exception('密文 Base64 解碼失敗');
}
// 3. 提取 GCM 標籤(密文最後 16 字節)
$tag = substr($ciphertextDecoded, -16);
$ciphertextWithoutTag = substr($ciphertextDecoded, 0, -16);
// 4. 執行 AES-256-GCM 解密
$decrypted = openssl_decrypt(
$ciphertextWithoutTag,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA, // 原始數據模式
$nonce,
$tag,
$associatedData
);
if ($decrypted === false) {
throw new \Exception('AES-GCM 解密失敗: ' . openssl_error_string());
}
return $decrypted;
}
///////////支付分相關方法/////////////
/**
* @notes 發送HTTP請求支付分相關接口
* @param $url
* @param array $queryData
* @param string $httpMethod 請求方式
* @return mixed
*/
public static function sendScoreHttpQuery($url, array $queryData = [], string $httpMethod = 'POST')
{
if ($httpMethod == 'GET' && $queryData) {
$url .= '?' . http_build_query($queryData);
$queryData = [];
}
$config = self::getPayConfig();
$token = self::token($url, $httpMethod, $queryData, $config); //獲取token
$result = self::https_request($url, $queryData, $token); //發送請求
$resultArr = json_decode($result, true);
if (!empty($resultArr['code']) && !empty($resultArr['message'])) {
$logData = ['url' => $url, 'queryData' => $queryData, 'resultArr' => $resultArr];
MonologUtil::stream('請求支付分相關接口失敗', $logData, 'score_pay_error');
exception($resultArr['message']);
}
return $resultArr;
}
//創建微信支付分訂單
public static function createScoreOrder($outOrderNo, $deviceSn)
{
//請求URL
$url = 'https://api.mch.weixin.qq.com/v3/payscore/serviceorder';
//請求參數
$queryData = [
'out_order_no' => $outOrderNo, // 商户訂單號(必填)
'appid' => '小程序appid必填)
'service_id' => '支付分服務id>支付分->我的服務->服務ID(必填)
'service_introduction' => '無人智慧零售', // 服務描述(必填,不超過100字)
'time_range' => [ // 服務時間範圍(必填)
'start_time' => 'OnAccept', // 開始時間(格式:YYYYMMDDHHMMSS)
'start_time_remark' => '用户確認訂單成功時間為服務開始時間'
],
'risk_fund' => [ // 【服務風險金】本筆訂單的風險金額描述
'name' => 'ESTIMATE_ORDER_COST',
'amount' => 200 * 100, // 風險金額,單位分(必填)
'description' => '自助售賣貨櫃訂單風險金額',
],
'attach' => 'daoheng',//用於存放訂單的商户自定義數據
'need_user_confirm' => true, // 是否需要用户確認(選填)
'notify_url' => request()->domain() . '/ysd/wechatpay/notify', // 回調地址(必填)
'device' => [
'start_devide_id' => $deviceSn, // 服務開始設備ID(選填)
'end_devide_id' => $deviceSn, // 服務結束設備ID(選填)
],
];
return self::sendScoreHttpQuery($url, $queryData, 'POST');
}
//查詢微信支付分訂單
public static function queryScoreOrder($outOrderNo)
{
$config = self::getPayConfig();
//請求URL
$url = 'https://api.mch.weixin.qq.com/v3/payscore/serviceorder';
$params = [
'out_order_no' => $outOrderNo,
'service_id' => $config['service_id'],
'appid' => $config['app_id'],
];
return self::sendScoreHttpQuery($url, $params, 'GET');
}
//取消微信支付分訂單
//當支付分訂單狀態在已創建(state:CREATED)、用户已確認訂單(collection.state: USER_CONFIRM)
//和待支付狀態(collection.state: USER_PAYING)情況下,商户可以取消服務訂單,調用接口後會同步返回取消結果
public static function cancelScoreOrder($outOrderNo, $reason = '主動申請訂單取消')
{
//請求URL
$url = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder/{$outOrderNo}/cancel";
//請求參數
$config = self::getPayConfig();
$queryData = [
'appid' => $config['app_id'],
'service_id' => $config['service_id'],
'reason' => $reason,//支付分取消訂單原因
];
return self::sendScoreHttpQuery($url, $queryData, 'POST');
}
//完結微信支付分訂單
public static function completeScoreOrder($queryData)
{
$outOrderSn = $queryData['out_order_sn'];
//請求URL
$url = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder/{$outOrderSn}/complete";
//請求參數
$config = self::getPayConfig();
// post_payments格式如下所示:
// [
// [
// 'name' => '商品信息',
// 'count' => 1,
// 'amount' => 0,
// 'description' => '康師傅飲料',
// ],
// [
// 'name' => '商品信息',
// 'count' => 2,
// 'amount' => 1,
// 'description' => '娃哈哈飲料',
// ]
// ]
$queryData = [
'service_id' => $config['service_id'],
'appid' => $config['app_id'],
'total_amount' => $queryData['total_amount'],//【總金額】訂單最終收款總金額
'profit_sharing' => false,//微信支付服務分賬標記 false:不需分賬
'post_payments' => $queryData['post_payments'],
'time_range' => [
'end_time' => date('YmdHis'),
'end_time_remark' => '服務結束',
],
'device' => [
'start_devide_id' => $queryData['selfbuy_sn'],
'end_devide_id' => $queryData['selfbuy_sn'],
]
];
return self::sendScoreHttpQuery($url, $queryData, 'POST');
}
//訂單state為USER_PAYING待支付狀態時,如果用户通過其他渠道支付了,商户可調用該接口將支付分訂單改為已完成狀態
public static function syncOrderByPay($outOrderNo)
{
//請求URL
$url = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder/{$outOrderNo}/sync";
$config = self::getPayConfig();
$queryData = [
'appid' => $config['app_id'],
'service_id' => $config['service_id'],
'type' => 'Order_Paid',//收款場景,商户固定傳“Order_Paid”,表示訂單收款成功
'detail' => [
'paid_time' => date('YmdHis'),//收款成功時間
]
];
return self::sendScoreHttpQuery($url, $queryData, 'POST');
}
//支付分訂單申請退款
//申請退款接口返回成功僅代表退款單受理成功,具體退款結果以退款結果通知和查單退款返回為準
public static function refundScoreOrder($refundInfo)
{
//請求URL
$url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
//請求參數格式
// $refundInfo = [
// 'transaction_id' => '支付分訂單交易transaction_id編號',
// 'notify_url' => 'https://you-domain/ysd/wechatpay/notify',
// 'out_refund_no' => "RF202509181450561300",
// 'reason' => '退款原因',//退款原因
// 'amount' => [
// 'refund' => 1,//退款金額
// 'total' => 1,//原支付交易的訂單總金額
// 'currency' => 'CNY',//CNY:人民幣,境內商户號僅支持人民幣支付
// ],
// ];
return self::sendScoreHttpQuery($url, $refundInfo, 'POST');
}
//查詢退款
public static function queryRefund($outRefundNo)
{
//請求URL
$url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{$outRefundNo}";
return self::sendScoreHttpQuery($url, [], 'GET');
}
}
附錄: 處理微信支付接口回調的方法
部分操作涉及到微信支付回調處理才能完成業務邏輯閉環, 後面的是處理微信支付回調方法示例
class Notify extends Api
{
//微信支付分回調處理方法
public function scorePayCallback()
{
$header = $this->request->header();
$params = request()->param();
MonologUtil::stream('支付分回調數據raw', ['header' => $header, 'data' => $params], 'score_pay');
$resource = $params['resource'] ?? [];
$nonce = $resource['nonce'];
$associatedData = $resource['associated_data'];
$ciphertext = $resource['ciphertext'];
$decrypted = WechatUtil::decryptAesGcm($nonce, $associatedData, $ciphertext);
$decryptedArr = json_decode($decrypted, true);
MonologUtil::stream('解密後支付分回調數據', ['decrypted' => $decryptedArr], 'score_pay');
$eventType = $params['event_type'];//事件類型
$eventSummary = $params['summary'];
// 處理業務邏輯
$this->handleBusiness($eventType, $decryptedArr);
//合規應答:驗籤通過必須返回 200/204 空響應(微信要求5秒內返回)
return response('', 200)->header('Content-Type', 'text/plain');
}
/**
* 處理回調業務邏輯
* 根據不同事件類型處理訂單狀態更新等操作
*/
private function handleBusiness($eventType, array $data)
{
//服務訂單狀態 state
//CREATED:商户已創建服務訂單,DOING:服務訂單進行中,DONE:服務訂單完成(終態),REVOKED:商户取消服務訂單(終態)
//EXPIRED:服務訂單已失效,"商户已創建服務訂單"狀態超過30天未變動,則訂單失效(終態)
// 根據事件類型處理不同業務
switch ($eventType) {
case 'PAYSCORE.USER_CONFIRM':
//微信支付分服務訂單用户已確認
$outOrderNo = $data['out_order_no'];//商户訂單號
$order = ScoreOrderModel::where('order_sn', $outOrderNo)
->where('score_status', 1)
->find();
if ($order) {
$order->save(['score_status' => 2]);//更新訂單狀態為已確認
}
break;
case 'PAYSCORE.USER_PAID':
//微信支付分服務訂單微信扣款成功
$outOrderNo = $data['out_order_no'];//商户訂單號
$order = ScoreOrderModel::where('order_sn', $outOrderNo)->find();
//訂單狀態服務訂單完成(終態)
if ($data['state'] == 'DONE') {
$totalAmount = $data['total_amount'];//訂單最終收款總金額,整型,單位為分
$collection = $data['collection'];//訂單收款信息詳情
//訂單收款狀態 USER_PAYING:待支付 USER_PAID:已支付
$payState = $collection['state'];
if ($payState == 'USER_PAID') {
$firstDetail = reset($collection['details']);//訂單收款明細列表
$firstAmount = $firstDetail['amount'];//訂單收款明細金額
//paid_type類型分以下兩種
//NEWTON:微信支付分渠道,通過微信支付分渠道自動扣款或用户在支付分訂單頁主動支付後返回。
//MCH:商户渠道,用户通過其他渠道支付後,商户調用同步訂單狀態接口成功後返回。
$firstPaidType = $firstDetail['paid_type'];//訂單收款明細支付類型
$firstPaidTime = strtotime($firstDetail['paid_time']);//訂單收款明細支付時間
//微信支付訂單號,申請退款使用
$firstTransactionId = $firstDetail['transaction_id'];
if ($order['score_status'] != 4) {
$order->save([
'score_status' => 4,//已支付
'pay_money' => bcdiv($totalAmount, 100, 2),//訂單支付分實際收款金額
'pay_time' => $firstPaidTime,//訂單收款明細支付時間
'transaction_id' => $firstTransactionId,//訂單收款明細微信支付訂單號
]);
}
}
}
break;
case 'REFUND.SUCCESS'://微信支付分服務訂單退款成功
$refundNo = $data['out_refund_no'];//退款訂單號
$refundOrder = ScoreOrderRefundModel::where('refund_sn', $refundNo)
->where('money_status', 1)->find();
if ($refundOrder) {
$orderTransactionId = $data['transaction_id'];//微信支付分扣款單的唯一標識
$refundId = $data['refund_id'];//微信退款單的唯一標識
$refundAmount = $data['amount']['refund'];//退款金額
$moneyStatus = 2;//退款成功
$refundOrder->save([
'money_status' => $moneyStatus,
'refund_transaction_id' => $refundId,
]);
$order = ScoreOrderModel::where('order_sn', $refundOrder['order_sn'])->find();
if ($order) {
$order->save(['is_refund' => 2]);//更新訂單狀態為2:售後完成
}
}
break;
case 'REFUND.ABNORMAL'://退款異常通知,需商户平台手動處理
case 'REFUND.CLOSED'://退款關閉通知
$moneyStatus = 3;//退款失敗
$refundNo = $data['out_refund_no'];//退款訂單號
$refundOrder = ScoreOrderRefundModel::where('refund_sn', $refundNo)
->where('money_status', 1)->find();
if ($refundOrder) {
$refundOrder->save([
'money_status' => $moneyStatus,
'money_reason' => $eventType,//微信退款失敗原因
]);
}
break;
default:
break;
}
}
}