低代码与工作流引擎集成

拆解低代码平台与工作流引擎的集成:流程模型的表达(BPMN 与简化 DSL)、节点类型与执行语义、表单与流程的绑定、会签或签加签、条件分支与表达式、状态机持久化、定时任务与超时、外部系统集成、流程版本与灰度,以及可观测性,给出可运行的流程定义与状态流转实现,回答如何把审批流做对做稳。

引言

工作流是低代码平台里最接近「业务本质」的部分。表单收集数据,流程决定数据如何流转、由谁处理、何时终止。审批、报销、工单、订单状态机,本质上都是流程。低代码平台如果不内置流程能力,就只能做「静态的表单工具」,价值大打折扣。

但流程引擎的复杂度远高于表单:它要处理并发(多人同时审批)、持久化(流程可能挂起数天)、版本(流程定义改了,运行中的实例怎么办)、超时(三天未审批自动升级)、补偿(某步失败如何回滚)。这些问题在表单引擎里都不存在,在流程引擎里却是日常。

本文按「流程模型 → 节点语义 → 表单绑定 → 审批模式 → 条件分支 → 状态持久化 → 定时超时 → 外部集成 → 版本灰度 → 可观测性」展开,给出流程定义 DSL、状态流转代码与持久化表设计。读完后你应当能判断:一个流程引擎的复杂度分布在哪,以及哪些问题必须在设计阶段就解决。

目录

  1. 工作流在低代码中的位置
  2. 流程模型的表达
  3. 节点类型与执行语义
  4. 表单与流程的绑定
  5. 会签、或签与加签
  6. 条件分支与表达式
  7. 状态机与持久化
  8. 定时任务与超时
  9. 与外部系统集成
  10. 流程的版本与灰度
  11. 可观测性与运维

1. 工作流在低代码中的位置

三层分工:
  数据层:存什么(数据模型)
  界面层:怎么录入/查看(表单/页面)
  流程层:谁在什么时候做什么(工作流)

流程层的输入输出:
  输入:表单提交的数据 + 触发条件
  输出:任务分配 + 状态变更 + 通知

流程层是「粘合剂」:它把静态的数据与界面串成有生命周期的业务对象。

2. 流程模型的表达

流程定义有两种主流表达:完整的 BPMN 与简化的自定义 DSL。

BPMN 2.0:
  优点:标准、表达力强、有工具生态
  缺点:复杂、学习曲线陡、低代码用户难理解

简化 DSL:
  优点:易读易编辑、贴合低代码场景
  缺点:表达力有限、无标准

折中:
  内部用简化 DSL 建模,可导出 BPMN
  或直接采用 BPMN 子集(仅顺序/分支/并行/会签)
{
  "id": "leave_flow",
  "version": 3,
  "start": "submit",
  "nodes": {
    "submit":   { "type": "start", "form": "leave_form", "next": "mgr_approve" },
    "mgr_approve": {
      "type": "task",
      "assignee": { "type": "manager", "of": "{{ initiator }}" },
      "next": "days_check"
    },
    "days_check": {
      "type": "gateway",
      "branches": [
        { "when": "{{ form.days > 3 }}", "next": "hr_approve" },
        { "when": "else", "next": "end" }
      ]
    },
    "hr_approve": {
      "type": "task",
      "assignee": { "type": "role", "value": "hr" },
      "next": "end"
    },
    "end": { "type": "end" }
  }
}

建议采用简化 DSL 的子集:顺序、条件分支、并行、会签四种结构,覆盖 95% 的审批场景,且低代码用户能看懂。

3. 节点类型与执行语义

节点类型决定了引擎的执行逻辑。

节点语义是否等待典型场景
start流程入口否提交表单
task人工任务是审批、填写
service自动任务是(异步)调接口、发通知
gateway条件分支否金额分流
parallel并行分支是多部门会签
timer定时等待是超时提醒
end流程结束否归档
interface FlowNode {
  type: 'start' | 'task' | 'service' | 'gateway' | 'parallel' | 'timer' | 'end';
  assignee?: AssigneeRule;      // task
  branches?: Branch[];          // gateway
  service?: ServiceRef;         // service
  duration?: string;            // timer(如 PT72H)
  next?: string | string[];
}

「是否等待」是核心语义:等待节点会持久化实例并挂起,直到外部事件(用户操作、定时器、回调)唤醒它。

4. 表单与流程的绑定

流程与表单的关系有三种模式。

