为提升转账效率与安全性,并降低商家运营成本,
“商家转账到零钱”已于2025年1月15日升级为新版“商家转账”接口。本次升级要点如下:
主要升级内容1. 安全增强免费提供安全医生服务支持系统漏洞诊断实施严格安全策略,24小时资金保护2. 成本优化用户可主动选择入账(目前仅支持到零钱)避免因对账不清导致的客户投诉异常账户可在微信内闭环处理,降低客服介入率接口接入与使用说明原“商家转账到零钱”和“商家转账”为两套不同API,升级需重新接入API。商家转账仅支持微信商户向用户微信零钱转账,服务免费且安全。支持集成到商家自有业务系统,需具备研发能力。用户收款流程需拉起微信官方页面,用户确认收款方式后方可转账,资金实时到账,转账成功后资金不支持退回。原有转账到零钱接口已停用,需使用最新商家转账接口(V3版本)。微信小程序提现流程对比原提现流程用户在应用端提交提现申请平台后台审核通过后,通过“转账到零钱”接口给用户打款升级后提现流程用户在应用端提交提现申请平台后台审核通过用户在应用端确认后,系统请求商家转账接口,返回 package_info 和 mch_id前端拿到接口返回参数后,发起 JSAPI 调起用户确认收款,用户确认后转账完成参考文档发起转账: https://pay.weixin.qq.com/doc/v3/merchant/4012716434JSAPI调起用户确认收款:https://pay.weixin.qq.com/doc/v3/merchant/4012716430商户单号查询转账单:https://pay.weixin.qq.com/doc/v3/merchant/4012716437转账回调:https://pay.weixin.qq.com/doc/v3/merchant/4012712115转账场景:https://pay.weixin.qq.com/doc/v3/merchant/4012711988#%EF%BC%882%EF%BC%89%E6%8C%89%E8%BD%AC%E8%B4%A6%E5%9C%BA%E6%99%AF%E5%8F%91%E8%B5%B7%E8%BD%AC%E8%B4%A6配置参数获取地址1:https://pay.weixin.qq.com/index.php/core/account/info地址2:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/mch_id:商户号,路径:账户中心 → 商户信息 → 微信支付商户号merchant_serial:商户API证书的证书序列号,路径:API安全 → 商户API证书 → 管理证书platform_serial:微信支付平台证书的序列号,路径:API安全 → 平台证书 → 管理证书pay_cert:微信支付平台证书,需要通过命令行下载,参考下文证书获取方式pub_key:微信支付公钥,路径:API安全 → 支付公钥 → 查看
微信支付平台证书和微信支付公钥二选一即可,
推荐使用微信支付公钥。
获取微信支付平台证书(以 PHP 为例)
⚠️ 注意:请勿直接在开发项目中执行下载证书命令,否则操作会报错。
操作步骤:新建目录
mkdir wechat-v3-paycert-download
切换目录
cd wechat-v3-paycert-download
安装依赖
composer require wechatpay/wechatpay
执行命令获取平台证书(文件名类似于 wechatpay_123456777B4A9CC78902B44B65E04B9751CE12.pem)
composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
⚠️ 踩坑记录:
我在实际项目中遇到过一个问题,这个配置在开发环境正常,但生产环境会报错。
后来发现是因为生产环境的版本不一致导致的。建议大家在部署前一定要检查版本兼容性。
APIv3 key:APIv3密钥,设置路径:账号中心 → API安全 → APIv3密钥mchId:商户号mchPrivateKeyFilePath:商户API证书私钥路径(如 apiclient_key.pem)mchSerialNo:商户API证书序列号outputFilePath:平台证书保存路径,自定义报备信息
用户收款感知、转账场景报备信息需要特定格式,详见下方文档:
用户收款感知:https://pay.weixin.qq.com/doc/v3/merchant/4012711988#%EF%BC%882%EF%BC%89%E6%8C%89%E8%BD%AC%E8%B4%A6%E5%9C%BA%E6%99%AF%E5%8F%91%E8%B5%B7%E8%BD%AC%E8%B4%A6报备信息(转账场景):https://pay.weixin.qq.com/doc/v3/merchant/4013774588微信商户转账完整代码实例1. 安装依赖
composer require wechatpay/wechatpay
2. 使用微信支付证书
config = $config;
$this->handlerCert();
// 1、商户号
$merchantId = $this->config['mch_id'];
// 2、「商户API私钥」
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyFilePath = $this->key_path;
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 「商户API证书」的「证书序列号」
$merchantCertificateSerial = $this->config['merchant_serial'];
// 3和4 二选一
// 3、「微信支付平台证书」
// 从本地文件中加载「微信支付平台证书」,可由内置CLI工具下载到,用来验证微信支付应答的签名
$platformCertificateFilePath = $this->pay_cert;
$onePlatformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 「微信支付平台证书」的「平台证书序列号」
// 可以从「微信支付平台证书」文件解析,也可以在 商户平台 -> 账户中心 -> API安全 查询到
$platformCertificateSerial = $this->config['platform_serial'];
// 4、「微信支付公钥」
// 从本地文件中加载「微信支付公钥」,用来验证微信支付应答的签名
// $platformPublicKeyFilePath = $this->pub_key';
// $twoPlatformPublicKeyInstance = Rsa::from($platformPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC);
// 「微信支付公钥」的「微信支付公钥ID」
// 需要在 商户平台 -> 账户中心 -> API安全 查询
// $platformPublicKeyId = $this->config['public_key_id'];
$this->app = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $onePlatformPublicKeyInstance,
],
]);
}
public function handlerCert()
{
// 如果证书地址是远程地址,则下载证书到本地
$remote_cert_path = $this->config['cert_path'] ?? '';
$remote_key_path = $this->config['key_path'] ?? '';
$remote_pub_key = $this->config['pub_key'] ?? '';
$remote_pay_cert = $this->config['pay_cert'] ?? '';
$cert_path = RUNTIME_PATH.'certs/wechat/apiclient_cert.pem';
$key_path = RUNTIME_PATH.'certs/wechat/apiclient_key.pem';
$pub_key = RUNTIME_PATH.'certs/wechat/pub_key.pem';
$pay_cert = RUNTIME_PATH.'certs/wechat/wechatpay_2CE405ECCCFCA8E77E0DDEC.pem';
if (!is_file($cert_path) && $remote_cert_path && 0 === strpos($remote_cert_path, 'http')) {
download_file($remote_cert_path, $cert_path);
}
if (!is_file($key_path) && $remote_key_path && 0 === strpos($remote_key_path, 'http')) {
download_file($remote_key_path, $key_path);
}
if (!is_file($pub_key) && $remote_pub_key && 0 === strpos($remote_pub_key, 'http')) {
download_file($remote_pub_key, $pub_key);
}
if (!is_file($pay_cert) && $remote_pay_cert && 0 === strpos($remote_pay_cert, 'http')) {
download_file($remote_pay_cert, $pay_cert);
}
$cert_path = 'file://'.$cert_path;
$key_path = 'file://'.$key_path;
$pub_key = 'file://'.$pub_key;
$pay_cert = 'file://'.$pay_cert;
$this->cert_path = $cert_path;
$this->key_path = $key_path;
$this->pub_key = $pub_key;
$this->pay_cert = $pay_cert;
}
public function trans($params)
{
$notify_url = $params['trans_notify_url'] ?? '';
if (!$notify_url) {
$notify_url = $this->config['trans_notify_url'];
}
$price = $params['price'];
$data = [
'appid' => $this->config['app_id'],
'out_bill_no' => $params['withdraw_no'],
'transfer_scene_id' => '1005',
'openid' => $params['openid'],
'transfer_amount' => (int) ($price * 100),
'transfer_remark' => $params['desc'],
'user_recv_perception' => '劳务报酬',
'transfer_scene_report_infos' => [
[
'info_type' => '岗位类型',
'info_content' => '佣金报酬',
],
[
'info_type' => '报酬说明',
'info_content' => '向劳务提供方,如销售、团长、主播支付佣金、报酬等',
],
],
];
if ($notify_url) {
$data['notify_url'] = cdnurlv2($notify_url, true);
}
if ($price < 0.3) {
// 转账金额小于0.3元 必然不要传用户名
unset($data['user_name']);
}
if ($price > 2000) {
// 大于2000必传用户名
$data['user_name'] = $params['username'];
}
$result = $this->curlPost('/v3/fund-app/mch-transfer/transfer-bills', $data);
record_log('WechatV3Pay:trans', $result);
return $result;
}
/**
* 转账账单查询
*/
public function transQuery($params)
{
$out_bill_no = $params['withdraw_no'];
return $this->curlGet("/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{$out_bill_no}");
}
/**
* 转账回调.
*
* @param mixed $params
* @param mixed $header
*/
public function transNotify($params, $header)
{
record_log('wxTransNotify:header', $header);
if ('encrypt-resource' != $params['resource_type']) {
throw new ValidateException('通知的资源数据类型异常');
}
if ('MCHTRANSFER.BILL.FINISHED' != $params['event_type']) {
throw new ValidateException('通知的类型不符合要求');
}
$inWechatpaySignature = $header['wechatpay-signature']; // 请根据实际情况获取
$inWechatpayTimestamp = $header['wechatpay-timestamp']; // 请根据实际情况获取
$inWechatpaySerial = $header['wechatpay-serial']; // 请根据实际情况获取
$inWechatpayNonce = $header['wechatpay-nonce']; // 请根据实际情况获取
$inBody = file_get_contents('php://input'); // 请根据实际情况获取,例如: file_get_contents('php://input');
$apiv3Key = $this->config['key']; // 在商户平台上设置的APIv3密钥
$platformCertificateFilePath = $this->pay_cert;
$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 (!$verifiedStatus) {
// throw new ValidateException('签名验证失败');
// }
$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);
return $inBodyResourceArray;
}
private function curlGet($url)
{
try {
$resp = $this->app->chain($url)->get([
'headers' => [
'Wechatpay-Serial' => $this->config['platform_serial'],
],
]);
return json_decode((string) $resp->getBody(), true);
} catch (\Throwable $th) {
if ($th instanceof \GuzzleHttp\Exception\RequestException && $th->hasResponse()) {
$r = $th->getResponse();
$result = json_decode((string) $r->getBody(), true);
throw new ValidateException($result['code'].':'.$result['message']);
}
throw new ValidateException($th->getMessage());
}
}
private function curlPost($url, $data)
{
try {
$resp = $this->app->chain($url)->post([
'json' => $data,
'headers' => [
'Wechatpay-Serial' => $this->config['platform_serial'],
],
]);
return json_decode((string) $resp->getBody(), true);
} catch (\Throwable $th) {
if ($th instanceof \GuzzleHttp\Exception\RequestException && $th->hasResponse()) {
$r = $th->getResponse();
$result = json_decode((string) $r->getBody(), true);
throw new ValidateException($result['code'].':'.$result['message']);
}
throw new ValidateException($th->getMessage());
}
}
}
3. 使用微信支付公钥(参考代码片段)
// 3、「微信支付平台证书」
// $platformCertificateFilePath = $this->cert_path;
// $onePlatformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// $platformCertificateSerial = $this->config['platform_serial'];
// 4、「微信支付公钥」
$platformPublicKeyFilePath = $this->pub_key;
$twoPlatformPublicKeyInstance = Rsa::from($platformPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC);
$platformPublicKeyId = $this->config['public_key_id'];
$this->app = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
// $platformCertificateSerial => $onePlatformPublicKeyInstance,
$platformPublicKeyId => $twoPlatformPublicKeyInstance,
],
]);
注意: 证书文件路径需增加 file:// 前缀