四类事实形态与域模型
平台不是「所有数据都进事件流」,也不是「每个业务一套机制」。 统一的是寻址、因果、权限三个契约;存储形态按事实形态选择。
为什么需要这个模型
企业里同一个对象在各系统中状态不同步,根源不是系统太多,是写入路径太多。
所以平台的做法是:每个事实只有一个可写路径,且每次写入都留下带因果的证据。 但不同事实的形态差异很大——订单需要因果链,库存需要原子扣减,物料需要唯一约束。
把它们塞进同一种存储,必然有地方别扭。于是按形态分四类。
四类事实
| 形态 | 用来存 | 存储 | 为什么 |
|---|---|---|---|
| 事件 | 订单、期间、交付、审批 | state_events + state_entries | 有「谁在何时做了什么」,需要因果与定责 |
| 余额资源 | 库存、配额、积分 | ledger_accounts + ledger_movements | 要 O(1) 读余额 + 原子扣减 |
| 主数据 | 物料、供应商、客户 | master_entities + master_aliases | 要唯一约束 + 实体合并 |
| 计算 | 定价、MRP、排程 | computation_runs | 是算法,不是状态;只存运行证据 |
四类共享同一个访问契约:
typescript
interface IDomainStore {
read(path) / write(path, value, actor) / history(path) / list(namespace)
}所以「加一个业务域」= 选一种存储 + 一份声明,而不是再写一套因果与审计。
域与命名空间
/tasks/{taskId}/deliveries/{id} domain=tasks namespace=/tasks/{taskId}
/orders/{orderId}/lines/{id} domain=orders namespace=/orders/{orderId}
/periods/{periodId}/entries/{id} domain=periods namespace=/periods/{periodId}
/master/{entityType}/{code} domain=master namespace=/master/{entityType}| 概念 | 定义 | 作用 |
|---|---|---|
| 域 | path 第一段 | 治理单位:权威、期间策略、写入白名单 |
| 命名空间 | path 前两段 | 串行单位:序号单调、复放边界 |
一个域下可以有多个命名空间(每个实体一个),同一实体的写入串行、不同实体并行。
域声明与默认拒绝
每个域必须显式声明:
| 字段 | 作用 |
|---|---|
factClass | 选哪种存储;声明后不可更改(换类别 = 数据迁移级事件) |
authority | native(我们说了算)/ mirrored(外部说了算)/ federated |
periodPolicy | 这个域是否参与期间结账 |
writers | 域级写入白名单 |
未声明的顶层域写入被拒——新增域必须是显式决策,不是偶然结果。
越权在机制上不可能
三条约束都在存储层执行,不在应用层:
| 约束 | 强制点 | 绕得过吗 |
|---|---|---|
| 谁能写哪种事实 | 写入权威表 | 绕不过(写入口唯一) |
| 已关闭期间不接受新分录 | 期间门禁 | 绕不过(在 writeMany 里) |
| 已合并的主数据不会读错 | 主数据存储内部解析 | 绕不过(读路径统一) |
| 余额不会被扣成负数 | 数据库条件更新 | 绕不过(原子操作) |
不靠调用方自觉,靠机制拒绝。
一条业务串起四类
① 物料(主数据) /master/material/M-001
② 算价(计算) pricing v1 → 100000
③ 订单(事件) /orders/{orderId}
④ 预留库存(余额) /ledger/stock/M-001 debit 1 → 余额 99
⑤ 交付 / 验收(事件) /tasks/{taskId}/deliveries|acceptance
⑥ 结算(事件) escrow.settle
⑦ 分录(账务) /periods/task:{taskId}/entries/{entryId}这条链路可以用一条命令跑完并打印试算平衡表:
bash
npm run case:reference -- --commit同一事实只有一个权威
这是跨四类的最重要规则:
✅ 订单行存 materialCode
❌ 订单行再存一份 materialName(物料改名时两边就漂移了)
✅ 分录存 sourceEventId,需要金额时回查
❌ 分录冗余存订单金额(除非是结账时点快照,且带 snapshotAt)违反它就等于把多系统的问题搬进同一个数据库。
边界
| 不做 | 原因 |
|---|---|
| 通用会计引擎 | 只做托管池内部怎么分;多准则、税务不在范围 |
| 全量数据集成 / 数据中台 | 会变成 ETL 产品,且没有实体解析能力 |
| 主数据自动概率匹配 | 相似度阈值需要真实数据才能调 |
| 计算的分布式执行 | 当前算法足够轻 |