Appearance
对账差错处理模块设计
对账发现差异只是开始,"发现了能收口"才是闭环。本篇设计差错单这个工单对象:DDL、状态机与 CAS 迁移、长款/短款/挂账自动化决策表、SLA 超时升级机制。对账体系的上层视图(三横一纵、对碰算法)见幂等与对账-资金安全底座。对账中心与其他系统的划分见对账中心设计。
差错单的数据结构
对账程序永不直接修数据,只产差异单——差错单是"对账"与"修数"之间唯一的合法通道:
sql
CREATE TABLE recon_diff (
id BIGINT PRIMARY KEY, -- 雪花 id
batch_no VARCHAR(32) NOT NULL, -- 产出差异的对账批次(T+1 文件/准实时)
diff_type VARCHAR(16) NOT NULL, -- LONG(他有我无·长款) / SHORT(我有他无·短款)
-- / AMOUNT(金额不符)
our_order_no VARCHAR(48), -- 我方单号(我有账侧才有)
channel VARCHAR(16) NOT NULL,
channel_txn_no VARCHAR(64), -- 渠道单号(渠道有账侧才有)
our_amount DECIMAL(16,2),
their_amount DECIMAL(16,2),
status VARCHAR(16) NOT NULL DEFAULT 'NEW',
root_cause VARCHAR(32), -- 掉单/重复/程序bug/渠道延迟/误报(闭环必填)
handle_mode TINYINT NOT NULL, -- 0=自动 1=人工 2=升级
deadline DATETIME NOT NULL, -- SLA 到期时刻(产单时按级别算好)
assignee VARCHAR(32),
escalated_at DATETIME,
resolved_at DATETIME,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_batch_diff (batch_no, our_order_no, diff_type) -- 同批同单同类不重复产单
);root_cause 是闭环的落点:处理完成必须归因。月度归因报表反哺幂等与对账规则——差错模块不只是"清队列",是资金系统的缺陷反馈回路。
差错单状态机
┌─────────────────────────────────────────────────┐
│ 超时扫描器:任一在途状态停留 > deadline │
│ → escalated_at 标记 + 通知升级链(不迁状态) │
▼ │
NEW ──归类(自动决策表/人工)──▶ ASSIGNED ──认领/开始──▶ PROCESSING ──修复执行──▶ RESOLVED
│ │ │ │
│ 复对后消失(在途到账) │ 归类错误退回 │ 修数凭证被驳回 │ 复核通过/自动单抽样复核
▼ ▼ ▼ ▼
FALSE_ALARM ◀───────────────────┘ (回 PROCESSING 重修) CLOSED
│ ▲
└── 误报也必须归档 root_cause=误报 ──────────────────────────────────────────┘状态口径映射:前两篇(幂等底座/对账中心)用 DISCOVERED→CONFIRMED→PROCESSING→CLOSED 简化口径;本篇细化为 NEW→ASSIGNED→PROCESSING→RESOLVED→CLOSED(ASSIGNED=认领、RESOLVED=复核),两套映射:NEW≈DISCOVERED、RESOLVED≈CONFIRMED 后置。
状态迁移全部走 CAS(与 star-site 支付单 casStatus 同款:判断压进 WHERE,行锁分出赢家):
sql
-- 认领:两个处理员同时点开,只有一人成功
UPDATE recon_diff SET status = 'PROCESSING', assignee = #{user}
WHERE id = #{id} AND status = 'ASSIGNED'; -- 0 行 = 已被认领,前端提示刷新
-- 关闭:只有 RESOLVED 可关;RESOLVED 停留超复核期自动关(定时批)
UPDATE recon_diff SET status = 'CLOSED', resolved_at = NOW()
WHERE id = #{id} AND status = 'RESOLVED';RESOLVED 与 CLOSED 之间留一道复核缝:修复 ≠ 闭环——红冲蓝补凭证要有人复核,自动处理的单要抽样复核。状态机的不变量:终态只有 CLOSED 与 FALSE_ALARM。其余状态停留超时即升级(升级是标记不是状态,避免状态机被运维动作污染)。
长款短款挂账的自动化处理流
自动化边界是本模块最核心的设计决策,用决策表而非散落的 if:
长款(渠道有钱、我方无账·疑似掉单):
渠道查单接口确认终态 ─┬─ 终态=成功 且 金额一致 ─▶ 【自动补单】走回调同款幂等路径
│ (CAS 入账, 补单不引入新写路径)→ RESOLVED
├─ 终态≠成功 ─▶ FALSE_ALARM(账单早于终态,次日复对)
└─ 查单失败/金额不符 ─▶ 挂账[暂收 2103] + 转人工
短款(我方有账、渠道无钱·疑似误记):
T+1 复对仍无 且 非在途 ─┬─ 金额 ≤ 阈值 且 root_cause 可自动判定 ─▶ 【自动冲正】
│ 红字冲销凭证(双人复核配置)→ RESOLVED
└─ 大额 / 疑似重复入账 / 渠道批量延迟 ─▶ 人工审核升级
金额不符: 永远人工(部分退款/费率口径/币种,自动化误判率不可接受)
挂账(暂收/暂付科目余额>0): 账龄扫描 ─┬─ ≤ N 天 ─▶ 持续计时巡检
└─ > N 天 ─▶ 强制归因 + 升级(蓄水池没排水=事故前兆)两条铁律决定自动化天花板。自动补单只复用正常路径:掉单补录走和渠道回调完全相同的 CAS 幂等入账,天然防补重。见额度中心模块设计的同一哲学:修复动作不引入新写路径。
自动冲正只对小额短款——冲正是改钱。改钱必须落红字冲销凭证并保留人工审批点(大额双人复核)。凭证落账后差错单挂凭证号关单。"自动只补信息、改钱必走审批"是差错模块与资损之间的最后一段缓冲。
冲正凭证的落账形态(短款冲正示例)——差错模块的输出永远是一张凭证 + 一个归因:
短款冲正(我方多记 10300,渠道实际无此笔):
审批通过 → 同一本地事务内两组分录:
① 红字冲销(原样负向对冲)
借:银行存款—渠道往来户 -10300 # 冲掉虚增的"收到钱"
贷:贷款本金—张三户 -10000
贷:利息收入 -300
② 蓝字转挂账(钱去哪了的诚实表达:先挂暂收待查)
借:暂收挂账—对账差异池 10300
贷:银行存款—渠道往来户 10300
→ 差错单回填 voucher_id + root_cause → RESOLVED
→ T+1 渠道资金真实到账后:暂收挂账对冲转正(另一张凭证),挂账余额归零闭环红冲在前、蓝补在后且同事务提交:任何时刻试算平衡都成立(红字组自身借贷相等、蓝字组亦然)。流水表留下"错—冲—正"完整三段。这就是账务系统设计-复式记账与日切的"永不 UPDATE 流水"铁律在差错模块的兑现。
处理时效 SLA 与超时升级
SLA 不是口号,落成 deadline 字段 + 定时扫描器。挂账的账龄巡检另有一条独立的科目级扫描(差错单关了,挂账余额还在科目上挂着):
| 级别 | 判定 | 处理 SLA | 关闭 SLA | 升级链 |
|---|---|---|---|---|
| P0 资金敏感 | 长款已挂账 / 短款待冲正 | 30 分钟响应 | 2 小时 | 组长(15m) → 部门负责人(30m) → 日报通报 |
| P1 一般差异 | 金额不符、查单失败 | 4 小时 | 24 小时 | 日报通报 |
| P2 挂账账龄 | 暂收/暂付余额 > 0 | — | 账龄 ≤ 7 天 | 超 7 天强制归因进周会 |
sql
-- 超时扫描器(每分钟):只标记 + 通知,不代替人处理
UPDATE recon_diff SET escalated_at = NOW()
WHERE status IN ('NEW', 'ASSIGNED', 'PROCESSING')
AND deadline < NOW() AND escalated_at IS NULL; -- escalated_at 幂等:只升级一次
-- 紧随其后按 handle_mode 分发通知(IM/电话),升级链按 escalation 次数递进设计取舍:扫描器只升级不接管。超时的正确反应是把人的注意力拉进来,而不是放宽自动化授权。自动化边界由决策表定义(可自动化的早就自动了)。超时单恰恰是需要"新信息或新决策"的单。交给更高权限的人比交给更大的自动化半径安全得多。升级动作用 escalated_at IS NULL 做一次性谓词,扫描器重复跑不会重复轰炸。
sql
-- 挂账账龄巡检(科目级,独立于差错单生命周期):
-- 暂收挂账户余额 > 0 的每一笔都要能回答"对应哪张差错单、账龄几天"
SELECT e.voucher_no, e.amount, DATEDIFF(NOW(), MIN(l.created_at)) AS aging_days
FROM acc_entry e JOIN acc_entry_log l ON l.entry_id = e.id
WHERE e.account_id = 210301 -- 2103 暂收挂账—对账差异池
GROUP BY e.voucher_no, e.amount
HAVING aging_days > 7; -- 命中即升级:强制归因进周会,禁止"挂着不看"追问链
Q1:为什么长款敢自动补单,短款却要 T+1 复对才敢自动冲正? 不对称的本质是事实强度不同。长款有渠道查单接口的实时终态兜底——文件只是疑点来源,接口终态才是事实。事实确凿即可自动入账。
短款"渠道没有钱"是一个否定性事实,可能只是渠道账单延迟或在途,无法实时证伪。必须等 T+1 文件复对仍无才敢动我方的账。自动化的边界 = 能拿到多强的事实。
Q2:RESOLVED 之后为什么还要一道 CLOSED,自动处理的单能不能直接 CLOSED? 修复动作的正确性和"差异真的消失了"是两个命题。人工修复要复核凭证;自动修复也要抽样复核——自动化规则有 bug 时,抽样复核是唯一的发现通道。全量直关等于放弃了对自动化系统本身的审计,差错率突增时没有缓冲。可以配置"小额自动单直关 + 大额复核",但抽样永远保留。
Q3:FALSE_ALARM 误报直接关闭,会不会掩盖系统性问题? 单笔误报无害,误报率是关键监控指标。同一渠道误报率突增,几乎必然是渠道账单格式变更或我方对碰规则失效。这正是内部对账的②层要报警的信号。所以误报不是"扔掉",是"归档为 root_cause=误报 并进入归因报表",让它以统计形态继续发声。
Q4:为什么差错单不能直接 UPDATE 账务余额,必须绕道冲正凭证? 差错单是工单不是账。若允许它直接改余额:单边修改击穿复式记账的借贷恒等(试算平衡立刻变红)。绕过凭证链导致余额连续性纵切断裂、无审批留痕。红字冲销 + 蓝字补记让每一分修数都携带完整审计轨迹,账务不变量在任何时刻都成立。差错模块的全部输出,最终都是"一张凭证 + 一个归因"。
Q5:超时升级为什么不做成立"到点自动执行处理动作"? 到点超时的单,多是"决策所需信息不在系统里"的单(渠道侧需要人工沟通、修数方案需要业务判断)。自动执行的授权边界在产单时的决策表里已经划完了——决策表没敢自动的,超时更没理由自动。升级解决的是"有人在装睡",不是"流程缺自动"。
Q6:同一天同一笔交易既出长款又出短款(不同批次),怎么防止处理打架?uk_batch_diff 只防同批重复。跨批冲突靠业务键去重:产单前按 our_order_no + diff_type 查在途未关单。命中则合并进既有单(追加批次号)而不是新开单。类似支付域 uk_biz 的"业务在途唯一"思想——工单域同样需要幂等键。
面试官视角小结
差错处理模块的面试考察点,表面是"长款短款怎么处理",实际是三层设计能力。
第一层,能否把"对账发现"与"修数执行"解耦成工单模型并给出状态机。状态迁移全 CAS、终态封闭、超时升级做成标记而非状态,是工程成熟度信号。
第二层,自动化边界的论证。长款自动补单的依据(接口终态兜底 + 复用幂等路径)。短款冲正的克制(T+1 复对 + 审批凭证)。金额不符永不自动。能说出"事实强度决定自动化半径"的候选人是真推演过失败窗口的。
第三层,与账务不变量的关系:修数必须落凭证、误报要进归因闭环、SLA 只拉人不管事。最容易被追问穿的是 Q4。答"直接修正余额"的候选人,说明没理解复式记账是数学不变量而非数据格式。能引用"红冲蓝补留三段审计轨迹"的,才摸到资金系统的门槛。
2026-10 新增(金融信贷域 · 模块设计层)。