原有的企业转账到零钱已不支持开通,升级后采用微信商家转账到零钱实现用户提现分销发放奖励等场景。
为了快速实现功能,本文讲解使用第三方库方式对接转账到零钱,wechatpay/wechatpay , 基于 Guzzle HTTP Client 的微信支付 PHP 开发库。
功能介绍
- 微信支付 APIv2 和 APIv3 的 Guzzle HTTP 客户端,支持 同步 或 异步 发送请求,并自动进行请求签名和应答验签
- 链式实现的 URI Template
- 敏感信息加解密
- 回调通知的验签和解密
详细实现步骤,参考wechatpay
链接地址:https://github.com/wechatpay-apiv3/wechatpay-php
开发前需要到微信商户平台申请api证书,获取证书序列号,生成平台证书,完善好配置参数。
Certificate Downloader 是 PHP版 微信支付 APIv3 平台证书的命令行下载工具。该工具可从 https://api.mch.weixin.qq.com/v3/certificates
接口获取商户可用证书,并使用 APIv3 密钥 和 AES_256_GCM 算法进行解密,并把解密后证书下载到指定位置。链接地址:https://github.com/wechatpay-apiv3/wechatpay-php/blob/main/bin/README.md
商户转账到零钱和回调通知代码如下:
private function pay($outBatchNo = '', $outDetailNo = '', $total_amount = 0, $openid = '',$remark='', $notifyUrl = '', $userName = ''){
// 设置参数,获取后台配置信息根据实际情况修改
$payConfig = \addons\epay\library\Service::getConfig('miniapp');
//var_dump($payConfig);exit;
//参考 https://packagist.org/packages/wechatpay/wechatpay
//vendor/bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
//示例 ./bin/CertificateDownloader.php -k ek00riolullp0fh0pxs22v0z4gzq -m 1669097 -f /www/wwwroot/web/addons/epay/certs/apiclient_key.pem -s 2424BCF93CDD4CD2E4 -o /www/wwwroot/web/addons/epay/certs/
// 商户号
$merchantId = $payConfig['mch_id'];
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名,注意file://不可省略
$merchantPrivateKeyFilePath = 'file://'.$payConfig['cert_key'];//'file:///path/to/merchant/apiclient_key.pem';
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 「商户API证书」的「证书序列号」,可改为参数形式
$merchantCertificateSerial = '24245FFA92E4';//不同证书注意替换该参数
// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名,
$platformCertificateFilePath = 'file://'.ROOT_PATH.'addons/epay/certs/wechDAFC.pem';
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 从「微信支付平台证书」中获取「证书序列号」
$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
// 构造一个 APIv3 客户端实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
]);
// 发送请求
// $resp = $instance->chain('v3/certificates')->get(
// ['debug' => true] // 调试模式,https://docs.guzzlephp.org/en/stable/request-options.html#debug
// );
// echo $resp->getBody(), PHP_EOL;
$transferDetail = [[ //注意二位数组
'out_detail_no' => $outDetailNo, // 明细单号
'transfer_amount' => intval($total_amount), // 转账总金额
'transfer_remark' => $remark, // 单条转账备注
'openid' => $openid, // 收款方openid
]];
//m敏感信息加密
$encryptor = static function(string $msg) use ($platformPublicKeyInstance): string {
return Rsa::encrypt($msg, $platformPublicKeyInstance);
};
// 转账金额 >= 2,000元,收款用户姓名必填,明细转账金额<0.3元时,不允许填写收款用户姓
if($total_amount >= 200000){
$transferDetail[0]['user_name'] = $encryptor($userName);
}
try {
$resp = $instance
->chain('v3/transfer/batches')
->post(['json' => [
'appid' => $payConfig['app_id'],
'out_batch_no' => $outBatchNo,
'batch_name' => $remark,
'batch_remark' => $remark,
'total_amount' => intval($total_amount),
'total_num' => 1,
'transfer_detail_list' => $transferDetail,
'notify_url' => $notifyUrl
]]);
$res = ['code' => $resp->getStatusCode()];
$body = (array)json_decode($resp->getBody(),true);
[
'batch_id' => $batch_id,
'batch_status' => $batch_status,
'out_batch_no' => $out_batch_no
] = $body;
$res['data'] = [
'batch_id' => $batch_id,
'batch_status' => $batch_status,
'out_batch_no' => $out_batch_no
];
return $res;
// echo $resp->getStatusCode(), PHP_EOL;
// echo $resp->getBody(), PHP_EOL;
//200 {"batch_id":"131006950931882","batch_status":"ACCEPTED","create_time":"2024-05-10T23:38:12+08:00","out_batch_no":"ECxq3526"}
} catch (\Exception $e) {
// 进行错误处理
//echo $e->getMessage(), PHP_EOL;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$body = (array)json_decode($r->getBody(),true);
$res = ['code' => $r->getStatusCode(), 'data' => $body];
return $res;
// echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
// echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
//echo $e->getTraceAsString(), PHP_EOL;
}
}
public function callback(){
//获取后台配置参数,根据实际情况修改
$payConfig = \addons\epay\library\Service::getConfig('miniapp');
$headers = $this->request->header();
$inWechatpaySignature = $headers['wechatpay-signature'];// 请根据实际情况获取
$inWechatpayTimestamp = $headers['wechatpay-timestamp'];// 请根据实际情况获取
$inWechatpaySerial = $headers['wechatpay-serial'];// 请根据实际情况获取
$inWechatpayNonce = $headers['wechatpay-nonce'];// 请根据实际情况获取
$inBody = $this->request->post();// 请根据实际情况获取,例如: file_get_contents('php://input');
$apiv3Key = $payConfig['key_v3'];// 在商户平台上设置的APIv3密钥
// 根据通知的平台证书序列号,查询本地平台证书文件,
// 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
$platformPublicKeyInstance = Rsa::from('file://'.ROOT_PATH.'addons/epay/certs/wecAD7DAFC.pem', 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数组
$inBodyResourceArray = (array)json_decode($inBodyResource, true);
print_r($inBodyResourceArray);// 打印解密后的结果
//更新支付结果,根据实际业务修改
//$this->model->where(['out_batch_no' => $outBatchNo])->update(['status' => 1, 'fedback' => $batch_status]);
}
//(1)如果未处理,进行处理;(2)如果已处理,则直接返回结果成功;
//若通知回调的签名验证失败,商户系统应返回失败(即应答 4xx 或 5xx 的状态码
}