前言

微信支付的文档其实挺全,但接入的时候总有些”文档写了、你却未必留意”的坑。下面是我自己踩过或者帮人排查过的几条,不绑具体项目,只说通用经验。

平台公钥模式 vs 证书模式,怎么选

微信支付回调验签现在主要有两种配置:

  • 证书模式(RSAAutoCertificateConfig):SDK 自动下载并轮换平台证书。
  • 平台公钥模式(RSAPublicKeyNotificationConfig):用一份固定的平台公钥验签。

证书模式省事,但在平台证书切换的那段时间,有可能 SDK 还没更新到新证书,验签就会失败。公钥模式稳定些,缺点是真到了换公钥的时候,得自己手动换文件。

我的选择是:商户号稳定、回调量不大的,用公钥模式,自己可控;商户号活跃、证书换得勤的,用证书模式,但切换那几天盯着点失败率。

回调 body 必须取原始字符串

这是最容易踩的坑。微信支付回调的签名是对原始请求体算的,所以接回调时 body 得原封不动拿到,不能让框架先帮你反序列化:

@PostMapping("/notify")
public ResponseEntity<String> notify(
@RequestBody String body, // 必须是 String,不能是 DTO
@RequestHeader("Wechatpay-Signature") String signature,
...) { ... }

一旦让 Spring 先反序列化成对象再转回字符串,字段顺序、空白字符都可能变,签名就对不上了。

金额全链路用整数分

微信支付接口里金额单位是分,整数。从下单、落库、回调到查单,一路都用 int 传,中途别转 BigDecimaldouble

int amountFen = 19900;  // 199 元

这样浮点精度问题压根不存在,比较金额直接 ==!= 就行。

顺带一提,微信有些字段给的是元(比如下载下来的账单),对接的时候盯紧单位,别把元和分混了。

商户单号要不要和订单号复用

微信接口要传 out_trade_no(商户订单号),常见的两种做法:

  • 复用自己的订单号:省事,回调带回来的 out_trade_no 直接就能定位订单,少维护一列映射。
  • 单独生成商户单号:更规整,但订单表要多一列,回调时多查一次。

业务简单,复用订单号就够;订单系统复杂、要对接多个支付渠道的,建议单独维护。另外 out_trade_no 有长度限制(32 位以内),订单号别设计得太长。

notify-url 必须是外网可访问的 HTTPS

微信回调只走 HTTPS,而且得是外网能访问到的地址。本地开发要用内网穿透(cpolar、ngrok、natapp 之类的)把 localhost:8080 暴露成公网 HTTPS。

有些穿透工具默认给的是 HTTP,得自己在前面套一层 nginx 或 CDN 转成 HTTPS。

回调应答格式要对

回调处理完,得回一个固定格式的成功应答,不然微信会接着重试:

{"code":"SUCCESS","message":"成功"}

如果是验签失败或者处理时抛异常,返回非 200,让微信重试;但如果是这笔已经处理过了(幂等命中),要回 200 + SUCCESS,告诉微信别再发了。

别全信回调里的字段

回调里虽然带了金额、状态这些,但关键字段(尤其是金额)一定得自己校验。不能看见 trade_state=SUCCESS 就放行,至少要查:

  • 实付金额是不是等于应付金额;
  • 订单还在不在有效期;
  • 这笔交易之前处理过没有(幂等)。

测试和生产商户号分开

不少人图省事,联调和生产用同一个商户号。建议分开。不然测试时一堆异常回调、重复下单会污染生产数据,出问题排查非常难受。

上面这些坑其实不止微信支付,支付宝、Apple Pay 换过来也差不多,记住这几条,换个渠道照样能少走弯路。