Skip to content

Activiti BPMN流程设计与可执行建模

BPMN 流程图不是给领导看的静态图片,而是一份会被引擎解析和执行的模型。节点 ID、连线方向、变量表达式、候选组、边界事件和多实例完成条件,都会直接决定运行时产生哪些 Execution、Task、Job 和历史记录。

真正的流程设计不是把现有 if/else 画成菱形,而是先澄清业务角色、事实、权限、状态、异常和一致性边界,再把适合由流程引擎协调的部分翻译成 BPMN。流程图负责“过程如何推进”,业务表负责“业务事实是什么”,权限服务负责“谁真的有权操作”,三者不能互相替代。

学习目标

完成本页后,你应该能够:

  1. 解释 BPMN XML 为什么是可执行模型,而不是普通图片。
  2. 区分 Event、Task、Gateway、SubProcess、Call Activity 和 Sequence Flow。
  3. 从业务语言提取角色、动作、事实、状态、异常和审计要求。
  4. 为节点 ID、流程 key、变量和候选组建立稳定契约。
  5. 正确选择 User Task、Service Task、Receive Task 和异步任务。
  6. 正确选择排他、并行、包容与事件型网关。
  7. 设计 Timer、Error、Message 等事件和边界事件。
  8. 区分嵌入子流程、调用活动和多实例任务。
  9. 写出可执行 BPMN、Java Delegate 和模型测试 Demo。
  10. 识别万能流程图、任意跳转、大变量、硬编码审批人等反模式。
  11. 设计商业流程评审、版本兼容、监控和线上排查证据。

一、为什么要先建模再编码

“合同提交后需要审批”不是可实现需求。至少还缺少:

  • 谁能提交,提交时业务状态必须是什么?
  • 主管、法务、财务是固定人、角色、部门负责人还是动态人员集合?
  • 三个部门串行还是并行,谁拒绝后是否立即结束?
  • 金额或合同类型怎样影响分支?
  • 审批人离职、休假或组织变更怎么办?
  • 多人同时点审批,谁成功、谁得到幂等结果?
  • 超过 24 小时如何提醒,超过 72 小时是否升级?
  • 驳回到发起人还是上一节点,修改后是原实例继续还是重开?
  • 流程结束、监听器失败、消息发送失败时业务状态怎样保持一致?
  • 新流程发布后,旧实例和旧变量怎样兼容?

如果不先回答,问题不会消失,只会变成散落在 Controller、Listener、SQL、定时任务和前端按钮中的隐式规则。

mermaid
flowchart TD
    A["模糊业务需求"] --> B["识别角色、动作和业务事实"]
    B --> C["明确主路径、分支和异常矩阵"]
    C --> D["划分流程引擎、业务服务和权限边界"]
    D --> E["映射为BPMN元素和变量契约"]
    E --> F["模型评审与真值表测试"]
    F --> G["部署、冒烟和生产观察"]

二、BPMN文件到底包含什么

一个 BPMN 文件通常包含两类内容:

  1. 语义模型:process、Event、Task、Gateway、Sequence Flow 等,决定怎样执行。
  2. 图形布局:BPMN DI 中节点坐标和连线拐点,决定建模器怎样显示。

引擎真正执行的是语义模型。没有 DI 时,XML 仍可能可执行,但建模器可能无法正确显示布局;只有一张 PNG 图则无法执行,因为没有节点 ID、连接和表达式语义。

mermaid
flowchart TD
    A["BPMN XML"] --> B["语义部分:可执行模型"]
    A --> C["DI部分:图形坐标和连线"]
    B --> D["引擎解析为ProcessDefinition"]
    C --> E["Modeler或查看器渲染流程图"]

核心根元素:

xml
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:activiti="http://activiti.org/bpmn"
             targetNamespace="https://example.com/processes">

    <process id="contract_approval"
             name="合同审批"
             isExecutable="true">
        <!-- 可执行节点和连线 -->
    </process>
</definitions>
  • process id 部署后通常成为 processDefinitionKey,是长期版本线身份。
  • name 是面向人的名称,可以调整,但仍应经过变更审计。
  • isExecutable="true" 表示该模型用于执行,而不只是描述协作。
  • activiti 命名空间承载候选人、Delegate 等引擎扩展属性。

三、先掌握Token/Execution心智模型

可以把流程实例想成一个沿 Sequence Flow 移动的执行 Token。引擎内部用 Execution 表达执行路径;并行拆分会产生多个并发 Execution,汇聚会等待并合并相应路径。

mermaid
flowchart TD
    A["Start Event创建执行路径"] --> B["Execution进入User Task并等待"]
    B --> C["用户完成Task"]
    C --> D["Execution沿Sequence Flow继续"]
    D --> E{"并行网关"}
    E --> F["Execution A"]
    E --> G["Execution B"]
    F --> H{"汇聚"}
    G --> H
    H --> I["End Event结束"]

这个模型能解释:

  • User Task 产生待办并让 Execution 等待,不占用 Java 线程一直阻塞。
  • Service Task 通常立即执行代码,除非配置异步边界。
  • 并行 BPMN 是执行路径并发,不等于操作系统线程并发。
  • 某分支没有到达 Join 时,流程会保留运行状态,而不是“引擎丢了任务”。

