首页工程笔记数据模型
legdger · 数据模型

余额为什么不该落库

把余额存成字段,就要在每次改动时同步维护它。这是数据不一致最主要的来源。复式记账给了我们一个更省心的答案。

绝大多数记账应用把余额存在钱包表的 balance 字段里。写入一条支出时,代码里要做两件事:插入这笔交易,然后更新余额。这两件事必须同时成功,否则账就不对了。

这在单机单线程时没问题。但一旦涉及同步、导入、批量对账、并发写入,维护余额字段就变成了一场持续的防御战——每次改动都要考虑「余额更新了吗、更新对了吗、有没有漏掉某个路径」。

换个思路:让余额成为派生值

复式记账的做法是:一条账目不是一个金额,而是一组 Leg(腿)。每个 Leg 是一个「袋子 + 金额」。核心约束只有一条——同一笔交易里,每个币种的 Leg 求和必须为零。

LedgerCore — Leg 求和为零
01// 一条账目 = 一组求和为零的 Leg
02// 午饭 38 元,用招行卡支付:
03transaction(term: .expense) {
04 leg(from: .asset(.cmbCard), amount: -38.00)
05 leg(from: .expense(.dining), amount: +38.00)
06} // sum == 0 ✓
07
08// 余额不存库,永远派生:
09// balance = 期初 + Σ 该袋子下所有 Leg

余额因此变成了一个查询结果,而不是一个需要维护的状态。写入交易时只需要保证 Leg 求和为零——这是可以在内核层用一个校验门强制的事情。

袋子模型:十一类,够用且不膨胀

资产、负债、证券、加密持仓、待对账、分账中间袋……十一类袋型覆盖了个人记账里会遇到的场景。袋型是固定的枚举,不允许用户自定义——这是刻意的收敛,避免数据模型随需求无限膨胀。

代价是什么

  • 查询余额要聚合计算,比读一个字段慢(靠索引与缓存兜)
  • 数据量大时聚合成本上升,需要按时间窗分段
  • 对开发者要求更高:每次写入都必须构造合法的 Leg 组合,不能随手改个数字

我们用一个「写起来更麻烦」的模型,换掉了「余额对不上」这类问题的整类可能性。对记账工具来说,这笔交易很值。

More Notes
Get in touch

把身份、Agent 与私域的复杂度,交给一套可治理的内核

无论你是要替换现有的 IAM、搭建 Agent 中台,还是想把企微私域真正运营起来——先从一次 30 分钟的架构沟通开始。我们会先判断问题是不是我们擅长解的那类,不合适会直接说。