3. 银行交易领域建模
在事件溯源(Event Sourcing)中,领域模型不是围绕“当前状态”设计的,而是围绕“发生了什么”设计的。银行交易场景尤其如此:账户余额只是历史事件累积后的投影,真正不可变的事实是每一笔交易、每一次冲正、每一次挂失与解挂。理解这些核心概念及其业务规则,是后续设计事件流、聚合边界和读模型的基础。
核心概念:从状态思维转向事件思维
传统建模习惯先定义账户表、余额字段、流水表,再通过更新语句维护状态。事件溯源要求我们反过来问:哪些业务事实一旦发生就不可撤销?在银行交易域中,至少包括以下几类核心对象。
- 账户(Account):客户资金归属的载体,具有账号、户名、币种、状态等属性。账户状态包括正常、冻结、挂失、销户等,状态变化本身也是事件。
- 交易(Transaction):一次资金变动的业务请求,如存款、取款、转账、消费。交易具有唯一业务流水号、发生时间、金额、方向、对手方等。
- 余额(Balance):账户在某一时点的可用资金与账面资金。余额不是独立事实,而是由所有已入账交易事件推导出的读模型。
- 流水(Ledger Entry / Statement):面向客户或对账的明细记录,通常按时间顺序展示每笔资金变动。流水是余额变化的可读表达。
- 冲正(Reversal):对已入账交易的纠正操作,生成一笔方向相反、金额相等的反向交易,而不是删除原交易。
- 挂失(Loss Report):客户声明账户凭证或卡片遗失,触发账户控制状态变更,限制交易权限。
业务规则与状态约束
银行交易领域有严格的业务规则,这些规则在事件溯源中通常体现为命令校验和事件约束。以下清单列出关键规则。
| 规则类别 | 规则描述 | 事件溯源中的体现 |
|---|---|---|
| 余额约束 | 取款、转账、消费不得导致可用余额为负(除非允许透支) | 命令处理时基于当前余额投影校验,通过后追加交易事件 |
| 账户状态 | 挂失、冻结、销户状态下禁止出账交易 | 账户聚合根据历史事件计算当前状态,拒绝非法命令 |
| 冲正约束 | 只能冲正已入账且未冲正的交易;冲正金额等于原交易金额 | 检查原交易事件是否存在、是否已有冲正事件 |
| 幂等性 | 同一业务流水号重复提交不得产生重复入账 | 事件流中按业务流水号去重,或使用唯一约束 |
| 时序一致 | 余额变化必须与交易事件顺序一致 | 事件按聚合版本号严格排序,投影按序消费 |
案例场景:一笔转账的完整事件流
假设客户 A 向客户 B 转账 500 元。在事件溯源系统中,不会直接更新两条账户记录,而是产生一系列事件。以下用伪代码展示事件流与余额投影的关系。
事件流(按时间顺序):
1. AccountOpened(accountId=A, currency=CNY)
2. AccountOpened(accountId=B, currency=CNY)
3. MoneyDeposited(accountId=A, amount=1000, txnId=T1)
4. TransferRequested(txnId=T2, from=A, to=B, amount=500)
5. MoneyTransferred(txnId=T2, from=A, to=B, amount=500)
6. LossReported(accountId=B, reason=CARD_LOST)
7. TransferRejected(txnId=T3, from=B, reason=ACCOUNT_LOST)
余额投影(读模型):
A: 1000 - 500 = 500
B: 0 + 500 = 500
B 状态:挂失,禁止出账
注意第 6 步挂失事件发生后,B 账户的出账能力被限制。第 7 步展示了一个被拒绝的转账命令,它不会产生资金变动事件,但可以记录为审计事件。冲正场景则是在第 5 步之后追加 TransferReversed(txnId=T2, amount=500),使 A 余额回到 1000,B 余额回到 0,同时保留原始交易和冲正交易的完整痕迹。
误区与复盘:常见建模陷阱
误区一:把余额当作聚合根。余额是投影结果,不是事件源。若把余额作为独立聚合直接修改,就退化为传统 CRUD,失去事件溯源的可审计性。
误区二:冲正时删除原事件。事件存储只允许追加,不允许删除或修改。冲正必须通过反向事件表达,否则审计链断裂。
误区三:挂失只改状态字段。挂失应作为领域事件追加,读模型根据事件序列计算当前状态。这样可追溯挂失时间、操作人、解挂历史。
误区四:流水与事件流混为一谈。事件流是内部事实记录,流水是面向客户的读模型。两者结构不同,流水可按客户习惯排序、分页、脱敏。
复盘时可以用一个简单问题检验模型是否合格:“如果系统崩溃,仅凭事件流能否重建所有账户余额和状态?” 如果答案是否定的,说明仍有状态被隐藏在事件之外。银行交易领域建模的关键,就是把账户、交易、余额、流水、冲正、挂失都统一到“事件是唯一事实来源”这一原则上。