微信支付 V3 对接踩坑记录:从签名失败到「已付款仍显示未支付」
我们在企业评估系统上完整跑通了微信支付 V3 的真实收费闭环,中间踩的坑一个不落。这篇把原因和改法都写清楚了,能帮你少熬两个通宵。
微信支付 V3 的文档是完整的,但它把最容易错的几件事分散在四五个页面里。我们把真实踩过的坑按「症状 → 原因 → 改法」记下来。
一、签名失败:先分清三把「钥匙」
V3 里同时存在三个容易混的东西,签错任何一个都是签名失败:
| 名称 | 作用 | 常见错法 |
|---|---|---|
| APIv3 密钥 | 给回调通知做解密/验签 | 把它当私钥去签名请求 |
| 商户 API 私钥(.pem) | 给你的请求签名 | 和上面那个搞混 |
| 平台证书 | 验微信返回的签名 | 不下载、不更新,硬编码在代码里 |
改法:请求签名只用商户私钥;回调验签只用平台证书。平台证书要能自动下载并按需轮换,不要把一个证书序列号写死在代码里——证书是会过期的。
二、签名串格式:一个字符都不能差
V3 的签名串是五行拼接:
HTTP方法\n
URL路径(含 query,不含域名)\n
时间戳\n
随机串\n
请求体原始字符串\n
踩过的三个细节:
- URL 必须带 query,且只能是路径部分。拼成完整 URL(带
https://)必失败。 - 请求体要用原始字节串。如果你先把 JSON 反序列化再序列化,键顺序和空格变了,签名就对不上——签名的串和发出去的串必须是同一个。最稳的做法是先序列化成字符串,用这个字符串同时去签名和发送。
- 每次都要重新生成时间戳和随机串,不要复用。
三、回调不生效:域名、重试、幂等三件事
1. 回调地址必须公网可达的 HTTPS,且不能重定向。内网地址、HTTP、301/302 跳转,微信都不会跟。
2. 回调必须返回 200。这是最容易踩的一条:如果业务代码在回调里抛异常、或者返回了非 200,微信会认为通知失败并按策略反复重试。结果就是同一条支付通知被处理多次。
正确做法是先验签 → 落库标记录 → 立刻返回 200 → 异步去做后续业务逻辑。不要把重业务塞在回调的同步链路里。
3. 业务必须幂等。用「商户订单号」做唯一键,重复通知直接返回成功、不重复发货。这是我们踩过的最贵的一个坑,下面单独说。
四、最贵的那个坑:「已付款,但订单还显示未支付」
这是我们真实遇到、也是最花时间定位的一个问题。用户在微信里明明付了钱,回到我们系统订单状态还是「待支付」。
根因:回调处理链路里,业务逻辑(更新订单状态、写日志、发通知)和返回 200 是同一个事务/同一段同步代码。中间任何一步抛错,整个请求就返回了非 200,微信判定通知失败并重试;而重试时业务代码又因为「已经部分写入」等原因提前 return,导致状态永远停在中间态。
改法:把回调拆成两步,中间用数据库状态做锚点。
- 同步阶段(必须在 200 之前完成,且要极短):验签 → 解密通知 → 用「商户订单号 + 通知 ID」写入一张通知流水表(带唯一索引)→ 返回 200。
- 异步阶段:由定时任务或队列消费流水表,去做真正的业务(改订单状态、发货、通知)。失败有重试,且天然幂等。
- 兜底主动查单:再加一个定时任务,扫「超过 N 分钟仍是待支付」的订单,主动调查询订单接口核对真实状态。这一步能救掉所有因网络或部署窗口期丢掉的通知——回调是不可靠的,主动查单才是最终事实来源。
加上第 3 步之后,这个问题在我们线上再没复现过。
五、几个小但会卡住人的点
- 金额单位是「分」。前端传元、后端传分,是经典的差 100 倍 bug。
- 旧证书和新证书并存时,序列号要用当前生效的那本。我们在项目里同时持有 2022 与 2024 两套商户证书,切换时最容易出错的地方就是序列号还指向旧证书。
- 配置文件的行尾符会咬人。密钥从环境变量或配置文件读取时,如果文件是 Windows 行尾(CRLF),读出来的字符串尾部可能带不可见的 \r,签名必失败。排查时先把读到的密钥打印长度核对,一眼就能发现多出来的字符。
- 沙箱和正式环境的密钥不通用,上线前确认所有配置都切到了正式环境。
一句话总结
V3 的难点不在密码学,在顺序和边界:什么时候验签、什么时候返回 200、什么该同步做、什么必须异步做、事实以谁为准。把这几个边界划清楚,它就没有想象中那么难。
回调和主动查单都要有,且要有通知流水表 —— 这三样齐了,支付才算真的跑通。