文章目录
前言
之前我使用的sdk是“wechatpay-guzzle-middleware”这个,后面发现官方文档里面“wechatpay-php”后面带了个推荐,以前没用过这个,正好有新项目试一下。
下面是全部的支付流程代码,已经跑通,拉起支付了。支付回调还没测试,等测试完继续更新。
一、wechatpay-php下载
我用的是composer,简单。
composer require wechatpay/wechatpay
二、各种证书环境配置
1.商户号API证书
这个就去要用的商户号里面,下载,没设置的话设置一下就行了。
2.平台证书
这个比较难搞,官方文档给了一个代码示例,我没用,嫌麻烦。这个我记得有效期挺久的,我就下载了一个。
代码如下(官方文档):
// apiV3key:商户号v3秘钥
// mchId:商户号
// mchPrivateKeyFilePath:API证书私钥
// mchSerialNo:API证书序列号
// outputFilePath:保存证书文件的地址(默认在项目的public文件下,我这里设置的是“file:///www/wwwroot/项目名/public/cert/”)
composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
三、接入jsapi支付
1.环境配置
我是把所有的证书相关信息写在类初始化里面,直接写在方法里面也可以。
代码如下(示例):
// 商户私钥,文件路径假定为 `cert/apiclient_key.pem`
// 我是把API证书私钥将整个文件放在cert文件夹下
$merchantPrivateKeyFilePath = public_path() . 'cert/apiclient_key.pem';
// 加载商户私钥
$this->merchantPrivateKeyInstance = PemUtil::loadPrivateKey($merchantPrivateKeyFilePath);
// 商户证书,文件路径假定为 `cert/apiclient_cert.pem`
// 我是把API证书公钥将整个文件放在cert文件夹下
$merchantCertificateFilePath = public_path() . 'cert/apiclient_cert.pem';
// 加载商户证书
$this->merchantCertificateInstance = PemUtil::loadCertificate($merchantCertificateFilePath);
// 解析商户证书序列号
$this->merchantCertificateSerial = PemUtil::parseCertificateSerialNo($this->merchantCertificateInstance);
// 平台证书,就是上一步下载的平台证书,我直接丢到cert文件夹了,它生成名字很长,大概长下面这样
$platformCertificateFilePath = public_path() . 'cert/wechatpay_48E897EFB18EE05C97B4EEF20F64193xxxxxxxxx.pem';
// 加载平台证书
$this->platformCertificateInstance = PemUtil::loadCertificate($platformCertificateFilePath);
// 解析平台证书序列号
$this->platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateInstance);
2.请求jsapi接口前的参数准备等
$PaymentAmount = bcmul($pay_price, 100, 0); // 支付金额 单位:分
// 商户号,我是取的配置里面的服务商账号,你可以用自己的,写成固定值也可。普通商户号也是直接填,一样的。
$merchantId = $this->config['service_member'];
// 工厂方法构造一个实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $this->merchantCertificateSerial, // 商户证书序列号用上了
'privateKey' => $this->merchantPrivateKeyInstance, //商户私钥
'certs' => [
$this->platformCertificateSerial => $this->platformCertificateInstance, // 平台序列号和平台证书
],
]);
$params = [
'sp_appid' => $this->config['wx_app_appid'], // 微信小程序appid
'sp_mchid' => $merchantId, // 服务商商户号
'sub_mchid' => $sub_mchid, // 特约商户号,如果是直连商户号,根据api文档改下参数就行了
'description' => '购买商品',
'out_trade_no' => $out_trade_no, // 自己生成一个唯一的支付订单号
'notify_url' => $this->config['wechat_notify'], //支付回调地址,我是写到配置里面了
'settle_info' => [
'profit_sharing' => true, // 这个是分账与否,开启会冻结资金,后续就可以做分账了。直连商户号无需填写 true/false
],
'amount' => [
'total' => (int)$PaymentAmount, // 单位:分
'currency' => 'CNY'
],
'payer' => [
'sp_openid' => $weapp_open_id, // 支付用户在该小程序下的openid
]
];
$authorization = $this->getAuthorization("POST", json_encode($params, JSON_UNESCAPED_UNICODE));
// 这个是请求头的签名,新文档里面很清晰。getAuthorization()这个是我写的方法,下一步给到。
3.获取Authorization
(我对sdk理解有问题,这一步省略,无需自己生成签名,SDK自己生成了)。
public function getAuthorization($http_method = "POST", $body = [], $url = "v3/pay/partner/transactions/jsapi"): string
{
$serial_no = $this->merchantCertificateSerial; // 商户证书序列号
$mchid = $this->config['service_member']; // 商户号
$timestamp = time();
$nonce = $this->nonce_str();
$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, $this->merchantPrivateKeyInstance, 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
return sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$mchid, $nonce, $timestamp, $serial_no, $sign);
}
4.请求jsapi接口
这一步有点坑,很多时候报错的$resp拿不到,直接就报错了。我都是把参数记录下来,然后直接在postman里面访问jsapi接口,看下到底返回什么。如果遇到了相同的问题,可以参考我的解决方案。
$resp = $instance
->chain('v3/pay/partner/transactions/jsapi') // 我这里是服务商(合作伙伴)的接口,直连商户去看下文档用哪个
->post([
'json' => $params,
//'headers' => [
// 'Content-type' => 'application/json;charset=utf-8',
// 'Accept' => 'application/json',
// 'User-Agent' => '*/*',
// 'Authorization' => $this->schema . " " . $authorization //这个schema是认证类型,默认值填这个'WECHATPAY2-SHA256-RSA2048'
//],
]);
$result_code = $resp->getStatusCode();
5.生成支付签名
这一步是生成支付签名,前端调用就可以拉起微信JASPI支付了。
if ($result_code == 200) { // 根据返回状态判断下,如果不是200,去查看下原因
$result_data = json_decode($resp->getBody(), true);
$merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyInstance); //商户私钥
$arouse_data = [
'appId' => $this->config['wx_app_appid'], //微信小程序appid
'timeStamp' => strval(Formatter::timestamp()),
'nonceStr' => Formatter::nonce(),
'package' => 'prepay_id=' . $result_data['prepay_id'], // 这个prepay_id是有有效期的,具体多少我忘了,我自己设置的30分钟,超了就重新请求
];
$arouse_data += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($arouse_data)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];
$arouse_data['out_trade_no'] = $out_trade_no;
return $this->setSuccess('success', $arouse_data); //返回签名给前端就可以了,setSuccess()这个是我封装的方法
}
6.支付回调
还没测试,后面测试了再来更新。
public function weChatNotify()
{
$notifyData = file_get_contents('php://input');
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($notifyData, true);
// 使用PHP7的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
// 加密文本消息解密
$inBodyResource = AesGcm::decrypt($ciphertext, config('wechatpay.apiv3'), $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
$inBodyResourceArray = (array)json_decode($inBodyResource, true);
if ($inBodyResourceArray['trade_state'] == "SUCCESS") {
Db::startTrans();
try {
//编写逻辑
$order_no = $inBodyResourceArray['out_trade_no'];
$attach = json_decode($inBodyResourceArray['attach'], true);
Db::commit();
json([
'return_code' => 'SUCCESS',
'return_msg' => 'OK',
])->send(); // 这个是返回接收成功信息
} catch (Exception $e) {
Db::rollback();
return true;
}
} else {
// 回调异常
Log::write($inBodyResourceArray, 'error');
file_put_contents("NotifyError.txt", var_export($inBodyResourceArray, 1));
}
return true;
}
四、完整的jsapi支付代码,服务商版本
protected $config;
protected $merchantPrivateKeyInstance;
protected $merchantCertificateInstance;
protected $merchantCertificateSerial;
protected $platformCertificateInstance;
protected $platformCertificateSerial;
protected $schema = 'WECHATPAY2-SHA256-RSA2048';
public function __construct()
{
$this->config = $this->getConfigGroup(); // 获取配置
// 商户私钥,api证书,整个文件放在cert文件夹下
$merchantPrivateKeyFilePath = public_path() . 'cert/apiclient_key.pem';
// 加载商户私钥
$this->merchantPrivateKeyInstance = PemUtil::loadPrivateKey($merchantPrivateKeyFilePath);
// 商户证书
$merchantCertificateFilePath = public_path() . 'cert/apiclient_cert.pem';
// 加载商户证书
$this->merchantCertificateInstance = PemUtil::loadCertificate($merchantCertificateFilePath);
// 解析商户证书序列号
$this->merchantCertificateSerial = PemUtil::parseCertificateSerialNo($this->merchantCertificateInstance);
// 平台证书
$platformCertificateFilePath = public_path() . 'cert/wechatpay_48E897EFB18EE05C97B4EEF20F64193Cxxxxxxxxx.pem';
// 加载平台证书
$this->platformCertificateInstance = PemUtil::loadCertificate($platformCertificateFilePath);
// 解析平台证书序列号
$this->platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateInstance);
}
public function wechatArousePay($order, $member)
{
$PaymentAmount = bcmul($order['pay_price'], 100, 0);
// 服务商商户号
$merchantId = $this->config['service_member'];
// 工厂方法构造一个实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $this->merchantCertificateSerial,
'privateKey' => $this->merchantPrivateKeyInstance,
'certs' => [
$this->platformCertificateSerial => $this->platformCertificateInstance,
],
]);
$params = [
'sp_appid' => $this->config['wx_app_appid'],
'sp_mchid' => $merchantId,
'sub_mchid' => $order['sub_mchid'],
'description' => '购买商品',
'out_trade_no' => $order['out_trade_no'],
'notify_url' => $this->config['wechat_notify'],
'settle_info' => [
'profit_sharing' => true,
],
'amount' => [
'total' => (int)$PaymentAmount,
'currency' => 'CNY'
],
'payer' => [
'sp_openid' => $member['weapp_open_id'],
]
];
// $authorization = $this->getAuthorization("POST", json_encode($params, JSON_UNESCAPED_UNICODE));
try {
$resp = $instance
->chain('v3/pay/partner/transactions/jsapi')
->post([
'json' => $params,
// 'headers' => [
// 'Content-type' => 'application/json;charset=utf-8',
// 'Accept' => 'application/json',
// 'User-Agent' => '*/*',
// 'Authorization' => $this->schema . " " . $authorization
// ],
]);
$result_code = $resp->getStatusCode();
if ($result_code == 200) {
$result_data = json_decode($resp->getBody(), true);
$merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyInstance);
$arouse_data = [
'appId' => $this->config['wx_app_appid'],
'timeStamp' => strval(Formatter::timestamp()),
'nonceStr' => Formatter::nonce(),
'package' => 'prepay_id=' . $result_data['prepay_id'],
];
$arouse_data += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($arouse_data)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];
$arouse_data['out_trade_no'] = $order['out_trade_no'];
return $this->setSuccess('success', $arouse_data);
}
} catch (\Exception $exception) {
// 进行错误处理
if ($exception instanceof RequestException && $exception->hasResponse()) {
$r = $exception->getResponse();
return $this->setError($r->getStatusCode(), $r->getReasonPhrase());
} else {
return $this->setError($exception->getMessage());
}
}
}
public function getAuthorization($http_method = "POST", $body = [], $url = "v3/pay/partner/transactions/jsapi"): string
{
$serial_no = $this->merchantCertificateSerial; // 商户证书序列号
$mchid = $this->config['service_member']; // 商户号
$timestamp = time();
$nonce = $this->nonce_str();
$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, $this->merchantPrivateKeyInstance, 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
return sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$mchid, $nonce, $timestamp, $serial_no, $sign);
}
总结
"wechatpay-guzzle-middleware"这个sdk的用法有时间再更新。
后续分账的接口使用方法,下次继续更新。
“wechatpay-php”这个还是挺好用的。微信文档现在更新得可以了。
按照上面的写法,基本不会踩什么坑,我遇到问题的地方也记录下来供给参考了。
如果遇到什么新问题,也可以发出来我看看。