一、项目背景
初衷
最近开发的项目使用到了微信支付API v3,通过对代码复盘,重新梳理了整个实现流程和遇到的坑,希望对有接触到微信支付开发的你有所帮助。
开发环境
php:7.3.25
laravel 8.1
composer组件:
wechatpay 1.4
二、开发流程
1、安装微信支付 SDK
在 composer.json 文件中添加微信支付 SDK 的依赖,并运行 Composer 更新来安装 SDK。
编辑composer.json文件
// 新增wechatpay组件
"require": {
"wechatpay/wechatpay": "^1.4",
},
执行composer install安装即可。
或者,直接在终端执行命令:composer require wechatpay/wechatpay
2、支付接口微信下单
// 小程序调用的支付接口
public function wxindex()
{
Log::info('微信下单');
try {
$data = $this->request->input();
$validator = Validator::make($data, $this->wxindexRules);
if ($validator->fails()) {
return httpreturn(1, '参数校验失败', $validator->errors());
}
$userData = Auth::guard('api')->user();
$userData['gid'] = $data['gid'];
$userData['order_id'] = empty($data['order_id'])?'':$data['order_id'];
list($bool, $ret) = $this->service->wx_index($userData); // 下面有代码详解
if ($bool) {
return httpreturn(0, '请求成功', $ret);
} else {
return httpreturn(1, $ret?:'请求失败');
}
} catch (\Exception $e) {
return httpreturn(1, '系统错误', $e->getFile(). '-' .$e->getLine(). '-' .$e->getMessage());
}
}
// 其中的wx_index方法进行订单创建,并发起支付
public function wx_index($data)
{
$good = DB::table('product')->find($data['gid']);
$price = intval(($good->sale_price));
if(!empty($data['order_id'])){
$orderid = $data['order_id'];
$order = $this->model->find(['id'=>$orderid]);
if (empty($order)) {
return [false, $orderid];
}
$oid = $order['oid'];
}else{
list($orderid, $oid) = $this->model->add([
'uid' => $data['id'],
'gid' => $data['gid'],
'ctime' => date('Y-m-d H:i:s', time()),
'total_fee' => $price,
]);
}
$config = config('wechat')['payment']['default'];
if ($good->sale_price > 0) {
$des = mb_substr($good->product_name,0,30);
try {
$app = Factory::payment($config);
//v3接口
$prepay = $this->wxPayV3($oid,$price,$des,$data['openid']); // 下面有详细详解
$ret = [];
if (!empty($prepay)) {
$prepay_id = json_decode($prepay,true)['prepay_id'];
$jssdk = $app->jssdk;
$ret = $jssdk->bridgeConfigRSA($prepay_id,false);
$ret['oid'] = $orderid;
}else{
return [false, '调起支付失败'];
}
return [true, $ret];
} catch (\Exception $e) {
return [false, '支付失败'];
}
}
}
// 微信支付v3接口
public function wxPayV3($oid,$price,$des,$openid){
try {
$merchantPrivateKeyFilePath = $config['key_path'];
$merchantPrivateKeyInstance = Rsa::from('file://'.$merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
$merchantCertificateSerial = $config['serial']; $platformCertificateFilePath = $config['serial_path'];
$platformPublicKeyInstance = Rsa::from('file://'.$platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 从「微信支付平台证书」中获取「证书序列号」
// // 微信支付平台证书(下面有详细证书下载方法,一般在后台支付设置时调用)
$platformCertificateSerial = PemUtil::parseCertificateSerialNo('file://'.$platformCertificateFilePath);
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
]);
$json = [
'mchid' => $config['mch_id'],
'out_trade_no' => $oid,
'appid' => $config['app_id'],
'description' => $des,
'notify_url' => $_ENV['APP_URL'] . '/api/v1/wxNotifyV3/'.$config['mer_id'], // 下面有回调接口详解
'amount' => [
'total' => $price,
'currency' => 'CNY'
],
'payer' => [
'openid' => $openid,
],
'attach' => json_encode(['gid'=>$config['mer_id']]),
'settle_info' => [
'profit_sharing'=>true
],
'time_expire' => date('Y-m-d\TH:i:sP', strtotime("+3 minutes", time()))
];
$start = microtime(true);
$resp = $instance
->chain('v3/pay/transactions/jsapi')
->post(['json' => $json]);
$end = microtime(true);
$time = bcmul(bcsub($end, $start, 6), 1000, 2).'ms';
return $resp->getBody();
} catch (\Exception $e) {
// 进行错误处理
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
}
return '';
}
}
3、处理支付结果回调
发送支付结果通知,在支付结果回调中,您可以根据支付结果的状态进行相应的处理。
// 回调接口
/**
* 微信支付V3
* {
* "mchid": "1378592502",
* "appid": "wx945a8bf1b352f2b2",
* "out_trade_no": "2023101812456352461771",
* "transaction_id": "4200002041202310188575676363",
* "trade_type": "JSAPI",
* "trade_state": "SUCCESS",
* "trade_state_desc": "支付成功",
* "bank_type": "CMB_DEBIT",
* "attach": "",
* "success_time": "2023-10-18T15:01:01+08:00",
* "payer": {
* "openid": "ogOWZ5XyjE83swcHdG4hJtr-haDg"
* },
* "amount": {
* "total": 1000,
* "payer_total": 1000,
* "currency": "CNY",
* "payer_currency": "CNY"
* }
* }
* @param Request $request
* @return true|void
*/
public function wxNotifyV3(Request $request,$mer_id)
{
$headers = $request->header();
try {
$config = config('wechat')['payment']['default'];
$inWechatpaySignature = $headers['wechatpay-signature'][0];
$inWechatpayTimestamp = $headers['wechatpay-timestamp'][0];
$inWechatpaySerial = $config['serial'];
$inWechatpayNonce = $headers['wechatpay-nonce'][0];
$inBody = file_get_contents('php://input');
$apiv3Key = $config['key'];
// 微信支付平台证书(下面有详细证书下载方法,一般在后台支付设置时调用)
$platformPublicKeyInstance = Rsa::from('file://'.$config['serial_path'], Rsa::KEY_TYPE_PUBLIC);
$verifiedStatus = Rsa::verify(
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature,
$platformPublicKeyInstance
);
if ($verifiedStatus) {
$inBodyArray = (array)json_decode($inBody, true);
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
$inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
$inBodyResourceArray = (array)json_decode($inBodyResource, true);
return $this->service->wxNotifyV3( // 下面有代码详解
$inBodyResourceArray['out_trade_no'],
$inBodyResourceArray['amount']['total'],
$inBodyResourceArray['transaction_id'],
$inBodyResourceArray['trade_type'],
$inBodyResourceArray['trade_state'],
$inBodyResourceArray['payer']['openid'],
);
}
}catch (\Exception $e) {
return json_encode(['code'=>'FAIL','message'=>'失败']);
}
}
4、处理支付结果
此方法为微信支付回调接口中封装的方法,根据支付结果的状态进行相应的处理。例如,此方法会修改订单状态和商品销量等信息。
public function wxNotifyV3($out_trade_no,$total_fee,$transaction_id,$trade_type,$trade_state,$openid)
{
event(new ApiLogEvent('/app/wechat/wxNotifyV3', '微信支付回调:'.$trade_state));
$order = DB::table('order')->where(['oid' => $out_trade_no])->first();
if(empty($order) || $order->status == 1){
return true;
}
if($trade_state == 'SUCCESS'){
$save['status'] = 1;
}else{
$save['status'] = -2;
}
$order->total_fee = $save['total_fee'] = $total_fee;
$order->transaction_id = $save['transaction_id'] = $transaction_id;
$order->trade_type = $save['trade_type'] = $trade_type;
$order->paytime = $save['paytime'] = date('Y-m-d H:i:s');
DB::table('order')->where(['id' => $order->id])->update($save);
$good = DB::table('product')->find($order->gid);
if($save['status']<=0){
return json_encode(['code'=>'SUCCESS','message'=>'处理完成']); // 返回处理完成
}
//支付完成
DB::table('product')->where('id', $good->id)->increment('sales_num', 1);
DB::table('product')->where('id', $good->id)->increment('sales_real', 1);
return json_encode(['code'=>'SUCCESS','message'=>'支付成功']); // 返回处理完成
}
小结
此处补充下微信平台证书生成方法,即后台界面样式:
/**
* 下载平台证书
* @param string> $mchid
* @param string> $api_key
* @param string> $serialno
* @return string
*/
public function CertificateDownload($mchid,$api_key,$serialno)
{
$local_path = 'xxxxxxxx';
$opts['privatekey'] = storage_path($local_path.'apiclient_key.pem');
$privateKey = file_get_contents($opts['privatekey']);
$opts['output'] = storage_path($local_path);
$opts['key'] = $api_key;
$opts['mchid'] = $mchid;
$opts['serialno'] = $serialno;
$opts['baseuri'] = 'https://api.mch.weixin.qq.com/';
static $certs = ['any' => null];
$outputDir = $opts['output'];
$apiv3Key = (string) $opts['key'];
$instance = Builder::factory([
'mchid' => $opts['mchid'],
'serial' => $opts['serialno'],
'privateKey' => $privateKey,
'certs' => &$certs,
'base_uri' => $opts['baseuri'],
]);
/** @var \GuzzleHttp\HandlerStack $stack */
$stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler');
$stack->after('verifier', Middleware::mapResponse(self::certsInjector($apiv3Key, $certs)), 'injector');
$stack->before('verifier', Middleware::mapResponse(self::certsRecorder((string) $outputDir, $certs)), 'recorder');
try {
$resp = $instance->chain('v3/certificates')->get();
$body = (string) $resp->getBody();
$json = \json_decode($body,true);
$data = $json['data'][0];
return $local_path.'wechatpay_'. $data['serial_no']. '.pem';
}catch (\Exception $e) {
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
}
return '';
}
}
调用情景:
后台支付设置,填写微信相关信息后,点击确认按钮调用该方法来下载微信支付平台证书。