Escrow
托管层(L2)负责:钱在什么状态、什么条件下释放、按什么比例分。当前只维护状态与结算指令,不转移真实资金。
职责
| 负责 | 不负责 |
|---|---|
| 托管状态机 | 真实资金冻结 / 划转 |
| 金额守恒等 6 条不变量 | 决定赔付比例(商务 / 法务) |
结算指令(EscrowSettlement) | 汇率换算 |
| 赔付策略接线 | 争议受理(fulfillment) |
| 事实写入状态层 | 对外写端点 |
数据模型
集合:escrows
| 字段 | 说明 |
|---|---|
taskId | 一单一托管 |
amount | 金额(整数最小单位) |
currency | 币种(只记录,不换算) |
status | pending / held / released / refunded / split |
events | 仅追加时间线 |
settlement | EscrowSettlement |
EscrowSettlement
json
{
"outcome": "split",
"toAgent": 70000,
"toBuyer": 30000,
"toPlatform": 0,
"currency": "CNY",
"reason": "missing_items → 合同 §5.2 → 30%"
}状态机
pending ──fund──▶ held ──┬── release ──▶ released
├── refund ──▶ refunded
└── split ──▶ split六条不变量
| 不变量 | 校验 |
|---|---|
| 一单一托管 | taskId 唯一 |
只有 held 能结算 | 状态前置检查 |
| 金额守恒 | toAgent + toBuyer + toPlatform = amount |
| 时间线只追加 | events 无覆写 |
| 终态不可再迁移 | 终态直接拒绝 |
| actor 可定责 | 每次变更记录责任方 |
为什么金额用整数
js
899.99 + 100 !== 999.99 // true浮点误差会让金额守恒失效,因此所有金额以"分"为单位,open() 强校验整数。
策略分工
| 组件 | 回答 | 可替换 |
|---|---|---|
EscrowSettlementPolicy | 怎么分 | ✅ 纯函数 |
CompensationPolicy | 赔多少 | ✅ 纯函数 |
SettlementCoordinator | 把裁定接到结算 | ✅ 独立服务 |
| 策略设置 | 费率 / 上限 / SLA / 担保范围 | ✅ 在线修改 |
CompensationPolicy 的输出必须带依据:
json
{
"amount": 30000,
"reason": "争议事由 missing_items → 条款 §5.2 → 比例 30%"
}没有对应条款且裁定未给金额时返回 409,不猜比例。
与状态层
托管事实写入 /tasks/{taskId}/escrow:
- 强制写入,失败即抛错
- 与 Mongo 写入同一事务
actor区分买家托管 / 平台结算
将来接入资金体系时,只需消费 EscrowSettlement,状态机与痕迹不用改。
结算与分录
结算成功后自动派生分录(挂在 settle() 这个唯一出口,而不是协调器):
借 escrow_held amount
贷 payable_supplier toAgent
贷 refundable_buyer toBuyer
贷 payable_platform toPlatform- 零金额行不输出
- 金额守恒由结算层保证,所以分录借贷平衡是结构保证
- 记账失败不回滚结算(补偿是重放分录,不是重放结算)
详见 Accounting 期间与账务。
参考案例
bash
npm run case:reference # dry-run
npm run case:reference -- --commit # 写入两条路径:
| 路径 | 结果 |
|---|---|
| released | toAgent 100000 / toBuyer 0 |
| split | toAgent 70000 / toBuyer 30000 |
边界与限制
| 限制 | 说明 |
|---|---|
| 不动真钱 | 只产出划转指令 |
| 单币种 | 不换算 |
| 无部分释放 | 一次结算完成 |
| 无退款重试 | 失败需人工处理 |
| 赔付比例是假设 | 缺件 30% / 质量不符 50% / 逾期 10% |
| 无对外端点 | 只读视图 + 策略设置 |
测试
bash
npx jest libs/modules/src/escrow
npx jest --config ./apps/hushwork/test/jest-e2e.json --runInBand \
--testPathPatterns="escrow"源码位置
libs/modules/src/escrow/
├── schemas/escrow.schema.ts
└── services/
├── escrow.service.ts
├── escrow-state.service.ts
├── escrow-settlement.policy.ts
├── compensation.policy.ts
└── settlement.coordinator.ts