☰
微信支付商家转账对接全攻略:从企业付款到零钱到API v3实战
2026/10/3 13:27:10 网站建设 项目流程

做微信支付开发的人,早晚会接到一个需求:把一笔钱从平台转到用户的微信零钱里。这不是退款,也不是分账,而是平台主动给用户发钱。最典型的场景就是分销返佣、活动奖励、押金退回、补贴发放这类业务。

真去对接的时候,会发现这个功能比想象中麻烦。尤其从旧版“企业付款到零钱”升级到新版“商家转账”之后,接口从 API v2 迁到 API v3,参数、签名、回调全部换了一遍,网上很多教程还停留在老接口,直接照搬很容易踩坑。这篇文章我把整个对接过程拆开讲清楚,包括开通要求、参数设计、签名流程、回调处理,还有我实际踩过的几个坑。

适合谁看?正在对接微信支付商家转账的后端同学,以及准备做返佣、分销、奖励系统的技术负责人。看完至少能避开一半的坑。

1. 先把这个功能理解透:转账到零钱的钱从哪来、到哪去

1.1 哪些业务场景最常用

转账到零钱这个能力,业务价值在于把“线上支付收进来的钱”以合规的路径返还或分配给用户。我在实际对接中见过的主要场景有这么几类:

第一类是分销返佣。用户在平台下单,推荐人获得佣金,平台按月或者按订单把佣金打到推荐人的微信零钱。这类业务对转账的“及时性”要求不高,但对批次管理要求高,几千上万个推荐人需要分批打款,还要能对账。

第二类是营销奖励。签到得现金红包、邀请新用户得奖励、抽奖中的现金奖品,本质上都是平台向用户零钱转账。这类业务量大、单笔金额小,最容易触发微信的风控规则,所以转账频率、单笔限额、用户实名情况都得提前设计。

第三类是余额提现和押金退还。平台账户有余额体系,用户申请提现后,平台通过商家转账把钱打给用户;或者用户在平台交过押金,流程结束后原路退回。这部分大部分平台会走“原路退回”而不是商家转账,但难免有超期、换号、补发之类的异常场景,到头来还是得靠商家转账兜底。

第四类是平台补贴和学费退款。课程平台、招聘平台都有类似场景,用户缴纳的费用需要部分退回,或者平台为了补偿用户体验直接发一笔补偿金。这类场景金额通常不固定,需要后台手动填写金额发起转账。

1.2 接口演进:从“企业付款到零钱”到“商家转账”

如果你是老开发,可能听过“企业付款到零钱”这个叫法。它是微信支付早期推的接口,基于 API v2,通过mch_pay或者promotion/transfers路径调用。老接口的优点是接入简单,一个 XML 报文加一个签名就能跑通;缺点也很明显,回调机制薄弱、参数命名混乱、安全等级偏低。

后来微信支付把这类资金操作统一收归到商家转账产品,走 API v3。接口文档挂在“商家转账”产品下,真实请求路径是/v3/transfer/batches,通过批次(batch)和明细(detail)两层结构来管理资金。我在项目里切换之后最大的感受是:签名方式变了,换成 RSA-SHA256;回调通知更完整,批次和明细都有明确状态;接口文档约束更强,一个字段的错误都会直接报出来,不像旧接口经常“看起来成功了,实际没到账”。

这个变化对老项目影响很大。如果你维护的是一个用了很多年的老支付系统,建议规划迁移时先理清楚三件事:旧接口的历史交易数据怎么对账、新旧两套回调同时在线怎么区分、商户证书续期是否影响新接口。我见过一个项目因为老接口的证书到期,导致新接口请求全部失败,排查了很久才发现是同一个证书体系的问题。

这里要特别提醒一点:商家转账不是所有商户号都能直接开通的。普通商户还好说,如果是小微商户、个体户,有些产品线是不支持商家转账的;另外,如果商户号近期有交易纠纷或者被投诉,微信侧可能会暂停该功能的使用权限。所以开户前先仔细看文档中“准入条件”那一节,别等代码写完了才发现号上没有权限。

2. 动手开发前,先把这些准备工作做扎实

2.1 开通商家转账:权限、签约、账户要求

开通流程并不复杂,登录微信支付商户平台,在产品中心找到“商家转账”,按引导完成签约即可。但签约过程中有几个细节经常被忽略:

一个是结算账户的验证。商家转账涉及资金流出,微信侧会要求商户号绑定的银行账户信息准确。签约时如果提示“银行账户校验不通过”,不要反复重试,先去核对开户行联行号是不是最新的。

