Skip to content

履约与托管 ​

执行链路解决"把事情做完",履约与托管解决"做完之后,责任和钱怎么算"。这是平台从工具变成交易基础设施的关键一步。


两层分工 ​

L1 履约(fulfillment)              L2 托管(escrow)
交付 / 验收 / 争议 / 裁定      ──▶   资金状态 / 结算指令 / 赔付
事实与责任                          金额与划转
L1 履约L2 托管
回答是否交付、是否验收、谁的责任钱在什么状态、怎么分
存储fulfillment_deliveries 等escrows
强约束状态迁移表 + 3 条不变量状态机 + 6 条不变量
对外只读运营视图只读运营视图 + 策略设置

两者都不直接对外提供写接口:履约字段的形状仍在等真实样本校准,过早固化会限制业务表达。


L1 履约 ​

实体 ​

实体作用
Delivery一次交付
Acceptance对交付的验收
Dispute争议
Resolution裁定

状态机 ​

Delivery:  created → delivered → settled
                         └─────→ superseded

Dispute:   raised → under_review → resolved → closed
                    ↕
              awaiting_evidence

三条不变量 ​

  1. 一次 Delivery 只能被验收一次——重交必须新建 Delivery 并用 supersedes 关联
  2. timeline[] 只追加,从不覆写历史
  3. 先定责再关闭——Resolution 只能挂在 under_review / awaiting_evidence 的 Dispute 上

并发控制 ​

状态迁移使用"当前状态必须匹配"的条件更新。两个并发迁移会有一个失败并返回 409,而不是静默覆盖。


L2 托管 ​

状态机 ​

pending ──fund──▶ held ──┬── release ──▶ released   终态
                         ├── refund  ──▶ refunded   终态
                         └── split   ──▶ split      终态

六条不变量 ​

不变量含义
一单一托管一个 Task 只能有一个 Escrow
只有 held 能结算其他状态不可释放/退款/拆分
金额守恒toAgent + toBuyer + toPlatform = amount
时间线只追加不可改写历史
终态不可再迁移released / refunded / split 是终点
actor 可定责每次状态变更都有责任方

金额使用整数最小单位(分)。原因很直接:在 JS 浮点下 899.99 + 100 !== 999.99,金额守恒会失效。因此 open() 强校验整数。


谁决定怎么分 ​

这是最容易含糊的地方。平台把职责拆成四个独立部分:

组件回答的问题位置
EscrowService状态能不能转、金额守不守恒escrow.service.ts
EscrowSettlementPolicy怎么分(纯函数,可整条替换)escrow-settlement.policy.ts
CompensationPolicy赔多少(事由 → 条款 → 比例 → 金额)compensation.policy.ts
SettlementCoordinator把 L1 的裁定接到结算上settlement.coordinator.ts
商务 / 法务费率、上限、SLA、担保范围策略设置

CompensationPolicy 的输出会写明依据:

json
{
  "amount": 30000,
  "reason": "争议事由 missing_items → 条款 §5.2 → 比例 30%"
}

如果事由没有对应条款、裁定也没有给金额,返回 409,不猜比例。


跨层事务 ​

履约与托管的每一次 mutation 都同时写 Mongo 与 L0:

FulfillmentService / EscrowService
  ├─ Mongo 业务写入
  └─ L0 状态写入
        └─ 同一事务
             ├─ 都成功 → commit → 派发订阅
             └─ L0 拒绝 → Mongo 回滚

被覆盖的 mutation:

  • 履约 6 个:创建交付、确认交付、验收、发起争议、裁定、关闭
  • 托管 3 个:fund、release/refund、split

这条约束防止出现"钱划转了但查不到"或"状态写了但业务表没有"。


参考案例 ​

仓库里有一条可执行的完整业务:

bash
npm run case:reference              # dry-run
npm run case:reference -- --commit  # 真库写入

流程:

支付 → 托管(held)
  → 交付(delivered)
  → 验收(rejected)
  → 争议(missing_items)
  → 裁定(按条款赔 30%)
  → 结算(held → split:toAgent 70000 / toBuyer 30000)

另一条分支:

bash
npm run case:reference -- --branch=disputed --commit

参考案例不是"真实成交",它的作用是让整条机制第一次可以被跑出来、查出来、测试住。


已知边界 ​

边界说明
不动真实资金只产出 EscrowSettlement 划转指令
赔付比例是假设基线缺件 30% / 质量不符 50% / 逾期 10%,需商务确认
担保范围默认 process只担保"按时交了",不担保质量
争议分类法未定grounds 枚举仍需法务输入
超时默认 hold不自动放款,避免误判
履约字段形状待校准交付物、异议、裁定的真实结构需要样本

测试与验证 ​

bash
# 履约:状态机 + 不变量
npx jest libs/modules/src/fulfillment

# 托管:状态机 + 金额守恒 + 策略
npx jest libs/modules/src/escrow

# 跨层事务 e2e
npx jest --config ./apps/hushwork/test/jest-e2e.json --runInBand \
  --testPathPatterns="cross-layer-transaction"

下一步 ​

Released under the MIT License.