第 21 章 账务与对账:支付系统真正的骨骼

所属:第五部分 · 玩家与风险(第 16–21 章) 前置:第 1 章(账本、原子性)、第 7 章(授权与请款分离)、第 18 章(Synapse 账本对不上)、第 20 章(合规动作最终落在账上) 本章新概念:复式记账、分录、对账、破账、幂等、状态机、异步回调

前面所有章节讲过的机制,最后都要落成这一章里的几张表和几条规则。


一、上一章留下的问题

上一章最后问:制裁筛查拦下一笔交易,这笔钱现在算在谁头上?一笔交易退回了,原来的记录怎么处理?某个用户的余额,凭什么证明就是这个数?

Neo Bank 解剖的 Synapse 案例已经给出了答案的反面——账本坏了,钱就等于不存在。

这一章讲怎么让账本不坏。


二、复式记账不是会计偏好,是强制要求

回到支付的本质那张表。一笔转账,Dean 减 100、Alice 加 100,两行必须同时改成功。

复式记账(double-entry,即每一笔资金变动同时记一借一贷,两边金额必须相等)把这条要求变成了数据结构上的约束。

一笔用户充值 100 元的分录(journal entry,即一笔记账动作的最小单位):

科目
银行存款(我们在银行的钱) 100
应付用户款(我们欠这个用户的钱) 100
合计 100 100

关键在于:借贷必须相等,这是系统层面强制的。

它带来的性质是:“钱凭空多出来”或“钱凭空消失”这两件事,在数据结构上不可能发生。 任何余额异常,必然对应着某一笔分录不平衡,而不平衡的分录会被系统直接拒绝写入。

对比一下不用复式记账的做法。

最常见的那一种,叫在用户表上加一个余额字段:数据库里每个用户存一个余额数字,转账就是把一行的余额减 100、另一行加 100,改完就结束。

它的问题不是跑不通——它跑得通,很多系统就是这么起步的。问题在于它只存结果,不存过程

做法 账上存的是什么 出错时的表现 能否发现
用户表加一个余额字段 只有余额这一个数 余额变成了一个错的数 很难,因为没有参照
复式记账 每一次变动的借贷分录,余额由分录算出来 分录写不进去,或一合计就发现借贷两边不等 立刻发现

这就是为什么支付系统必须用复式记账。 余额字段那种做法在功能上能跑通,在出错时无法自证。


三、对账:跟外面对,不是跟自己对

内部账本自洽只是第一步。第二步是拿它和外部对手方的流水逐笔比对,这叫对账(reconciliation)。

要对的对象至少三类:

