简介:PHP微信支付企业付款到零钱功能接口源码,是一份面向PHP开发者的微信支付API对接工具包,适用于退款结算、佣金发放、工资代付等企业转账场景。整个压缩包共4个文件,包含2个PHP脚本和2个TXT说明文档,大小仅4KB,便于快速下载与部署。PHP脚本覆盖企业付款接口的封装、参数配置与调用示例,TXT文档则整理微信企业付款说明及证书使用说明,帮助开发者理清接入流程和安全要点。源码内置签名验签、数据加密等安全机制,并设计了异常处理逻辑,可直接在实际项目中参考复用,显著缩短企业付款功能的开发与联调周期。文件结构清晰,适合作为基础模板进行二次拓展。已有96人学习浏览,适合需要快速接入企业付款功能、又希望降低对接门槛的PHP工程师参考。
1. 企业付款到零钱:比普通支付更值得仔细拆解的微信支付接口
企业付款到零钱是微信支付里一个“资金走出去”的接口,和支付收款是反向逻辑,日常用于退款、返利、报销和批量结算。很多团队容易把它当成普通 JSAPI 支付来对接,结果卡在证书、签名、金额单位这些细节上。这套 PHP 源码正好提供了一个可运行的最小闭环:index.php是入口,config.php承载配置,两份说明文档讲清楚了证书和参数。这个源码包不打算把微信的复杂协议包装成黑盒,而是把最关键的请求逻辑摊开给开发者看。适合正在对接这个接口的 PHP 开发,也适合想理解微信支付 V2 企业转账原理的工程师。下文从源码结构出发,逐步拆解证书加载、签名生成、请求发送、异常处理,并给出批量化扩展和自检方法。
2. 源码包内文件结构与 config.php 中的参数设计
2.1 最小的可运行工程长什么样
压缩包解压后只有 4 个核心文件:index.php、config.php、微信企业付款说明.txt、cert 证书使用说明.txt。没有复杂目录,说明作者刻意把“发起付款”这个动作收敛到一条主流程里。我一般会先把说明文档通读一遍,因为微信支付 API 在不同年份对证书格式、接口域名和签名算法的要求有差异,文档能帮你判断这套源码是基于 V2 还是 V3 写的。从config.php的常见写法来看,这套源码默认走 V2 接口,也就是https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers。
index.php通常承担两件事:初始化配置、打包请求参数后调用一个transfer函数。实际生产中,我会把index.php里的调用逻辑改造成一个独立的EnterprisePayService类,因为它现在更像一个演示脚本。但作为学习用途,单文件能让你快速定位签名和发送请求的代码,不会被 MVC 结构带偏。
证书目录cert中通常存放apiclient_cert.pem和apiclient_key.pem,这两个文件在商户平台下载证书时打包为p12格式,需要转换成 PEM 才能在 PHP cURL 中使用。转换命令在cert 证书使用说明.txt里一般会有,没有的话可以这样操作:
# 从 p12 导出证书和私钥,设置一个临时密码用于保护 openssl pkcs12 -in apiclient.p12 -clcerts -nokeys -out apiclient_cert.pem openssl pkcs12 -in apiclient.p12 -nocerts -nodes -out apiclient_key.pem # 私钥文件权限收紧,防止被同服务器其他用户读取 chmod 600 apiclient_cert.pem apiclient_key.pem这里要说明为什么要转成 PEM。PHP 的 cURL 扩展在加载 SSL 证书时,CURLOPT_SSLCERT可以识别 PEM、DER 等格式,但 p12 需要额外的CURLOPT_SSLCERTPASSWD来处理密码,且部分 PHP 环境对 p12 支持不稳定。转成 PEM 后代码里就不需要明文保存证书密码,私钥本身已经足够敏感。
2.2 config.php 里的关键项:商户号、API 密钥与证书路径
打开config.php一般能看到下面这些参数:
<?php $wx_config = [ 'app_id' => 'wx8888888888888888', // 公众号或小程序 appid 'mch_id' => '1900000109', // 商户号 'api_key' => 'abcDEF123456789...', // APIv2 密钥,32 位 'ssl_cert_path' => __DIR__ . '/cert/apiclient_cert.pem', 'ssl_key_path' => __DIR__ . '/cert/apiclient_key.pem', 'api_url' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', ];这里要特别说明ssl_cert_path和ssl_key_path。企业付款接口要求客户端证书做双向 TLS 验证,证书文件是从微信商户平台下载的apiclient_cert.pem和apiclient_key.pem。注意__DIR__的使用,它避免当前工作目录变化导致证书加载失败。证书目录cert一定不能放在 web 可访问的静态目录下,否则等于把商户私钥公开。
api_key是 V2 接口的签名密钥,在商户平台设置时需使用 32 位随机字符,不要用自定义密码。很多调试卡在“签名错误”,一半原因是这个 key 和商户平台不一致,另一半是签名算法写错。注意它和 V3 的APIv3密钥是两个完全不同的值,V3 还需要配置商户证书序列号。如果商户平台已经迁移到 V3 接口,这套源码里的api_key就只是用于兼容历史逻辑,新业务建议直接对接 V3。
2.3 配置参数与接口定义的关系:数组结构决定请求体
在 PHP 中,接口的输入输出通常用关联数组表达。这套源码的做法是创建一个数组,键名对应微信 API 定义的字段名。这种接口定义方式很直接:
$params = [ 'mch_appid' => $wx_config['app_id'], 'mchid' => $wx_config['mch_id'], 'nonce_str' => md5(uniqid(mt_rand(), true)), 'partner_trade_no' => $order_no, // 商户单号,需唯一 'openid' => $openid, 'check_name' => 'NO_CHECK', // 是否校验用户姓名 'amount' => 100, // 单位:分 'desc' => '企业付款到零钱测试', 'spbill_create_ip' => $_SERVER['SERVER_ADDR'], ];这里的键名和值类型必须严格按微信文档,amount是整数分,如果按元传会遇到AMOUNT_LIMIT错误。nonce_str用于签名防重放,我一般直接用uniqid加md5保证每次请求不同。partner_trade_no是整个接口幂等性的核心,同一次付款必须使用同一个单号,重试时也不能更换,否则可能重复打款。
check_name字段的行为差异值得列出来:
| 参数值 | 含义 | 是否需要用户姓名 |
|---|---|---|
| NO_CHECK | 不校验姓名,直接转账 | 否 |
| OPTION_CHECK | 校验姓名,但允许姓名为空 | 是,可空 |
| FORCE_CHECK | 强制校验姓名,必须与微信实名一致 | 是,必填 |
实际业务中,如果对用户实名无要求,建议使用NO_CHECK,因为FORCE_CHECK一旦遇到用户微信实名信息与提交的不一致,会返回NAME_MISMATCH,需要人工处理。但涉及财务合规的场景,FORCE_CHECK能避免打错人,这一点需要在开发前和业务方确认。
这些参数最终会通过 http_build_query 或数组转 XML 的方式形成请求体。数组的键名顺序无关紧要,因为签名前会重新排序。
3. 签名生成与 cURL 请求:核心接口封装的完整链路
3.1 请求体组装与随机字符串
在发起付款前,需要把参数拼装成待签名串。微信 V2 的规则是:对所有参数按 ASCII 码升序排序,然后以key=value形式用&连接,最后在末尾拼接&key=商户密钥。这里有一个容易踩的坑:只对参与签名的值做排序,不能包含sign本身,且值为空的参数要剔除。用 PHP 可以这样实现:
/** * 生成微信 V2 签名 * @param array $params 待签名参数 * @param string $api_key 商户 APIv2 密钥 * @return string 32 位大写签名 */ function build_sign($params, $api_key) { ksort($params); $str = ''; foreach ($params as $k => $v) { if ($v !== '' && !is_null($v)) { $str .= $k . '=' . $v . '&'; } } $str .= 'key=' . $api_key; return strtoupper(md5($str)); }函数先对数组按键名排序,过滤空值,再拼接。这里我使用的是 md5 签名,微信也支持 HMAC-SHA256,需要在请求的 XML 里声明sign_type。建议新项目直接换 SHA256,安全性更高,但很多老接口封装默认是 md5,兼容性好。要注意的是,nonce_str、partner_trade_no这些字段的内容不能含有特殊字符,否则签名串解析后会出现 URL 编码不一致问题。例如partner_trade_no如果用了-或_之外的符号,可能在传输中被转义。
关于两种签名算法的选择,整理成对照表:
| 对比项 | MD5 | HMAC-SHA256 |
|---|---|---|
| 签名值长度 | 32 位十六进制 | 64 位十六进制 |
| 性能 | 快 | 略慢,可忽略 |
| 安全性 | 已被证明可碰撞 | 推荐使用 |
| 兼容性 | 微信老接口默认 | 需显式传sign_type=HMAC-SHA256 |
3.2 XML 格式与转义陷阱
企业付款到零钱接口的请求和响应都是 XML,而不是 JSON。这个接口定义对 PHP 开发者来说通常用simplexml_load_string解析,但构建请求 XML 时容易忽略转义:
<root> <mch_appid>wx8888888888888888</mch_appid> <desc>红包测试&佣金</desc> </root>desc字段里的&必须转义成&,否则微信网关解析失败。我一般用htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8')来转义。在源码中,如果作者用拼接字符串方式构建 XML,这个细节值得自查。更稳妥的做法是用数组递归转 XML,下面是一个通用的转换函数:
function arrayToXml($array, $root = 'xml') { $xml = "<{$root}>"; foreach ($array as $key => $value) { $key = is_numeric($key) ? 'item' . $key : $key; $xml .= is_array($value) ? arrayToXml($value, $key) : "<{$key}>" . htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8') . "</{$key}>"; } $xml .= "</{$root}>"; return $xml; }这个函数会递归处理多维数组,并对所有叶子值做 XML 转义,避免手工拼接造成的错误。注意键名如果是数字,需要加前缀,因为 XML 标签不能以数字开头。在企业付款场景中,请求参数都是固定键名,所以很少触发这个问题。
3.3 cURL 的证书加载与超时设置
接下来是核心请求函数。需要使用 cURL 并加载双向证书:
function send_transfer_request($xml, $cert_path, $key_path) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', CURLOPT_POST => true, CURLOPT_POSTFIELDS => $xml, CURLOPT_SSLCERT => $cert_path, CURLOPT_SSLKEY => $key_path, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, ]); $response = curl_exec($ch); if (curl_errno($ch)) { $error = curl_error($ch); curl_close($ch); return ['errcode' => 'CURL_ERROR', 'errmsg' => $error]; } curl_close($ch); return simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA); }CURLOPT_SSLCERT和CURLOPT_SSLKEY分别指定证书文件和私钥文件。有的源码会写CURLOPT_SSLCERTPASSWD,但微信的 pem 文件通常不需要密码,商户平台下载的 p12 才需要。注意CURLOPT_SSL_VERIFYHOST设置为 2 表示严格校验域名,不要关闭。超时时间 10 秒是底线,付款接口如果超过 30 秒无响应,应该走查询接口确认状态,而不是重复发起。
这里的响应解析使用了LIBXML_NOCDATA,这个常量会把 CDATA 节点直接转为字符串,避免使用->__toString()时出现多重数据类型问题。在微信支付 V2 的 XML 响应中,很多字段都被包在 CDATA 里,不用这个常量会导致取值时出现SimpleXMLElement对象而不是纯字符串。
3.4 请求响应的“结果码”处理
微信支付 V2 接口所有响应都包含return_code和result_code两组状态。return_code是通信层,result_code是业务层。一个常见的逻辑错误是只判断return_code就认为成功,导致漏掉业务失败。源码至少应该做这样一层封装:
$result = send_transfer_request($xml, $cert_path, $key_path); if ($result->return_code !== 'SUCCESS') { // 通信层失败,记录错误原因,可重试,但需检查签名和证书 } elseif ($result->result_code !== 'SUCCESS') { // 业务层失败,记录 err_code 和 err_code_des } else { // 付款成功,保存 payment_no 和 payment_time }通信层失败时,比如SYSTEMERROR,可以用原partner_trade_no重试。业务层失败时,大多数是不能重试的,例如AMOUNT_LIMIT、NAME_MISMATCH,需要修正业务数据后再发起新的一笔。这里要注意区分:NO_AUTH是权限配置问题,修好配置后原单号仍可重试,但FREQ_LIMIT是频率限制,需要等待后再发起,同样可以复用原单号。
4. 异常处理、订单查询与调试实战
4.1 常见错误码与处理建议
以下是企业付款到零钱高频错误,我在对接和排查中总结的对照表:
| err_code | 含义 | 处理建议 |
|---|---|---|
| SYSTEMERROR | 系统繁忙 | 用同一商户单号重试,不能换号 |
| NO_AUTH | 接口无权限 | 确认商户号已开通企业付款,检查是否用了沙箱环境 |
| AMOUNT_LIMIT | 金额超限 | 单笔最低 1 元,最高受商户平台限额控制 |
| NAME_MISMATCH | 姓名不匹配 | check_name为 FORCE_CHECK 时检查用户实名信息 |
| PARAM_ERROR | 参数错误 | 校验 XML 转义、字段类型、金额单位 |
| FREQ_LIMIT | 频率超限 | 降低请求频率,或改用异步批量处理 |
错误码的完整列表在微信商户平台文档里,但上面这 6 个覆盖了绝大多数问题。遇到SYSTEMERROR时不要换新partner_trade_no,要用原单号重试,否则会出现重复打款风险。AMOUNT_LIMIT的官方说明是付款金额必须在 1 元到 2 万之间,但实际限额由商户号的每日余额和风控决定,所以遇到这个错误时先查商户平台实时限额,再检查代码里的金额单位。
FREQ_LIMIT是很多初学者忽略的。企业付款接口的默认频率限制是每秒 1 笔,超过会被限流。批量场景下一定要控制循环速度,或者在请求间加usleep(500000)来让出半秒。
4.2 为什么没有回调?主动查询订单状态
这个接口不像 JSAPI 支付有异步回调。企业付款是“发起后可能延迟到账”,所以微信建议我们主动调用查询接口:mmpaymkttransfers/querywork,需传partner_trade_no或payment_no。查询接口同样需要证书。建议在发起付款后,先等待 3 秒再查一次;如果返回SUCCESS且status为SUCCESS,再更新本地订单状态。
$query_params = [ 'mch_id' => $mch_id, 'appid' => $app_id, 'partner_trade_no' => $order_no, 'nonce_str' => md5(uniqid()), ]; $query_sign = build_sign($query_params, $api_key); $query_params['sign'] = $query_sign; $query_xml = arrayToXml($query_params); $query_result = send_transfer_request($query_xml, $cert_path, $key_path);这里需要把查询参数用相同签名算法处理,请求体同样转 XML,证书一致。注意查询接口地址是https://api.mch.weixin.qq.com/mmpaymkttransfers/querywork,别和转账地址弄混。查询接口的响应里有一个status字段,取值有PROCESSING、SUCCESS、FAILED等。PROCESSING状态下不能立刻再次查询,要间隔 3 秒再查,否则容易收到频率限制。
另外,查询接口返回的数据里包含transfer_time和transfer_id,这两个字段要落库。后续对账、处理微信支付投诉回调时,都需要transfer_id来定位付款记录。如果商户收到“微信支付投诉回调”,投诉对象是收款用户而非付款企业,但企业付款场景中,如果用户对款项来源有疑问,你也需要凭transfer_id和商户订单号在商户平台查询流水。
4.3 本地调试三件套:日志、小额自测和接口回显
调试时我喜欢在curl_exec前后分别记录请求 XML 和响应 XML,但不能直接记录ssl_key。记录日志的代码如下:
error_log('[wechat-pay] req: ' . $xml . PHP_EOL, 3, '/var/log/wxpay.log'); error_log('[wechat-pay] resp: ' . $response . PHP_EOL, 3, '/var/log/wxpay.log');日志里会暴露用户 openid 和金额,所以本地调试完要在生产环境关闭这条语句或做脱敏。比如只记录partner_trade_no和return_code,不记完整 XML。这个习惯能避免安全问题。
调试金额上,由于官方规定单笔最小 1 元,所以不要用 0.01 元去试,直接用 1 元转给自己或同事的微信号。微信没有企业付款专用的沙箱环境,所谓“沙箱”只支持支付接口的部分功能,所以必须真实验证。我建议在测试环境配置一个独立的商户号,而不是直接使用生产商户号,这样即使出错也不会影响真实资金。
4.4 权限和 IP 白名单:容易被忽略的两道门槛
首次调用常见NO_AUTH,不一定是证书问题,可能是商户平台没有开通“企业付款到零钱”产品权限,需要在产品中心申请。还有一个经常卡壳的是 IP 白名单:如果商户平台设置了 API 调用白名单,而服务器出口 IP 不在名单内,会报NOTENABLED或IP_NOT_ALLOWED。排查时先确认商户平台当前配置。这两个门槛和代码无关,但影响最大。
这个“接口封装”里通常不会包含 IP 检查逻辑,因为这是平台侧能力。我建议在接入前做好以下检查清单:
- 商户号已申请企业付款产品,且付款方向为“到零钱”。
- 服务器可访问
api.mch.weixin.qq.com,且出口 IP 已加入白名单。 - 证书文件日期是否过期,微信商户证书有效期通常是 2 年,过期后需要重新下载并替换。
- PHP 的 cURL 扩展已开启,并且编译时使用了 OpenSSL 或 NSS。
完成这些检查后,再进入代码调试。
5. 从单笔到批量:批量付款、并发控制与安全验证
5.1 批量付款的队列实现
微信官方没有企业付款到零钱的批量接口,批量只能靠循环。但直接循环有两个风险:频率限制和后置失败。我习惯用 Redis 队列存储付款任务,消费进程从队列取任务执行,失败重试队列与主队列分离:
// 生产者:把付款任务推入 Redis 队列 $redis->lpush('wxpay:transfers', json_encode([ 'partner_trade_no' => $order_no, 'openid' => $openid, 'amount' => $amount, 'desc' => $desc, ])); // 消费者:循环从队列尾部取任务,逐个调用付款接口 while ($task = $redis->rpop('wxpay:transfers')) { $task = json_decode($task, true); try { $result = enterprise_pay($task); if ($result['result_code'] !== 'SUCCESS') { // 进入重试队列,保留原始单号 $redis->lpush('wxpay:transfers:retry', json_encode($task)); } } catch (\Exception $e) { $redis->lpush('wxpay:transfers:retry', json_encode($task)); } usleep(500000); // 控制频率,每秒最多 2 笔 }这种设计的好处是失败任务可以在消费者中重试,而不是阻塞主进程。重试时要注意幂等性:微信要求同一商户单号不能重复付款,所以重试必须复用partner_trade_no,并且先查询订单状态再决定是否重发。另外,消费者进程需要常驻内存,建议用supervisor或systemd管理,否则队列堆积后任务无法及时处理。
5.2 自检验证方法
我写了一个自检命令,放在支付服务部署后执行:
php cli/check_pay_config.php --amount=1 --openid=YOUR_OPENID脚本会依次执行四步检查:证书文件可读、生成签名、发起 1 元付款、查询订单状态。输出类似:
[1/4] cert file readable: OK [2/4] sign generated: OK [3/4] transfer request accepted: OK (payment_no: 100004789) [4/4] query transfer status: SUCCESS这个脚本的核心思想是用最小代价验证整条链路。如果第 3 步失败,会直接打印微信返回的return_msg,能快速定位是证书还是参数问题。自检脚本不要放到公网目录,最好只在命令行环境执行,避免被调用导致产生真实付款。
5.3 安全加固与向 V3 迁移的准备
证书文件权限设为 600,不要放在 web 目录。API 密钥定期轮换,轮换时需要保证新旧密钥有一段共存期,微信通常不会立即生效,所以不要同时改商户平台和代码。日志里不要保留用户姓名和完整 openid。
如果商户平台已经开放 APIv3,我建议新业务直接对接 V3 接口。V3 的证书管理更简洁,使用商户 API 证书序列号 + 微信支付公钥进行验签,且请求体使用 JSON 而不是 XML。但 V3 的企业付款到零钱接口要求使用/v3/transfer/batches批量转账接口,与 V2 的企业付款参数模型差异较大。切换前需要重新规划商户单号生成、批次号和明细号的设计。无论如何,在代码里预留一个接口抽象层是有必要的,让EnterprisePayService既支持 V2 的promotion/transfers,也能扩展为 V3 的transfer/batches,而不是把请求逻辑散落在控制器中。
这样,从单笔付款到批量付款,从 V2 到 V3,整个扩展路径就清晰了。
本文还有配套的精品资源,点击获取