另一个是付款上限和单笔限额。签约之后,默认的转账金额和频率是有限制的,具体限额在“商家转账-产品设置”里能看到。早期跑业务一定要做“小额先行”的压测:先转 0.1 元、1 元、10 元各测一遍,再用接近限额的金额测一次,确认接口没有隐藏限制。

还有就是用户实名要求。商家转账要求收款用户完成微信实名认证。如果用户没实名,接口会返回USER_NOT_VERIFIED之类的错误码。业务上要提前设计好兜底方案——比如提示用户先完成实名,再发起转账;或者转账失败后进入人工处理队列。

提示:如果你只是想做测试,又不想马上开通线上权限,可以在商户平台的“API 安全”里配置沙箱环境。但微信支付的沙箱和真实环境是两套体系,沙箱中的商户号、证书都不能直接用于生产环境,代码里要做好环境隔离。

2.2 证书与API v3密钥:签名体系的基本盘

API v3 的签名体系,核心有三样东西:商户API证书(apiclient_cert.pem)、商户API私钥(apiclient_key.pem)、APIv3密钥(APIv3Key)。这三样缺一不可,而且各有用途:

  • 商户API证书:用来声明“我是这个商户”,平台端在做身份识别时会校验它;
  • 商户API私钥:用来给请求报文做签名,保证报文在传输中不被篡改;
  • APIv3密钥:用来解密微信支付回调通知里的敏感信息,比如用户的真实姓名、手机号等。

很多新手在这里会犯一个混淆:以为 APIv3 密钥和微信支付商户平台的登录密码是一回事。不是的。APIv3 密钥是在「账户中心 - API安全 - 设置APIv3密钥」里单独设置的,是一串 32 字节的随机字符串,设置之后要妥善保存,最好放到配置中心或者密钥管理服务里,别硬编码进代码库。

证书过期是另一个高频问题。微信支付商户证书有效期一般是 5 年(新申请的也可能是更短周期),到期前要在商户平台重新申请并替换。替换证书不只是换文件那么简单,如果你的请求工具是通过 SDK 读取证书的,记得同步验证新证书的私钥是否能正常签名,否则会出现“证书不匹配”的报错。

2.3 参数规范里最容易被忽略的“名称坑”

这个点我想单独拿出来讲,因为太多人在这里翻车了。就是字段命名风格的问题。

