一、概述
小程序支付需要后端配合,这里以后端php的ThinkPhP6.0框架为例,其他语言的可以参考,流程都是一样的。商户类型有服务商和直连商户两种,对接方式不一样,这里是直连商户。
二、相关文档地址
//微信支付文档
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml
//官网支付SDK(php版本),官方还有java和go语言版本,其他语言就得自己去网上找民间大佬自制版本了
https://github.com/wechatpay-apiv3/wechatpay-php
//小程序调起支付文档
https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html
三、平台配置
1、小程序微信认证,每年需要缴费,如果同主体其他公众号已经认证缴费了,可以关联自动认证,就不需要再缴费了。
2、小程序平台关联商户号
3、小程序平台配置接口地址
4、商户平台配置密钥和证书,因为我们用的APIv3接口,所以APIv2密钥不用管。证书申请按照页面提示流程来就行了,这里就不截图了,可以看官网文档(https://kf.qq.com/faq/161222NneAJf161222U7fARv.html),注意证书申请之后,点击"管理证书",查看证书序列号,后面代码中要用。
四、引入SDK
composer require wechatpay/wechatpay
五、生成微信支付平台证书
有好几种方式,这里介绍一种通过接口生成,然后手动复制粘贴到文件中的方式,其他方式可以参考官方文档。
//获取微信平台证书
public function getCert()
{
WxPayService::certificates(); //这个类的代码在下面
}
通过网页访问这个接口,手动创建cert.pem文件,然后将证书部分复制粘贴到cert.pem中
到目前为止,你应该有3个证书了,其中2个是通过商户平台配置下载的,再加上上面这,一共三个,代码中会用到的是apiclient_key.pem和cert.pem,apiclient_cert.pem暂时没用到
六、对接微信支付
有如下api,但是不是每个都需要对接,如果仅仅是对接支付的话,小程序下单->小程序调起支付->支付通知,这个流程就够了
1、小程序下单
//小程序微信支付
public function xcxWxPay()
{
$payData = [
'out_trade_no' => '唯一的随机字符串',
'description' => '支付测试',
'amount' => 1, //单位分
'openid' => 'xxxxxxxxxx',
'notify_url' => 'https://www.xxx.com/api/WxPay/wxNotify' //支付成功回调地址
];
list($code, $param) = WxPayService::init()->xcxPay($payData);
return $this->result($param);
}
/**
* 小程序微信支付
* @param $payParam
* @return array
*/
public function xcxPay($payParam = [])
{
$merchantId = $this->wxPayConfig['mchid'];
$appid = $this->wxPayConfig['appid'];
$payJsonData = [
'mchid' => $merchantId,
'appid' => $appid,
'out_trade_no' => $payParam['out_trade_no'],
'description' => $payParam['description'],
'notify_url' => $payParam['notify_url'],
'amount' => [
'total' => $payParam['amount'], // 分
'currency' => 'CNY'
],
'payer' => [
'openid' => $payParam['openid']
]
];
try {
$resp = $this->instance
->chain('v3/pay/transactions/jsapi')
->post(['json' => $payJsonData]);
if ($resp->getStatusCode() == 200) {
$wo = json_decode($resp->getBody(), true); //prepay_id
$params = [
'appid' => $appid,
'timeStamp' => (string)Formatter::timestamp(),
'nonceStr' => Formatter::nonce(),
'package' => 'prepay_id=' . $wo["prepay_id"],
];
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$this->merchantPrivateKeyInstance
)];
return [1, $params];
} else {
return [0, '微信支付失败'];
}
} catch (\Exception $e) {
$code = $e->getCode();
$data = '';
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$data = json_decode($r->getBody(), true);
}
}
return [$code, $data];
}
2、小程序调起支付
xcxWxPay().then(res => {
console.log(res);
var param = res.data.data
wx.requestPayment({
nonceStr: param.nonceStr,
package: param.package,
paySign: param.paySign,
timeStamp: param.timeStamp,
signType: 'RSA',
success(res) {
console.log(res)
},
fail(res) {
console.log(res)
}
})
})
3、支付通知
//小程序微信支付回调(支付/退款)
public function wxNotify()
{
WxPayService::notify(Request::header(), file_get_contents('php://input'));
}
/**
* 支付/退款回调
* @param $header
* @param $inBody
* @return array|string[]|void
*/
public static function notify($header, $inBody)
{
$inWechatpaySignature = $header['wechatpay-signature'];
$inWechatpayTimestamp = $header['wechatpay-timestamp'];
$inWechatpaySerial = $header['wechatpay-serial'];
$inWechatpayNonce = $header['wechatpay-nonce'];
if (!$inWechatpaySignature or !$inWechatpayTimestamp or !$inWechatpaySerial or !$inWechatpayNonce) {
//请求头错误,不是来自微信调用
exit;
}
$wxPayConfig = config('wxpay');
$merchantId = $wxPayConfig['mchid'];
$apiv3Key = $wxPayConfig['api_v3_key'];
$certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $merchantId;
// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
$platformCertificateFilePath = $certPath . '/cert.pem';
// 根据通知的平台证书序列号,查询本地平台证书文件,这里是自己生成的证书
$platformPublicKeyInstance = Rsa::from(file_get_contents($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, $apiv3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
return (array)json_decode($inBodyResource, true);
} else {
return ['trade_state' => 'FAIL'];
}
}
退款、查询账单接口都差不多,就不单独贴出来了,下面一起发不出来
七、源码
1、配置代码config/wxpay.php
<?php
return [
// +----------------------------------------------------------------------
// | 微信支付配置
// +----------------------------------------------------------------------
'mchid' => '', //商户ID
'appid' => '', //小程序appid
'cert_serial' => '', //商户api证书序列号
'api_v3_key' => '', //在商户平台上设置的APIv3密钥
];
2、接口代码WxPay.php
<?php
namespace app\api\controller;
use app\BaseController;
use app\common\conf\ErrConfig;
use app\common\service\IotService;
use app\common\service\WxPayService;
use think\facade\Request;
class WxPay extends BaseController
{
//小程序微信支付
public function xcxWxPay()
{
$payData = [
'out_trade_no' => '唯一的随机字符串',
'description' => '支付测试',
'amount' => 1,
'openid' => 'xx',
'notify_url' => 'https://www.xxx.com/api/WxPay/wxNotify'
];
list($code, $param) = WxPayService::init()->xcxPay($payData);
return $this->result($param);
}
//小程序微信退款
public function xcxWxRefund()
{
$refundParam = [
'transaction_id' => 'xxx',
'out_refund_no' => '唯一的随机字符串',
'reason' => '退款测试',
'notify_url' => 'https://www.xxx.com/api/WxPay/wxNotify',
'amount' => 1,
];
list($code, $res) = WxPayService::init()->refundOrder($refundParam);
}
//支付账单查询
public function getPayBill()
{
list($code, $res) = WxPayService::init()->getPayBill('xxx');
}
//退款账单查询
public function getRefundBill()
{
list($code, $res) = WxPayService::init()->getRefundBill('xxx');
}
//获取微信平台证书
public function getCert()
{
WxPayService::certificates();
}
//小程序微信支付回调(支付/退款)
public function wxNotify()
{
WxPayService::notify(Request::header(), file_get_contents('php://input'));
}
}
3、微信支付服务类WxPayService.php
<?php
namespace app\common\service;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;
//微信支付
class WxPayService
{
private $merchantPrivateKeyInstance;
private static $wxPayService = null;
private $instance = null;
private $wxPayConfig = null;
private function __construct()
{
}
private function __clone()
{
}
public static function Init()
{
if (!self::$wxPayService instanceof self) {
self::$wxPayService = new self();
self::$wxPayService->wxPayConfig = config('wxpay');
$mchId = self::$wxPayService->wxPayConfig['mchid']; // 商户号
$serial = self::$wxPayService->wxPayConfig['cert_serial']; //「商户API证书」的「证书序列号」
$certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $mchId;
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyFilePath = $certPath . '/apiclient_key.pem';
self::$wxPayService->merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
$platformCertificateFilePath = $certPath . '/cert.pem';
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 从「微信支付平台证书」中获取「证书序列号」
$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
// 构造一个 APIv3 客户端实例
self::$wxPayService->instance = Builder::factory([
'mchid' => $mchId,
'serial' => $serial,
'privateKey' => self::$wxPayService->merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
]);
}
return self::$wxPayService;
}
/**
* 小程序微信支付
* @param $payParam
* @return array
*/
public function xcxPay($payParam = [])
{
$merchantId = $this->wxPayConfig['mchid'];
$appid = $this->wxPayConfig['appid'];
$payJsonData = [
'mchid' => $merchantId,
'appid' => $appid,
'out_trade_no' => $payParam['out_trade_no'],
'description' => $payParam['description'],
'notify_url' => $payParam['notify_url'],
'amount' => [
'total' => $payParam['amount'], // 分
'currency' => 'CNY'
],
'payer' => [
'openid' => $payParam['openid']
]
];
try {
$resp = $this->instance
->chain('v3/pay/transactions/jsapi')
->post(['json' => $payJsonData]);
if ($resp->getStatusCode() == 200) {
$wo = json_decode($resp->getBody(), true); //prepay_id
$params = [
'appid' => $appid,
'timeStamp' => (string)Formatter::timestamp(),
'nonceStr' => Formatter::nonce(),
'package' => 'prepay_id=' . $wo["prepay_id"],
];
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$this->merchantPrivateKeyInstance
)];
return [1, $params];
} else {
return [0, '微信支付失败'];
}
} catch (\Exception $e) {
$code = $e->getCode();
$data = '';
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$data = json_decode($r->getBody(), true);
}
}
return [$code, $data];
}
/**
* 查询支付账单
* @param $transactionId
* @return array
*/
public function getPayBill($transactionId)
{
try {
$response = $this->instance
->chain('v3/pay/transactions/id/' . $transactionId)
->get([
'query' => ['mchid' => $this->wxPayConfig['mchid']],
]);
$code = $response->getStatusCode();
$response = json_decode($response->getBody(), true);
$status = $response['status'];
return [1, $response];
} catch (\Exception $e) {
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$code = $r->getStatusCode();
$response = json_decode($r->getBody(), true);
$subCode = $response['code'];
$subMsg = $response['message'];
return [$code, $response];
}
}
return [0, '查询失败'];
}
/**
* 支付/退款回调
* @param $header
* @param $inBody
* @return array|string[]|void
*/
public static function notify($header, $inBody)
{
$inWechatpaySignature = $header['wechatpay-signature'];
$inWechatpayTimestamp = $header['wechatpay-timestamp'];
$inWechatpaySerial = $header['wechatpay-serial'];
$inWechatpayNonce = $header['wechatpay-nonce'];
if (!$inWechatpaySignature or !$inWechatpayTimestamp or !$inWechatpaySerial or !$inWechatpayNonce) {
//请求头错误,不是来自微信调用
exit;
}
$wxPayConfig = config('wxpay');
$merchantId = $wxPayConfig['mchid'];
$apiv3Key = $wxPayConfig['api_v3_key'];
$certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $merchantId;
// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
$platformCertificateFilePath = $certPath . '/cert.pem';
// 根据通知的平台证书序列号,查询本地平台证书文件,这里是自己生成的证书
$platformPublicKeyInstance = Rsa::from(file_get_contents($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, $apiv3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
return (array)json_decode($inBodyResource, true);
} else {
return ['trade_state' => 'FAIL'];
}
}
/**
* 退款
* @param $refundParam
* @return array|mixed array [$code,$data] $code=1 成功
*/
public function refundOrder($refundParam = [])
{
try {
$response = $this->instance
->chain('v3/refund/domestic/refunds')
->post([
'json' => [
'transaction_id' => $refundParam['transaction_id'],
'out_refund_no' => $refundParam['out_refund_no'],
'reason' => $refundParam['reason'],
'notify_url' => $refundParam['notify_url'],
'amount' => [
'refund' => $refundParam['amount'],
'total' => $refundParam['amount'],
'currency' => 'CNY',
],
],
]);
$code = $response->getStatusCode();
$response = json_decode($response->getBody(), true);
$status = $response['status'] ?? '';
return [1, $response];
} catch (\Exception $e) {
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$code = $r->getStatusCode();
$response = json_decode($r->getBody(), true);
$subCode = $response['code'];
$subMsg = $response['message'];
$listErrorCode = [
'NOT_ENOUGH', // 余额不足',
'MCH_NOT_EXISTS', //MCHID不存在',
'NO_AUTH', //没有退款权限',
];
if (in_array($subCode, $listErrorCode)) {
//失败处理代码
}
return [$code, $response];
}
}
return [0, '退款失败'];
}
/**
* 查询退款账单
* @param $outRefundNo
* @return array
*/
public function getRefundBill($outRefundNo)
{
try {
$response = $this->instance
->chain('v3/refund/domestic/refunds/' . $outRefundNo)
->get();
$code = $response->getStatusCode();
$response = json_decode($response->getBody(), true);
$status = $response['status'];
return [1, $response];
} catch (\Exception $e) {
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$code = $r->getStatusCode();
$response = json_decode($r->getBody(), true);
$subCode = $response['code'];
$subMsg = $response['message'];
return [$code, $response];
}
}
return [0, '查询失败'];
}
/**
* 获取证书
* @return mixed
*/
public static function certificates()
{
$wxPayConfig = config('wxpay');
$api_v3_key = $wxPayConfig['api_v3_key'];
$result = self::wxCurlQuery('v3/certificates','GET');
$result = json_decode($result, true);
$ciphertext = $result['data'][0]['encrypt_certificate']['ciphertext'] ?? '';
$nonce = $result['data'][0]['encrypt_certificate']['nonce'] ?? '';
$associated_data = $result['data'][0]['encrypt_certificate']['associated_data'] ?? '';
$decryptedMsg = AesGcm::decrypt($ciphertext, $api_v3_key, $nonce, $associated_data);
$result['decryptedMsg'] = ($decryptedMsg); //解密后的内容,就是证书内容
dump($result);
}
/**
* get请求
* @param string $urlV3
* @param string $http_method
* @param string $body
* @return bool|string
*/
public static function wxCurlQuery($urlV3 = '', $http_method = 'POST', $body = '')
{
$url = 'https://api.mch.weixin.qq.com/' . $urlV3;
$config = config('wxpay');
$mchId = $config['mchid'];
$serial_no = $config['cert_serial'];
$certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $mchId;
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyFilePath = $certPath . '/apiclient_key.pem';
$mch_private_key = openssl_get_privatekey(file_get_contents($merchantPrivateKeyFilePath));//私钥
if(is_array($body)){
$body = json_encode($body); // 接口参数
}
// 生成token 验签
$timestamp = time();//时间戳
$nonce = Formatter::nonce(32);//随机串
$url_parts = parse_url($url);
$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);
//设置HTTP头
$token = sprintf('WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$mchId, $nonce, $timestamp, $serial_no, $sign);
$headers = [
'Accept: application/json',
'User-Agent: */*',
'Content-Type: application/json; charset=utf-8',
'Authorization: ' . $token,
"Wechatpay-Serial:{$serial_no}"
];
$info = curl_init();
curl_setopt($info, CURLOPT_RETURNTRANSFER, true);
curl_setopt($info, CURLOPT_HEADER, 0);
curl_setopt($info, CURLOPT_NOBODY, 0);
curl_setopt($info, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($info, CURLOPT_SSL_VERIFYHOST, false);
if ($http_method == 'POST') {
curl_setopt($info, CURLOPT_POST, 1);
curl_setopt($info, CURLOPT_POSTFIELDS, json_encode($body));
}
//设置header头
curl_setopt($info, CURLOPT_HTTPHEADER, $headers);
curl_setopt($info, CURLOPT_URL, $url);
$output = curl_exec($info);
curl_close($info);
return $output;
}
}