深入见:网关、Execution与分支汇聚原理

四、Event:流程为什么开始、等待和结束

Event 表示发生的事情,而不是人工动作。

4.1 Start Event

常见启动方式:

类型含义场景
None Start Event由API直接启动用户提交申请
Message Start Event收到特定业务消息后启动接收外部工单事件
Timer Start Event按时间规则启动每月结算流程
Signal Start Event收到广播信号启动多流程共同响应事件,需谨慎

商业 API 发起审批通常用 None Start Event,并由业务 Service 在事务内保存业务单、校验幂等并启动实例。

4.2 Intermediate Catch Event

流程运行到这里后等待外部事件,例如消息、信号或定时器。等待期间不会让一个 Web 请求一直占线程;引擎保存订阅或 Job,事件到达时再恢复 Execution。

4.3 Boundary Event

边界事件附着在 Task/SubProcess 上,表示节点执行期间发生超时或错误时怎样处理。

mermaid
flowchart TD
    A["经理审批User Task"] --> B["正常完成后进入下一节点"]
    A --> C["边界Timer:24小时到期"]
    C --> D["发送催办或升级任务"]

边界事件分为中断和非中断:

  • 中断边界事件:触发后取消被附着 Activity,再走异常路径。
  • 非中断边界事件:触发后额外产生一条路径,原 Activity 继续等待。

催办通常适合非中断 Timer;超时自动取消可能用中断 Timer。模型符号和引擎支持以当前 Activiti 版本为准。

4.4 End Event

普通 End Event 结束到达它的当前执行路径。在并行结构中,一条路径到达普通 End 不一定终止整个流程实例;Terminate End Event 通常会终止所在作用域中的其他活动路径。

因此“任一审批拒绝就结束全部并行任务”不能只画普通 End,必须明确终止范围并测试其他 Task/Execution 是否被取消。

五、Task:谁真正执行工作

5.1 User Task

User Task 表示需要人处理。到达后通常写入运行时任务表,等待认领或完成。

xml
<userTask id="legalReview"
          name="法务审核"
          activiti:candidateGroups="LEGAL_REVIEWER"/>

它只表达流程候选关系,业务 Service 仍要校验:

  • 当前登录用户是否属于候选组或就是 assignee。
  • 是否拥有该合同/部门的数据权限。
  • Task 是否仍处于可处理状态。
  • 请求是否重复,审批意见是否满足必填规则。

前端“看不到按钮”不是权限控制。

5.2 Service Task

Service Task 让引擎调用应用代码,例如生成发布记录、调用风险服务、发送命令。

常见配置方式会随版本不同,包括:

xml
<serviceTask id="freezeBudget"
             name="冻结预算"
             activiti:delegateExpression="${budgetFreezeDelegate}"/>

Java Delegate 示例:

java
@Component("budgetFreezeDelegate")
public class BudgetFreezeDelegate implements JavaDelegate {

    private final BudgetService budgetService;

    public BudgetFreezeDelegate(BudgetService budgetService) {
        this.budgetService = budgetService;
    }

    @Override
    public void execute(DelegateExecution execution) {
        String businessKey = execution.getProcessInstanceBusinessKey();
        String operationId = execution.getProcessInstanceId()
                + ":" + execution.getCurrentActivityId();

        budgetService.freezeIdempotently(businessKey, operationId);
    }
}

Delegate 必须幂等,因为事务重试、Job 重试、网络超时和人工补偿都可能再次触发。operationId 应在下游建立唯一约束,而不是只在内存 Set 中去重。

5.3 Receive Task

Receive Task 表示流程主动停下,等待外部 API/消息触发继续。适合外部系统回执,但必须设计 Correlation Key、重复消息、超时和消息先到/订阅后建的竞态。

5.4 Script Task

脚本任务灵活,但会引入脚本引擎依赖、沙箱、资源限制、供应链和任意代码执行风险。生产优先使用受版本控制和测试的 Delegate;若允许在线脚本,发布权限应按代码发布级别治理。

5.5 Manual Task与Business Rule Task

Manual Task 在可执行引擎中通常不会产生可办理 User Task;它更多表达引擎外人工动作。Business Rule Task 依赖规则引擎集成。不要看到名称就假设 Activiti 会自动提供表单、规则管理和人员待办。

六、Gateway:流程怎样选择和同步

需求应选择不应选择
金额决定唯一审批级别排他网关并行网关
法务和财务都必须审核并行网关两条条件重叠的排他网关
按风险选择一个或多个部门包容网关固定全部激活的并行网关
等待支付或超时谁先到事件型网关不断轮询变量的排他网关

网关的完整执行、汇聚和排障见:网关、Execution与分支汇聚原理

七、Sequence Flow:流程真正怎样连接

Sequence Flow 连接同一 Process/子流程范围内的节点。它可以带条件表达式:

xml
<sequenceFlow id="toDirector"
              sourceRef="amountGateway"
              targetRef="directorReview">
    <conditionExpression xsi:type="tFormalExpression"><![CDATA[
        ${approvalLevel == 'DIRECTOR'}
    ]]></conditionExpression>
</sequenceFlow>

节点在画布上靠得近不代表有连接;线条穿过节点也不代表执行经过该节点。唯一权威是 XML 中 sourceReftargetRef 和引擎解析模型。