微信支付 API v3 的接口文档里,字段名采用小写蛇形命名(snake_case)。比如“总金额”的字段名是total_amount,“总笔数”是total_num,“转账明细列表”是transfer_detail_list。这本来没什么,但当你用强类型语言(尤其是 C# / Java 这种对字段名敏感的语言)对接时,一不小心就对不上。

我接过一个 C# 项目,老板把报错截图发我:“无法将 json 输入源“/body/total_amount”映射到目标字段“转账总金额”中”。我当时一看就知道是典型的序列化命名映射问题:微信支付回调里返回的是total_amount,但后端实体类里定义的是TotalAmount(PascalCase),反序列化工具默认不会自动把小写蛇形转成大写驼峰,于是直接抛 JsonException。

解决方式其实很简单,给字段加上JsonPropertyName特性或者在反序列化配置里统一指定命名策略。比如 System.Text.Json 环境,可以这样写:

public class TransferBatch { [JsonPropertyName("batch_id")] public string BatchId { get; set; } [JsonPropertyName("batch_status")] public string BatchStatus { get; set; } [JsonPropertyName("total_amount")] public int TotalAmount { get; set; } }

如果是 Java + Jackson,可以在全局配置里设置PropertyNamingStrategy.SNAKE_CASE,或者给每个字段加@JsonProperty("total_amount")。这个坑很小,但一旦触发,影响面是整个回调流程,可能让你误以为微信没回调。

3. 核心代码流程:如何发起一笔转账到零钱

3.1 创建商家转账请求:接口地址、请求头、参数表

先看接口本身。创建商家转账的请求地址是:

POST https://api.mch.weixin.qq.com/v3/transfer/batches

请求头除了常规的Content-Type: application/json之外,还必须带上Authorization和Wechatpay-Serial。Authorization是签名后的认证信息,Wechatpay-Serial是用来解密回调的证书序列号。

请求体核心参数可以整理成一张表:

参数名类型说明
appidstring(32)商户号绑定的 AppID,必须是已认证的小程序或公众号
out_batch_nostring(32)商户系统内部的批次单号,需唯一
batch_namestring(64)批次名称,会展示给用户
batch_remarkstring(256)批次备注
total_amountinteger转账总金额,单位:分
total_numinteger转账总笔数
transfer_detail_listarray转账明细列表,最多 1000 笔
transfer_scene_idstring(64)转账场景ID,部分特殊场景需要申请
transfer_scene_report_infosarray场景上报信息,按需传递

这里最核心的校验规则是:total_amount 必须等于所有明细 transfer_amount 之和,total_num 必须等于明细数量。一旦对不上,接口直接拒绝。

金额单位这里再强调一次:单位是分,整数。有人会在请求体里传total_amount: 10.50,接口直接报错。至于“虚拟支付代币数量是否支持小数点”,道理相同——微信支付的所有金额字段都按最小货币单位“分”来计,虚拟代币也应该用等效的最小单位,避免浮点数精度问题。

另外,如果你是服务商模式下的特约商户发起转账,请求参数里还要额外注意sp_appid、sp_mchid等参数以及sub_mchid的使用。特约商户的转账权限、结算规则和普通商户有差异,建议先跟微信支付服务商确认清楚,再定技术方案。这一块网上资料比较少,大多要靠实际联调趟出来。

3.2 请求签名与代码实现

API v3 的签名逻辑相对固定,步骤如下:

  1. 构造签名串:HTTP方法\nURL路径\n请求时间戳\n请求随机串\n请求体\n
  2. 使用商户API私钥对签名串做 SHA256withRSA 签名;
  3. 将签名结果放入Authorization头,格式为WECHATPAY2-SHA256-RSA2048开头的一长串。

直接用官方 SDK 更快。以 PHP 为例,如果用wechatpay/wechatpay官方 SDK,核心代码其实很短:

use WechatPay\GuzzleMiddleware\WechatPayMiddleware; use WechatPay\GuzzleMiddleware\Util\PemUtil; $merchantId = '你的商户号'; $merchantSerial = '商户证书序列号'; $privateKey = PemUtil::loadPrivateKey('/path/to/apiclient_key.pem'); $wechatpayMiddleware = WechatPayMiddleware::builder() ->withMerchant($merchantId, $merchantSerial, $privateKey) ->build(); $client = new \GuzzleHttp\Client(['handler' => $wechatpayMiddleware]); $resp = $client->request('POST', 'https://api.mch.weixin.qq.com/v3/transfer/batches', [ 'json' => [ 'appid' => '你的AppID', 'out_batch_no' => 'R202406070001', 'batch_name' => '六月分销佣金', 'batch_remark' => '六月分销佣金结算', 'total_amount' => 100, 'total_num' => 1, 'transfer_detail_list' => [ [ 'out_detail_no' => 'D202406070001', 'transfer_amount' => 100, 'transfer_remark' => '分销佣金', 'openid' => '用户OpenID', 'user_name' => '用户实名', ], ], ], ]);

这段代码里有两个容易忽略的点:

第一,user_name是用户实名信息。商家转账要求收款方实名校验,所以转账前最好先确认你拿到了用户的真实姓名。如果业务上不方便收集实名信息,微信侧也支持关闭强制校验收款方姓名,但这样风险自担,转账出错后追回困难。

第二,transfer_amount有最小单位和上限。单笔最低一般是 0.3 元(即 30 分),具体以文档为准;单笔上限跟商户号、产品类型有关。设计转账金额时,要把这些边界写进业务校验逻辑,别等接口报错才发现。

3.3 批次与明细的幂等设计

我见过不少团队做转账功能时,把“幂等”当成一个可选优化项,结果线上出了重复打款的重大事故。

商家转账的幂等键是out_batch_no(批次单号)和out_detail_no(明细单号)。这两个单号必须在整个商户号维度内唯一。用同一个 out_batch_no 重复请求,微信侧会返回已存在的批次;用同一个 out_detail_no 重复发起,明细会直接失败或者返回原单。

业务上建议的做法是:批次单号用统一的规则生成,比如日期 + 业务类型 + 随机数。同时,在本地数据库里给这两个字段加上唯一索引。这样即使代码逻辑出现并发重复调用,数据库层面也能兜住。

转账状态机的设计也要提前规划。一次转账会经历:待转账 -> 转账中 -> 转账成功/转账失败/部分成功。回调解析时,不要只处理“成功”一种状态,“失败”和“部分成功”必须单独建表记录失败原因,方便人工介入。我习惯在明细表里同时存转账金额、实际到账金额、失败原因、回调时间,这样出问题对账时能少走很多弯路。

3.4 回调通知:发了钱,一定要等结果

商家转账的结果通过回调通知给商户。回调地址在创建转账时可以传notify_url字段,也可以在商户平台的产品设置里统一配置。

回调通知的报文同样走 API v3 协议:微信侧用平台证书对报文做签名,敏感字段用 APIv3 密钥加密。收到回调后,你的程序要做的第一件事不是解析业务数据,而是验签。验签通过后才允许继续处理。这一步如果省掉,等于把接口裸奔暴露给别人,别人伪造一条“转账成功”的通知,你的系统就可能把一笔没到账的钱标记成已发放。

验签通过后,解密resource里的数据,核心字段包括batch_id(微信批次单号)、out_batch_no(商户批次单号)、batch_status(批次状态)、transfer_detail_list(明细列表)。其中明细里的detail_status字段是SUCCESS还是FAIL,直接决定你这笔钱到底发没发出去。

处理回调时一定要做成幂等操作:同一个 notify_id 可能会重复推送,数据库要按业务单号做去重判断。

4. 投诉回调与异常处理:不能只管发钱,不管结果

4.1 商家转账结果回调的完整处理链路

微信侧的推荐流程是这样的:

  1. 用户发起转账后,微信支付异步推送转账结果通知到notify_url;
  2. 商户服务端接收通知,先验证Wechatpay-Signature请求头;
  3. 验签通过后,用 APIv3 密钥解密resource.ciphertext;
  4. 解密得到 JSON 后,处理批次状态和明细状态;
  5. 业务处理成功后,返回 HTTP 200 并返回{"code": "SUCCESS"};如果返回非 200,微信侧会按间隔重新推送。

这个流程看起来清晰,但实际对接时,很多人会栽在“返回报文格式”上。微信支付要求回调接收端返回的报文是纯 JSON 字符串,而且必须在 5 秒内返回。如果业务处理逻辑太慢(比如还要去查数据库、调外部接口),可以先在回调里快速校验签名、解密、落库,再异步去更新业务状态。

另外,回调解密时要用到平台证书和 APIv3 密钥。平台证书需要定期从微信侧下载更新,建议写一个定时任务,每周自动刷新一次平台证书缓存。证书一旦过期,所有回调验签都会失败,而且不会有人主动提醒你。

4.2 投诉回调:用户说没收到钱怎么办

商家转账有一个很特别的机制,也是我最早对接时忽略掉的:投诉回调。

用户如果在微信支付账单里对某笔商家转账发起投诉,微信会向商户的投诉回调地址推送通知。这个地址和转账结果回调地址不是同一个,需要在“商家转账-产品设置-投诉管理”里单独配置。

收到投诉回调之后,平台的第一反应不应该是人工去转账,而是先去查这一单的实际状态。很多时候用户说“没收到钱”,其实不是转账失败,而是用户没有点开短信或者消息通知导致的误解。查完状态后按实际结果处理:

  • 转账确实成功了:给用户解释到账渠道,或者在 App 订单页显示转账凭证;
  • 转账失败了:查失败原因,重新发起转账或者走线下退款;
  • 部分成功:按明细状态逐笔处理。

这里要特别提醒:不要用“手机银行转账模拟器”这类工具去模拟验收转账流程。第三方模拟器只能帮你验证界面和流程是否通顺,它发出的请求不会真正经过微信支付服务器,也不会触发真实的回调。测试阶段老老实实用微信支付提供的沙箱环境,或者用小金额真实转账来验证。

4.3 转账失败的资金怎么处理

转账失败是家常便饭。用户注销了微信、实名信息不匹配、金额超过单笔限制、余额不足,都会导致明细失败。关键是失败之后,资金去哪了?

好消息是,商家转账失败的明细金额会原路退回到商户号的可用余额里,不会凭空消失。但退回不是实时的,可能需要几分钟到几个工作日,具体看银行侧处理。所以财务对账时,不要把“转账失败”直接等同于“钱回来了”,要以商户平台的资金流水为准。

业务处理上,我建议单独做一个“转账异常流水表”,记录每一笔失败明细的单号、金额、失败原因、状态。再配一个定时任务,每隔一段时间扫描这张表,满足重试条件的自动重新发起转账,不满足的进入人工处理队列。这样既不会漏掉用户的资金需求,也不会因为重复转账造成资损。

5. 常见问题与排查技巧实录

5.1 高频报错速查表

把我在实际开发中遇到的高频报错整理成了一张表:

报错信息可能原因解决办法
无法将 json 输入源“/body/total_amount”映射到目标字段“转账总金额”中C#/Java 反序列化字段名映射不匹配添加 JsonPropertyName 特性或配置全局命名策略
商户号未开通该产品权限未签约商家转账去商户平台产品中心完成签约
校验签名失败请求签名串与服务器计算不一致检查 HTTP 方法、URL、请求体是否与签名串保持一致
转账金额超出限制单笔或单日金额超限查询产品设置里的限额,拆分销单
用户未实名或姓名校验不通过收款方未实名或 user_name 错误前端引导先实名,或关闭强制校验(风险自担)
余额不足商户号可用余额不足给商户号充值,或启用账户自动充值
批次单号重复out_batch_no 已存在更换唯一批次单号

这几类错误里,签名失败是最容易反复出现的一类,但它也是最容易排查的一类。建议在调试阶段把请求的Authorization和签名串打印出来,跟微信支付官方签名工具生成的对比一下,很快就能定位是时间戳偏移、换了私钥、还是请求体不一致。

5.2 金额映射报错的完整排查过程

再回到前面提到的那个报错,我完整还原一下当时是怎么排查的。

项目用的是 .NET 8,后端收到微信支付回调时,一个 JSON 解析异常抛到了日志系统。关键报错就是:

无法将 json 输入源 “/body/total_amount” 映射到目标字段 “转账总金额” 中。

一开始我以为是微信返回结构变了,先去商户平台确认回调参数,参数还是total_amount。那就不是微信的问题。接着看本地实体类,发现定义的是:

public class TransferBatch { public int 转账总金额 { get; set; } }

这里的“转账总金额”是一个中文属性名。如果 JSON 里没有完全一致的字段名,System.Text.Json 默认不会去猜测命名策略,直接抛错。

解决办法有两个:

第一,用特性指定映射:

public class TransferBatch { [JsonPropertyName("total_amount")] public int 转账总金额 { get; set; } }

第二,在JsonSerializerOptions里配置:

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };

