Lifecycle 承载审批业务
用 @nocobase/lifecycle 实现 28 个传统审批场景:能不能做、哪里不够、审批该怎么抽象,以及接下来改什么。
limitation:文档
按建议的阅读顺序排列。"阅读"打开渲染后的页面,文档间的链接也能直接跳转;"原文"是 Markdown 源文件。
lifecycle.md(库设计笔记)和 ../2026-09-06-v3-approval/approval-scenarios-and-design-constraints.md。这两份没有放进来,点这些链接会显示"找不到"。原文在 worklog/ 下。主要结论
1可行:能承载传统审批
28 个场景全部能表达、能跑通,所有 P0 要求都达成,期间没有改动库本身。同事务提交、乐观并发、请求去重、副作用重试与恢复都按预期工作。
2库只是执行内核
环节、责任人、决定、汇总规则、内容绑定、待办都是应用代码。另有 5 个库缺口在多个场景里反复出现,需要优先补齐。
3应当抽象成公共模块
两种不抽象的写法都验证过,各有代价:
- 手写状态机:772 行里业务相关只有几十行;
- JSON 计划 + 运行时解释:状态退化为粗粒度,计时、按钮可用性、待办查询都变差。
4两层结构,内层不是状态机
外层环节由声明生成真实状态。环节内多人用"任务集合 + 决策策略",每条任务有一个框架统一提供的小状态机。加签、转签、领取都落在任务层。
5必须有统一待办
需要任务表 + API,否则只能扫描业务表,委托和重派也无处落地。是否扩展到单人流转、人与 AI 协同,先由产品定义,技术上只保证将来可扩展。
6流程版本
- 在途审批按提交时的版本走完;
- 结构性改动必须升版本,由指纹检查强制;
- 旧版本保留到没有在途实例为止;
- 在途迁移是显式、逐条、留痕的管理操作。
28 个场景与达成度
按主题分组,点击跳到 scenario-cases.md 中的场景定义。标注的优先级取自 lifecycle-evaluation.md。
库层面的缺口
按影响面排序,高亮的前 5 项在多个场景里反复出现。详见 lifecycle-evaluation.md,其中标注了源码位置。
| # | 缺口 | 影响场景 | 为什么重要 |
|---|---|---|---|
| 1 | 转换内不能在同一事务中触发或创建其他记录 | 9 10 19 25 28 | 父子汇总只能最终一致。这是审批抽象的前置条件:一次表态要在同一个事务里完成写任务、计票、推进环节和推进业务记录 |
| 2 | 自转换必然刷新 statusChangedAt 并重跑 onEnter | 10 16 18 23 25 26 | 转签、投票、加人都会重置超时、重发通知。需要"内部转换" |
| 3 | 触发器读不到记录字段,也不能加过滤条件 | 17 18 27 28 | 无法按订单截止时间或任务 dueAt 计时,也无法按版本过滤 |
| 4 | route 只能同步,且和 set 各算一次 | 9 10 19 … | 同一份计算做两遍,汇总需要两次转换 |
| 5 | 没有字段保护,版本号只随转换变化 | 1 2 21 23 27 | 审批中的内容可以被直接写入绕过 |
| 6 | onFailure 只拿到 { error: message } | 18 27 | 失败类型只能靠消息前缀编码 |
| 7 | 幂等键按运行生成;retryRun() 可能重复执行副作用 | 1 17 23 27 | 外部调用必须自带业务键 |
| 8 | requestId 只按记录匹配,不比对转换 | 1 | 不同操作误用同一个键时会被当成重放 |
| 9 | create() 不经守卫和校验 | 1 22 25 | 谁能创建只能在路由里控制 |
| 10 | available() 以空输入调用守卫,守卫先于校验执行 | 20 23 | 逐项操作拿不到准确的可用性 |
| 11 | 内存存储回滚恢复整库快照;进程内派发忽略 runAfter | 测试 | 并发测试可能出现假象,必须先修 |
| 新 | 没有在进入/离开状态时、在事务内写字段的钩子 | 原生实验 | 每个业务都要手写约 100 行,已经出过领取人字段残留的 bug |
| 新 | 状态图按 from × to 展开,看不到真实路径 | 原生实验 | 合同审批画出 126 条边,实际只可能走 12 种 |
| 新 | 定义没有版本,同一张表的多个定义缺少区分字段 | 3 19 | 版本策略需要按记录上的版本分派定义 |
推荐的审批抽象
外层状态机 + 任务状态机 + 策略函数。详见 summary.md · 如何抽象。
表达推进到哪个审批环节。由声明生成真实的 lifecycle 状态,一个环节对应一个状态。保留了按环节超时、准确的按钮可用性、可读的历史。一轮对应一个审批实例,提交时快照计划,只生成可达的边,并做内容绑定。
表达多个审批人如何共同得出环节结论。策略是作用在任务上的纯函数。 singleall({ onReject })any / firstthreshold({ min, vetoers })claimablesequential
所有审批共用的小状态机:待处理 / 被阻塞 / 已领取 / 挂起 / 已完成 / 已作废 / 已过期。转签、重派、加签、代理都落在这一层,外层状态不会成倍增加。
统一待办
审批层自带任务表(每人每次处理一行)和 API:
- 查询四类:待处理、已处理、我发起的、抄送,按"责任人 + 状态"建索引;
- 操作:领取、释放、转签、回应、批量重派。
流程版本
- 每个版本单独注册,共用实例表,用版本字段区分;
- 结构指纹写在锁文件里,指纹变了却没升版本,CI 就失败;
- 还有在途实例引用的版本被删掉时,应用拒绝启动;
- 历史不依赖旧代码。
不一刀切
- 单人为主的简单状态流转:直接用 lifecycle;
- 多人、多环节的传统审批:用审批层;
- 人与 AI 协同:等产品定义。
改进路线
20 项改进按依赖关系排成 4 个阶段。详见 improvement-plan.md · 改进点汇总。
1 · 独立的小修复
- 内存 store 回滚只撤销本事务;派发尊重
runAfter - 请求号重放时比对转换名
create()走守卫和校验- effect 结构化失败;限制
retryRun can()带输入,先校验再走守卫
2 · 审批层的前置条件
fire/create加入调用方事务(最关键)- 内部转换:不刷新计时、不重跑
onEnter - 进入/离开状态时写字段的钩子
- 按记录字段计时 + 触发器过滤
- 一次算出
{ to, values }的异步钩子 - 按状态冻结字段
- 状态图只画可达的边;同一张表支持多个定义
3 · 审批层原型
- 任务表 + 任务小状态机
- 策略库
defineApproval:计划快照、一轮一个实例、内容绑定、退回与加签- 待办 API
- 版本指纹、锁文件、启动检查、显式迁移
4 · 验证
- 用审批层重写 772 行合同审批,以及 S1、S5–S8、S11–S16、S24,对比代码量
- 在 PostgreSQL / MySQL 上验证唯一索引和"先锁父行"