微信支付 Native 扫码收款:后端接入时最容易踩的五个坑
先说适用边界。Native 支付适合 PC 网站、商家收银台这类场景:用户用手机上的微信扫电脑屏幕上的二维码完成付款。Native 不适用于「需要在微信内拉起收银台」的场景——那种需要 openid、走 JSAPI。Native 扫码不需要 openid,用户拿任意微信 App 扫码即可,服务端也不需要额外获取用户身份。
官方文档:
统一下单与 code_url
服务端以 trade_type=NATIVE 调用:
POST https://api.mch.weixin.qq.com/v3/pay/transactions/native
请求体核心字段:
| 字段 | 说明 |
|---|---|
appid | 商户 AppID |
mchid | 商户号 |
description | 商品描述 |
out_trade_no | 商户业务订单号 |
notify_url | 支付结果回调地址 |
amount.total | 金额,单位:分 |
time_expire | 订单失效时间 |
attach | 商户自定义数据,回调时会原样带回来 |
响应返回的关键参数是 code_url,形如:
weixin://wxpay/bizpayurl?pr=xxxxxxxx
这个值不是直接给前端用的,需要服务端/前端自己把它转成二维码图片展示给用户。
两个时效注意点:
code_url有效期 2 小时;- 同一订单号下单选用的
prepay_id也是 2 小时有效。
下单幂等:别每次刷新都生成新订单号
微信支付侧对「同一 out_trade_no」在有效期内是幂等的。也就是说,同一笔业务订单在 2 小时内重复调统一下单,返回的 code_url 是同一个。
如果你的页面一刷新就重新生成业务订单号再去下单,会产生大量待支付脏单。正确做法:
- 业务订单创建后,
out_trade_no固定不变; - 二维码超时/页面刷新时直接用同一个
out_trade_no重新调统一下单,拿到的是同一个code_url。
只有确认该笔订单已关单或已支付,才需要换新订单号。
扫码后“已支付”的判断来源
用户扫码支付是一个完整的交互过程。微信 没有 给网页 JS 提供可靠的“用户支付成功”即时信号。真正可信的支付结果只有两个来源:
- 服务端收到的支付结果回调
notify_url; - 服务端主动调查单接口确认。
先看查单接口与状态。
查单:
GET /v3/pay/transactions/out-trade-no/{out_trade_no}
trade_state 取值:
| 状态 | 含义 |
|---|---|
SUCCESS | 支付成功 |
REFUND | 转入退款 |
NOTPAY | 未支付 |
CLOSED | 已关闭 |
REVOKED | 已撤销(仅付款码支付) |
USERPAYING | 用户支付中,正在确认扣款 |
PAYERROR | 支付失败(其他原因) |
需要特别留意 USERPAYING:它表示用户已经点了确认/正在输入密码或生物识别,但微信还没完成扣款。此时业务上 不能 当作支付成功。
推荐做法:
- 用户在下单成功那一刻,把订单写入轮询任务;
- 前端约每秒轮询一次自己的服务端订单状态接口;
- 服务端订单状态接口在本地订单状态非终态时,主动调微信查单接口兜底,把微信侧的
trade_state与本地订单状态对齐; - 当微信侧连续多次返回
SUCCESS(或收到回调并验签通过)后,本地订单置为已支付,此后前端再轮询即返回“已支付”。
关键一点:前端轮询和后端兜底查单,最终都要汇到 同一个幂等入口。不要在客户端依据任何微信支付后的前端返回值(Web 场景下也基本拿不到)直接置“已支付”。服务端入口统一处理后,前端只消费自己服务端的状态。
未支付订单的超时与关单
code_url 本身 2 小时失效。当业务订单超时(例如你设置的 time_expire 早于 code_url 失效时间),需要做两步:
- 先调查单接口确认
trade_state == NOTPAY; - 再调关单接口。
关单接口:
POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close
边界条件:
- 只能对 未支付成功 的订单调关单;
- 订单已支付成功或已关闭的,调关单会报错(如
ORDER_CLOSED); - 如果你给订单设置了
time_expire,到达该时间后微信侧会自动关单,此时可以不再手动调关单;只有需要提前关闭时才需要主动调。
所以代码写到位的话,关单前必须查单,确认是 NOTPAY 才继续。
接入与排障清单
- Native 场景下单前确认:商品页是 PC/线下扫码,用户用手机微信扫;不需要 openid;不要误走 JSAPI。
- 统一下单请求体里
amount.total单位是 分,不是元。 - 下单响应只取
code_url,自己生成二维码图片后展示给用户。 - 业务层缓存同一
out_trade_no的code_url,重复下单要拿到相同链接。 - 后端配置好
notify_url,回调里做签名验证 + 金额核对(回调里的amount要与本地订单一致)。 - 前端轮询频率别太高,建议 1 秒一次;服务端查单兜底频率建议降低(例如每 5~10 秒一次),避免触发微信接口频率限制。
- 本地订单状态机至少要覆盖:待支付、支付中、已支付、已关闭、已退款。
- 收到
USERPAYING时继续等待,不要置为已支付。 - 关单前先查单,确认
NOTPAY再调关单接口。