不能只靠回调

支付平台一般是异步回调通知商户:用户付完钱,平台往你的 /notify 地址 POST 一个请求,告诉你”这笔订单付了”。

但回调这东西不能全信。网络抖动、服务器重启、notify-url 改了、DNS 出问题、甚至平台那边延迟,都可能让回调丢掉或者晚到。只靠回调,迟早会遇到”用户钱都付了,你这边还不知道”这种情况。

所以支付状态同步一般做成:异步回调(推)+ 主动查单(拉),两条腿走路。

推和拉各管什么

支付平台                 商户服务
│ │
│ ① 用户完成支付 │
│ │
│──── POST /notify ────▶│ 推:被动接收支付结果
│ │
│◀── 返回 SUCCESS ──────│
│ │
│ │
│◀── GET /queryOrder ───│ 拉:主动查询支付结果
│ │
│──── 返回交易状态 ─────▶│
  • 推(回调):实时性好,正常情况用户付完几秒就通知到了。
  • 拉(查单):兜底,处理回调丢了、晚了、或者用户主动刷新页面的情况。

回调与查单共用同一份落账逻辑

不管支付结果是从回调来的还是查单来的,最后都得进同一个落账函数。这是数据一致性的根本。

如果回调写一套、查单写另一套,时间一长,”回调能过、查单过不了”或者两边状态对不上这种 bug 迟早会出现。尤其金额校验、幂等判断、发放权益这几步,必须收到一个函数里。

回调入口 ──┐
├──▶ markPaid(outTradeNo, transactionId, paidAmountFen)
查单入口 ──┘

markPaid 内部依次做:状态机检查 → 金额校验 → 幂等判断 → 更新订单 → 发放权益。这个函数只写一份、测透,两条入口都调它,谁也不许自己另起一套。

查单什么时候触发

常见的三种时机:

  1. 用户主动触发:页面上点”我已支付”,前端调你的查单接口,你再回头去查支付平台。
  2. 轮询:客户端等支付的时候定时轮询你的接口,你这边定期查平台。
  3. 定时补偿任务:扫库里”挂了很久、快过期”的订单,批量去查单。

三种可以叠加着用。用户主动触发最快,轮询覆盖一般场景,定时任务捞异常单。

回调和查单返回怎么处理

回调收到后,得按平台要求的格式回一个成功应答(微信是 {"code":"SUCCESS"}),不然平台会认为通知失败、接着重试。

查单更像普通接口调用:拿到状态,如果已支付就调共用的落账函数;没付就记一下状态,接着等。

对账

推 + 拉能盖住大多数情况,但还是不够保险。每天都得跑一遍对账:

  • 把本地所有已支付订单和平台的账单核对一遍;
  • 挑出”本地已付但平台没记录”或”平台已付但本地没处理”的异常单;
  • 这些单子走人工或自动的退款 / 补单流程。

对账这道别图省事省掉,它是你最后能发现钱对不上的机会。

回过头看,其实就是不把鸡蛋放一个篮子:回调来得快但靠不住,那就再主动查单补一道;两边的钱最后都得进同一个落账函数,免得对不上。哪怕这样还是会有漏网的,所以每天再跑一遍对账兜个底。推、拉、对账,三样凑齐了,支付状态同步这事儿才算踏实。