Files
caiwuzongzhang/docs/decisions/005-canonical-transfer-matching.md
T

3.8 KiB
Raw Blame History

005 规范转账事件与双边归并技术决策

对应 IssueB-43/B-83/B-114docs/issues/006-p1-canonical-transfer-matching.md)。

分层模型

  • 银行源行是不可变观察;匹配层从不修改 source_rows
  • 新增“不可变决定日志 + 可重建当前投影”的规范事件层:
    • transfer_match_decisions 等证据表只增不改(UPDATE/DELETE 被触发器拒绝)。
    • current_transfer_decisionstransfer_observation_claims 是可更新、可重建的 当前投影,source_row_id 主键保证一条观察不可能同时属于两个当前事件。
  • B-44 余额计算只读 eligible_intercompany_events 视图,只含 active + current + classification='intercompany';单边内部、待审、同公司调拨、 外部事件全部排除。

已确认业务口径(B-114

  1. 单边流水即使双方公司都能由批准账号唯一确认,也先进入未决金额,不进入 B-44 已确认往来余额;只有完成双边归并(intercompany + paired)或管理员按证据确认 intercompany + locked 单边)后才可计算。
  2. 跨日事件经济日期取付款方 outgoing 银行入账时间,与导入顺序无关。
  3. 自动窗口 v1:非空参考号相同且账号镜像 <= 3 自然日(M1);无参考号精确账号镜像 <= 1 自然日(M2);别名/个人映射镜像 <= 1 自然日且参考号或摘要相等(M3)。 超窗或同层多候选全部进入人工审核,禁止按行号、导入顺序或名字破平局。
  4. 个人过账映射绑定具体账号、生效区间、允许方向和代表公司,必须管理员批准; 姓名只作辅助证据,不能单独定案。

参与方解析

  • 本方:源行 own_account 精确命中生效期内已批准账户(own_exact);缺失时才用 上传时持久化的批准上传账户(upload_account),且必须与批次公司一致。冲突进入 审核,不以 company_id 或户名反推。
  • 对方:严格按账号层解析——精确账号 -> 已审核账号别名 -> 已批准且在窗口内、方向 允许的个人过账映射;每层唯一最佳结果才继续。户名/户名别名只用于一致性/冲突证据, 冲突降级到审核。
  • 方向:expense > 0 且 income = 0 为 outgoingincome > 0 且 expense = 0 为 incoming;双正/双零/负数/缺币种进入 unresolved。金额用 Decimal 精确相等, 不设手续费容差。

匹配与状态机

  • 候选硬门槛:双方工作表 confirmed、方向相反、金额与币种相等、公司端点互为反向。
  • M1/M2/M3 唯一候选自动 paired;R1(端点可确认但镜像/参考号证据不足)与 R2(同层 多候选)进入 needs_review;待审行不能被其他自动匹配抢占。
  • 状态:unresolved -> internal_single -> matchedneeds_review -> matched | same_company_transfer | external;单边确认两端同公司立即为同公司调拨。
  • 幂等:rule_version + 当前观察集合 + 证据未变时零写入;locked 人工决定自动重跑 跳过;纠错只能通过 reversemode='reversal' 新记录)+ 原因,之后允许重跑。

事务、并发与回滚

  • “确认工作表 + 为新增确认行建立/更新事件 + audit”在同一个服务事务,任一失败整体 回滚,不存在 confirmed 但未匹配的半成品。
  • 独立重跑使用 BEGIN IMMEDIATE,按 source_row_id 升序写,SQLite 30 秒 busy timeout 保留;投影可从只增日志重建(rebuild_current_projection)。
  • migration down 只用于测试/上线前回退;生产产生决定后默认保留 migration 5 与审计 数据,不执行破坏性 down。

Decimal

金额全程以规范十进制字符串存储和比较,任何路径都不转 float100.0100.00 视为相等,100.00100.01 不匹配。