节点和连线 ID 必须稳定:

  • task1gateway2 没有业务含义,日志和迁移难读。
  • 修改 Activity ID 会影响运行实例迁移、历史报表和监控聚合。
  • 同一版本内 ID 必须唯一。
  • 建议使用英文稳定 ID,加中文业务名称。

示例:

text
ID: securityReview
Name: 安全审核

八、SubProcess和Call Activity怎么选

8.1 Embedded SubProcess

嵌入子流程属于当前 ProcessDefinition,适合组织局部复杂度和定义统一边界事件。

优点:同一版本、变量作用域关联直观;缺点:不能被多个流程独立复用和发布。

8.2 Event SubProcess

事件子流程由事件触发,可中断或非中断当前作用域。适合统一处理取消、异常或补偿,但执行语义复杂,必须验证与主流程并发路径的关系。

8.3 Call Activity

Call Activity 调用另一个独立流程定义,适合可复用的标准流程,例如“统一法务审查”。

xml
<callActivity id="callLegalReview"
              name="调用法务审查子流程"
              calledElement="legal_review"/>

风险:

  • calledElement 按 key 解析时可能调用到最新子流程版本。
  • 父流程与子流程版本组合需要可追踪。
  • 输入/输出变量映射不清会污染父作用域。
  • 子流程挂起、失败或升级会影响多个父流程。

8.4 选型表

需求Embedded SubProcessCall Activity
只为当前流程折叠复杂度适合过重
多个流程复用不适合适合
独立部署版本不支持支持
与父流程同版本发布天然一致需要组合版本治理
局部变量隔离有作用域但仍在同定义需要显式输入输出设计

不要仅为了让图“看起来小”就拆 Call Activity。独立发布、复用和团队边界才是更重要的依据。

九、多实例任务:动态多人审批

多人会签不是画很多固定并行线。Multi-instance 会根据集合为同一个 Activity 创建多个实例。

xml
<userTask id="expertReview"
          name="专家会审"
          activiti:assignee="${expertUserId}">
    <multiInstanceLoopCharacteristics
            isSequential="false"
            activiti:collection="expertUserIds"
            activiti:elementVariable="expertUserId">
        <completionCondition><![CDATA[
            ${nrOfCompletedInstances == nrOfInstances}
        ]]></completionCondition>
    </multiInstanceLoopCharacteristics>
</userTask>

常见内置计数变量语义:

  • nrOfInstances:总实例数。
  • nrOfActiveInstances:当前活动实例数。
  • nrOfCompletedInstances:已完成实例数。
  • loopCounter:当前实例索引。

具体名称和可见范围以当前 Activiti 版本为准。

9.1 并行会签

isSequential="false" 通常一次生成多个并行任务,适合专家同时评审。要考虑任务数量峰值、撤销剩余任务、审批意见冲突和同一人重复出现。

9.2 串行会签

isSequential="true" 一次只激活一个实例,按集合顺序处理。组织人员集合如果无稳定排序,实际审批顺序可能不可预测。

9.3 completionCondition

全票完成:

xml
${nrOfCompletedInstances == nrOfInstances}

达到半数即结束的示意思路:

xml
${nrOfCompletedInstances * 2 >= nrOfInstances}

但“完成”不等于“同意”。实际会签需要单独统计批准和拒绝结果,并定义剩余活动 Task 是否被取消。不要把完成数量误当通过票数。

十、Listener不是万能业务层

Activiti 常见 Task Listener 和 Execution Listener,可在创建、分配、完成、节点开始/结束等生命周期执行代码。

适合:

  • 记录必要的流程审计事件。
  • 计算简单候选人。
  • 发布事务内领域事件到 Outbox。
  • 做稳定、短时、幂等的流程技术动作。

不适合:

  • 把全部业务状态机藏在 Listener 中。
  • 同步调用多个慢外部接口。
  • 吞掉异常继续提交不一致状态。
  • 依赖前端参数决定权限。
  • 在多个 Listener 重复更新同一业务字段。

Listener 通常与引擎命令处于同一事务。抛异常会导致完成任务或推进流程回滚;吞异常则可能让流程继续但业务动作没完成。必须明确失败策略。

十一、同步和异步边界

同步 Service Task 在当前 API 请求和引擎事务中执行:

mermaid
flowchart TD
    A["用户完成审批Task"] --> B["Execution进入Service Task"]
    B --> C["当前线程调用Delegate"]
    C --> D["Delegate成功"]
    D --> E["创建下一任务并提交事务"]

优点是事务结果直接;缺点是慢调用拖长锁和响应时间,外部超时也可能导致整体回滚。

异步边界通常会创建 Job,由 Job Executor 后续推进:

mermaid
flowchart TD
    A["审批完成到达异步边界"] --> B["持久化Job并提交当前事务"]
    B --> C["Job Executor获取任务"]
    C --> D["执行Service Task"]
    D --> E{"执行结果"}
    E -->|"成功"| F["推进到下一节点"]
    E -->|"失败"| G["记录异常并按策略重试"]

异步提高故障隔离,但带来最终一致性、重复执行、Job 重试和监控要求。它不是简单加一个属性就自动可靠。

十二、流程变量怎样设计