模式 A:流程内嵌表单
  每个节点可指定一个表单,用于该节点的输入
  适合:不同节点填不同信息(申请填 A,审批填 B)

模式 B:流程引用表单
  流程只引用一个主表单,节点做只读/编辑切换
  适合:单一单据的审批

模式 C:表单驱动流程
  表单提交即触发流程,流程无独立表单
  适合:简单的一次性审批
{
  "mgr_approve": {
    "type": "task",
    "form": {
      "ref": "leave_form",
      "mode": "readonly",
      "editableFields": ["approveComment"]
    }
  }
}

字段级权限在流程节点上的表达是难点:同一表单在不同节点,不同角色看到的可编辑字段不同。这要求表单渲染器支持「节点上下文」下的字段权限。

5. 会签、或签与加签

多人审批是最容易做错的部分。

会签(all):所有人同意才通过
或签(any):任一人同意即通过
依次(sequential):按顺序逐个审批
加签(add):审批过程中临时增加审批人
{
  "hr_approve": {
    "type": "task",
    "assignee": { "type": "role", "value": "hr" },
    "approvalMode": "all",
    "strategy": {
      "onReject": "toStart",
      "onTimeout": "escalate",
      "escalateTo": { "type": "role", "value": "hr_lead" }
    }
  }
}

5.1 会签的判定与短路

会签要处理两个边界:短路(或签中第一个同意即通过,其余任务自动取消)与驳回的传播(一人驳回,其余待办如何处理)。这些必须显式定义,否则会出现「流程卡死」或「重复审批」。

6. 条件分支与表达式

条件分支决定了流程的走向。

表达式来源:
  表单字段:{{ form.amount }}
  流程变量:{{ variables.level }}
  上下文:{{ initiator.dept }}
  系统:{{ now }}

求值语义:
  按顺序匹配,第一个为 true 的分支胜出
  else 分支兜底
function evaluateGateway(node: FlowNode, ctx: FlowContext): string {
  for (const branch of node.branches ?? []) {
    if (branch.when === 'else') return branch.next;
    if (evaluateExpr(branch.when, ctx)) return branch.next;
  }
  throw new Error(`gateway ${node.id} no branch matched`);
}

表达式引擎与表单引擎共用同一套实现,确保语义一致——这是 Schema 驱动的表单引擎 中「一次描述、多处消费」的延续。

7. 状态机与持久化

流程实例必须持久化,因为它可能挂起数天。

CREATE TABLE wf_instance (
  id            varchar(36) PRIMARY KEY,
  def_id        varchar(64) NOT NULL,
  def_version   int NOT NULL,
  tenant_id     varchar(36) NOT NULL,
  business_key  varchar(64),          -- 业务单据 id
  status        varchar(20) NOT NULL, -- running/completed/terminated
  current_node  varchar(64),
  variables     jsonb NOT NULL DEFAULT '{}',
  started_at    timestamptz NOT NULL DEFAULT now(),
  ended_at      timestamptz
);

CREATE TABLE wf_task (
  id            varchar(36) PRIMARY KEY,
  instance_id   varchar(36) NOT NULL REFERENCES wf_instance(id),
  node_id       varchar(64) NOT NULL,
  assignee      varchar(64),
  status        varchar(20) NOT NULL, -- pending/approved/rejected/canceled
  created_at    timestamptz NOT NULL DEFAULT now(),
  acted_at      timestamptz,
  comment       text
);

CREATE INDEX idx_task_assignee ON wf_task(assignee, status);
状态流转:
  instance: running → completed / terminated
  task:     pending → approved / rejected / canceled

7.1 幂等与并发

同一任务被两个请求同时处理(双击、重试)会导致重复流转。用「任务状态乐观锁」:UPDATE wf_task SET status='approved' WHERE id=? AND status='pending',影响行数为 0 说明已被处理。

8. 定时任务与超时

超时是流程的常见需求,实现依赖定时器。

超时场景:
  - 节点超时提醒(3 天未审批 → 发提醒)
  - 节点超时自动处理(5 天 → 自动通过/升级)
  - 流程级 SLA(整体 7 天未完成 → 告警)

实现:
  方案 A:数据库轮询(扫 due 时间到期的任务)
  方案 B:延迟队列(Redis ZSet / MQ 延迟消息)
  方案 C:调度框架(Quartz / Temporal)
