文接上篇PHP如何更科学地接入第三方渠道,既然已经写到这了,索性创建了一个gitee仓库,地址:https://gitee.com/wuzhh/tp6-payment,有需要的可以去看看。
言归正传,微信支付v3版本刚推出不久,鉴于微信官方一贯语焉不详的尿性,论坛上自然仍旧一片哀嚎,鄙人一路踩坑下来,倒也还算顺利,把过程分享给大家参考~
一、微信支付平台相关配置
1. 配置API证书和API v3密钥
在微信商户平台中找到API安全,这一步按照官方提示操作即可,比较简单不再赘述
2. 分别加载guzzlehttp和wechatpay的composer包
composer require guzzlehttp/guzzle:~6.3
composer require wechatpay/wechatpay-guzzle-middleware:^0.2.2
3. 生成微信支付平台证书
注意,第1步中导出的证书有三个文件,以我的经验只有apiclient_key.pem是有用的,apiclient_cert.pem则没什么用(没发现它有什么用),拿到apiclient_key.pem的路径之后:
- 进入项目根目录的vendor/wechatpay/wechatpay-guzzle-middleware下
- 执行如下shell命令:
php tool/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath} -c ${wechatpayCertificateFilePath}
上面是官方提供的命令,此处:
apiV3key = 设置的v3秘钥
mchId = 商户号
mchPrivateKeyFilePath = apiclient_key.pem的路径
mchSerialNo = 商户API证书序列号
outputFilePath = 微信支付平台证书的存储路径
你可能会问,-c参数填啥?这里需要说一下这个参数是验证证书用的,填的是微信支付平台证书的路径,因为我们现在是第一次创建证书,所以-c参数不需要填写,需要特别注意一下。
4. 将微信支付的配置填入payment表
CREATE TABLE `payment` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '渠道ID',
`channel_name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付渠道名称',
`channel_code` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付渠道代码',
`channel_logo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付渠道LOGO',
`slogan` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '支付渠道标语',
`class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '处理模块',
`merchant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商户ID',
`appid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Appid',
`app_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'App Secret',
`gateway_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支付地址',
`notify_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '通知地址',
`cipher_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RSA' COMMENT '加密方式',
`private_key` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '加密私钥,通常rsa模式需要',
`public_key` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '加密公钥,通常rsa模式需要',
`api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '加密秘钥,通常md5模式需要',
`extra_params` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外参数',
`format` char(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'JSON' COMMENT '接口参数格式,默认json',
`return` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'success' COMMENT '回调成功返回标识,success',
`os` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '支持系统,android,ios',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态,1=开启,0=关闭',
`sort` int(10) NULL DEFAULT NULL COMMENT '排序值',
`pay_test` tinyint(1) NOT NULL DEFAULT 0 COMMENT '测试模式,开启后支付1分',
`created_at` int(10) NULL DEFAULT NULL COMMENT '创建时间',
`updated_at` int(10) NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_code`(`channel_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
二、微信支付后台处理模块
1. 为了确保所有支付类有共同的调用接口,实现支付接口类
<?php
namespace payment;
interface PaymentInterface {
public function doPay($params);
public function notify($content);
}
这样就能确保上文注入的支付实例都有共同的支付和回调方法
2. 微信支付具体逻辑:
<?php
namespace payment\wxpay;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Exception\RequestException;
use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
use WechatPay\GuzzleMiddleware\Util\PemUtil;
use app\model\Payment;
use payment\PaymentInterface;
/**
* 微信支付
*/
class WxPay implements PaymentInterface
{
private $appid; // appid
private $appSecret; // appSecret
private $apiV3Key; // V3秘钥
private $notifyUrl; // 通知地址
private $gatewayUrl; // 接口地址
private $merchantId; // 商户号
private $merchantSerialNumber; // 商户API证书序列号
private $merchantPrivateKey; // 商户私钥
private $payCertificate; // 支付平台证书
private $privateKey; // 商户私钥
private $publicKey; // 商户公钥
private $payTest; // 测试支付开关
private $extraConfigs; // 额外配置
public function __construct()
{
// code...
}
public function config(Payment $config)
{
$this->appid = $config->appid;
$this->appSecret = $config->app_secret;
$this->extraConfigs = $config->extra_params;
$this->apiV3Key = $this->extraConfigs->apiV3Key ?? '';
$this->notifyUrl = $config->notify_url;
// https://api.mch.weixin.qq.com/pay/unifiedorder
$this->gatewayUrl = $config->gateway_url; // 支付网关
// 商户相关配置
$this->merchantId = $config->merchant_id; // 商户号
$this->merchantSerialNumber = $this->extraConfigs->serialNumber ?? ''; // 商户API证书序列号
$this->merchantPrivateKey = PemUtil::loadPrivateKeyFromString($config->private_key); // 商户私钥
// $this->merchantPrivateKey = PemUtil::loadPrivateKey(dirname(__FILE__)."/apiclient_key.pem"); // 商户私钥
$this->privateKey = $config->private_key; // 商户私钥
// 微信支付平台配置
$this->payCertificate = PemUtil::loadCertificateFromString($config->public_key); // 微信支付平台证书
// $this->payCertificate = PemUtil::loadCertificate(dirname(__FILE__)."/wechatpay.pem"); // 微信支付平台证书
$this->publicKey = $config->public_key; // 微信支付平台证书
// 测试支付开关
$this->payTest = $config->pay_test;
}
/*
* 支付方法
*/
public function doPay($params)
{
if (! $this->apiV3Key) {
return ['status' => 0, 'message' => '支付API秘钥未配置'];
}
if (! $this->merchantSerialNumber) {
return ['status' => 0, 'message' => '商户API证书序列号未配置'];
}
// 构造一个WechatPayMiddleware
$wechatpayMiddleware = WechatPayMiddleware::builder()
->withMerchant($this->merchantId, $this->merchantSerialNumber, $this->merchantPrivateKey) // 传入商户相关配置
->withWechatPay([ $this->payCertificate ]) // 可传入多个微信支付平台证书,参数类型为array
->build();
// 将WechatPayMiddleware添加到Guzzle的HandlerStack中
$stack = HandlerStack::create();
$stack->push($wechatpayMiddleware, 'wechatpay');
// 创建Guzzle HTTP Client时,将HandlerStack传入
$client = new Client(['handler' => $stack]);
// 接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
try {
$trade_type = $params['trade_type'] ?? 'app';
$requestData = [
'json' => [ // JSON请求体
'appid' => $this->appid,
'mchid' => $this->merchantId,
'description' => $params['goods_name'],
'out_trade_no' => $params['order_no'],
'notify_url' => $this->notifyUrl,
'amount' => [
'total' => !$this->payTest ? $params['amount'] : 1,
'currency' => 'CNY'
],
'scene_info' => [
'payer_client_ip' => $params['client_ip'] ?? '127.0.0.1'
],
'attach' => $params['attach'] ?? ''
],
'headers' => ['Accept' => 'application/json']
];
if ($trade_type == 'jsapi') {
if (!isset($params['openid']) || empty($params['openid'])) {
return ['status' => 0, 'message' => 'Openid不能为空'];
}
$requestData['json']['payer'] = [
'openid' => $params['openid']
];
}
if ($trade_type == 'h5') {
$requestData['json']['scene_info']['h5_info']['type'] = "Wap";
}
$resp = $client->request('POST', $this->gatewayUrl . "pay/transactions/{$trade_type}", $requestData);
$content = $resp->getBody();
$data = json_decode($content, true);
$ret = [];
switch ($trade_type) {
// APP支付
case 'app':
$ret = ['orderInfo' => $this->getOrderInfo($data['prepay_id'])];
break;
// 公众号支付
case 'jsapi':
$ret = ['prepay_id' => $data['prepay_id']];
break;
// h5支付
case 'h5':
$ret = ['h5_url' => $data['h5_url']];
break;
// 扫码支付
case 'native':
$ret = ['code_url' => $data['code_url']];
break;
default:
# code...
break;
}
} catch (RequestException $e) {
// 进行错误处理
$data = [];
if ($e->hasResponse()) {
// echo $e->getResponse()->getStatusCode().' '.$e->getResponse()->getReasonPhrase()."\n";
// echo $e->getResponse()->getBody();
$content = $e->getResponse()->getBody();
$data = json_decode($content, true);
}
return ['status' => 0, 'message' => $data['message'] ?? '支付失败:'.$e->getMessage()];
}
return ['status' => 1, 'message' => 'SUCCESS', 'data' => $ret];
}
public function getOrderInfo($prepay_id)
{
$nonceStr = randomString(16);
// $package = "prepay_id={$prepay_id}";
$package = "Sign=WXPay";
$timestamp = time();
$paySign = $this->paySign($this->appid, $timestamp, $nonceStr, $package);
return [
'appid' => $this->appid,
'noncestr' => $nonceStr,
'package' => $package,
'partnerid' => $this->merchantId,
'prepayid' => $prepay_id,
'timestamp' => $timestamp,
'sign' => $paySign,
];
}
public function transactions(array $params)
{
$url = '';
if (isset($params['transaction_id']) && !empty($params['transaction_id'])) {
$url = 'pay/ transactions/id/'.$params['transaction_id'];
}
if (isset($params['out_trade_no']) && !empty($params['out_trade_no'])) {
$url = 'pay/transactions/out-trade-no/'.$params['out_trade_no'];
}
if (empty($url)) {
return false;
}
$url .= '?mchid='.$this->merchantId;
// 构造一个WechatPayMiddleware
$wechatpayMiddleware = WechatPayMiddleware::builder()
->withMerchant($this->merchantId, $this->merchantSerialNumber, $this->merchantPrivateKey) // 传入商户相关配置
->withWechatPay([ $this->payCertificate ]) // 可传入多个微信支付平台证书,参数类型为array
->build();
// 将WechatPayMiddleware添加到Guzzle的HandlerStack中
$stack = HandlerStack::create();
$stack->push($wechatpayMiddleware, 'wechatpay');
// 创建Guzzle HTTP Client时,将HandlerStack传入
$client = new Client(['handler' => $stack]);
// 接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
try {
$resp = $client->request('GET', $this->requestUrl . $url,[
'headers' => ['Accept' => 'application/json']
]);
// echo $resp->getStatusCode().' '.$resp->getReasonPhrase()."\n";
// echo $resp->getBody()."\n";exit;
$content = $resp->getBody();
$data = json_decode($content, true);
if ($data['trade_state'] === 'SUCCESS') {
return ['status' => 1, 'message' => "SUCCESS", 'data' => $data];
}else{
return ['status' => 0, 'message' => $data['trade_state_desc'] ?? '', 'trade_state' => $data['trade_state']];
}
} catch (RequestException $e) {
// 进行错误处理
$res_data = [];
if ($e->hasResponse()) {
$content = $e->getResponse()->getBody();
$res_data = json_decode($content, true);
}
return ['status' => 0, 'message' => $res_data['message'] ?? '支付失败'];
}
}
public function decryptCiphertext($ciphertext)
{
$ciphertext = $this->urlsafe_b64decode($ciphertext);
$privateKey = $this->privateKey;
$iv = substr($privateKey, 0, 16);
$decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $privateKey, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
return $decrypted;
}
// 支付签名
public function paySign($appId, $timeStamp, $nonceStr, $package)
{
$str = "{$appId}\n{$timeStamp}\n{$nonceStr}\n{$package}\n";
$privateKey = $this->privateKey;
openssl_sign($str, $encrypt_data, openssl_get_privatekey($privateKey), 'sha256WithRSAEncryption');
$encrypt_data = base64_encode($encrypt_data);
return $encrypt_data;
}
// 解密数据
public function decryptSign($ciphertext, $associatedData, $nonceStr)
{
$ciphertext = base64_decode($ciphertext);
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
//$APIv3_KEY就是在商户平台后端设置是APIv3秘钥
$orderData = \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->apiV3Key);
$orderData = json_decode($orderData, true);
return $orderData;
}else{
exit('缺乏PHP扩展:sodium,请安装该扩展或切换到PHP7.3+版本');
}
return $result;
}
/**将字符串安全编码
*
* @param $string
*
* @return string
*/
public function urlsafe_b64encode($string)
{
$data = base64_encode($string);
$data = str_replace(array('+', '/', '='), array('-', '_', ''), $data);
return $data;
}
/**将字符串安全解码
*
* @param $string
*
* @return string
*/
public function urlsafe_b64decode($string)
{
$data = str_replace(array('-', '_'), array('+', '/'), $string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}
public function notify($content)
{
// 通知部分过段时间再更,端午节就更到这了
}
}
有部分方法文中没上,我个人的项目中用上了,就暂且保留吧
3. 微信支付回调逻辑:
通知部分过段时间再更,明天就算端午节就更到这了,有需要的小伙伴可以留言,我争取尽快补上~