支付行业有句行话,没有对账的支付系统都是赌博。下单链路做得再顺,支付回调处理得再稳,只要没有对账这道兜底,你永远不知道系统里藏着多少丢单、漏单、算错的账。线上跑着的支付系统,每天都有零星的回调丢失、事务失败、网络抖动,这些小问题单看都能容忍,积累起来就是资金对不上的窟窿。对账系统的价值,是把这些隐性的资金差错定期捞出来修掉,给整个支付链路兜一张安全网。
对账这件事听起来简单,把两边的账逐笔比对一遍,对不上的挑出来处理。真正做起来,难点全在数据规模和边界口径上。大支付系统一天千万笔交易,对账文件几十 GB,怎么高效比对是个工程问题。两边的时间窗口和清算口径要对齐,差一点就全是假差异,这是经验和踩坑堆出来的细节。
对账在比对什么
图:三类对账差异——长款自动核查、短款自动补单、金额不符必须人工介入,绝不自动调账
对账的核心动作,是把自己系统的账和外部权威数据源逐笔比对。外部数据源是银行、微信、支付宝这些渠道,它们给的账是权威的,因为我方的账始终有出错的可能,渠道的清算结果是最终事实。
两边数据拉齐之后,用交易号做关联键逐笔比对。一笔交易两边都有,金额也一致,才算平了。对不上的就是差异,差异分成三类,长款是我方有渠道没有,短款是渠道有我方没有,金额不符是两边都有但金额对不上。这三类差异的根因和处理方式完全不同,必须分开。
长款往往是我方重复记账,或者渠道回调丢了但我方以为支付成功了。短款通常是掉单,用户那边支付成功了,渠道也记账了,但我方没收到回调,订单还卡在待支付状态。金额不符是最严重的,意味着记账逻辑有 bug 或者数据被篡改,这是资损的直接信号。
比对逻辑的工程实现
图:对账内存匹配流程——双向扫描分类出长款、短款、金额不符三类差异
比对逻辑本身不复杂,复杂的是在大数据量下把它跑得又快又准。
最朴素的做法是逐条查 DB 比对,一笔一笔地两边查出来对。这在千万级数据量下完全不可行,逐条查 DB 的开销会拖垮整个对账任务。生产里的标准做法是把两边的数据都加载进内存,建 hash 索引,用交易号做 key 做内存匹配。一千万笔数据,单机内存够的话几分钟能比对完。
// 比对的核心是两个方向的扫描// 正向,渠道有,查我方有没有for (Record channelRec : channelRecords) { Record myRec = myRecords.get(channelRec.getTxnId()); if (myRec == null) { // 渠道有我方无,短款,掉单 diffTable.insert(channelRec, DiffType.SHORT); } else if (!myRec.getAmount().equals(channelRec.getAmount())) { // 金额不符 diffTable.insert(channelRec, DiffType.MISMATCH, myRec); }}// 反向,我方有,查渠道有没有for (Record myRec : myRecords) { if (!channelRecords.containsKey(myRec.getTxnId())) { // 我方有渠道无,长款 diffTable.insert(myRec, DiffType.LONG); }}数据量再大,单机内存装不下时,按交易号 hash 分片,多线程并行比对,每个分片独立在内存里跑。这套分片并行的思路和大数据处理是一致的,把数据按 key 切开,每个切片能独立完成比对。我在风控平台处理 TB 级发票数据时用的也是这个套路,ES 做辅助索引加 HBase 存大宽表,按维度分片查询,对账比对同理。
数据获取和时间窗口
对账的第一步是把两边的账拉齐,这一步最容易踩口径的坑。
渠道通常在 T+1 提供对账文件,通过 FTP、CSV 或者接口下发。我方的流水从本地 DB 拉。两边的时间窗口必须对齐,关键是要按渠道的清算日期,而不是我方的下单日期。一笔交易可能是我方今天下的单,渠道明天才清算,按我方下单日期对账就会差一天,这笔交易在今天的对账里算差异,明天又自动消失,制造一堆无意义的假差异。
时区也是个大坑。渠道的清算日期可能按 UTC 切,我方的业务日期按东八区切,两个零点不在同一时刻。落在边界上的交易,渠道算今天我方算明天,或者反过来,这种时区错位会制造大量假差异。我有一次遇到对账突然报出几万笔差异,排查下来全是时区口径不一致导致的边界交易错位。对账之前必须把两边的时间口径确认清楚,宁可多花时间对齐口径,也别在对账跑起来之后再去解释一堆假差异。
差异分级处理
不是所有差异都要人工处理,按类型分级能极大降低人工成本。
短款,也就是掉单,处理比例最高也最适合自动化。渠道确认这笔交易成功了,我方却没有,根因通常是回调丢失。自动处理逻辑是查渠道确认状态,确认成功的就直接补更新订单状态,把掉的单补回来。这类差异只要自动补单逻辑写稳,基本不用人工介入。
长款,疑似重复记账或者回调丢失,要自动核查。确认是重复记账的做冲正,确认是回调丢失但实际支付成功的,补记一笔。核查逻辑能覆盖大部分情况,剩下的边界再人工。
金额不符必须人工介入,绝不能自动修复。金额对不上,可能意味着系统 bug、数据篡改、甚至安全事件。这类差异要冻结相关交易,人工审查根因,查清楚了才能动账。自动把金额改一致,会把真正的资损问题掩盖掉,等窟窿大到藏不住了才发现,损失已经不可挽回。
自动修复金额差异,等于把最严重的资损信号人为抹平。我曾经坚持一条原则,任何金额不符的差异,宁可让订单卡在异常状态等人查,也绝不让程序自动调账。自动调账短期看是省人工,长期是放任资损在暗处增长。金额差异是系统出问题的红色警报,要的就是它响起来让人去查根因。
掉单的根源和预防
图:防掉单三道防线——被动回调、主动查询、T+1 对账层层兜底
掉单是对账最常捞出来的差异,根因几乎都在回调链路。
渠道通知我方支付结果,靠的是回调。回调在我方这边失败的常见原因有几类,网络抖动导致回调请求没到,我方服务刚好在重启或者不可用,回调处理逻辑本身报错。渠道的回调通常有有限次重试,重试几次还不成功就放弃了,这笔支付在我方就彻底丢失,订单永远停在待支付。
预防掉单不能只靠被动等回调,必须配主动查询。我方定时扫所有待支付的订单,去渠道查它的真实支付状态,查到成功的就主动更新。主动查询和被动回调互为双保险,回调漏了,查询兜住,查询时机没赶上,下一轮查询兜住。掉单率在这套双保险下能压到极低。
对账是第三道防线。前两道,回调加主动查询,把绝大多数掉单在实时链路里就补上了。对账兜的是前两道都漏掉的那极少部分,用 T+1 的全量比对把它们捞出来。三道防线层层兜底,资金差错才真正无处藏身。
报告和告警
每次对账跑完要生成报告,总笔数、平账数、各类差异数、自动处理数、待人工数,这些数字长期看就是支付系统健康度的体检表。
差异率要设告警阈值。正常情况下对账差异率应该极低,掉单是零星的,差异率万分之几甚至更低。一旦差异率突然飙升,比如从万分之零点几跳到百分之一,几乎一定是系统出了系统性问题,可能是某个渠道接口变了、可能是我方某次发版引入了 bug。这种异常必须立即告警人工排查,不能等到对账报告第二天才有人看。
对于资金高度敏感的场景,日终 T+1 对账的发现周期太长,可以加一层小时级的准实时对账。每个小时把这一小时的交易和渠道对一次,发现差异更早,处置更及时。准实时对账的数据量小,比对快,代价是要做一套增量拉取和比对的链路,适合对资损容忍度极低的业务。
对账系统真正的价值在于提供信心。一个差异率长期稳定在极低水平的对账系统,是支付系统稳定性最有力的事实证明。账天天平,说明资金流转是正确的,那套看起来运转正常的支付系统,真的经得起逐笔核验。