两种方案都行。我更推荐第二种,因为万一后面新增了字段,只要命名规范统一,反序列化基本不会再出问题。

5.3 虚拟代币与金额精度问题

在微信支付的生态里,除了实物商品和真实人民币交易,还有大量虚拟支付场景,比如游戏币、会员积分、代币充值。有同学问:虚拟支付里的代币数量支持小数点吗?

这个问题的本质是金额精度问题。微信支付所有资金类接口,金额字段都以“分”为单位、整数传输。虚拟代币如果要换算成人民币,必须按最小货币单位来设计,不能直接传小数。比如你的代币定价是“1 元 = 10 个币”,那就应该用 10 的整数倍去定义充值档位,千万别在支付金额里出现10.5这类数值。用浮点数存金额,一旦出现精度误差,后续对账就是灾难。

我见过最理想的做法是:数据库存“分”或者“最小单位整数”,展示层才做格式化。转账逻辑里的所有比较、求和、校验都基于最小单位整数,这样既能避免浮点误差,也不会被微信接口的字段类型卡住。

5.4 一个容易忽略但后果很大的配置项

最后这条经验,是我在一次灰度发布时踩到的。我们当时把回调地址从测试环境切到生产环境,结果发现生产环境收不到任何回调。查了很久,最后发现问题出在商户平台“API 安全-回调配置”里的 TLS 协议版本。

微信支付要求回调地址必须支持TLS 1.2 及以上。如果服务器上跑的是老旧的中间件,只支持 TLS 1.0/1.1,回调会一直失败,但接口层面不会报任何错误。排查方法也很简单,用curl -v直接看 TLS 握手版本:

curl -v https://你的回调地址/api/notify

如果发现是 TLS 1.0,就去升级 Nginx 或者应用服务器的协议配置,把ssl_protocols TLSv1.2 TLSv1.3;加上。

我自己对接下来最大的感受是,商家转账这套接口,文档写得再全,不上手跑一遍总会漏掉一两处细节。尤其是回调验签和字段命名这两块,几乎每个项目都会有人栽跟头。如果你正在做这个功能,建议把准备工作做扎实再动手写代码,别急着直接调接口。

最后再分享一个小技巧:日常开发里可以在本地把微信支付的回调报文存成 JSON 文件,写一个 Mock 服务,按微信的推送格式自动重放。这样测试回调逻辑时不需要真的去转账,既省成本又方便调试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询