接完微信支付再对接支付宝,这 5 处差异最容易把对账搞崩
先交代背景:项目里微信支付 V3 先上线,跑了大半年,后面接支付宝开放平台。我照搬微信那套回调处理逻辑去写支付宝,结果对账连续红了三天。最后定位到的原因都不是“算法写错”,而是两边这五件事的语义和结构完全不对应。记录一下,给后来人排雷。
前提和边界:本文结论基于“微信支付 API v3 + 支付宝开放平台 RSA2 + 公钥证书模式”。如果你用的是微信 V2(MD5/HMAC-SHA256 签名)、支付宝老 RSA 密钥模式,或者走的是第三方聚合支付平台转发回调,以下细节不一定完全适用,需要注意区分。
1. 回调验签:都叫“验签”,但微信多了一层解密
微信支付 V3 的回调通知,拿到手的时候并不是能直接读的 JSON。
请求头里带着这几个关键字段:
Wechatpay-SignatureWechatpay-TimestampWechatpay-NonceWechatpay-Serial
通知 body 的大致结构是:
{
"id": "EV-...",
"event_type": "TRANSACTION.SUCCESS",
"resource": {
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "...",
"associated_data": "...",
"nonce": "..."
}
}
处理步骤是:
- 用
Wechatpay-Serial找到对应的微信支付平台证书。 - 按“请求方法\n请求URL\n时间戳\n随机串\n报文主体”构造签名串,用平台证书公钥验
Wechatpay-Signature。 - 验签通过后,再用 APIv3 Key 对
resource.ciphertext做 AES-256-GCM 解密。 - 解密后拿到的才是订单详情。
支付宝的回调则不是 JSON,而是 application/x-www-form-urlencoded 表单参数。里面直接是业务参数 + sign + sign_type,没有加密包裹。验签时把除 sign、sign_type 以外的参数按 key 的 ASCII 码升序拼好,用支付宝公钥做 RSA2 验签。没有解密这一步。
最容易踩的坑:微信那边验完签不记得解密,或者以为支付宝也要“先解密再验签”。等你把微信那套处理逻辑套到支付宝上,会发现支付宝通知里根本没有 ciphertext 给你,一开始就懵了。
验签规则本身两边官方文档都写了,可查。但“网关或聚合平台是否把原始签名头转发给你”这个不能查文档,必须自己在接的渠道上验证。我之前接的一个聚合渠道就把 Wechatpay-* 头全剥了,导致业务侧无法自己验签,最后只能改成“网关验签 + 转发时带验签结果”。
2. 金额单位:微信是 int 分,支付宝是 string 元
这个是最直接的对账杀手,而且两边文档都写得很明确,但接的时候容易顺手就过了。
微信支付 V3 下单参数和回调里的金额单位都是“分”,整数类型。
比如 1 元:
"amount": {
"total": 100,
"payer_total": 100
}
支付宝呢?单位是“元”,字符串类型,保留两位小数。
下单参数:
total_amount=1.00
异步通知里同样:
total_amount=1.00
buyer_pay_amount=1.00
我最初写对账逻辑时,直接从两边通知里取金额字段做相等判断:
- 微信取
amount.total,值是100 - 支付宝取
total_amount,值是"1.00"
结果显然整张对账表全是红的。后来统一先转成“分”再比较:支付宝的字符串元用 BigDecimal 解析后乘以 100,微信那边直接拿 int 分。
另外一个坑是精度。支付宝金额如果解析成 double,再参与累加或比较,会出现 0.30000000000000004 这种问题。“金额全部用 Decimal/BigDecimal,避免 double”属于常识,但我确实见过有人拿 float 写对账脚本,这种坑只能自己踩出来。字段单位两边文档都能查到,不算冷门知识点。
补充一点:如果要核对“用户实际支付金额”,微信 回调里对应的是 amount.payer_total,支付宝对应的是 buyer_pay_amount。直接用订单金额 total_amount 去对,在有优惠、红包的场景下会差出几毛钱。这个建议在测试环境用真实优惠订单验证一次,光看文档容易漏。
3. 成功应答:微信看状态码,支付宝认“success”文本
回调处理完之后的应答,两边要求不一样,而且支付宝那边要求非常死板。
微信支付 V3 的通知,处理成功后返回 HTTP 200 或 204,推荐在 body 里带:
{
"code": "SUCCESS",
"message": "成功"
}
实测中,返回 200 + 空 body 微信也认为成功,不会重发。但建议按文档来,别赌。
支付宝那边:处理成功后必须输出纯文本:
success
注意,是纯文本 success,不能是 JSON,不能带引号,不能有空格换行之外的多余内容。很多 Web 框架里你写 return "success";,结果被序列化成 "success"(带双引号)返回,支付宝就认为处理失败,然后继续重发通知。这个坑当时排查了很久,最后是抓包看响应 body 才发现多了引号。
支付宝官方文档明确写了必须返回 success,这一点可查。但“你的框架到底会不会给字符串加引号”这个没法查文档,跟你用的框架和序列化配置有关,属于需要实测的部分。
另外记住一个处理顺序:先落库、再返回应答。别为了“先响应渠道”把业务更新放在异步任务里,一旦异步任务挂了,对账就平不了。
4. 重试策略:支付宝有明确时间表,微信的别完全信文档
两边回调失败后都会重试,但节奏不一样。
支付宝异步通知的重试间隔官方文档有明确说明:
- 4 分钟
- 10 分钟
- 10 分钟
- 1 小时
- 2 小时
- 6 小时
- 15 小时
之后是否继续发、总时长到多少,以官方文档为准,我这边没有完整压到最后一轮。
微信支付 V3 的重试,官方文档里给过一个类似的时间序列:
- 15 秒、15 秒、30 秒、3 分钟、10 分钟、20 分钟、30 分钟、30 分钟、30 分钟、60 分钟
- 然后 3 小时、3 小时、3 小时、6 小时、6 小时
但微信不同产品线之间可能不完全一样,而且生产环境真不适合等完整个序列去验证。这个建议在测试环境自己模拟回调失败,或者直接抓包看日志,不要完全依赖网上流传的版本。文档能查到“会重试”,但完整间隔表需要自行验证。
重试带来的另一个问题是幂等处理。
支付宝异步通知每次重发的 notify_id 会变,但 out_trade_no、trade_no 不会变。所以幂等键要用 out_trade_no + trade_no,不能用 notify_id。微信那边 event id 每次重发是否保持不变,我没有完全确认,日志里看起来不变,但不敢保证所有场景都这样。稳妥做法是用商户订单号 out_trade_no 加渠道交易号 transaction_id 做幂等。
5. 退款字段:看起来都在说“退款”,口径完全不一样
退款回调是最容易翻车的地方,因为两边字段名都带 refund,但含义和类型对不上。
微信支付 V3 退款结果通知,resource 解密后关键字段:
out_refund_no:商户退款单号refund_status:退款状态(SUCCESS、CLOSED、PROCESSING、ABNORMAL)amount.refund:退款金额,int,单位分out_trade_no:商户订单号transaction_id:微信订单号
支付宝退款异步通知的关键字段:
out_biz_no:商户退款请求号(对应微信的out_refund_no)refund_fee:退款金额,字符串,单位元fund_change:资金变动状态,Y表示退款成功out_trade_no:商户订单号trade_no:支付宝交易号
两边对照起来,最容易踩的坑是“用微信的字段名去支付宝里找”:
- 微信的
out_refund_no,支付宝叫out_biz_no。 - 微信判断退款成功看
refund_status == "SUCCESS",支付宝没有refund_status,要看fund_change == "Y"。 - 微信退款金额在
amount.refund里,支付宝在refund_fee里,而且一个分一个元,直接比较差 100 倍。
还有一个容易忽略的:支付宝退款通知里的 refund_fee 是字符串,直接做数值比较前要先解析成 Decimal,别用 int() 去强转。
字段名和单位两个渠道的文档都能查到,属于官方可查信息。但“退款成功后异步通知到底在哪些场景会发、会不会因下游返回错而漏发”这个和具体产品配置有关,需要在测试环境真实退款一笔看日志。
补充:聚合渠道的额外坑
如果走的是聚合支付平台(自己只对接一个网关,再由网关对接微信/支付宝),还要注意回调转发问题。
有些聚合网关会把微信/支付宝原始回调吸收掉,然后按自己的统一格式转发给业务系统。这时候:
- 微信的
Wechatpay-Signature等请求头大概率没了。 - 支付宝的原始
sign也不一定透传。
业务侧如果拿到的是“网关二次封装后的报文”,就别尝试自己验签了,验不过很正常。要么在网关侧验签,要么让网关把原始通知报文完整落库、给你提供追踪 ID,方便对账排查。
这个不是官方文档能覆盖的,纯粹是渠道实现问题,需要开通服务后实测确认。
另外,对账出现异常时,建议先拉两边原始通知日志比对字段,不要直接改业务代码。像金额单位差 100 倍这种问题,经常是肉眼对一条真实订单就能发现的,没必要先查半天代码。