Ledger 余额资源
存的是「还剩多少」,不是「钱怎么变成这个数」。
职责
| 负责 | 不负责 |
|---|---|
| 余额账户(当前值 + 版本) | 账本(分录 → 余额推导,见 Accounting) |
| 原子扣减(不超卖) | 流水对账 |
幂等变动(movementId) | 多币种 |
| 防负余额 | 分布式事务 |
它不是账本。 账本回答「这笔钱怎么变成这个数」(可复算、可审计); 余额资源回答「现在还剩多少」(O(1) 读、原子扣)。两者不要合并。
数据模型
| 集合 | 内容 | 约束 |
|---|---|---|
ledger_accounts | 余额 + version + updatedBy | path 唯一 |
ledger_movements | 变动流水 | movementId 唯一;pending → done / failed |
路径:/ledger/{accountType}/{accountId},如 /ledger/stock/M-001。
三个保证
原子扣减
typescript
findOneAndUpdate(
{ path, balance: { $gte: amount } }, // 条件与自增在同一次原子操作里
{ $inc: { balance: -amount, version: 1 } },
)并发验证:余额 50,10 个并发各扣 10 ⇒ 恰好 5 笔成功,余额 0。
幂等
movementId 唯一索引先占位(pending),扣减成功后回填 done。 重复提交返回已有结果(duplicate: true),不会重复扣。
防负
默认拒绝扣成负数(409);allowNegative 需显式开启。
接口
| Method | Path | 说明 |
|---|---|---|
| GET | /api/ledger/:accountType/:accountId | 读余额 |
| POST | /api/ledger/:accountType/:accountId/open | 开户(幂等) |
| POST | /api/ledger/:accountType/:accountId/debit | 出账 |
| POST | /api/ledger/:accountType/:accountId/credit | 入账 |
| GET | /api/ledger/:accountType/:accountId/history | 变动流水 |
业务扣减务必带 movementId(幂等键),否则重试会重复扣。
边界
- 未做量测:是否真的比事件流更合适,没有真实数据支撑
- 当前状态:已实现、未验证、默认不启用
- 无流水归档策略;
movementId永久占用 - 单币种整数余额
源码
libs/modules/src/state/
├── schemas/ledger.schema.ts 账户 + 流水
├── services/ledger.store.ts open / balance / credit / debit
└── ledger.controller.ts 5 个端点