流程变量只保存推进流程真正需要的小型上下文:

适合放变量不适合放变量
approvalLevel=HIGH完整合同对象JSON
sensitive=true附件二进制
applicantId=U1001医疗明细列表
reviewerIds=[...]可通过业务ID查询的重复大对象
ruleVersion=2026-07密码、Token、私钥

大对象会造成:

  • Runtime/History变量表膨胀。
  • 每次序列化、反序列化和查询成本增加。
  • Java序列化类版本变化后难以读取旧变量。
  • 敏感信息扩散到日志、历史和备份。
  • 业务查询绕开正式业务表。

推荐保存业务 ID,在业务 Service 中按权限读取最新数据;若审批必须基于提交时快照,则在业务审计表保存不可变快照和版本,流程变量只保存快照 ID。

十三、业务状态和流程状态怎样划分

流程引擎表回答:

  • 当前 Execution 在哪个 Activity。
  • 有哪些 User Task、Job 和变量。
  • 流程历史经过哪些节点。

业务表回答:

  • 合同金额、主体、版本、附件和业务合法性。
  • 当前业务状态,例如 APPROVINGAPPROVEDREJECTED
  • 审批意见、操作者、幂等请求和审计快照。
  • 数据权限、报表和外部系统同步状态。
mermaid
flowchart TD
    A["业务表:合同事实和业务状态"] --> C["businessKey/processInstanceId关联"]
    B["Activiti:Execution、Task和History"] --> C
    D["审批记录表:每次业务动作"] --> C

不能通过当前 Activity ID 直接替代所有业务状态,因为同一业务状态可能包含多个并行 Activity,流程结束后运行时记录会清理,业务查询也不应强依赖引擎内部表。

十四、审批人分配怎样设计

14.1 固定assignee

xml
activiti:assignee="user001"

只适合测试。人员离职、调岗后模型需要重新发布,不适合生产。

14.2 候选组

xml
activiti:candidateGroups="LEGAL_REVIEWER"

流程保存稳定角色代码,实际人员由组织权限系统解析。待办查询和完成接口都要验证用户当前组关系。

14.3 表达式解析动态人员

xml
activiti:assignee="${departmentManagerId}"

业务 Service 在启动或前置节点计算 departmentManagerId。必须处理为空、离职、多人、代理和组织变更。

14.4 自定义人员解析服务

复杂系统可以通过白名单 Bean 查询组织:

xml
activiti:assignee="${approvalAssigneeResolver.resolve(execution)}"

这会让模型依赖应用 Bean、数据库和组织服务。要设置超时、缓存、降级和兼容期,并防止普通模型作者调用任意 Bean。

14.5 任务创建时快照还是实时解析

  • 创建 Task 时确定 assignee:轨迹稳定,但人员离职后需转办。
  • 查询待办时实时按候选组:组织变更立即生效,但审计要记录真正办理人。

两者可以结合:角色候选关系实时解析,认领后固定 assignee 并记录处理快照。

十五、驳回、撤回和任意跳转不是一个动作

驳回

当前审批人根据业务规则拒绝,可能结束流程、退回发起人修改或返回固定复核节点。

撤回

通常由发起人在下游尚未处理时撤销申请。要校验当前节点、并发任务、外部副作用和权限。

退回上一步

“上一步”在并行、循环、子流程中不一定唯一。根据历史最后节点动态跳转可能得到错误目标。

任意跳转

改变当前 Activity 会影响 Execution、Task、并行 Token、Job、历史和业务状态。它不是改一个 ACT_RU_TASK.TASK_DEF_KEY_ 字段。

推荐策略:

  • 常见驳回路径明确画在 BPMN 中,语义可读、可测试。
  • 复杂人工纠错由受控管理员操作,记录原因、前后节点、业务状态和补偿。
  • 不向普通用户开放任意 Activity ID。
  • 动态状态变更 API 能力随 Activiti 版本不同,必须以当前版本测试。

十六、超时、催办和升级怎样建模

需求:“24 小时未审批就催办,72 小时未审批就升级给上级,但原审批仍可处理。”

可以用两个非中断 Timer Boundary Event,或者将提醒交给独立 SLA 服务。选择取决于提醒是否属于流程语义、任务量和调度治理能力。

mermaid
flowchart TD
    A["审批Task"] --> B["正常审批完成"]
    A --> C["24小时非中断Timer"]
    C --> D["发送催办"]
    A --> E["72小时非中断Timer"]
    E --> F["创建升级通知或管理员任务"]

必须明确:

  • 计时从 Task 创建还是业务提交开始。
  • 节假日和工作日历怎样计算。
  • 应用停机期间错过 Timer 怎样补触发。
  • 多次催办频率和幂等键。
  • 任务完成后 Timer Job 是否清理。
  • 升级是通知、转办还是新增并行任务。

十七、错误、重试和补偿怎样设计

三类失败不要混在一起:

失败示例处理
业务拒绝法务认为合同条款不合规明确BPMN业务分支,不应作为系统异常重试
瞬时技术失败下游接口超时、数据库连接短暂失败异步Job有限重试、退避、幂等
永久技术失败参数非法、目标不存在、权限配置错误停止自动重试,告警和人工修复

