尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

TypeScript实现有类型条件工作流:从类型设计到工程实践

TypeScript实现有类型条件工作流:从类型设计到工程实践 条件工作流在大型业务系统里非常常见但要把它写成“有类型”的版本并不容易。很多人一开始用字符串、JSON 和 switch 写条件分支等到条件多了、节点多了才发现类型系统完全帮不上忙字段传错、分支写反、节点跳错都只能等到运行时才暴露。所谓“条件工作流的有类型写法”本质是让工作流里的节点、条件、上下文和动作都拥有明确的数据类型让编译器在运行前就替你拦截一大批低级错误。这篇文章会从条件工作流的设计痛点出发用 TypeScript 实现一个带条件分支的最小工作流执行器最后给出类型设计、运行验证、排错路径以及生产环境下的工程实践。适用范围是两类读者一类是需要在业务代码里实现审批流、发布流、策略编排的开发者另一类是刚接触 TypeScript 判别联合和类型守卫想找一个贴近真实业务场景来练习的人。读完以后你可以把这里的类型思路迁移到订单审批、内容审核、CI 流程、消息路由等场景也可以继续扩展为支持并行节点、超时回退、持久化恢复的完整工作流引擎。1. 先理解条件工作流业务上为什么需要“有类型写法”1.1 条件工作流在解决什么问题工作流的核心不只是“按顺序执行几个动作”而是“根据当前输入决定接下来走哪条分支”。典型的条件工作流包含四类节点开始节点、条件节点、动作节点、结束节点。开始节点负责入口条件节点读取上下文并返回 true 或 false动作节点执行具体业务动作结束节点给出最终结果。例如一个订单审批流程订单金额小于 1000 元自动通过。金额在 1000 到 5000 元之间进入经理审批。金额大于 5000 元进入总监审批。客户等级为 VIP 时即使金额较大也自动通过。这段规则如果用代码直接写在业务逻辑里初看并不复杂。但业务规则会持续变化比如增加“黑名单客户直接拒绝”“工作日与非工作日走不同通知渠道”“订单来源为小程序或 App 时审批等级不同”。每增加一个条件原本的函数就会膨胀if 嵌套越来越深最终没有人能一眼说出某个订单到底会走哪条路径。工作流引擎的作用是把这些规则从散落的 if 语句中抽出来变成可配置、可执行、可追踪的数据结构。条件节点负责“判断”动作节点负责“执行”节点之间的边负责“流转”。这样的设计天然适合“有类型写法”因为每一个节点、每一条条件、每一步流转都可以被类型系统明确约束。1.2 无类型写法为什么会在项目里失控很多团队第一版工作流引擎是“无类型”的。节点用一个对象表示条件用一个字符串表示动作用一个字符串表示执行器内部靠switch (string)分发。const node { id: node_1, type: condition, condition: amount 1000 customerLevel vip, whenTrue: node_2, whenFalse: node_3, };表面上看很灵活任何条件都可以写成字符串。但代价非常明显条件表达式是字符串IDE 无法检查amount 1000中的amount是否真的存在于上下文。条件写错字段名时只有运行时把字符串交给 eval 或 Function 后才会报错。节点跳转目标写成一个不存在的节点 id不会在编译期被发现。动作类型拼错比如auto_approve写成autu_approve运行时才会走到默认分支。上下文字段被随意修改比如金额字段从amount改成totalAmount所有字符串条件都要人工检查。这些问题的根因是数据在“编译期可见的静态含义”和“运行时才执行的动态行为”之间断裂了。类型系统本来可以帮助你把错误挡在编译期但字符串和any把这条防线完全拆掉。更隐蔽的问题是维护成本。无类型工作流从数据库或前端配置读取后执行器无法知道节点、条件、上下文的真实结构只能写大量防御式判断字段是否存在、类型是否正确、值为 null 时怎么处理。代码里全是if (typeof node.condition string)这类运行时防御业务逻辑反而被淹没。1.3 有类型写法的核心收益有类型写法并不是“把字符串换成枚举”这么简单而是让工作流的所有关键实体都成为类型系统可以校验的数据节点用判别联合表达“开始、条件、动作、结束”四种形态。条件用嵌套的判别联合表达比较、逻辑组合、上下文字段访问。上下文用接口定义字段和类型条件语句访问字段时得到编译期检查。执行器在节点分发处用switch收窄类型不需要大量as any。最终收益可以总结为一句话能在编译期发现的错误不要留到运行时。比如ctx.amount cond.threshold如果ctx没有amount字段编译直接失败条件节点缺少whenTrue或whenFalse无法构造出合法节点执行器处理完所有条件类型后编译器要求必须有never穷尽检查避免新增条件类型时漏掉分支。对比维度无类型写法有类型写法条件表达方式字符串表达式结构化条件对象字段错误发现时机运行时编译期节点跳转校验人工检查类型与运行前校验重构影响范围全局字符串查找类型定义联动执行器代码大量运行时防御类型守卫和穷尽检查这种写法的代价是初始建模成本更高需要先花时间设计“条件表达式”和“节点结构”。但只要工作流会在项目中长期演进这笔投入非常值得。2. 准备工作用 TypeScript 搭建一个最小的可试验环境2.1 前置知识与版本选择在开始写代码前先确认三块前置知识TypeScript 基础类型尤其是联合类型、交叉类型、接口和泛型。判别联合discriminated union的用法也就是通过type字段区分不同对象形态。switch配合类型守卫实现类型收窄的基本模式。示例代码使用的核心依赖是typescript和tsx。tsx是一个基于 esbuild 的 TypeScript 运行工具它可以避免每次改代码都手动编译适合本地快速验证。生产环境可以按团队习惯选择tsc编译或tsx运行核心类型逻辑不受影响。建议版本Node.js 18 或更高版本。TypeScript 5.0 或更高版本。tsx最新稳定版即可。如果原始项目版本不是这些落地前先确认依赖版本兼容性。这里的示例代码没有使用高版本专属语法TypeScript 4.5 以上大概率也能跑通但推荐使用 5.x 以获得更准确的类型提示。2.2 初始化项目与依赖创建一个干净目录并初始化 npm 项目。mkdir typed-workflow cd typed-workflow npm init -y npm install -D typescript tsx接下来生成 TypeScript 配置文件。npx tsc --init生成的tsconfig.json中有大量默认配置示例项目只需要关注几个关键项strict必须开启target建议为ES2020module使用NodeNext或CommonJS都可以。这里为了示例简单使用CommonJS。{ compilerOptions: { target: ES2020, module: CommonJS, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src] }注意strict是开启有类型写法的前提。如果关闭strict很多类型错误会被悄悄放宽例如null检查失效、隐式any出现类型系统的作用会大打折扣。2.3 目录结构与文件规划示例工程按关注点拆分文件方便后续扩展。typed-workflow/ ├── src/ │ ├── types.ts # 节点、条件、上下文类型定义 │ ├── engine.ts # 条件求值和工作流执行器 │ ├── workflow.ts # 示例工作流定义 │ └── index.ts # 入口负责运行验证 ├── package.json └── tsconfig.jsontypes.ts只放类型不放实现engine.ts负责执行逻辑workflow.ts定义一份具体工作流index.ts作为程序入口。这样拆分以后后续增加“从 JSON 加载工作流”“持久化执行状态”“可视化节点图”时不需要重构类型层。先建立src/types.ts后面逐步补充内容。目录结构并不复杂但建议从第一版就保持清晰否则类型定义和执行器混在一起读起来会很吃力。3. 先定义领域模型让条件的每一种形态都有明确类型3.1 节点类型、条件类型和上下文类型的整体设计条件工作流的核心数据模型可以分为三层上下文、条件、节点。上下文是工作流执行时输入的数据比如订单金额、客户等级、用户是否黑名单。它必须是一个明确的接口因为条件求值时要读取上下文字段。export type CustomerLevel normal | vip; export interface WorkflowContext { orderId: string; amount: number; customerLevel: CustomerLevel; inBlacklist: boolean; source: miniProgram | app | h5; }条件表示“对上下文的一次判断”。为了避免字符串条件带来的运行时风险条件用结构化对象表达并根据条件类型使用不同字段。export type Condition | { type: amountGreaterThan; threshold: number } | { type: amountLessThan; threshold: number } | { type: customerLevelIs; level: CustomerLevel } | { type: sourceIs; source: WorkflowContext[source] } | { type: isBlacklist } | { type: and; left: Condition; right: Condition } | { type: or; left: Condition; right: Condition };节点是所有可执行元素的统一形态。使用判别联合后每个节点都带有id和kind但不同的kind拥有不同的字段export type NodeId string; export type WorkflowNode | { id: NodeId; kind: start; next: NodeId } | { id: NodeId; kind: condition; condition: Condition; whenTrue: NodeId; whenFalse: NodeId } | { id: NodeId; kind: action; actionType: autoApprove | managerApprove | directorApprove | reject | notify; next: NodeId } | { id: NodeId; kind: end; result: approved | rejected | pending };这里的关键是condition节点必须同时提供whenTrue和whenFalse缺一不可action节点必须提供actionType和nextend节点没有next因为它就是终点。类型系统把这些约束固化下来构造工作流时少写字段会直接编译报错。3.2 用判别联合表达条件表达式判别联合的特点是每一个成员都共享一个字面量字段这里叫type于是 TypeScript 可以在switch或if中自动收窄类型。以amountGreaterThan为例const condition: Condition { type: amountGreaterThan, threshold: 1000, };通过分支收窄后cond.threshold可以被安全访问。如果是customerLevelIs则cond.level是CustomerLevel类型。复杂条件通过and和or嵌套组合const approveCondition: Condition { type: and, left: { type: amountGreaterThan, threshold: 1000, }, right: { type: customerLevelIs, level: vip, }, };这里的含义是金额大于 1000 并且客户等级为 VIP。你不需要把写进字符串类型系统可以逐层检查每一个子条件。这种结构化条件的另一个好处是它可以被 JSON 序列化保存到数据库也可以被前端配置器识别。因为它的语义是显式的而不是一段需要解析的代码字符串。要想新增一种条件类型只需要扩展Condition联合、在求值函数中增加一个 case、在never穷尽检查中确认不漏分支。三条改动都有类型系统把关。3.3 用类型守卫收窄运行时条件类型定义本身不会自动执行真正运行时还需要“类型守卫”把联合类型收窄成具体形态。例如在求值函数中我们可以用switch (cond.type)让 TypeScript 知道当前分支是哪一个具体条件。export function evaluateCondition(cond: Condition, ctx: WorkflowContext): boolean { switch (cond.type) { case amountGreaterThan: return ctx.amount cond.threshold; case amountLessThan: return ctx.amount cond.threshold; case customerLevelIs: return ctx.customerLevel cond.level; case sourceIs: return ctx.source cond.source; case isBlacklist: return ctx.inBlacklist; case and: return evaluateCondition(cond.left, ctx) evaluateCondition(cond.right, ctx); case or: return evaluateCondition(cond.left, ctx) || evaluateCondition(cond.right, ctx); default: { const never: never cond; throw new Error(未处理的条件类型: ${JSON.stringify(never)}); } } }这里的default分支很关键。当未来给Condition增加新的条件类型时比如增加amountBetween如果忘记在switch中处理never类型的赋值会编译报错。这就是“穷尽检查”的用法它让类型系统强制开发者补全所有分支。注意类型守卫只负责“收窄类型”不负责“保证业务数据正确”。例如ctx.amount是负数、threshold是 NaN这些是业务校验问题应该在工作流执行前由上下文校验层解决。4. 实现一个带类型条件的工作流执行器4.1 执行器的核心数据结构设计执行器时为了让工作流定义和运行时状态分开可以定义两个数据结构工作流定义一个以节点 id 为键、节点对象为值的RecordNodeId, WorkflowNode。运行时状态当前节点 id、已访问节点列表、最大步数等。export interface WorkflowDefinition { startNodeId: NodeId; nodes: RecordNodeId, WorkflowNode; } export interface WorkflowRuntimeState { currentNodeId: NodeId; visitedNodeIds: NodeId[]; finished: boolean; result?: approved | rejected | pending; }startNodeId告诉执行器从哪个节点开始。nodes是节点集合要求每一个节点都能通过 id 找到。运行时状态用于执行过程追踪和后续扩展比如“执行到第几步”“是否已经结束”。执行器还需要一个最大步数限制。如果节点配置成循环边或者条件逻辑异常导致在几个节点间反复跳转没有步数限制的程序会死循环。const DEFAULT_MAX_STEPS 100;生产环境可以根据节点规模调整但建议始终保留这个保护。4.2 条件求值与节点跳转逻辑执行器的核心是状态机循环。每一轮从当前节点开始根据节点类型决定下一跳。import { WorkflowDefinition, WorkflowNode, WorkflowContext, WorkflowRuntimeState, NodeId } from ./types; import { evaluateCondition } from ./types; export function runWorkflow( definition: WorkflowDefinition, ctx: WorkflowContext, maxSteps: number DEFAULT_MAX_STEPS, ): WorkflowRuntimeState { let currentNodeId: NodeId definition.startNodeId; const visitedNodeIds: NodeId[] []; let step 0; while (step maxSteps) { const node definition.nodes[currentNodeId]; if (!node) { throw new Error(节点不存在: ${currentNodeId}); } visitedNodeIds.push(currentNodeId); step 1; switch (node.kind) { case start: currentNodeId node.next; break; case condition: { const result evaluateCondition(node.condition, ctx); currentNodeId result ? node.whenTrue : node.whenFalse; break; } case action: executeAction(node.actionType, ctx); currentNodeId node.next; break; case end: return { currentNodeId, visitedNodeIds, finished: true, result: node.result, }; } if (currentNodeId end) { return { currentNodeId, visitedNodeIds, finished: true, result: pending, }; } } throw new Error(工作流执行超过最大步数限制 ${maxSteps}疑似存在循环); }这段代码说明了几个关键决策第一start节点只负责指定下一跳它本身不执行动作。这样保证所有工作流都有唯一入口。第二condition节点调用evaluateCondition拿到布尔值后决定走whenTrue还是whenFalse。这个分支不会写错因为whenTrue和whenFalse都是经过类型检查的字段。第三action节点通过executeAction执行业务动作。executeAction的入参是WorkflowContext如果动作需要读取订单金额直接使用ctx.amount。第四遇到end节点立即返回避免继续执行不存在的next。currentNodeId end这层保护是为了兼容“节点 id 恰好叫 end 但又不是 end 节点”的场景也可以直接把 end 节点的 id 固定为end。更稳妥的做法是仅通过node.kind end判断终点避免 id 语义和节点语义冲突。下面给出更推荐的写法while (step maxSteps) { const node definition.nodes[currentNodeId]; if (!node) throw new Error(节点不存在: ${currentNodeId}); visitedNodeIds.push(currentNodeId); step 1; if (node.kind end) { return { currentNodeId, visitedNodeIds, finished: true, result: node.result }; } switch (node.kind) { case start: currentNodeId node.next; break; case condition: { const result evaluateCondition(node.condition, ctx); currentNodeId result ? node.whenTrue : node.whenFalse; break; } case action: executeAction(node.actionType, ctx); currentNodeId node.next; break; case end: break; } }这样可以避免依赖“节点 id 叫 end”的特殊约定。4.3 无类型断言与类型安全调用之间的取舍实际开发中执行器读取的节点可能来自 JSON 反序列化而不是直接由 TypeScript 对象构造函数生成。此时外部数据确实没有静态类型开发者可能会忍不住使用as any或as unknown as WorkflowNode。不推荐直接as any因为那样会绕过所有类型检查。更好的做法是写一个“解析校验函数”对外部输入做运行时校验再把通过校验的数据转成类型安全的WorkflowNode。export function parseWorkflowNode(raw: unknown): WorkflowNode { if (typeof raw ! object || raw null) { throw new Error(节点必须是一个对象); } const node raw as Recordstring, unknown; const id node.id; const kind node.kind; if (typeof id ! string) { throw new Error(节点缺少 id); } switch (kind) { case start: return { id, kind: start, next: node.next as string }; case condition: return { id, kind: condition, condition: node.condition as Condition, whenTrue: node.whenTrue as string, whenFalse: node.whenFalse as string, }; case action: return { id, kind: action, actionType: node.actionType as WorkflowNode { kind: action } extends never ? never : autoApprove, next: node.next as string, }; case end: return { id, kind: end, result: node.result as approved | rejected | pending }; default: throw new Error(未知节点类型: ${String(kind)}); } }这里的断言仍然存在但范围被限制在“输入解析边界”。核心业务代码不需要再到处使用as外部数据进入系统后就被转换为受控类型。这种做法是生产环境的常见方案用运行时校验保护类型安全而不是依赖as any。注意示例中的node.next as string只做了类型断言没有真正校验字段值是否为字符串也没有校验whenTrue指向的节点是否确实存在。生产环境需要更完整的运行时 schema 校验可以使用zod、io-ts或手写守卫函数。这个话题会在第七章展开。5. 用一段完整示例验证类型检查和运行结果5.1 示例工作流定义为了验证前面的类型和执行器我们定义一个订单自动审批工作流。规则如下黑名单客户直接拒绝。非黑名单客户金额小于 1000 自动通过。金额在 1000 到 5000 之间进入经理审批。金额大于等于 5000进入总监审批。客户等级为 VIP 时即使金额较大也自动通过。转换成节点图start - checkBlacklist(condition) checkBlacklist true - reject checkBlacklist false - checkAmount(condition) checkAmount and(amount 5000, not vip) - directorApprove checkAmount and(amount 1000, amount 5000, not vip) - managerApprove 其余 - autoApprove由于当前条件类型没有not和amountBetween这里用组合条件表示。为了让示例简单先放宽规则不精确表达所有分支而是写成能展示条件组合的版本const workflow: WorkflowDefinition { startNodeId: start, nodes: { start: { id: start, kind: start, next: check_blacklist }, check_blacklist: { id: check_blacklist, kind: condition, condition: { type: isBlacklist }, whenTrue: reject, whenFalse: check_amount, }, check_amount: { id: check_amount, kind: condition, condition: { type: and, left: { type: amountGreaterThan, threshold: 5000 }, right: { type: and, left: { type: customerLevelIs, level: vip }, right: { type: isBlacklist }, }, }, whenTrue: autoApprove, whenFalse: managerApprove, }, autoApprove: { id: autoApprove, kind: action, actionType: autoApprove, next: end_approved }, managerApprove: { id: managerApprove, kind: action, actionType: managerApprove, next: end_approved }, directorApprove: { id: directorApprove, kind: action, actionType: directorApprove, next: end_approved }, reject: { id: reject, kind: action, actionType: reject, next: end_rejected }, end_approved: { id: end_approved, kind: end, result: approved }, end_rejected: { id: end_rejected, kind: end, result: rejected }, }, };这个工作流设计并不完美check_amount里的条件组合有些绕但重点是展示“条件节点可以被任意组合成树形结构”。实际业务中应该把“是否 VIP 且金额大于 5000”这种语义封装成独立条件类型或者提供not条件来减少嵌套。不过作为最小示例它已经足够跑通类型检查。5.2 运行结果与断言验证接着在src/index.ts中运行多个上下文并用断言验证结果。import { runWorkflow } from ./engine; import { WorkflowContext } from ./types; const samples: Array{ name: string; ctx: WorkflowContext } [ { name: 普通低金额订单, ctx: { orderId: A001, amount: 500, customerLevel: normal, inBlacklist: false, source: app }, }, { name: 普通高金额订单, ctx: { orderId: A002, amount: 8000, customerLevel: normal, inBlacklist: false, source: app }, }, { name: VIP高金额订单, ctx: { orderId: A003, amount: 8000, customerLevel: vip, inBlacklist: false, source: app }, }, { name: 黑名单订单, ctx: { orderId: A004, amount: 500, customerLevel: normal, inBlacklist: true, source: h5 }, }, ]; for (const sample of samples) { const state runWorkflow(workflow, sample.ctx); console.log(${sample.name}: ${state.result}); console.log( 访问路径: ${state.visitedNodeIds.join( - )}); }运行命令npx tsx src/index.ts预期输出类似普通低金额订单: approved 访问路径: start - check_blacklist - check_amount - managerApprove - end_approved 普通高金额订单: rejected 访问路径: start - check_blacklist - check_amount - reject - end_rejected VIP高金额订单: approved 访问路径: start - check_blacklist - check_amount - autoApprove - end_approved 黑名单订单: rejected 访问路径: start - check_blacklist - reject - end_rejected这里不给出精确输出了因为示例工作流里的check_amount条件组合可能需要调整。你可以根据自己的规则修改条件树然后观察路径变化。重要的是状态机确实按照条件结果选择了不同分支且整个过程中没有使用字符串 eval。5.3 编译期类型检查的验证方式类型检查的价值在于 IDE 和tsc提前发现问题。修改src/workflow.ts故意把whenFalse字段删掉再运行下面的命令npx tsc --noEmit此时 TypeScript 会报错因为condition节点缺少whenFalse。如果删除end_rejected节点但reject节点的next仍然指向end_rejectedtsc本身不会报错因为类型系统不知道end_rejected是否存在于nodes对象中。这是类型系统的一个边界它检查的是“字段是否合法”不检查“引用是否存在于运行时集合”。要解决“跳转目标不存在”的问题可以在工作流定义层增加一个校验函数在所有节点注册后检查每一个next、whenTrue、whenFalse是否都指向已存在节点。这属于“运行时数据完整性校验”应该作为工作流的启动前检查项。6. 常见问题排查条件工作流在类型化过程中容易踩的坑6.1 条件类型写得太宽等于没有类型很多人在第一步就把条件类型定义为Recordstring, unknown或any导致后续完全无法收窄。比如export type Condition { type: string; [key: string]: unknown; };这种定义虽然写起来快但cond.type amountGreaterThan时TypeScript 不会知道对象是否具有threshold字段。你仍然需要if (threshold in cond)来判断类型系统变成摆设。正确做法是使用判别联合让每一个条件成员拥有自己的字段。如果条件字段较多可以通过type字面量激活类型收窄。6.2 执行器忘记处理新增条件类型导致运行时静默失败当Condition联合新增成员后如果没有处理default分支的never赋值执行器可能走到一个default分支返回错误结果。正确写法是在switch末尾加default: { const exhaustiveCheck: never cond; throw new Error(未处理的条件类型: ${JSON.stringify(exhaustiveCheck)}); }这样新增条件类型后编译期就会提示你补全分支。6.3 节点跳转目标没有做完整性校验类型系统能保证whenTrue字段存在但无法保证它指向的节点存在于nodes对象中。如果配置写错运行时会抛“节点不存在”的错误。排查方法是启动前做一次全量遍历收集所有被引用的节点 id再检查是否都在nodes中。export function validateWorkflow(definition: WorkflowDefinition): string[] { const errors: string[] []; const ids new Set(Object.keys(definition.nodes)); const referenced: NodeId[] []; referenced.push(definition.startNodeId); for (const node of Object.values(definition.nodes)) { switch (node.kind) { case start: referenced.push(node.next); break; case condition: referenced.push(node.whenTrue, node.whenFalse); break; case action: referenced.push(node.next); break; case end: break; } } for (const ref of referenced) { if (!ids.has(ref)) { errors.push(引用了不存在的节点: ${ref}); } } return errors; }在工作流启动前调用validateWorkflow可以把运行时错误变成启动时错误这是生产环境必备的一步。6.4 上下文校验缺失导致条件求值出现 NaN 或 undefined有类型字段不意味着运行时值一定正确。比如外部传入的amount是字符串500虽然WorkflowContext类型标注为number但数据来自 JSON 或表单时TypeScript 无法保证真实类型。条件求值里的ctx.amount cond.threshold会变成字符串与数字比较结果不可预期。排查方式在工作流入口对上下文做值校验确认amount是有限数字customerLevel是枚举值之一。对阈值参数也做校验避免NaN或负数。在条件求值前打印关键字段便于定位。6.5 条件嵌套过深调试困难当and和or嵌套超过三层阅读和工作流配置会变得吃力。建议提供not条件或amountBetween这类组合条件减少不必要的嵌套。这个不是类型问题而是可维护性问题。类型系统可以帮助你安全地增加条件类型但不能替你消除语义复杂的业务规则。7. 最佳实践从学习示例到生产环境的条件工作流7.1 生产环境还需要做的工程保障学习示例里的执行器可以直接跑通条件分支但距离生产使用还有一段距离。以下保障措施需要根据项目情况逐步补充配置外置化。工作流定义不应该硬编码在代码里更常见的做法是存到数据库或配置文件通过管理后台编辑。此时类型定义作为“内部领域模型”与配置层之间需要有序列化、反序列化、校验三层逻辑。千万不能让未校验的 JSON 直接进入执行器。日志和追踪。每一次流程执行都应该记录开始时间、输入上下文、每个节点的访问顺序、条件表达式、条件结果、动作执行结果、结束节点和最终状态。建议为每次执行生成一个executionId后续排查时按executionId查询全部日志。持久化和恢复。真实工作流往往不会在几毫秒内完成可能因为等待人工审批而挂起数天。执行状态需要持久化到数据库并在应用重启后恢复。此时执行器的状态管理要更复杂需要支持暂停、恢复、超时、重新触发等操作。监控和告警。对执行失败、执行超过最大步数、节点访问异常进行监控。比如统计平均执行步数、条件分支命中比例这些数据可以用来优化工作流结构。版本管理。工作流定义会不断演进。线上正在执行的流程和最新版本的工作流不应该互相影响。常见做法是给每次发布生成一个版本号执行时锁定当前版本避免运行中被修改。7.2 可复用检查清单在从一个新工作流上线前可以按这个清单检查工作流定义只有一个开始节点且开始节点存在。所有next、whenTrue、whenFalse引用的节点都存在。从开始节点到任意结束节点之间不存在不可达节点。条件表达式没有重复字段、没有引用不存在的上下文字段。所有end节点都设置了业务结果。上下文在入口处完成字段类型和枚举值校验。执行器配置了最大步数限制避免死循环。每次执行都有唯一标识日志覆盖节点访问路径。条件新增类型后求值函数已完成never穷尽检查。工作流定义有版本号生产环境支持回滚到上一版本。7.3 扩展方向这套“有类型条件工作流”可以继续扩展的方向很多。如果条件类型越来越复杂可以增加not、between、in等操作符如果希望更灵活可以把条件从树形结构改成表达式 DSL再用类型系统约束 AST如果需要并行节点可以引入fork和join节点运行时状态需要支持多个活动分支如果接入前端可视化编辑器类型定义可以直接作为协议 schema生成表单控件和校验规则。对于新手来说最值得做的练习是先保留当前的WorkflowContext新增一个条件类型比如amountBetween完整走一遍“扩展类型、扩展求值函数、新增用例、运行验证”的流程。这个过程会帮助你真正理解判别联合、类型收窄和穷尽检查的价值。条件工作流的有类型写法核心不是把所有东西都塞进强类型里而是把容易出错的边界尽可能交给编译器和管理工具。编译器能帮你挡住字段写错、条件漏写、分支缺失运行时校验能挡住外部数据不合法启动前校验能挡住数据完整性错误。三层防线配合起来工作流才是真正可以在生产环境长期演进的代码。
返回列表