支付回调幂等与对账怎么设计?一次讲清重复通知、状态校验、补单与差异修复
大家好,我是一名有 4 年工作经验的 Java 后端开发。
支付回调看起来只是一个回调接口,但真正做过的人都知道,这几乎是订单系统里最容易出问题、也最需要兜底的一环。
这篇文章我想系统聊一聊支付回调幂等和对账到底应该怎么设计。
🦅个人主页
🐼
文章目录
- 支付回调幂等与对账怎么设计?一次讲清重复通知、状态校验、补单与差异修复
- 一、为什么支付回调特别关键
- 二、为什么支付回调一定会重复
- 三、最推荐的处理思路
- 四、幂等的核心怎么做
- 五、为什么还要做对账
- 六、推荐的表设计
- 6.1 支付流水表
- 6.2 回调日志表
- 七、对账怎么做
- 7.1 拉第三方账单
- 7.2 和本地支付流水比对
- 7.3 差异单入库
- 7.4 补单或人工处理
- 八、最容易踩的坑
- 8.1 支付回调里不做状态幂等
- 8.2 回调里同步做太多事情
- 8.3 只依赖回调,不做对账
- 8.4 不校验金额和商户信息
- 九、面试中怎么回答
- 十、总结
- 十一、结尾
一、为什么支付回调特别关键
因为支付回调一旦设计不好,后果通常很直接:
- 用户已经支付成功,但订单还是待支付
- 回调重复通知,后续逻辑重复执行
- 订单状态改了,但积分、发货、库存没跟上
- 支付平台成功了,你本地却没记账
也就是说,支付回调不仅是一个接口问题,更是:
订单状态流转和资金结果对齐的核心入口。
二、为什么支付回调一定会重复
很多支付平台的通知语义本来就是:
- 至少通知一次
所以这些情况都很正常:
- 重复通知
- 延迟通知
- 回调超时后再次通知
- 先异步回调,后主动查询
因此支付回调最核心的要求就是:
必须幂等。
三、最推荐的处理思路
我更建议支付回调按这个顺序设计:
- 验签
- 校验商户号 / 应用号
- 校验订单号和支付金额
- 用条件更新做状态流转
- 只对第一次成功流转执行业务后续
- 写支付流水和回调日志
- 后续链路做异步化
四、幂等的核心怎么做
最常见也是最稳的做法之一是:
updateorder_infosetstatus='PAID'whereorder_id=#{orderId}andstatus='WAIT_PAY'如果影响行数是:
1:说明第一次处理成功0:说明这笔订单已经处理过,后续直接幂等返回
这样做的好处非常明显:
- 简单
- 天然幂等
- 不依赖额外分布式锁
五、为什么还要做对账
因为支付回调再好,也不能假设:
- 所有通知一定都能成功到达
- 所有系统状态一定都能实时一致
所以支付系统里通常还要有对账能力,解决这些问题:
- 第三方显示成功,本地订单没成功
- 本地支付流水缺失
- 少单
- 差单
也就是说:
回调解决实时一致性,对账解决事后修正。
六、推荐的表设计
6.1 支付流水表
CREATETABLEpay_order(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_idBIGINTNOTNULL,pay_noVARCHAR(64)NOTNULL,third_trade_noVARCHAR(64)DEFAULTNULL,amountDECIMAL(10,2)NOTNULL,statusVARCHAR(16)NOTNULL,paid_atDATETIMEDEFAULTNULL,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,UNIQUEKEYuk_order_id(order_id));6.2 回调日志表
CREATETABLEpay_notify_log(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_idBIGINTNOTNULL,third_trade_noVARCHAR(64)DEFAULTNULL,notify_bodyTEXTNOTNULL,process_resultVARCHAR(32)NOTNULL,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP);这样后面排查重复通知、差异单都更方便。
七、对账怎么做
常见对账思路:
7.1 拉第三方账单
定时拉:
- 支付成功单
- 退款单
7.2 和本地支付流水比对
比对结果可能出现:
- 第三方有,本地没有
- 本地有,第三方没有
- 金额不一致
- 状态不一致
7.3 差异单入库
比如建一张差异表:
reconcile_diff
记录:
- 差异类型
- 本地状态
- 第三方状态
- 补单状态
7.4 补单或人工处理
一些差异可以自动修复:
- 自动补写支付成功
- 自动重试回调逻辑
一些差异则需要人工介入。
八、最容易踩的坑
8.1 支付回调里不做状态幂等
这是最大坑之一。
8.2 回调里同步做太多事情
比如:
- 改订单
- 发积分
- 发 MQ
- 推通知
- 更库存
全都同步做,很容易把回调接口拖慢。
更稳妥的是:
- 先把支付成功状态落稳
- 后续动作异步化
8.3 只依赖回调,不做对账
这样少单问题会长期积累。
8.4 不校验金额和商户信息
这会非常危险。
九、面试中怎么回答
如果面试官问你:
支付回调幂等和对账一般怎么做?
你可以这样回答:
第一,支付回调一定要按至少一次通知来设计,所以核心必须做幂等。最常见也最稳的方式就是通过订单状态条件更新,只允许订单从WAIT_PAY更新到PAID一次。
第二,回调处理时我会先做验签、商户号校验、金额校验,再去更新订单状态和支付流水。只有第一次成功流转时,才触发后续异步业务,比如发积分、通知库存、推送消息等。
第三,支付回调不能代替对账,因为真实线上一定会存在通知丢失、处理失败或状态不一致的问题,所以通常还会有定时对账任务,去拉第三方账单和本地支付流水做比对,识别差异单后再补单或人工处理。
十、总结
支付回调最怕的不是重复通知本身,而是:
- 没有幂等
- 没有日志
- 没有对账
- 没有补单
如果只记一句结论,我觉得可以记住这句:
支付回调负责尽快把支付结果落稳,对账负责兜底修正差异,幂等和补单是这条链路里绝对不能少的两层保险。
十一、结尾
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注。
后面我会继续整理一些更偏实战的 Java 后端和电商系统设计文章。