-- 方案 A:扫到期任务
SELECT id FROM wf_task
WHERE status = 'pending' AND due_at <= now()
ORDER BY due_at LIMIT 100;

方案 B 更精确但需要额外基础设施;方案 A 简单但有轮询延迟。多数低代码平台用「延迟队列 + 兜底轮询」组合。

9. 与外部系统集成

流程中的自动节点常需调用外部系统。

{
  "notify": {
    "type": "service",
    "service": {
      "kind": "http",
      "method": "POST",
      "url": "/internal/api/notify",
      "headers": { "Authorization": "Bearer {{ secrets.notifyToken }}" },
      "body": { "to": "{{ task.assignee }}", "title": "待审批" },
      "retry": { "max": 3, "backoff": "exponential" },
      "timeout": "PT10S"
    },
    "onError": "continue"
  }
}
失败策略:
  - 重试:网络抖动,指数退避
  - 跳过(continue):非关键步骤
  - 挂起(suspend):关键步骤失败则暂停流程等人工处理
  - 终止(terminate):不可恢复错误

关键原则:自动节点失败不能让流程凭空消失。要么重试成功,要么进入「异常待处理」状态,绝不能静默丢弃。

10. 流程的版本与灰度

流程定义会变,但运行中的实例不能受影响。

版本策略:
  1. 新实例用新版本,旧实例继续用旧版本(推荐)
  2. 强制所有实例升级(危险,可能不兼容)
  3. 双版本并行(灰度,按比例分流)

实现:
  实例持久化时记录 def_version
  引擎按实例的 def_version 加载对应定义
function loadDefinition(defId: string, version: number): FlowDefinition {
  // 版本化加载:确保运行中实例不受新版本影响
  return definitionStore.get(defId, version);
}

这是流程引擎与普通代码发布最大的不同:代码发布后所有请求用新代码,流程发布后旧实例仍跑旧定义。

11. 可观测性与运维

流程是长事务,出问题时排查困难。

必备能力:
  - 实例视图:当前节点、停留时长、处理人
  - 任务视图:待办、超时、异常
  - 流转日志:每次状态变更的时间与操作人
  - 死信/异常队列:失败自动节点的重试入口
  - 看板:各节点平均耗时、超时率

没有流转日志,用户问「我的单子为什么卡住」时你无法回答。日志是流程引擎的一等公民,不是可选项。

权衡取舍

决策点选项 A选项 B建议
模型表达完整 BPMN简化 DSL简化子集,覆盖 95%
超时实现轮询延迟队列延迟队列 + 轮询兜底
版本策略强制升级实例隔离实例隔离,安全
自动节点失败静默跳过挂起待处理挂起,不可静默丢弃
任务并发无锁乐观锁乐观锁,防重复流转

常见坑清单

  1. 会签驳回不定义传播:一人驳回后其余待办状态不明,流程卡死,需显式定义。
  2. 或签不短路:多人同意后仍等待其余人,体验差且浪费。
  3. 自动节点静默失败:外部调用失败后流程凭空消失,必须挂起或重试。
  4. 无任务乐观锁:双击导致重复审批与重复流转。
  5. 流程升级影响运行实例:新定义与旧实例不兼容导致报错,需实例级版本隔离。
  6. 无流转日志:无法回答「为什么卡住」,日志必须一等公民。
  7. 超时只做轮询:延迟大且扫表压力高,应用延迟队列。
  8. 表达式引擎与表单不一致:同一表达式两处结果不同,必须共用实现。
  9. 节点级字段权限未表达:不同节点可编辑字段不同却无法配置。
  10. 会签任务无取消机制:分支合并后残留待办,需在合并时取消。

小结

低代码与工作流引擎的集成,骨架是「流程模型 → 节点语义 → 表单绑定 → 审批模式 → 条件分支 → 状态持久化 → 定时超时 → 外部集成 → 版本灰度 → 可观测性」。其中三条原则最容易被忽视却最致命:实例级版本隔离、自动节点失败不静默、流转日志一等公民。

流程引擎与表单引擎共享表达式求值、字段权限等基础能力,因此两者应构建在同一套元数据之上,而不是各自为政。这也是低代码平台「统一内核」的价值所在。

流程的落地离不开数据模型的定义,见 数据模型设计器 ;而流程中需要调用外部能力时,如何以插件形式扩展,见 插件机制与扩展体系 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「低代码」更多文章

  1. 自定义代码与逃生舱
  2. 低代码应用测试与质量
  3. 连接器与 API 编排