0%

php如何对接微信支付V3版本(保姆级别)

本文主要记录php对接微信支付v3版本的整个过程,包含支付、退款、回调等功能,会从申请证书开始记录

一、证书申请 V3

1、入口介绍

在微信商户平台中,[账户中心]->[API安全]中可以看到这样的界面

证书的申请在商户API证书下面的管理证书

2、证书工具下载


目前支持windows和mac版本,下载安装到本机

3、修改证书保存地址

运行证书工具后,修改证书保存路径

4、生成证书请求串

5、生成证书

将证书请求串复制到商户平台证书管理界面粘贴进去,然后在工具上点击生成证书,会在你证书保存路径的地方生成两个文件:apiclient_cert.pemapiclinet_key.pem

二、cert.pem证书生成

注意:V3版本中cert.pem证书不同于apiclient_cert.pem证书,它是由命令工具生成,确切的说apiclient_cert.pem是没有用的,而cert.pem才是真正在代码对接中有用的,这里相对于v2版本有很大的区别,这里也是最容易混淆的地方。

1、下载php依赖

1
composer require wechatpay/wechatpay

github地址:https://github.com/wechatpay-apiv3/wechatpay-php

2、运行命令生成证书

找到该路径: /vendor/wechatpay/wechatpay/bin 下,有个 CertificateDownloader.php 文件,可通过php去执行它能看到命令行的提示:

  • -m 商户号:这个不用说
  • -s 商户证书序列号:在商户平台生成证书后所对应的证书序列号
  • -f 商户的秘钥文件:通过证书工具生成的apiclient_key.pem这个文件
  • -k APIv3: 在商户平台API安全中可以找到,本文中图一可以看到

全部设置好后执行命令(参考):

1
php CertificateDownloader.php -m 1635059363 -k NrAsJhcefMRkHKkMtNGrAtTdKtKaTTmY -f ../../../../config/merchant/apiclient_key.pem -s 7A56DCFEEE02E7F56C18E04EFF7D488A44D6AABB 

成功后会得到以下结果:

此时就得到了cert.pem的内容,自行保存证书。

三、业务对接

以下WechatPayServiceV3.php为实例,以app支付为场景,提供如下service,里面有微信支付、退款、回调等业务逻辑,特别注意微信配置的地方,也可以参照github上提供的参考代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
<?php

namespace app\api\service;

use think\facade\Log;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;

/**
* 微信支付 V3
* Class WechatPayServiceV3
*/
class WechatPayServiceV3
{
/** @var Builder::factory $resp */
protected $instance;

public function __construct()
{
$this->_init();
}

/**
* - apiV3实例初始化
* @return void
*/
public function _init()
{
// 商户号,假定为`1000100`
$merchantId = config('wechat.merchantId', '');
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyFilePath = "file://".config_path().'merchant/apiclient_key.pem';
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

// 「商户API证书」的「证书序列号」 ,从微信支付提供的「平台证书」中获取
$merchantCertificateSerial = config('wechat.mchSerialNo', '');

// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
// 【**】【打死要特别注意】:这里是坑,以下的cert.pem并不是工具生成的apiclient_cert.pem这个证书,而是通过/vendor/wechatpay/wechatpay/bin 下的工具生成的!
$platformCertificateFilePath = "file://".config_path().'merchant/cert.pem';
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

// 从「微信支付平台证书」中获取「证书序列号」
$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);

// 构造一个 APIv3 客户端实例
$this->instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
]);
}

/**
* - 生成支付签名
* @param $prepayId
* @return array
*/
public function getPaySign($prepayId): array
{
$merchantPrivateKeyFilePath = "file://".config_path().'merchant/apiclient_key.pem';
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);
$params = [
'appId' => config('wechat.appid', ''),
'timeStamp' => (string)Formatter::timestamp(),
'nonceStr' => Formatter::nonce(),
'prepay_id' => $prepayId,
];
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];
return $params;
}

/**
* - 微信支付 V3
* #依赖包文档: https://packagist.org/packages/wechatpay/wechatpay
* #微信app支付文档:https://pay.weixin.qq.com/docs/merchant/apis/in-app-payment/direct-jsons/app-prepay.html
* @param $options
* @param $notifyURL // 回调地址
* @return array
* @throws \Exception
*/
public function pay($options, $notifyURL): array
{
try {
$appid = config('wechat.appid', '');
$mchid = config('wechat.merchantId', '');
// 同步支付
try {
// 参数整理 请参照:https://pay.weixin.qq.com/docs/merchant/apis/in-app-payment/direct-jsons/app-prepay.html
$params = [
'json' => [
'appid' => $appid, // 微信appid
'mchid' => $mchid, // 商户号
'description' => $options['subject'], //商品描述
'out_trade_no' => $options['outTradeNo'], // 商户系统内部订单号
'notify_url' => $notifyURL, // 回调地址
'amount' => [
'total' => intval($options['totalAmount'] * 100), // 订单总金额,单位为分。
'currency' => 'CNY',
],
],
];
$resp = $this->instance->chain('v3/pay/transactions/app')->post($params);
$resp = $resp->getBody()->getContents();
} catch (\GuzzleHttp\Exception\RequestException $e) {
$r = $e->getResponse();
$resp = $r->getBody()->getContents();
Log::channel('wechat_notify')->error("微信支付创建订单 请求异常", ['StatusCode' => $r->getStatusCode() . ' ' . $r->getReasonPhrase(), 'Body' => $r->getBody(), 'outTradeNo' => $options['outTradeNo']]);
} catch (\Exception $e) {
Log::channel('wechat_notify')->error("微信支付创建订单 异常", ['description' => $options['subject'], 'outTradeNo' => $options['outTradeNo'], 'money' => $options['totalAmount'], 'TraceAsString' => $e->getTraceAsString(), 'msg' => $e->getMessage()]);
throw $e;
}
// 处理结果
$res = json_decode($resp, true);
$paySign = $this->getPaySign($res['prepay_id']);
return [
'appid' => $appid,
'partnerid' => $mchid,
'prepayid' => $res['prepay_id'],
'package' => "Sign=WXPay",
'noncestr' => $paySign['nonceStr'],
'timestamp' => $paySign['timeStamp'],
'sign' => $paySign['paySign'],
];

} catch (\Exception $e) {
// 进行错误处理
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
}
//echo $e->getTraceAsString(), PHP_EOL;
Log::channel('wechat_notify')->error("微信支付创建订单 异常". $e->getMessage()."|".$e->getLine());
throw new \Exception($e->getMessage());
}
}

