技术说明
1.前端:uniapp、vue3
2.接口:PHP8、ThinkPHP8、MySQL8.0
3.微信支付- PHP,官方示例文档
4.示例代码的模型及业务自己进行调整,不要一味的复制粘贴!!!
流程说明
1.小程序调用接口--获取拉起支付所用参数,生成订单
2.拉起微信支付
3.支付完成-更改订单状态
参数说明
1.appid - 小程序id
2.mchid -- 商户号ID
3.certificate_serial -- 证书序列号
4.api_v3_key -- 支付密钥(v3)
5.apiclient_key.pem -- 商户API私钥文件,根据微信支付下载器下载即可
6.cert.pem -- 微信支付平台证书文件(注意:此文件必须是手动下载的,具体下载方式下方有说明!!!)
其他说明
1.本示例采用微信支付sdk
2.实际情况根据业务进行调整;
3.通知回调(未能正确返回)
4.其他没毛病。
项目示例
1.安装微信支付 wechatpay -- sdk
composer require wechatpay/wechatpay
2.下载微信支付平台证书文件
(1)下载微信支付平台证书下载器
(2)进行详情页(微信支付平台证书下载器)
(3)下载CertificateDownloader.php,点击下方红框,直接下载文件就行,文件位置随便放,只要能用php命令运行就行
(4)下载证书,直接复制下面命令,改参数即可。
-k 支付密钥(上方参数4)
-m 商户号(上方参数2)
-f 商户密钥(上方参数5,需要完整路径)
-s 证书序列号(上方参数3)
-o 生成证书地址(需要本地完整路径)
php -f ./CertificateDownloader.php -- -k 4202c8***** -m 16***** -f /****/apiclient_key.pem -s 25***** -o /*****/cert/
3.封装支付类(完整示例如下)
<?php
namespace app\common\controller;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;
/**
* @note 微信支付操作
*/
class WechatPay
{
protected string $spAppid; // 小程序appid
protected string $spAppSecret; // 小程序密钥
protected string $merchantId; // 商户号
protected string $certificateSerial; // 证书序列号
protected string $apiV3Key; // APIv3密钥
protected object $instance; // 实例
protected string $merchantPrivateKeyFilePath;
public function __construct()
{
$this->spAppid = config('wechat.sp.appid');
$this->spAppSecret = config('wechat.sp.secret');
$this->merchantId = config('wechat.pay.mchid');
$this->certificateSerial = config('wechat.pay.certificate_serial');
$this->apiV3Key = config('wechat.pay.api_v3_key');
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$this->merchantPrivateKeyFilePath = root_path() . 'wxcert/apiclient_key.pem';
if (!file_exists($this->merchantPrivateKeyFilePath)) throw new \Exception('商户API私钥文件不存在');
$merchantPrivateKeyFilePath = 'file://' . $this->merchantPrivateKeyFilePath;
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
$platformCertificateFilePath = root_path() . 'wxcert/cert.pem';
if (!file_exists($platformCertificateFilePath)) throw new \Exception('微信支付平台证书文件不存在');
$platformCertificateFilePath = 'file://' . $platformCertificateFilePath;
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 从「微信支付平台证书」中获取「证书序列号」
$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
// 构造一个 APIv3 客户端实例
$this->instance = Builder::factory([
'mchid' => $this->merchantId, // 商户号
'serial' => $this->certificateSerial, //「商户API证书」的「证书序列号」
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
]);
}
/**
* @note 获取微信支付预交易订单
* @param string $openid 用户openid
* @param string $out_trade_no 订单号
* @param string $notify_url 回调地址
* @param float $price 价格
* @param string $desc 描述
*/
public function spPrepayId(string $openid, string $out_trade_no, string $notify_url, float $price = 0.01, string $desc = '订单')
{
$prepay_id = '';
try {
$resp = $this->instance
->chain('/v3/pay/transactions/jsapi')
->post([
'json' => [
'mchid' => $this->merchantId,
'out_trade_no' => $out_trade_no,
'appid' => $this->spAppid,
'description' => $desc,
'notify_url' => $notify_url,
'amount' => [
'total' => $price * 100,
'currency' => 'CNY'
],
'payer' => [
'openid' => $openid
]
]
]);
$res = json_decode($resp->getBody());
$prepay_id = $res->prepay_id;
} catch (\Exception $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;
}
return $prepay_id;
}
/**
* @note 生成签名
* @param string $prepay_id 预交易订单
* @param string $nonceStr 随机字符串
* @param string $timeStamp 时间戳
* @return string
*/
public function makeSign(string $prepay_id, string $nonceStr, string $timeStamp): string
{
if (!file_exists($this->merchantPrivateKeyFilePath)) return '';
$merchantPrivateKeyFilePath = 'file://' . $this->merchantPrivateKeyFilePath;
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);
$params = [
'appId' => $this->spAppid,
'timeStamp' => $timeStamp,
'nonceStr' => $nonceStr,
'package' => 'prepay_id=' . $prepay_id,
];
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];
return $params['paySign'] ?? '';
}
/**
* @note 回调通知,参数解密
* @param string $inWechatpaySignature 微信支付平台签名
* @param string $inWechatpayTimestamp 微信支付平台时间戳
* @param string $inWechatpayNonce 微信支付平台随机串
* @param string $inBody 通知内容
* @param string $inWechatpaySerial 平台证书序列号
* @param string $inRequestID 请求ID
* @return array
*/
public function notifyDecrypt(string $inWechatpaySignature, string $inWechatpayTimestamp, string $inWechatpayNonce, string $inBody, string $inWechatpaySerial, string $inRequestID = ''): array
{
// 根据通知的平台证书序列号,查询本地平台证书文件,
$platformCertificateFilePath = root_path() . 'wxcert/cert.pem';
if (!file_exists($platformCertificateFilePath)) throw new \Exception('微信支付平台证书文件不存在');
$platformCertificateFilePath = 'file://' . $platformCertificateFilePath;
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 检查通知时间偏移量,允许5分钟之内的偏移
$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
$verifiedStatus = Rsa::verify(
// 构造验签名串
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature,
$platformPublicKeyInstance
);
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, $this->apiV3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
return (array)json_decode($inBodyResource, true);
}
return [];
}
/**
* @note 加密消息解密
*/
public function decryptMsg($encryptedData, $iv, $sessionKey): array|string
{
$pc = new WxBizDataCrypt($this->spAppid, $sessionKey);
$errCode = $pc->decryptData($encryptedData, $iv, $data);
if ($errCode == 0) {
return $data;
}
return [];
}
}
4.封装接口(完整示例如下)
<?php
namespace app\api\controller\sp;
use think\response\Json;
class Activity
{
/**
* @note 生成订单
*/
public function prepayId(): void
{
$activityId = $this->request->post('ac_id/d', 1);
if (empty($activityId)) $this->error('赛事错误,请重试!');
$openid = $this->request->post('openid/s', '');
if (empty($openid)) $this->error('支付用户获取失败,请重试!');
$model = new ActivityModel();
$activity = $model->findOrEmpty($activityId)->toArray();
if (empty($activity)) $this->error('get Err');
if ($activity['status'] != 1) $this->error('get Err!');
// 订单信息
$orderInfo = [
'activity_id' => $activityId,
'openid' => $openid,
'number' => 'order' . date('YmdHis') . rand(1000, 9999),
'money' => $activity['price'],
'type' => 1,
'status' => 0
];
// 生成订单
$pay = new WechatPay();
$notify_url = env('domain') . 'index.php/api/sp.Activity/notify';
$prepayId = $pay->spPrepayId($openid, $orderInfo['number'], $notify_url);
if (empty($prepayId)) $this->error('订单生成失败,请重试!');
$orderInfo['prepay_id'] = $prepayId;
$order = new Order();
$order->save($orderInfo);
$timeStamp = (string)time();
$orderInfo['timeStamp'] = $timeStamp;
$nonceStr = getRandStr(32);
$orderInfo['nonceStr'] = $nonceStr;
$orderInfo['package'] = 'prepay_id=' . $prepayId;
$orderInfo['paySign'] = $pay->makeSign($prepayId, $nonceStr, $timeStamp);
$this->success('get Success', [
'order' => $orderInfo
]);
}
/**
* @note 支付回调
*/
public function notify(): Json
{
$inWechatpaySignature = request()->header('Wechatpay-Signature', ''); // header中获取签名
$inWechatpayTimestamp = request()->header('Wechatpay-Timestamp', ''); // header中获取时间戳
$inWechatpaySerial = request()->header('Wechatpay-Serial', ''); // header中获取证书序列号
$inWechatpayNonce = request()->header('Wechatpay-Nonce', ''); // header中获取随机字符串
$inRequestID = request()->header('Request-ID', ''); // 请根据实际情况获取
$inBody = file_get_contents('php://input'); // 请根据实际情况获取,例如: file_get_contents('php://input');
$pay = new WechatPay();
$res = $pay->notifyDecrypt($inWechatpaySignature, $inWechatpayTimestamp, $inWechatpayNonce, $inBody, $inWechatpaySerial, $inRequestID);
if (!empty($res)) {
// 进行订单数据修改
$order = new Order();
// 查询订单数据
$orderInfo = $order->where('number', $res['out_trade_no'])->find();
if (!empty($orderInfo)){
$result = $order->where('id',$orderInfo['id'])->save([
'transaction_id' => $res['transaction_id'],
'status' => $res['trade_state'] == 'SUCCESS' ? 1 : 0,
'trade_type' => $res['trade_type'],
'trade_state_desc' => $res['trade_state_desc'],
'bank_type' => $res['bank_type'],
'success_time' => $res['success_time']
]);
cache(':order_' . $res['out_trade_no'], $result, 3600);
}
return json(['code' => 'SUCCESS']);
}
return json(['message' => '失败', 'code' => 'FAIL']);
}
}
5.uniapp示例
<template>
<view class="box">
<view><up-button text="立即支付" type="primary" @click="toPay"></up-button>
</view>
<up-toast ref="uToastRef"></up-toast>
</view>
</template>
<script setup>
import {
onLoad
} from '@dcloudio/uni-app'
import {
ref,
} from 'vue';
import {
getPrepayId
} from '@/utils/api/order.js'
const uToastRef = ref(null)
// 点击支付
const toPay = () => {
getPrepayId({
openid: ''
}).then((res) => {
if (res.code == 1) {
const order = res.data.order
uni.requestPayment({
provider: 'wxpay',
timeStamp: order.timeStamp, // 时间戳
nonceStr: order.nonceStr, // 随机字符串,长度为32个字符以下
package: order.package, // 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
signType: 'RSA', // 签名算法,应与后台下单时的值一致
paySign: order.paySign, // 签名
success: function(res) {
console.log('success:' + JSON.stringify(res));
},
fail: function(err) {
console.log('fail:' + JSON.stringify(err),);
}
});
} else {
uToastRef.value.error(res.msg)
}
})
}
</script>
<style lang="scss">
.box {
width: 100%;
}
</style>