对账对象 比对什么 差异的常见原因
合作银行的流水 我们账上的余额 vs 银行实际余额 在途资金、手续费未入账
卡组织的清算文件 我们记的交易 vs 卡组织清算的交易 授权未请款、退款时差(三段生命周期
各支付通道的对账单 每笔交易的金额、状态、费用 通道侧状态更新延迟

对不上的那一笔叫破账(break)。

处理破账的唯一正确态度:每一笔都必须查清并归零,不允许挂着。

理由是Neo Bank 解剖那三条教训里的第二条——累积的小差异会在压力下变成不可还原的黑洞。 Synapse 出事时,问题不是某一天突然少了 8500 万,是长期对不平的差异攒到再也拆不开。

一条实操标准:

每日对账,破账当日归零;无法当日查清的,必须有明确的责任人和预期解决时间,且金额单独挂账可见。


四、三个必须掌握的系统性质

这三条是支付系统和普通业务系统最大的区别。

幂等(idempotency)

定义:同一个请求重复发送多次,结果只生效一次。

为什么必须有:网络会超时。客户端发起一笔付款,等待响应时超时了——此时这笔付款到底成功了没有?客户端不知道,只能重试。

有幂等 无幂等
重试识别为同一笔,返回第一次的结果 重试变成第二笔付款,用户被扣两次

实现方式:每笔请求带一个由发起方生成的唯一标识(幂等键),服务端记录已处理过的键,重复的直接返回原结果。

没有幂等的支付接口,出重复扣款只是时间问题。

状态机

定义:每笔交易的状态只能沿着预先定义好的路径流转。

三段生命周期那套卡交易状态举例:

当前状态 允许流转到 不允许
已授权 已请款、已撤销、已过期 已退款(没请款就没钱可退)
已请款 已结算、已退款 已撤销(过了撤销窗口)
已退款 终态 再次退款

不做状态机的后果:出现“已退款又被扣款”、“撤销之后又请款成功”这类非法状态。这类问题在生产环境极难排查,因为数据本身自相矛盾。

异步回调

定义:操作结果通过对方主动通知(回调)来告知,而不是靠同步等待返回。

三段生命周期讲过,卡交易的三段生命周期跨越一到两天。ACH跨越一到三天。没有任何同步接口能等这么久。

配套的三条要求:

要求 原因
回调必须可重试 你的服务可能刚好在宕机
回调处理必须幂等 重试会导致同一个回调收到多次
必须有主动查询兜底 回调可能永远不来,不能只依赖它

最后一条最常被忽略。 只做回调不做主动查询,遇到对方系统故障时,你的交易会永远卡在中间状态。


五、把前面讲过的都串起来

这一章的价值在于它是前面所有内容的落点。检验一下:

前面讲的机制 在账务上的体现
授权不等于扣钱(三段生命周期 授权只记备查,不生成正式分录;请款时才记
ACH 可被退回(ACH 收入在退回窗口关闭前不能确认为最终收入
拒付(三段生命周期 需要一笔反向分录 + 一笔争议费分录
滚存准备金(五类风险 单独科目挂账,到期释放时再转出
预置资金(跨境成本四来源 各国资金池分别设科目,便于监控流动性
制裁筛查冻结(合规骨架 资金转入冻结科目,不计入用户可用余额
FBO 账户(Neo Bank 解剖 内部账本必须能逐笔还原到每个用户

上面这七行是账务设计的最低检查清单。 Neo Bank 解剖 Synapse 那场事故,问题正落在最后一行上。


六、这一章引出的问题

到这里,第五部分讲完了,整个旧世界的骨架也就完整了:

现在可以问那个从Wise 模式末尾就悬着的问题了。原话是:

有没有一种资产,能让全球任意两方之间瞬时转移,且不需要在每个国家各趴一笔钱?

第六部分正面回答它。但回答之前要先把这种资产放回货币层级那张表——它是谁的负债?

下一章从这个问题开始。


七、自检题

  1. 为什么“在用户表上加一个余额字段、转账时直接加减这个数”的做法,在支付系统里不可接受?
  2. 你的服务收到一个支付成功的回调,处理时数据库写入失败。对方会重试。你的系统需要具备什么性质才不会出错?
  3. 一笔走 ACH 收进来的 1000 元,什么时候可以确认为最终收入?

八、参考答案

先自己答完再往下看。

  1. 因为它无法自证。这种做法只存余额这一个数,不存它是怎么变成这个数的。余额被写成一个错误的数值时,系统没有任何参照物可以发现这件事——你不知道正确值应该是多少,也不知道这个错误是什么时候、由哪一笔操作引入的。复式记账要求每次变动同时记一借一贷且金额相等,任何不平衡都会在写入时被拒绝,任何历史值都可以由分录流水重新算出来。前者只存结果,后者存了产生结果的全过程,这才是能被审计和还原的账本。
  2. 需要幂等。回调处理逻辑必须以一个唯一标识(通常是对方的交易号或回调事件 ID)为键,重复收到时识别为同一次并返回成功,而不是重复执行一次记账。同时还需要主动查询兜底——如果对方的重试次数用完了你还没处理成功,你要能自己去查这笔交易的最终状态。只做幂等不做主动查询,仍然可能永久卡住。
  3. 严格说要等退回窗口关闭。消费者账户的未授权退回(R10)窗口最长 60 天,所以最保守的做法是 60 天后确认。实务上通常按风险分层处理:低风险用户和小额交易在几个工作日后确认并计提坏账准备,高风险的延后。关键是这笔钱在到账当天不是最终收入——把它当成最终收入来做资金安排,是这类产品最常见的爆雷原因。

上一章:第 20 章 合规骨架:牌照、身份与监控 下一章:第 22 章 稳定币是什么:回到货币层级那张表