/**
* - 微信退单 V3
* @param $info
* @return bool
*/
public function orderRefund($info): bool
{
$promise = $this->instance
->chain('v3/refund/domestic/refunds')
// 参数整理 请参考:https://pay.weixin.qq.com/docs/merchant/apis/in-app-payment/create.html
->postAsync([
'json' => [
'transaction_id' => (string)$info['trade_no'], // 交易号
'out_refund_no' => (string)$info['order_sn'], // 系统内部订单号
'amount' => [
'refund' => $info['actual_price'] * 100, // 退款金额,单位为分,只能为整数,不能超过原订单支付金额。
'total' => $info['actual_price'] * 100, // 原支付交易的订单总金额,单位为分,只能为整数
'currency' => 'CNY',
],
],
])
->then(static function($response) {
// 正常逻辑回调处理
echo $response->getBody(), PHP_EOL;
return $response;
})
->otherwise(static function($e) {
// 异常错误处理
echo $e->getMessage(), PHP_EOL;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
echo $e->getTraceAsString(), PHP_EOL;
});
// 同步等待
$promise->wait();
Log::error("微信退款结果:".json_encode($promise));
$promise = json_decode($promise, true);
if(isset($promise['status']) && $promise['status'] == 'SUCCESS'){
return true;
}
return false;
}

/**
* - 解析回调数据
* @param $requestHead array 请求头
* @param $requestData array 获取的请求数据
* @return array
*/
public function analysisCallBackData(array $requestHead, array $requestData): array
{
Log::channel('wechat_notify')->info("requestHead:". json_encode($requestHead));
$inWechatpaySignature = $requestHead['wechatpay-signature'];
$inWechatpayTimestamp = $requestHead['wechatpay-timestamp'];
$inWechatpayNonce = $requestHead['wechatpay-nonce'];
$inBody = file_get_contents('php://input');// json类型字符串 请根据实际情况获取,例如: file_get_contents('php://input');
Log::channel('wechat_notify')->info("inBody:". json_encode($inBody));
$apiv3Key = config('wechat.apiv3Key', '');// 在商户平台上设置的APIv3密钥

// 根据通知的平台证书序列号,查询本地平台证书文件【**注意】这个证书也是通过命令生成的那个证书
$wechatPaySerialFilePath = "file://".config_path().'merchant/cert.pem';
$platformPublicKeyInstance = Rsa::from($wechatPaySerialFilePath, Rsa::KEY_TYPE_PUBLIC);
// 检查通知时间偏移量,允许5分钟之内的偏移
$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
if (!$timeOffsetStatus) {//超过5分钟的订单
Log::channel('wechat_notify')->info('超过5分钟才回调的信息'. json_encode(['requestHead' => $requestHead, 'requestData' => $requestData]));
return [];
}
$verifiedStatus = Rsa::verify(
// 构造验签名串
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature,
$platformPublicKeyInstance
);
if (!$verifiedStatus) {
Log::channel('wechat_notify')->info('验证签名失败'.json_encode(['requestHead' => $requestHead, 'requestData' => $requestData]));
return [];
}
if ($timeOffsetStatus && $verifiedStatus) {
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($inBody, true);
// 使用PHP7的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
// 加密文本消息解密
$inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
Log::channel("wechat_notify")->info("回调解密后的数据:".$inBodyResource);
// 把解密后的文本转换为PHP Array数组
return (array)json_decode($inBodyResource, true);
// 返回的示例数据
// {
// "mchid": "1635059363",
// "appid": "wx7dbfd345da91dc3d",
// "out_trade_no": "jprj20240820170409725215853",
// "transaction_id": "4200002354202408200706648259",
// "trade_type": "APP",
// "trade_state": "SUCCESS",
// "trade_state_desc": "支付成功",
// "bank_type": "OTHERS",
// "attach": "",
// "success_time": "2024-08-20T17:04:16+08:00",
// "payer": {
// "openid": "o1pFV6VnFfmWFz6VgilbErtxSalk"
// },
// "amount": {
// "total": 30,
// "payer_total": 30,
// "currency": "CNY",
// "payer_currency": "CNY"
// }
// }

} else {
return [];
}
}
}


/**
* ============== 》【微信V3】回调触发《====================
* 【微信V3】回调入口
* @return void
* @throws \think\db\exception\DbException
*/
public function wechatnotify()
{
// v3需要获取header信息
$headers = $this->request->header();
// 获取body信息
$inBody = file_get_contents("php://input");
// 解析回调数据
$parsedData = (new WechatPayServiceV3())->analysisCallBackData($headers, json_decode($inBody, true));
// 解析后的数据
// $parsedData = [
// 'trade_state' => "SUCCESS",
// 'transaction_id' => "20245543435551",
// 'out_trade_no' => "ysj20241008140356417220366"
// ];
}