大家好,今天想聊一个比较接地气的话题:条件工作流的有类型写法。
这段时间在业务代码里写了不少带分支判断的流程逻辑,比如订单审批、内容审核、任务分发、策略路由。早期版本为了快速上线,大量使用字符串常量和嵌套 if 来驱动流程分支。功能倒是能跑,但项目一大了以后,每改一个流程节点都像在雷区里走路:不知道哪个字段可能为空、不知道哪个分支永远走不到、不知道删掉一个节点会影响多少地方。
后面我逐步把这些条件分支按照“类型驱动”的思路重新设计了一遍,代码的可读性、安全性和可维护性都明显上了一个台阶。本文就把这套“条件工作流的有类型写法”完整拆解出来,包含核心概念、实践案例、可运行的示例代码,以及我在改造过程中遇到的高频问题。无论你是刚接触流程引擎的小白,还是正在优化中后台业务逻辑的开发者,这篇内容都应该能提供一些可以落地的思路。
1. 什么是条件工作流,为什么需要“有类型写法”
1.1 条件工作流是什么
先看一个最简单的场景。假设系统里有一笔订单,需要根据订单金额和用户等级决定走什么审批路径:
订单金额 > 5000 且 用户等级为 VIP → 财务经理审批 订单金额 > 5000 且 用户等级为普通 → 财务总监审批 订单金额 <= 5000 → 直属主管审批这种“根据某些输入条件,在多个分支路径中选择一条继续执行”的流程,就是条件工作流的典型形态。它不一定非要上 Camunda、Flowable 这类重量级引擎,很多业务系统里的规则判断、状态机流转、策略路由,本质上都是条件工作流的一种轻量实现。
条件工作流一般包含三个核心要素:
- 数据:流程运行时的输入,例如订单金额、用户等级、提交时间。
- 条件:作用于数据上的判断表达式,例如“金额大于 5000”。
- 动作/节点:条件满足或失败后要执行的环节,例如“分配给财务经理审批”。
1.2 “有类型写法”指的是什么
“有类型写法”不是说“给变量加个类型”这么简单,而是指:用类型系统来建模流程中的数据、条件和节点关系,让编译器在开发阶段就帮我们拦截非法条件字段、非法比较操作、非法节点跳转。
主流语言里常见的类型手段包括:
- TypeScript 的联合类型、字面量类型、可辨识联合、类型守卫、泛型。
- Java 的 sealed interface、enum、泛型上下界。
- Python 的 Literal、TypedDict、TypeGuard。
在有类型写法下,流程配置结构本身就是“可编译检查的”。
type OrderStatus = 'pending' | 'approved' | 'rejected';这样写之后,如果你在代码里拼了个'approve',编译阶段就会直接报错,不用等运行时才发现。
1.3 为什么开发者需要掌握这种写法
业务中条件分支的规模往往会超出预期。刚开始只有 3 个分支,半年后变成 30 个分支,配置散落在各个 if 里,字段和操作符全靠“人类约定”。这种无类型约束的写法,隐患非常集中:
- 字段拼错:
order.amount写成了order.amout,运行时才知道。 - 比较逻辑不当:把城市字段拿来和数字比较,得到完全错误的分支结果。
- 无法穷举分支:不知道某个枚举值有没有被处理,漏掉分支时只能靠线上事故发现。
- 不利于程序化配置:想要把流程配置存储到数据库,并在不发布代码的前提下调整分支,无类型写法很难保证配置的合法性。
掌握有类型写法,本质上是用编译器做第一道防线,把大量运行时错误提前到编码阶段。
2. 环境准备与演示项目结构
本文的示例代码使用 TypeScript 编写,因为 TypeScript 的类型系统表达能力很强,适合演示联合类型、可辨识联合、类型收窄和穷尽性检查。示例的核心代码是自包含的,不需要引入额外运行时依赖。
版本说明:
- 语言环境:Node.js 18 以上。
- TypeScript:5.x 常见版本即可。
- 编译器:建议使用 ts-node 直接运行,或者使用 tsx。
如果你的实际项目版本不同,请根据实际情况调整。本文重点演示的是一种设计思路,而不是绑定某个特定版本。
建议的目录结构如下:
condition-workflow-demo/ ├── src/ │ ├── types.ts # 类型定义 │ ├── engine.ts # 流程引擎核心逻辑 │ ├── actions.ts # 动作实现 │ ├── config.ts # 流程配置 │ └── index.ts # 入口演示文件 ├── package.json └── tsconfig.json初始化项目可以执行:
mkdir condition-workflow-demo cd condition-workflow-demo npm init -y npm install typescript ts-node @types/node --save-dev npx tsc --inittsconfig.json中建议开启严格模式:
{ "compilerOptions": { "target": "ES2020", "module": "CommonJS", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }运行示例:
npx ts-node src/index.ts3. 没有类型约束时,条件工作流踩过的坑
在介绍有类型写法之前,先还原一段无类型约束的典型实现。这段代码虽然能运行,但代表了条件工作流反模式的集中体现。
假设我们要实现订单审批条件工作流,先设计订单对象。
// src/bad-demo.ts interface Order { amount: number; userId: string; userLevel: string; // 'normal' | 'vip' | 'svip' city: string; } function runApprovalFlow(order: Order) { if (order.amount > 5000 && order.user_level === 'vip') { // 字段名拼错 console.log('走财务经理审批'); } else if (order.amount > 5000 && order.userLevel === 'vip') { console.log('走财务经理审批'); } else if (order.amounts > 5000 && order.userLevel === 'svip') { // 字段名拼错 console.log('走财务总监审批'); } else { console.log('走直属主管审批'); } }这个示例中有几个非常典型的问题:
第一个问题,字段拼写错误在编译期完全不会被发现。order.user_level、order.amounts这类错误,只有在程序运行到该分支时才会暴露。如果这段代码恰好处于高频分支,影响面会非常大。
第二个问题,userLevel的类型被定义成了string,意味着可以传入任意字符串。开发时大家约定用'vip',但某个调用方传了'VIP',流程就会走到默认分支。这类问题不会导致崩溃,但会产生错误审批路径,且非常难排查。
第三个问题,条件逻辑之间没有结构化约束。金额和城市这种“不可比”的字段类型,因为都变成了字符串和数字的宽松类型,编译器无法阻止你写出order.city > 10这种毫无意义的判断。
第四个问题,当条件分支越来越多时,漏掉某个分支处理不会被警告。比如后续增加了userLevel = 'svip'的分支,但这里忘了改,用户会直接落入 else 分支。
这类代码在业务系统里并不少见。问题不在于“代码写得不好”,而在于没有利用类型系统把边界条件显式表达出来。流程节点、条件字段、比较操作符、可能的分支路径,这些都应该被类型系统约束住,而不是依靠人的记忆和自觉。
4. 有类型写法的核心设计
有类型写法的核心不是写一堆 interface,而是设计一套“类型与运行时强一致”的规则集。下面从几个关键点展开。
4.1 用字面量联合类型替代宽泛字符串
第一步,把订单状态、用户等级、审批节点这类枚举值全部改成字面量联合类型。
// src/types.ts // 用户等级 export type UserLevel = 'normal' | 'vip' | 'svip'; // 订单审批状态 export type OrderStatus = 'pending' | 'approved' | 'rejected'; // 流程节点类型 export type NodeType = 'start' | 'condition' | 'approval' | 'end';这样定义之后,任何地方赋值给UserLevel的变量,都必须是这三个值之一。如果你传了'VIP',TypeScript 会直接给出编译错误。
4.2 用可辨识联合表达不同节点
条件工作流中的节点通常有不同的载荷。例如条件节点需要条件表达式,审批节点需要审批人,结束节点不需要额外信息。这时可以使用可辨识联合(Discriminated Union)。
// src/types.ts export type ConditionOperator = 'gt' | 'lt' | 'eq' | 'contains'; export interface BaseNode { id: string; name?: string; } export interface StartNode extends BaseNode { type: 'start'; } export interface EndNode extends BaseNode { type: 'end'; } export interface ApprovalNode extends BaseNode { type: 'approval'; approver: string; } export interface ConditionNode extends BaseNode { type: 'condition'; field: keyof OrderData; operator: ConditionOperator; value: string | number; yesNodeId: string; noNodeId: string; } export type WorkflowNode = | StartNode | ConditionNode | ApprovalNode | EndNode;这里的type字段就是“可辨识”的标记。TypeScript 看到node.type === 'approval'之后,就能自动把节点收窄为ApprovalNode,从而安全访问approver字段。
4.3 用 keyof 约束条件字段
条件节点中的field必须是订单对象上的真实字段,这就要用到keyof。
// src/types.ts export interface OrderData { amount: number; userId: string; userLevel: UserLevel; city: string; createdDays: number; } export type OrderField = keyof OrderData;把field定义为keyof OrderData后,如果配置里写field: 'amout',编译器会直接报错。这从根源上消灭了字段拼写错误问题。
4.4 穷尽性检查
有类型写法另一个重要价值,是让“漏分支”也被编译器发现。利用never类型可以做到穷尽性检查。
function assertNever(value: never): never { throw new Error(`Unexpected value: ${JSON.stringify(value)}`); }在 switch 的 default 分支调用assertNever(node),如果某个节点类型没被处理,TypeScript 会提示类型不兼容。
5. 完整实战:包装一个可扩展的条件工作流引擎
理论讲完,下面实现一个完整的轻量条件工作流引擎,支持节点定义、条件判断、节点流转和动作执行。
5.1 定义数据模型和节点类型
先完善src/types.ts。
// src/types.ts export type UserLevel = 'normal' | 'vip' | 'svip'; export interface OrderData { amount: number; userId: string; userLevel: UserLevel; city: string; createdDays: number; } export type ConditionOperator = 'gt' | 'lt' | 'eq' | 'contains'; export interface StartNode { type: 'start'; id: string; nextNodeId: string; } export interface EndNode { type: 'end'; id: string; } export interface ApprovalNode { type: 'approval'; id: string; approver: string; nextNodeId: string; } export interface ConditionNode { type: 'condition'; id: string; field: keyof OrderData; operator: ConditionOperator; value: string | number; yesNodeId: string; noNodeId: string; } export type WorkflowNode = | StartNode | ConditionNode | ApprovalNode | EndNode; export interface WorkflowDefinition { startNodeId: string; nodes: WorkflowNode[]; }这里把每个节点都设计成可辨识联合的成员,type字段区分节点种类,WorkflowNode把四种节点联合起来。后续要增加新的节点类型,只需要扩展联合类型,编译阶段会强制所有处理节点的地方同步更新。
5.2 实现条件求值
条件求值需要按操作符处理。这里我要特别说一个设计细节:field的值可能有多种类型,value也可能是字符串或数字,比较时要做一次类型归一化。
// src/engine.ts import { OrderData, ConditionOperator } from './types'; function normalizeCompareValue(value: unknown): string | number { if (typeof value === 'number') { return value; } return String(value); } export function evaluateCondition( fieldValue: unknown, operator: ConditionOperator, expectedValue: string | number ): boolean { const actual = normalizeCompareValue(fieldValue); const expected = normalizeCompareValue(expectedValue); switch (operator) { case 'eq': return actual === expected; case 'gt': if (typeof actual !== 'number' || typeof expected !== 'number') { return false; } return actual > expected; case 'lt': if (typeof actual !== 'number' || typeof expected !== 'number') { return false; } return actual < expected; case 'contains': return String(actual).includes(String(expected)); default: // 穷尽性检查 const _exhaustive: never = operator; return _exhaustive; } }这里有一个重要说明:对于gt和lt,如果字段值不是数字,直接返回false。这样做是为了避免把字符串强制转换成数字后产生“不可预期的比较结果”。业务上如果出现这种情况,应该作为配置错误记入日志,而不是悄悄继续执行。
5.3 实现流程引擎
流程引擎负责从起始节点开始,逐节点执行,遇到条件节点时评估条件并决定下一个节点。
// src/engine.ts import { WorkflowDefinition, WorkflowNode, ConditionNode, OrderData, } from './types'; export class WorkflowEngine { private nodes: Map<string, WorkflowNode>; constructor(private definition: WorkflowDefinition) { this.nodes = new Map(); for (const node of definition.nodes) { this.nodes.set(node.id, node); } } run(data: OrderData): string[] { const visited: string[] = []; let currentNodeId = this.definition.startNodeId; let guard = 0; const maxSteps = 100; while (currentNodeId && guard < maxSteps) { const node = this.nodes.get(currentNodeId); if (!node) { throw new Error(`节点 ${currentNodeId} 未找到`); } guard++; if (node.type === 'start') { visited.push(node.id); currentNodeId = node.nextNodeId; continue; } if (node.type === 'condition') { visited.push(node.id); const branch = this.resolveCondition(node, data); currentNodeId = branch ? node.yesNodeId : node.noNodeId; continue; } if (node.type === 'approval') { visited.push(node.id); currentNodeId = node.nextNodeId; continue; } if (node.type === 'end') { visited.push(node.id); currentNodeId = ''; continue; } // 穷尽性检查 const _exhaustive: never = node; return _exhaustive; } if (guard >= maxSteps) { throw new Error('流程可能存在死循环,执行步数超过限制'); } return visited; } private resolveCondition(node: ConditionNode, data: OrderData): boolean { const fieldValue = data[node.field]; return evaluateCondition(fieldValue, node.operator, node.value); } }guard变量是防止流程配置出现循环引用时无限循环的保护机制,这是条件工作流引擎里非常重要的一个工程细节。
5.4 定义动作执行
前面的引擎只做了节点流转,还没有真正执行业务动作。在真实项目中,审批节点往往需要把任务写入数据库、发送通知或调用外部 API。这里把动作抽取成一个独立函数数组,方便业务扩展。
// src/actions.ts import { OrderData, ConditionOperator } from './types'; export interface ActionContext { data: OrderData; visitHistory: string[]; } export type WorkflowAction = (context: ActionContext) => void; export const logStartAction: WorkflowAction = (context) => { console.log(`[开始] 订单 ${context.data.userId} 进入审批流程`); }; export const logApprovalAction: WorkflowAction = (context) => { console.log(`[审批] 当前节点审批人:${context.data.userLevel === 'svip' ? '财务总监' : '财务经理'}`); }; export const logEndAction: WorkflowAction = (context) => { console.log(`[结束] 流程处理完成,共访问节点:${context.visitHistory.join(' -> ')}`); };5.5 整合引擎与动作
为了让引擎执行节点时能够触发对应的动作,我这里用一个动作注册表把节点类型关联到动作函数。这个设计保持了引擎的通用性,同时允许业务按节点类型注册不同的处理逻辑。
// src/index.ts import { WorkflowDefinition, OrderData, ConditionNode, ApprovalNode, } from './types'; import { WorkflowEngine } from './engine'; import { logStartAction, logApprovalAction, logEndAction, ActionContext, } from './actions'; // 定义可执行动作映射 const actionMap: Record<string, (context: ActionContext) => void> = { start: logStartAction, approval: logApprovalAction, end: logEndAction, }; function runWorkflow(definition: WorkflowDefinition, data: OrderData) { const engine = new WorkflowEngine(definition); const visited = engine.run(data); const context: ActionContext = { data, visitHistory: visited, }; for (const nodeId of visited) { const node = definition.nodes.find((n) => n.id === nodeId); if (node && actionMap[node.type]) { actionMap[node.type](context); } } return visited; } // 流程定义 const workflowDefinition: WorkflowDefinition = { startNodeId: 'start', nodes: [ { type: 'start', id: 'start', nextNodeId: 'checkAmount' }, { type: 'condition', id: 'checkAmount', field: 'amount', operator: 'gt', value: 5000, yesNodeId: 'checkLevel', noNodeId: 'approvalManager', }, { type: 'condition', id: 'checkLevel', field: 'userLevel', operator: 'eq', value: 'svip', yesNodeId: 'approvalDirector', noNodeId: 'approvalManager', }, { type: 'approval', id: 'approvalManager', approver: '财务经理', nextNodeId: 'end', }, { type: 'approval', id: 'approvalDirector', approver: '财务总监', nextNodeId: 'end', }, { type: 'end', id: 'end' }, ], }; // 测试数据 const order1: OrderData = { amount: 8000, userId: 'user001', userLevel: 'svip', city: '杭州', createdDays: 10, }; const order2: OrderData = { amount: 3000, userId: 'user002', userLevel: 'normal', city: '上海', createdDays: 3, }; console.log('=== 案例 1:大额 SVIP 用户 ==='); const visited1 = runWorkflow(workflowDefinition, order1); console.log('流转路径:', visited1.join(' -> ')); console.log('\n=== 案例 2:普通金额用户 ==='); const visited2 = runWorkflow(workflowDefinition, order2); console.log('流转路径:', visited2.join(' -> '));5.6 运行与验证
在package.json中配置脚本:
{ "scripts": { "dev": "ts-node src/index.ts" } }运行命令:
npm run dev预期输出:
=== 案例 1:大额 SVIP 用户 === [开始] 订单 user001 进入审批流程 [审批] 当前节点审批人:财务总监 [结束] 流程处理完成,共访问节点:start -> checkAmount -> checkLevel -> approvalDirector -> end 流转路径: start -> checkAmount -> checkLevel -> approvalDirector -> end === 案例 2:普通金额用户 === [开始] 订单 user002 进入审批流程 [审批] 当前节点审批人:财务经理 [结束] 流程处理完成,共访问节点:start -> checkAmount -> approvalManager -> end 流转路径: start -> checkAmount -> approvalManager -> end从运行结果可以看到,大额 SVIP 用户走到了approvalDirector审批节点,普通金额用户直接走到了approvalManager审批节点。条件分支按照预期工作。
这里注意一个问题:approval动作在示例里是根据userLevel打印审批人,而不是使用节点上配置的approver。实际项目中应以节点配置为准,这里只是演示动作如何与数据互动,读者可以按需调整。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
TypeScript 报错:Type '"VIP"' is not assignable to type 'UserLevel' | 字面量联合类型限制了取值范围 | 检查业务数据源,统一数据规范,必要时写数据清洗映射函数 |
| 条件节点总是走同一条分支 | 字段值类型和运算符不匹配,例如userLevel是字符串却使用了gt | 在evaluateCondition中增加类型检查,非数值字段执行gt/lt时返回false并记日志 |
| 运行时报错:节点不存在 | 流程配置里yesNodeId或noNodeId指向了不存在的节点 id | 在引擎启动时对所有节点关系做一次全量校验 |
| 流程死循环 | 节点的 next 指向形成环 | 设置最大执行步数(如maxSteps=100),超过后抛出异常 |
| 流程定义文件可读性差 | 节点之间通过 id 关联,缺少可视化 | 条件允许时可以写一个节点清单输出脚本,按顺序打印每个节点和分支关系 |
下面针对几个情况做更详细的说明。
6.1 调试条件表达式
如果某个条件节点没有走到预期分支,最快的排查方法是在引擎里追加一个日志。可以在resolveCondition方法中临时打印字段名、字段值、操作符和期望值。
private resolveCondition(node: ConditionNode, data: OrderData): boolean { const fieldValue = data[node.field]; const result = evaluateCondition(fieldValue, node.operator, node.value); console.log(`[DEBUG] 条件评估:${node.field} ${node.operator} ${node.value} => ${result}`); return result; }这样能直观地看到条件求值是否按预期执行,避免对着大段代码猜逻辑。
6.2 区分配置错误和运行时错误
条件工作流里有很多错误本质上是配置错误,应该在启动阶段拦截,而不是运行到某一单时才崩溃。推荐在WorkflowEngine构造函数中做一次节点关系全量校验:
function validateDefinition(definition: WorkflowDefinition): void { const ids = new Set(definition.nodes.map((n) => n.id)); for (const node of definition.nodes) { if (node.type === 'start' && !ids.has(node.nextNodeId)) { throw new Error(`起始节点的下一个节点 ${node.nextNodeId} 不存在`); } if (node.type === 'condition') { if (!ids.has(node.yesNodeId)) { throw new Error(`条件节点 ${node.id} 的 yesNodeId 不存在`); } if (!ids.has(node.noNodeId)) { throw new Error(`条件节点 ${node.id} 的 noNodeId 不存在`); } } if (node.type === 'approval' && !ids.has(node.nextNodeId)) { throw new Error(`审批节点 ${node.id} 的 nextNodeId 不存在`); } } }在引擎构造时调用这段校验,可以在流程执行前发现大部分配置问题。
6.3 复杂条件组合怎么办
简单的单个条件可以使用ConditionNode。如果业务需要“金额大于 5000 且用户等级是 SVIP”这样的组合条件,有两种做法:
第一种是使用多个条件节点串联,前一个条件通过后再进入下一个条件判断。示例中的流程就是这种设计。
第二种是扩展条件节点,改成组合表达式。
export type ConditionExpression = | { kind: 'simple'; field: keyof OrderData; operator: ConditionOperator; value: string | number } | { kind: 'and'; left: ConditionExpression; right: ConditionExpression } | { kind: 'or'; left: ConditionExpression; right: ConditionExpression };这种树的写法表达能力更强,适合复杂规则配置,但实现复杂度也更高。我的建议是:优先用节点串联,等确实需要动态组合规则时再升级为表达式树。
7. 最佳实践与工程建议
7.1 从“无类型”改造时,不要一次性重写
如果你手头已经有大量无类型条件判断,不建议一次性推倒重写。推荐的方式是“新增代码走有类型写法,存量代码逐步迁移”。
可以先抽出核心类型,例如把用户等级、订单状态这些枚举值先改为字面量联合类型,让编译器帮你找出所有赋值不规范的位置。然后再把高频出现的条件分支改造成节点配置。
这样改造的风险小,每一步都是可验证的,不会影响正在运行的业务。
7.2 流程配置与业务代码的边界
条件工作流引擎本身应该是通用的,不要让它直接依赖具体业务函数。在本文示例中,引擎只负责节点流转和条件求值,业务动作通过 actionMap 注入。这样做的好处是:
- 引擎可以被多个业务流程复用。
- 新增业务流程只需要新增配置和动作,不需要改引擎。
- 引擎可以单独测试,覆盖条件分支逻辑。
7.3 配置校验要前置
凡是能通过静态检查发现的问题,就不要留到运行时。除了节点关系校验,还可以在配置层面做字段值类型校验。比如amount字段对应value必须是数字,如果写成字符串,在启动时就要报错。
function validateConditionField(condition: ConditionNode, sampleData: OrderData) { const fieldType = typeof sampleData[condition.field]; if (condition.operator === 'gt' || condition.operator === 'lt') { if (fieldType !== 'number' || typeof condition.value !== 'number') { throw new Error(`条件节点 ${condition.id} 使用了数字比较,但字段或值不是数字`); } } }7.4 日志和可观测性
条件工作流在线上出问题时,最难的是还原“数据经历了哪些分支”。建议每次流程执行都记录完整链路:
- 输入数据的关键字段。
- 经过每个节点的顺序。
- 每个条件节点的判断结果。
- 最终结束节点。
这些日志不需要很复杂,关键是完整。配合链路追踪 ID,可以让问题定位时间从小时级降到分钟级。
7.5 安全边界与最小权限
如果条件工作流要接入管理后台,允许运营人员通过界面配置流程节点,那么必须考虑权限控制。建议遵循最小权限原则:配置人员只能修改自己负责的流程,发布前要有审批和预览环节。节点配置中的动作名称应当采用白名单机制,不允许直接执行任意代码,避免出现越权操作。
7.6 性能优化方向
对于大多数中后台系统,条件工作流的性能瓶颈一般不在计算,而在外部调用。比如每个审批节点可能触发数据库更新、消息发送、外部系统回调。建议:
- 外部调用统一走异步队列,不要在流程引擎线程内阻塞。
- 条件求值只读,不做副作用。
- 流程定义可以缓存,避免每次执行都重新解析。
- 对于极高并发的场景,可以结合规则引擎或表达式引擎,但做好安全限制。
8. 总结与下一步学习路线
这篇文章围绕“条件工作流的有类型写法”展开,核心内容可以概括为以下几点:
第一,条件工作流并不一定要引入重量级流程引擎,很多业务里基于数据字段做分支跳转的逻辑,都可以用轻量设计实现。
第二,有类型写法的关键不是“用了 TypeScript 就有类型”,而是要主动使用字面量联合类型、可辨识联合、keyof约束和穷尽性检查,把这些类型工具作为业务规则的显式表达。
第三,完整的示例代码展示了如何定义流程节点类型、实现条件求值、流转引擎和动作注册。你可以直接作为模板使用,也可以根据业务需要扩展成组合条件、并行节点、子流程等更复杂的形态。
第四,异常处理、配置校验、循环保护、日志记录和权限控制,是条件工作流工程落地时最容易被忽视的几个环节,它们决定了这套代码能不能在真实业务中长期稳定运行。
如果你想继续深入学习,下一步可以关注这几个方向:
- 把条件节点升级为表达式树,支持 and/or/not 组合逻辑。
- 给流程定义增加版本管理,让流程配置的修改可以发布和回滚。
- 结合现有业务封装一个可视化配置面板,把类型定义作为面板表单的 schema 来源。
- 学习 Camunda、Flowable 等成熟流程引擎,对照它们的设计看轻量方案有哪些取舍。
建议你直接拿一个自己手头的业务分支场景,一步一步把它改造成有类型的条件工作流。动手改完一个真实案例以后,你对这篇文章里提到的类型设计和工程细节,会有比读十遍文章都更深的理解。如果本文对你有帮助,可以收藏备用,后续在这个主题上我还会继续补充表达式树和流程版本管理的内容。