银行扣款了天塌了也要记下来——回调接口的原子记账设计文章目录银行扣款了天塌了也要记下来——回调接口的原子记账设计一、银行扣款和普通业务不一样二、异常分类不是所有失败都能一样处理三、BaoPanTimer的教训先判断是否已处理四、TRADE_LOG的思路不删数据插一条负数五、异常的优先级先落地再处理六、扣款序号 回执确认 对账三道防线七、银行扣款和医保结算的区别八、结语一、银行扣款和普通业务不一样普通的接口调用请求来了→处理→返回。失败了可以重试重试失败了可以告警告警了可以人工介入。整个过程是可逆的——在最终确认之前一切都可以回滚。银行扣款不一样。银行告诉你扣了150.36元这150.36元已经从参保人的银行卡里划走了。你不能跟银行说我这边出错了你把钱退回去——退钱要走退费流程不是回调失败就能自动撤销的。所以回调接口的设计原则不是成功了才记而是任何情况下都要记下来。二、异常分类不是所有失败都能一样处理银行回调过来的信息通常是一条报文批次号、扣款明细列表、总金额、回执时间。处理这条报文时可能遇到的异常异常类型场景能不能重试记不记账网络超时报文到了但响应没返回能银行会重发先记重试成功后状态改已确认金额不一致银行扣的金额和社保算的不一样不能要人工核对记标记待核对报文格式错误能读但字段对不上不能要人工核对记存原始报文错误原因数据库挂了处理到一半写不进去能恢复后重放记重放时判断是否已记重复回调银行发了两次不用处理查回执确认表已处理则跳过关键点除了重复回调可以跳过其余所有情况都必须先把原始报文存下来。存下来之后能不能自动处理、要不要人工核对——那是第二步的事。第一步不能丢数据。三、BaoPanTimer的教训先判断是否已处理银行回调不是每次都准时到。有时候网络通了报文才到有时候银行系统重发。社保系统里的BaoPanTimer就是干这个的——定时检查银行回执到了没没到就重发请求到了就处理。处理之前有一个铁律先查回执确认表判断这批数据是否已经处理过。银行的扣款报文里每笔都有唯一的扣款序号这个序号就是天然的幂等键——同一笔扣款不会重复入账。查回执确认表只是多一层保险序号匹配上了就跳过匹配不上才是新数据。四、TRADE_LOG的思路不删数据插一条负数银行的扣款记录入库后万一发现某笔扣错了——比如同一个人被扣了两次——怎么办不是delete。是插入一条金额取负的记录同时把原记录的 TRADE_LOG 标记为已冲正。原记录: batchNo20260521, personId10001, amount150.36 冲正记录: batchNo20260521, personId10001, amount-150.36 TRADE_LOG: 原记录状态3(已冲正)新记录冲正流水号为什么delete不行因为审计的时候审计员要看的不只是现在的余额是多少还要看这150.36是什么时候扣的、谁的卡、后来为什么退了。TRADE_LOG表不是给程序员用的——是给审计查账用的。银行对账单过来了社保这边给你一张清单每一笔扣款、每一笔冲正、每一条对账结果。差一分钱都能定位到具体哪笔记录。五、异常的优先级先落地再处理回到最初的场景——银行回调接口收到了扣款报文处理流程应该是这样的收到报文 ├── ① 原始报文写入日志表无论后续发生什么这步必须完成 │ 字段批次号、报文原文、接收时间、处理状态待处理 ├── ② 解析报文校验格式和金额 │ ├── 格式错误 → 日志表状态格式错误结束 │ └── 格式正确 → 继续 ├── ③ 查询回执确认表判断是否重复 │ ├── 已处理 → 日志表状态重复报文结束 │ └── 未处理 → 继续 ├── ④ 金额校验银行扣款金额 vs 社保应扣金额 │ ├── 一致 → 状态自动处理 │ └── 不一致 → 状态待人工核对结束 ├── ⑤ 写入扣款记录到业务表更新个人账户余额 └── ⑥ 回执确认表标记已处理每一步都可以失败但第①步不能失败。如果连第①步都失败了——数据库连不上、磁盘满了——那回调接口应该返回失败让银行重发。这和你之前的BaoPanTimer配合起来社保这边没收到就重发收到了就一定落地。六、扣款序号 回执确认 对账三道防线银行回调处理有三层保障第一层扣款序号。银行报文中每笔扣款有唯一序号入库时它就是主键。同一笔扣款银行重发了插入直接报主键冲突——物理上不可能重复入账。第二层回执确认表。比主键约束多一层业务语义——不仅知道这条数据见过了还知道这批数据整个处理完了。第三层每日对账。就算前两层都没拦住的异常对账会兜底。每天银行给社保发对账文件社保系统做两件事按批次对总数银行批次总金额 - 社保系统该批次已入账总金额 0不等就查差异按个体对明细把银行报文里的每笔扣款和社保系统里的入账记录逐条比对任何人被扣了社保没记、或者社保记了银行没扣都能定位到具体的人对账不是在自动化处理的链路里——它是异步的、离线的、独立的。正因为它不在处理链路里它才能作为最终兜底。前面的序号冲突、回执确认都是实时防御对账是事后校验。三道防线合在一起才敢说银行扣了的钱社保一定记了。七、银行扣款和医保结算的区别银行扣款记录的只是钱被划走了不涉及业务规则校验。医保结算复杂得多——账户余额够不够、统筹支付封顶了没有、大病补充怎么算——算错了一个比例就出财务事故。所以银行回调的记账是无条件记录医保结算的记账是规则校验后记录。前者是只要发生了就要记后者是满足条件才能记。但两者有一个共同点记下来就不能删只能冲正。这是TRADE_LOG设计贯穿所有资金类业务的根本原因——不管钱是怎么动的动了就要留痕。八、结语银行扣款回调接口的设计核心不是怎么写代码是**“异常情况下的数据不丢失”**。网络超时、金额不一致、数据库挂了、银行重发了——每一种情况都有一个最低要求原始报文必须先落地。落地了后面的事可以慢慢处理——格式错了人工修正金额不对人工核对重复了跳过。但没有落地这一步后面的所有处理都没有依据。财务对账的时候问这笔钱银行说扣了社保这边怎么没有你拿不出任何记录——这就是事故。