补偿不是数据库回滚。外部系统已冻结预算、发送短信或创建账户后,当前事务回滚不能自动撤销远程副作用,需要可幂等的反向操作和审计。

十八、完整商业BPMN Demo

下面演示合同审批主链:主管审批后按金额决定是否进入并行法务/财务审核,最后归档。为了保持示例可读,省略 BPMN DI 坐标和部分扩展。

xml
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:activiti="http://activiti.org/bpmn"
             targetNamespace="https://example.com/processes">

    <process id="contract_approval"
             name="合同审批"
             isExecutable="true">

        <startEvent id="submitStart" name="提交合同"/>

        <userTask id="managerReview"
                  name="直属主管审批"
                  activiti:candidateGroups="CONTRACT_MANAGER"/>

        <exclusiveGateway id="managerResultGateway"
                          name="主管审批结果"
                          default="toRejectedEnd"/>

        <exclusiveGateway id="amountGateway"
                          name="判断是否需要联合审核"
                          default="toArchiveDirectly"/>

        <parallelGateway id="jointReviewSplit"
                         name="发起联合审核"/>

        <userTask id="legalReview"
                  name="法务审核"
                  activiti:candidateGroups="LEGAL_REVIEWER"/>

        <userTask id="financeReview"
                  name="财务审核"
                  activiti:candidateGroups="FINANCE_REVIEWER"/>

        <parallelGateway id="jointReviewJoin"
                         name="等待联合审核完成"/>

        <serviceTask id="archiveContract"
                     name="归档合同"
                     activiti:delegateExpression="${contractArchiveDelegate}"/>

        <endEvent id="approvedEnd" name="审批完成"/>
        <endEvent id="rejectedEnd" name="审批拒绝"/>

        <sequenceFlow id="f1" sourceRef="submitStart" targetRef="managerReview"/>
        <sequenceFlow id="f2" sourceRef="managerReview" targetRef="managerResultGateway"/>

        <sequenceFlow id="toAmountGateway"
                      sourceRef="managerResultGateway"
                      targetRef="amountGateway">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[
                ${managerApproved == true}
            ]]></conditionExpression>
        </sequenceFlow>

        <sequenceFlow id="toRejectedEnd"
                      sourceRef="managerResultGateway"
                      targetRef="rejectedEnd"/>

        <sequenceFlow id="toJointReview"
                      sourceRef="amountGateway"
                      targetRef="jointReviewSplit">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[
                ${jointReviewRequired == true}
            ]]></conditionExpression>
        </sequenceFlow>

        <sequenceFlow id="toArchiveDirectly"
                      sourceRef="amountGateway"
                      targetRef="archiveContract"/>

        <sequenceFlow id="toLegal" sourceRef="jointReviewSplit" targetRef="legalReview"/>
        <sequenceFlow id="toFinance" sourceRef="jointReviewSplit" targetRef="financeReview"/>
        <sequenceFlow id="legalDone" sourceRef="legalReview" targetRef="jointReviewJoin"/>
        <sequenceFlow id="financeDone" sourceRef="financeReview" targetRef="jointReviewJoin"/>
        <sequenceFlow id="jointDone" sourceRef="jointReviewJoin" targetRef="archiveContract"/>
        <sequenceFlow id="archiveDone" sourceRef="archiveContract" targetRef="approvedEnd"/>
    </process>
</definitions>

这个模型仍需补“法务/财务拒绝”的明确路径。不能因为 Demo 展示主链,就默认商业规则已完整。

十九、业务发起Demo

java
@Service
public class ContractProcessApplicationService {

    private static final BigDecimal JOINT_REVIEW_THRESHOLD =
            new BigDecimal("100000.00");

    private final ContractRepository contractRepository;
    private final RuntimeService runtimeService;

    public ContractProcessApplicationService(
            ContractRepository contractRepository,
            RuntimeService runtimeService) {
        this.contractRepository = contractRepository;
        this.runtimeService = runtimeService;
    }

    @Transactional
    public String submit(Long contractId, String operatorId) {
        Contract contract = contractRepository.findByIdForUpdate(contractId)
                .orElseThrow(() -> new IllegalArgumentException("合同不存在"));

        if (!contract.canSubmitBy(operatorId)) {
            throw new IllegalStateException("无权提交或状态不允许");
        }

        if (contract.getProcessInstanceId() != null) {
            return contract.getProcessInstanceId();
        }

        boolean jointReviewRequired = contract.getAmount()
                .compareTo(JOINT_REVIEW_THRESHOLD) >= 0;

        Map<String, Object> variables = new HashMap<>();
        variables.put("applicantId", operatorId);
        variables.put("jointReviewRequired", jointReviewRequired);
        variables.put("ruleVersion", "CONTRACT-2026-07");

        ProcessInstance instance = runtimeService.startProcessInstanceByKey(
                "contract_approval",
                contractId.toString(),
                variables);

        contract.markApproving(
                instance.getProcessInstanceId(),
                instance.getProcessDefinitionId());

        return instance.getProcessInstanceId();
    }
}

要点:

  • 使用数据库行锁或唯一约束防止同一合同重复启动。
  • businessKey 使用稳定业务 ID。
  • 业务规则由服务端可信数据计算。
  • 保存 processInstanceId 和 processDefinitionId,便于版本排查。
  • 金额留在业务表,流程变量只保存决策结果和规则版本。

