编程 接完微信支付再对接支付宝,这 5 处差异最容易把对账搞崩

2026-09-01 09:03:05

接完微信支付再对接支付宝,这 5 处差异最容易把对账搞崩

先交代背景:项目里微信支付 V3 先上线,跑了大半年,后面接支付宝开放平台。我照搬微信那套回调处理逻辑去写支付宝,结果对账连续红了三天。最后定位到的原因都不是“算法写错”,而是两边这五件事的语义和结构完全不对应。记录一下,给后来人排雷。

前提和边界:本文结论基于“微信支付 API v3 + 支付宝开放平台 RSA2 + 公钥证书模式”。如果你用的是微信 V2(MD5/HMAC-SHA256 签名)、支付宝老 RSA 密钥模式,或者走的是第三方聚合支付平台转发回调,以下细节不一定完全适用,需要注意区分。

1. 回调验签:都叫“验签”,但微信多了一层解密

微信支付 V3 的回调通知,拿到手的时候并不是能直接读的 JSON。

请求头里带着这几个关键字段:

  • Wechatpay-Signature
  • Wechatpay-Timestamp
  • Wechatpay-Nonce
  • Wechatpay-Serial

通知 body 的大致结构是:

{
  "id": "EV-...",
  "event_type": "TRANSACTION.SUCCESS",
  "resource": {
    "algorithm": "AEAD_AES_256_GCM",
    "ciphertext": "...",
    "associated_data": "...",
    "nonce": "..."
  }
}

处理步骤是:

  1. Wechatpay-Serial 找到对应的微信支付平台证书。
  2. 按“请求方法\n请求URL\n时间戳\n随机串\n报文主体”构造签名串,用平台证书公钥验 Wechatpay-Signature
  3. 验签通过后,再用 APIv3 Key 对 resource.ciphertext 做 AES-256-GCM 解密。
  4. 解密后拿到的才是订单详情。

支付宝的回调则不是 JSON,而是 application/x-www-form-urlencoded 表单参数。里面直接是业务参数 + sign + sign_type,没有加密包裹。验签时把除 signsign_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_notrade_no 不会变。所以幂等键要用 out_trade_no + trade_no,不能用 notify_id。微信那边 event id 每次重发是否保持不变,我没有完全确认,日志里看起来不变,但不敢保证所有场景都这样。稳妥做法是用商户订单号 out_trade_no 加渠道交易号 transaction_id 做幂等。

5. 退款字段:看起来都在说“退款”,口径完全不一样

退款回调是最容易翻车的地方,因为两边字段名都带 refund,但含义和类型对不上。

微信支付 V3 退款结果通知,resource 解密后关键字段:

  • out_refund_no:商户退款单号
  • refund_status:退款状态(SUCCESSCLOSEDPROCESSINGABNORMAL
  • 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 倍这种问题,经常是肉眼对一条真实订单就能发现的,没必要先查半天代码。

推荐文章

程序员茄子在线接单