微信小程序支付 V3
流程
准备 APPID,商户ID,证书号,回调地址,请求JSApi地址,V3密匙
设置设置商品信息,金额
将参数进行 签名算法,向微信小程序发送支付请求,获取prepay_id。同时将获取的数据再次进行相应规则的签名
小程序获取到参数后,发起微信支付
支付结果调用回调接口,返回支付结果
1. 接入前准备
支付开发指引-小程序支付文档
'mini' => [
'url' => 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi', // 请求地址
'app_id' => 'wxee5904f2b3b6b762', // APP ID
'mch_id' => '1629935040', // 商户ID
'serial_no' => '346AF1FA3B3D820A6F2B999D7E62FDE838C65B6E', // 证书号
'notify_url' => 'https://xjsxy.leadsvip.cn/api/pay/wechartCallback', // 回调地址
'api_v3_Key' => '48bojx563qkptu1aw2siglcz9erhfyvm', // V3 密匙(在微信商户平台设置)
]
2. 请求支付接口
public function _createOrder($params) {
try {
DB::beginTransaction();
$order = new Order;
$order->user_id = $params['user_id'];
$order->order_phone = $params['order_phone'];
$order->order_name = $params['order_name'];
$order->product_id = $params['product_id'];
$order->product_type = $params['product_type'];
$order->price = $params['price'];
$order->score = $params['score'];
$order->order_no = $params['order_no'];
$order->state = $params['state'];
$order->save();
Log::channel("miniwechart")->info('订单创建成功');
$result = $this->_miniWeixinPay($params);
if(!$result) {
DB::rollBack();
throw new \Exception('请求错误');
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
return '';
}
return $result;
}
public function _miniWeixinPay($params) {
Log::channel("miniwechart")->info('调用微信支付');
$url = config('pay.mini.url');
$urlarr = parse_url($url);
$appid = config('pay.mini.app_id');
$mchid = config('pay.mini.mch_id');
$xlid = config('pay.mini.serial_no');
$randstr = Util::getRanStr(16);
$time = time();
$data = array();
$data['appid'] = $appid;
$data['mchid'] = $mchid;
$data['description'] = $params['product_name']; // 商品描述
$data['out_trade_no'] = $params['order_no']; // 订单号
$data['notify_url'] = config('pay.mini.notify_url'); // 回调接口
$data['amount']['total'] = 1; // 金额
$data['payer']['openid'] = $params['open_id']; // 用户openID
$data = json_encode($data);
Log::channel("miniwechart")->info('生成参数_'.$data);
$key = $this->getSign($data, $urlarr['path'], $randstr, $time); // 签名
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $mchid, $xlid, $randstr, $time, $key); // 生成 Token
$header = array(
'Accept: application/json',
'Content-Type: application/json; charset=UTF-8',
'User-Agent: */*',
'Authorization: WECHATPAY2-SHA256-RSA2048 '.$token
);
$result = Util::curlPostHttps($url, $data, $header); // POST 请求
Log::channel("miniwechart")->info('请求结果_'.$result);
$disposePrepayId = json_decode($result, true);
$prepay_id = $disposePrepayId['prepay_id'];
$paySign = $this->paySignBody($appid, $time, $randstr, $prepay_id); // 构造对prepay_id再次签名的签名数据主体
$res = json_decode($data, true);
$getPayData = $this->requestPayData($randstr, $prepay_id, $paySign, $time, $res['out_trade_no']); // 返回小程序数据
return $getPayData;
}
// CURL POST 请求
public static function curlPostHttps($url, $data = [],$header=null, $timeout = 3){
$ch = curl_init();
if($header){
curl_setopt($ch, CURLOPT_HTTPHEADER,$header);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // 设置超时
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
// 生产随机字符串
public static function getRanStr($length) {
$str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$len = strlen($str)-1;
$randstr = '';
for ($i=0; $i<$length; $i++) {
$num = mt_rand(0,$len);
$randstr .= $str[$num];
}
return $randstr;
}
// 生成签名数据主体
// $url 去除域名后的url
// $randstr 随机数
// $data 请求报文主体
public function getSign($data, $url, $randstr, $time) {
$str = "POST"."\n".$url."\n".$time."\n".$randstr."\n".$data."\n";
$key = file_get_contents(storage_path('app/public/apiclient_key.pem')); // 在商户平台下载的秘钥
$str = $this->getSha256WithRSA($str,$key);
return $str;
}
// 生成签名值
public function getSha256WithRSA($content, $private_key) {
$private_key = openssl_pkey_get_private($private_key);
openssl_sign($content, $signature, $private_key, "SHA256");
openssl_free_key($private_key);
$sign = base64_encode($signature);
return $sign;
}
// 构造对prepay_id再次签名的签名数据主体
public function paySignBody($appid, $time, $randstr, $prepay_id) {
$str = $appid."\n".$time."\n".$randstr."\n"."prepay_id=$prepay_id"."\n";
$key = file_get_contents(storage_path('app/public/apiclient_key.pem')); // 在商户平台下载的秘钥
$str = $this->getSha256WithRSA($str,$key);
return $str;
}
// 返回小程序数据
public function requestPayData($nonce_str, $prepay_id, $paySign, $time, $out_trade_no) {
return [
'nonce_str' => $nonce_str,
'package' => 'prepay_id='.$prepay_id,
'paySign' => $paySign,
'timeStamp' => $time,
// 'signType' => 'RSA',
'signType' => 'MD5',
'out_trade_no' => $out_trade_no,
'app_id' => config('pay.mini.app_id'),
'mch_id' => config('pay.mini.mch_id'),
];
}
3. 回调接口
public function Callback() {
$xml = file_get_contents('php://input'); // 获取数据
if (empty($xml)) {
Log::channel("miniwechart")->info('weixin-notify:返回数据为空'. $xml);
exit;
}
Log::channel("miniwechart")->info('weixin-notify:打印数据'. $xml);
$orderData = $this->wechartDecrypt($xml); // 解密数据
$orderServer = new OrderService();
$result = $orderServer->_verifyOrder($orderData); // 验证订单数据
// 回调成功
if ($result) {
$this->replyNotify();
}
}
// 验证订单
public function _verifyOrder($data) {
if ($data['appid'] != config('pay.mini.app_id')) {
Log::channel("miniwechart")->info('APPID错误');
return false;
}
$tradeNo = $data['out_trade_no'] ?? '';
$orderInfo = Order::query()->where('order_no', $tradeNo)->first();
if(!$orderInfo) {
Log::channel("miniwechart")->info('回调未查询出订单');
return false;
}
if ($data['trade_state'] == 'SUCCESS') {
Log::channel("miniwechart")->info('支付成功, 开始处理回调数据');
if ($this->_afterNotifyFirst(config('pay.pay_type_wexin'), $orderInfo['id'], $data['transaction_id'])) {
return true;
}
}
return false;
}
// 回调保存数据
public function _afterNotifyFirst($type, $orderId, $transaction_id) {
Log::channel("miniwechart")->info('开始处理回调数据');
$orderModel = order::query()->find($orderId);
$orderModel->pay_type = $type;
$orderModel->state = config('pay.pay_state_paid');
$orderModel->transaction_id = $transaction_id;
$orderModel->save();
return true;
}
// 解密数据
// 需要php扩展 sodium
public function wechartDecrypt($str) {
$str = htmlspecialchars_decode($str,ENT_COMPAT);
$post = json_decode($str,true);
$key = config("pay.mini.api_v3_Key");
$text = base64_decode($post['resource']['ciphertext']);
$str = sodium_crypto_aead_aes256gcm_decrypt($text, $post['resource']['associated_data'], $post['resource']['nonce'], $key);
return json_decode($str,true);
}
// 回复微信notify方法
public function replyNotify() {
$xml = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
echo $xml;
}
4. 支付成功后微信回调发送的数据
{
"id": "2dd87d80-2478-5ab0-852c-6d20084f8175",
"create_time": "2022-07-26T17:07:28+08:00",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"summary": "支付成功",
"resource": {
"original_type": "transaction",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "此处显示密文,需要经过解密显示",
"associated_data": "transaction",
"nonce": "8oAAd7fCPvpO"
}
}