二十、从业务语言到BPMN的详细步骤

步骤1:写一句流程目标

例如:“保证高金额合同在归档前经过主管、法务和财务授权,并形成可审计记录。”

如果目标是“把所有业务都放进 Activiti”,说明边界尚未想清楚。

步骤2:列业务角色

角色能做什么不能做什么
申请人提交、在允许阶段撤回、查看本人单据审批自己的高风险合同
主管审批本部门合同处理无数据权限的部门合同
法务审查条款修改合同金额
财务审查预算和付款条件绕过法务结论
流程管理员故障纠正和转办无审计地修改业务结论

步骤3:列业务事实

金额、合同类型、数据等级、部门、风险结果等。标记来源、类型、是否可变和决策时点。

步骤4:画Happy Path

先只画正常提交、审批、结束,不立即塞入全部撤回和超时。

步骤5:补分支真值表

managerApprovedjointReviewRequired预期路径
false任意拒绝结束
truefalse直接归档
truetrue法务+财务并行后归档

步骤6:补异常矩阵

节点业务拒绝技术失败超时人员不可用
主管审批拒绝结束接口重试/保留原任务催办升级候选组转办
法务审批终止还是等待财务要明确同上催办代理人
归档Service Task不适用异步重试/人工处理超时告警不适用

步骤7:定义变量契约

记录名称、Java 类型、产生节点、消费节点、是否敏感、版本兼容期。

步骤8:定义业务状态映射

不要让前端直接根据 Activity ID 猜状态。明确在哪个业务动作更新 APPROVING/APPROVED/REJECTED

步骤9:定义权限和幂等

每个可操作节点都要说明服务端权限来源、requestId、重复提交结果和并发更新策略。

步骤10:生成路径测试

主路径、每条网关分支、并行部分完成、超时、Delegate失败、重复审批和新旧版本都要有测试。

二十一、流程评审应该有哪些人

角色关注点
业务负责人节点、角色、例外和SLA是否符合规则
开发API、变量、事务、幂等和版本兼容
测试路径组合、边界值、并发和故障注入
安全/合规权限分离、敏感变量、审计和保留
运维/SREJob、容量、监控、告警、发布和恢复
数据/DBA表增长、历史归档、索引和对账

只让流程设计器作者自审,容易验证“图能保存”,却没有验证商业正确性和生产可运行性。

二十二、模型静态检查清单

  • [ ] process key稳定、isExecutable=true
  • [ ] 每个节点/连线ID唯一且具有业务含义。
  • [ ] Start到所有正常End可达,没有孤立节点。
  • [ ] 每个User Task有明确候选/分配策略和无人处理兜底。
  • [ ] 排他条件互斥完整,并有安全Default Flow。
  • [ ] 并行/包容Split和Join结构成对、路径不无故绕过。
  • [ ] Service Task实现存在、幂等、超时和失败策略明确。
  • [ ] Timer时区、工作日和重复触发规则明确。
  • [ ] 边界事件中断/非中断语义正确。
  • [ ] Call Activity版本和变量映射明确。
  • [ ] Multi-instance集合为空、重复人员和完成条件有定义。
  • [ ] Listener/表达式只调用白名单能力。
  • [ ] 没有密码、Token、病历或大对象流程变量。

二十三、动态测试应该验证什么

部署成功只证明模型能解析。动态测试至少验证:

  1. 每种业务输入启动到正确第一任务。
  2. 候选人能查到,非候选人无法越权完成。
  3. 每条排他分支都能命中,异常值走安全默认线。
  4. 并行任务只完成一部分时 Join 保持等待。
  5. 所有已激活分支完成后下游只生成一次。
  6. Timer 到期和任务先完成两种竞态结果。
  7. Delegate 第一次失败、重试成功和永久失败。
  8. 同一 requestId 重复提交不产生重复审批记录。
  9. 流程结束后运行时 Task/Execution 清理,历史完整。
  10. 新定义发布后旧实例仍按旧定义可继续执行。

二十四、模型测试Demo

java
@SpringBootTest
@Transactional
class ContractApprovalProcessTest {

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    @Test
    void highAmountContractShouldCreateTwoParallelReviews() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("jointReviewRequired", Boolean.TRUE);

        ProcessInstance instance = runtimeService.startProcessInstanceByKey(
                "contract_approval",
                "contract-test-001",
                variables);

        Task manager = taskService.createTaskQuery()
                .processInstanceId(instance.getId())
                .taskDefinitionKey("managerReview")
                .singleResult();

        assertNotNull(manager);

        taskService.complete(manager.getId(), Map.of(
                "managerApproved", Boolean.TRUE,
                "jointReviewRequired", Boolean.TRUE));

        List<Task> reviews = taskService.createTaskQuery()
                .processInstanceId(instance.getId())
                .list();

        Set<String> keys = reviews.stream()
                .map(Task::getTaskDefinitionKey)
                .collect(Collectors.toSet());

        assertEquals(Set.of("legalReview", "financeReview"), keys);
    }
}

测试代码中的 Map.of 需要 Java 9+。如果项目基线是 JDK 8,应使用 HashMap,不能把文档 Demo 直接复制后误以为 Activiti API 不兼容。

二十五、版本设计必须考虑旧实例

V2 删除了 V1 节点 legalReview,但 V1 仍有 500 个实例停在这个节点。部署 V2 不会把它们自动改到新节点;应用也必须继续支持 V1 任务完成和后续 Delegate。

模型变更分类:

变更风险
只改节点显示名称通常较低,但审计文本变化
新增仅新实例使用的可选分支中等,要测试变量默认值
修改变量名称/类型高,旧实例契约可能失效
修改Activity ID高,历史、监控和迁移映射受影响
改并行分支数量/汇聚结构很高,旧Execution无法自动映射
删除Delegate/Listener很高,旧实例运行到该节点失败

详细版本原理见:部署、版本、缓存与生产发布全过程

二十六、流程图过大怎么办

不要只按节点数量机械拆分。先判断复杂度来源:

  • 多个业务子域混在一起:按领域拆 Call Activity。
  • 主流程中塞了大量技术调用:把技术实现下沉到幂等 Service。
  • 每种组织例外都画一条线:使用稳定角色/规则输出,避免组织细节污染模型。
  • 驳回到任意历史节点:收敛为少数受支持业务动作。
  • 一个 Process 承担所有业务类型:按真正生命周期拆流程,不靠数百个网关判断类型。

可读性原则:

  • 主流程从上到下或从左到右保持一致。
  • 一张图只表达一个业务层级。
  • Split/Join 命名成对。
  • 异常路径使用清晰 End/Boundary Event。
  • 复杂说明放文档,不把长段文字塞节点。

本知识库 Mermaid 坚持竖向小图,是为了避免页面溢出;真实 BPMN Modeler 可以横向布局,但仍要控制跨线和嵌套。

二十七、常见反模式

27.1 万能审批流程

一个模型通过 businessType 和几十个网关承载请假、合同、报销、采购。结果是任何业务变更都影响全局,变量和权限契约互相污染。

27.2 前端决定审批人和结果

前端传 assignee=adminapproved=true,服务端不重新校验。攻击者可以绕过权限或修改请求。

27.3 每个业务动作都放Listener

业务逻辑隐藏在几十个 Listener,流程图看不出副作用,事务失败也难以定位。

27.4 把工作流当业务数据库

完整表单、附件、领域状态全放变量,报表直接查 ACT 表。升级、归档、权限和性能都失控。

27.5 任意跳转修复所有问题

管理员输入 Activity ID 强制跳转,却没有处理并行 Token、Task、Job、业务状态和审计。

27.6 只测Happy Path

模型能走通一次就上线,未测试全部条件、超时、并发、重复请求和旧实例。

二十八、商业场景一:医疗数据资产发布

主链:提交资产 → 敏感性识别 → 按风险激活安全/合规/质量审核 → 数据负责人终审 → 发布目录。

设计要点:

  • 原始医疗数据不进入流程变量。
  • 变量保存 sensitivecrossBorderqualityRisk 和规则版本。
  • 审核依据快照存业务审计表,避免后续数据变化无法解释当时决定。
  • 发布目录 Service Task 使用 processInstanceId + activityId 作为幂等操作键。
  • 任一拒绝是否取消其他并行任务必须明确。
  • 审核人只能查看经脱敏且授权的数据范围。

二十九、商业场景二:合同审批

设计要点:

  • 金额使用 BigDecimal 在业务服务中计算审批级别。
  • 法务和财务职责不同,适合两个明确 User Task。
  • 盖章和归档是外部副作用,必须幂等、可补偿、可对账。
  • 合同正文和附件在文档系统保存,变量只存 contractId、版本和决策字段。
  • 合同修改后应产生新业务版本,明确原审批是否失效。

三十、商业场景三:工单SLA

设计要点:

  • 创建工单后按类别路由到支持组。
  • 24小时非中断Timer催办,72小时升级。
  • 用户补充信息可用消息/Receive Task恢复。
  • 工单关闭前验证解决结果,重复回调幂等。
  • 监控每个 Activity 停留时长,而不是只看流程总时长。

三十一、流程模型发布前后的证据

发布前保存:

  • BPMN 文件 Hash、Git commit 和评审记录。
  • 变量契约、角色矩阵和分支真值表。
  • 静态校验报告和动态测试结果。
  • 旧版本运行实例数和兼容矩阵。
  • 回退和存量实例处置方案。

发布后记录:

  • deploymentId、definitionId、key、version、tenantId。
  • 冒烟实例 businessKey 和完整执行结果。
  • 新版本实例启动量、错误率和节点停留时间。
  • Job 重试、表达式异常和对账差异。

三十二、流程卡住的建模排查

mermaid
flowchart TD
    A["锁定businessKey和processInstanceId"] --> B["确认实例绑定definitionId"]
    B --> C["下载该版本BPMN而非看最新图"]
    C --> D["查询当前Task、Execution、Job和Event Subscription"]
    D --> E["在模型中定位Activity ID"]
    E --> F["检查出线条件、并行到达和等待事件"]
    F --> G["检查变量名称、类型、值和作用域"]
    G --> H["对齐应用异常、事务回滚和业务状态"]

没有Task但流程未结束

可能停在 Service Task 异步 Job、Receive Task、Timer Catch Event、子流程或网关汇聚。User Task 表为空不能证明流程结束。

有多个Task

可能是并行网关、多实例或模型重复进入同一节点。先看 Task Definition Key 和 Execution,而不是随便保留一个删除其他任务。

图上应该走A,实际走B

确认实例真实定义版本、流程变量时点和类型。不要用当前业务表值代替当时变量快照。

End到了但业务未完成

普通 End 可能只结束并行中的一条路径;也可能流程结束历史已写入,但业务回写/外部消息跨事务失败。检查 Execution 是否仍存在和一致性方案。

三十三、可观测性设计

建议按低基数维度统计:

  • processKey、definitionVersion、activityId。
  • Task 创建量、完成量、拒绝量和停留时长分位数。
  • 网关各 Sequence Flow 命中数。
  • Job 成功、失败、重试和最老年龄。
  • 流程实例启动、完成、取消和最老运行年龄。
  • 业务状态与流程状态对账差异。

不要把 processInstanceId、businessKey、userId 作为 Prometheus Label。它们适合放结构化日志和 Trace,以免指标高基数爆炸。

三十四、常见误区与后果

误区正确理解后果
BPMN就是流程图片XML语义决定执行,DI只决定显示图能看但不能运行或实际连线不同
User Task自带业务权限候选关系不等于完整数据权限越权审批
Service Task失败引擎会自动解决同步会回滚,异步也需重试/幂等/告警重复副作用或永久卡住
End Event一定结束整个实例普通End可能只结束当前路径并行任务仍在
并行网关就是多线程它表达Execution路径并发错误容量和事务设计
多人审批就画很多并行线动态人员更适合Multi-instance模型无法维护
Call Activity只是折叠图它调用独立定义并引入版本组合子流程升级影响多个父流程
流程变量可以放全部表单变量不是业务数据库表膨胀、序列化和敏感数据风险
上一步永远唯一并行/循环/子流程中可能有多个历史路径动态退回目标错误
新模型只需测试新实例应用仍要处理旧定义实例旧Delegate/变量运行失败

三十五、面试标准回答

BPMN为什么不是普通流程图

BPMN部署的是可执行XML模型,process key、Activity ID、Sequence Flow、条件表达式、候选人和事件都会被引擎解析,并在运行时生成Execution、Task、Job和历史。图形DI只决定显示。模型设计必须同时考虑业务语义、变量契约、事务、权限、版本和故障路径,不能只追求图能打开。

User Task和Service Task有什么区别

User Task到达后创建人工待办,让Execution持久化等待用户认领或完成;Service Task由引擎调用Delegate/表达式自动执行。同步Service Task会拉长当前事务,异步Service Task通过Job解耦但需要幂等、重试和告警。User Task的候选关系也不能替代业务层权限校验。

SubProcess和Call Activity怎么选

Embedded SubProcess属于当前定义,适合折叠局部复杂度并与父流程同版本发布;Call Activity调用独立ProcessDefinition,适合多个流程复用和独立版本,但必须治理父子定义版本组合、变量映射和子流程故障。是否需要独立生命周期比“图太大”更重要。

多人会签怎么设计

动态审批人集合通常用Multi-instance User Task,而不是画固定并行线。要决定并行或串行、人员集合来源和稳定顺序、空集合、重复人员、同意票与完成数区别、completionCondition以及提前结束后剩余任务怎样处理。固定职责不同的法务和财务则更适合并行网关加两个任务。

流程变量为什么不能放完整业务对象

流程变量用于分支和执行上下文,不是业务数据库。大对象会导致运行时和历史表膨胀、序列化兼容问题、敏感数据扩散和查询困难。业务事实、附件、表单和审批快照应保存在业务表,变量只存业务ID、稳定枚举和必要决策字段。

三十六、学习实验与验收

  1. 编写最小可执行 BPMN,部署后查询定义 key 和 version。
  2. 去掉 DI,观察执行与建模器显示的区别。
  3. 创建 User Task,证明 Task 等待时不占用一个持续 Java 请求线程。
  4. 创建同步 Service Task,注入异常并观察任务完成事务回滚。
  5. 改成异步边界,观察 Job 产生、失败和重试。
  6. 建立并行法务/财务审核,证明只完成一个时 Join 等待。
  7. 建立三人 Multi-instance,会签并验证全票和比例条件。
  8. 给 User Task 添加非中断 Timer,验证催办不取消原任务。
  9. 部署 V2 改 Activity ID,分析对历史、监控和旧实例的影响。
  10. 建立业务表、审批记录和流程变量清单,说明每个字段为什么放在那里。

验收时必须能回答:

  • BPMN语义和DI分别做什么?
  • Execution为什么不是Task或Java Thread?
  • 普通End与Terminate End的作用域区别是什么?
  • User Task候选组为什么不能替代业务权限?
  • Service Task何时同步、何时异步,失败怎样处理?
  • Embedded SubProcess和Call Activity的版本边界是什么?
  • Multi-instance完成数量为什么不等于同意数量?
  • 流程变量、业务表和审批快照怎样划分?
  • 为什么模型发布还要兼容旧Delegate和旧变量?
  • 卡住时为什么必须先找实例绑定的definitionId?

关联知识点