Skip to content

Activiti 从零到生产级掌握

工作流最容易学成“画流程图 + 调几个 API”。但商业系统里真正难的是:流程图如何驱动业务状态,待办和已办怎么查,审批权限怎么控制,驳回撤回怎么设计,流程版本怎么兼容,流程表和业务表怎么关联,线上任务卡住怎么排查。

一句话建立主线:

Activiti 是 BPMN 工作流引擎,它把流程定义部署成可执行模型,再在运行时创建流程实例、生成任务、判断网关、保存变量和历史记录;业务系统负责业务数据、权限、表单、通知和最终业务状态。

学习目标

学完这一页,你要能做到:

  1. 解释 BPMN、流程定义、流程实例、任务、变量、网关、历史表分别是什么。
  2. 解释 Activiti 从部署流程到完成审批的完整执行链路。
  3. 设计业务表和流程实例的关联方式。
  4. 设计待办、已办、我的申请、流程轨迹查询。
  5. 解释用户任务、候选人、候选组、认领、完成任务的区别。
  6. 解释排他网关、并行网关、包容网关的使用边界。
  7. 设计通过、驳回、撤回、转办、委派、加签、会签。
  8. 解释流程版本升级后旧实例和新实例如何共存。
  9. 能写启动流程、完成任务、查询待办、查询轨迹的 Demo。
  10. 能排查任务不出现、流程卡住、条件不走、变量丢失、历史查不到。

如果要按“零基础能看懂、原理能讲清、项目能落地、面试能回答、线上能排查”的标准验收,请配合阅读 Activiti 从零到精通验收清单。本页负责建立主线,验收清单负责把 BPMN、引擎表、任务流转、网关、驳回撤回、版本、一致性和排查逐项拆开。

工作流适合什么场景

适合 Activiti 的场景通常有这些特征:

特征说明例子
多节点不止一步处理申请 -> 部门审批 -> 财务审批
多角色不同角色处理不同节点员工、主管、财务、管理员
条件分支根据金额、类型、区域走不同流程金额大于 1 万走总监
人工待办需要人登录系统处理待审批、待复核、待归档
留痕审计要知道谁在什么时候做了什么医疗数据审批、合同审核
版本变化流程规则会变化新旧审批规则并存

不适合的场景:

  1. 只有简单状态流转,例如订单待支付、已支付、已取消,状态机更轻。
  2. 高频毫秒级链路,工作流引擎和历史表会带来额外开销。
  3. 纯技术编排,例如微服务调用编排,通常用 Saga、任务编排或消息驱动更合适。

核心对象关系

mermaid
flowchart TD
    A["BPMN 文件"] --> B["流程定义 ProcessDefinition"]
    B --> C["流程实例 ProcessInstance"]
    C --> D["执行实例 Execution"]
    C --> E["用户任务 Task"]
    C --> F["流程变量 Variable"]
    E --> G["历史任务 HistoricTaskInstance"]
    C --> H["历史流程 HistoricProcessInstance"]
    I["业务表 business_id"] --> C

要特别理解业务表和流程表的边界:

数据应该放哪里原因
请假天数、申请原因、合同金额业务表业务查询、报表、权限都依赖它
当前审批节点Activiti 运行时表,也可冗余到业务表引擎负责准确流转,业务表方便列表展示
申请状态业务表页面和业务逻辑最终看业务状态
审批意见可以业务表记录,也可结合历史任务需要长期审计和业务展示
流程变量Activiti 表网关判断和流程上下文

不要把大 JSON、附件、完整业务对象都塞进流程变量。流程变量适合保存流程判断需要的小字段,例如金额、部门、审批结果、申请人 ID。

完整执行链路

mermaid
flowchart TD
    A["设计 BPMN"] --> B["部署流程定义"]
    B --> C["创建业务单据"]
    C --> D["启动流程实例"]
    D --> E["保存 businessKey 和 processInstanceId"]
    E --> F["生成第一个用户任务"]
    F --> G["查询待办"]
    G --> H["用户审批"]
    H --> I["设置变量并完成任务"]
    I --> J["引擎判断网关和连线条件"]
    J --> K{"是否还有节点"}
    K -- "有" --> F
    K -- "无" --> L["流程结束"]
    L --> M["更新业务状态和保存历史"]

这条链路里最容易出问题的是两处:

  1. 启动流程和更新业务表必须在同一个事务里,否则会出现业务单据已提交但流程没启动,或流程启动了但业务单据状态没变。
  2. 完成任务和更新业务状态也要保持一致,否则会出现流程已到下一节点,但业务列表仍显示旧状态。

表结构怎么理解

Activiti 常见表可以按前缀理解:

前缀含义常见用途
ACT_RE_*Repository,流程定义资源流程部署、BPMN 文件、定义版本
ACT_RU_*Runtime,运行时数据正在运行的流程、任务、变量
ACT_HI_*History,历史数据已办、轨迹、审计
ACT_ID_*Identity,用户组织用户、组,很多项目不用它
ACT_GE_*General,通用数据二进制资源、属性

运行时表和历史表的区别:

mermaid
flowchart TD
    A["流程运行中"] --> B["ACT_RU_TASK"]
    A --> C["ACT_RU_EXECUTION"]
    A --> D["ACT_RU_VARIABLE"]
    E["流程节点完成"] --> F["写入 ACT_HI_TASKINST"]
    E --> G["写入 ACT_HI_ACTINST"]
    H["流程结束"] --> I["运行时数据清理"]
    I --> J["历史数据保留"]

所以已办查询通常不是查运行时任务表,而是查历史任务表。

最小业务表设计

sql
CREATE TABLE leave_apply (
  id BIGINT PRIMARY KEY,
  apply_user_id BIGINT NOT NULL,
  days INT NOT NULL,
  reason VARCHAR(500),
  status VARCHAR(32) NOT NULL,
  process_instance_id VARCHAR(64),
  current_node_name VARCHAR(100),
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL
);

CREATE TABLE leave_approve_record (
  id BIGINT PRIMARY KEY,
  apply_id BIGINT NOT NULL,
  task_id VARCHAR(64) NOT NULL,
  approver_id BIGINT NOT NULL,
  action VARCHAR(32) NOT NULL,
  comment VARCHAR(500),
  created_at DATETIME NOT NULL
);

为什么要有业务审批记录表:

  1. Activiti 历史表偏流程引擎视角,业务页面展示不一定方便。
  2. 审批意见、附件、签名、业务备注可能需要自定义字段。
  3. 后续换流程引擎时,业务审计数据不能完全绑死在引擎表。

启动流程 Demo

java
@Service
public class LeaveWorkflowService {
    private final RuntimeService runtimeService;
    private final LeaveApplyMapper leaveApplyMapper;

    @Transactional
    public String start(Long applyId, Long applyUserId, int days) {
        LeaveApply apply = leaveApplyMapper.selectById(applyId);
        if (apply == null) {
            throw new IllegalArgumentException("申请单不存在");
        }
        if (!"DRAFT".equals(apply.getStatus())) {
            throw new IllegalStateException("只有草稿状态可以发起");
        }

        Map<String, Object> variables = new HashMap<>();
        variables.put("applyUserId", applyUserId);
        variables.put("days", days);
        variables.put("approved", false);

        ProcessInstance instance = runtimeService.startProcessInstanceByKey(
            "leave_process",
            applyId.toString(),
            variables
        );

        apply.setStatus("APPROVING");
        apply.setProcessInstanceId(instance.getProcessInstanceId());
        apply.setCurrentNodeName("提交申请");
        leaveApplyMapper.updateById(apply);

        return instance.getProcessInstanceId();
    }
}

关键点:

  1. processDefinitionKey 是 BPMN 中的流程 key。
  2. businessKey 用业务 ID,方便从流程反查业务单。
  3. 启动流程和更新业务表放在同一个事务。
  4. 业务状态不要只依赖 Activiti 表,业务系统要有自己的状态字段。

查询待办 Demo

java
public List<TaskDTO> todoList(Long userId, List<String> roleCodes) {
    List<Task> personalTasks = taskService.createTaskQuery()
        .taskAssignee(userId.toString())
        .active()
        .list();

    List<Task> candidateTasks = taskService.createTaskQuery()
        .taskCandidateGroupIn(roleCodes)
        .active()
        .list();

    return Stream.concat(personalTasks.stream(), candidateTasks.stream())
        .map(task -> new TaskDTO(
            task.getId(),
            task.getName(),
            task.getProcessInstanceId(),
            task.getCreateTime()
        ))
        .collect(Collectors.toList());
}

候选组任务和个人任务的区别:

类型含义常见操作
assignee已指定处理人直接完成
candidate user候选用户先 claim 再完成
candidate group候选角色/组组内用户可认领

如果候选组任务不认领就直接完成,权限边界会混乱;如果认领后不释放,其他人可能看不到任务。

完成审批 Demo

java
@Transactional
public void approve(String taskId, Long approverId, boolean approved, String comment) {
    Task task = taskService.createTaskQuery()
        .taskId(taskId)
        .singleResult();
    if (task == null) {
        throw new IllegalArgumentException("任务不存在或已处理");
    }

    String processInstanceId = task.getProcessInstanceId();
    ProcessInstance instance = runtimeService.createProcessInstanceQuery()
        .processInstanceId(processInstanceId)
        .singleResult();
    Long applyId = Long.valueOf(instance.getBusinessKey());

    checkApprovePermission(task, approverId);

    Map<String, Object> variables = new HashMap<>();
    variables.put("approved", approved);
    variables.put("approverId", approverId);

    taskService.addComment(taskId, processInstanceId, comment);
    taskService.complete(taskId, variables);

    leaveApproveRecordMapper.insert(buildRecord(applyId, taskId, approverId, approved, comment));

    if (!approved) {
        leaveApplyMapper.updateStatus(applyId, "REJECTED");
    } else if (isProcessEnded(processInstanceId)) {
        leaveApplyMapper.updateStatus(applyId, "APPROVED");
    } else {
        leaveApplyMapper.updateCurrentNode(applyId, findCurrentNodeName(processInstanceId));
    }
}

这里有一个非常重要的原则:完成任务前必须校验权限。不能只因为前端按钮隐藏了,就认为用户不能提交请求。

网关怎么选

网关原理适合场景用错后果
排他网关多条路只走一条金额不同走不同审批人条件重叠时结果不符合预期
并行网关多条路都走,等全部汇聚多部门同时会签某一路没完成会一直卡住
包容网关满足条件的多条路都走可选多部门审批条件设计复杂,排查成本高

排他网关示例:

mermaid
flowchart TD
    A["主管审批"] --> B{"请假天数"}
    B -- "days <= 3" --> C["流程结束"]
    B -- "days > 3" --> D["经理审批"]
    D --> E["流程结束"]

变量名写错、变量类型不对、条件表达式不互斥,都会导致流程不按预期走。

驳回、撤回、转办、加签怎么设计

能力含义推荐做法
驳回审批人拒绝申请简单场景走拒绝分支;复杂场景用业务动作和流程跳转
撤回发起人撤回未完成流程校验当前节点允许撤回,再删除或终止实例并更新业务状态
转办当前处理人把任务转给别人修改 assignee 并记录操作
委派让别人协助处理,完成后回到委派人使用委派语义或业务自定义
加签临时增加审批人简单系统用业务表加审批记录,复杂系统动态加节点需谨慎
会签多人共同审批使用多实例任务,明确通过比例和否决规则

不要为了“看起来灵活”把任意跳转开放给前端。任意跳转很容易破坏流程语义,导致历史轨迹和业务状态对不上。

流程版本

流程定义每次部署会形成新版本。老流程实例默认继续按老版本走,新发起实例可以使用新版本。

mermaid
flowchart TD
    A["leave_process v1"] --> B["旧流程实例 1001"]
    C["重新部署 BPMN"] --> D["leave_process v2"]
    D --> E["新流程实例 2001"]
    B --> F["继续按 v1 执行"]
    E --> G["按 v2 执行"]

生产注意:

  1. 不要直接删除旧流程定义,旧实例可能还依赖它。
  2. BPMN key 要稳定,否则新流程不是升级,而是另一个流程。
  3. 表单字段变化要兼容旧实例。
  4. 流程升级前要确认是否需要迁移未完成实例。

商业场景:医疗数据资产审批

场景:医院数据资产平台中,科室提交数据集发布申请,需要数据管理员审核、信息科安全审核、负责人终审,最终发布到目录。

mermaid
flowchart TD
    A["科室提交数据集"] --> B["数据管理员审核"]
    B --> C{"数据质量是否通过"}
    C -- "否" --> D["驳回科室修改"]
    C -- "是" --> E["信息科安全审核"]
    E --> F{"是否包含敏感字段"}
    F -- "是" --> G["脱敏方案确认"]
    F -- "否" --> H["负责人终审"]
    G --> H
    H --> I{"是否通过"}
    I -- "通过" --> J["发布数据资产目录"]
    I -- "拒绝" --> D

设计要点:

  1. 业务表保存数据集 ID、申请人、状态、当前节点。
  2. 流程变量只保存是否敏感、数据等级、审核结果等判断字段。
  3. 每个审批动作写业务审批记录。
  4. 发布目录是业务动作,不能只依赖流程结束事件。
  5. 审批权限按科室、角色、数据等级校验。

线上排查

mermaid
flowchart TD
    A["工作流问题"] --> B{"表现"}
    B -- "没有待办" --> C["查 assignee、candidate、流程是否到用户任务"]
    B -- "流程卡住" --> D["查当前 execution、网关条件、并行分支"]
    B -- "变量不对" --> E["查启动和完成任务时变量名与类型"]
    B -- "历史查不到" --> F["查 history level 和历史表"]
    B -- "业务状态错" --> G["查业务事务和流程事务是否一致"]
    C --> H["TaskQuery 和 ACT_RU_TASK"]
    D --> I["ACT_RU_EXECUTION 和流程图"]
    E --> J["ACT_RU_VARIABLE 或 ACT_HI_VARINST"]

常用排查清单:

现象可能原因证据
待办列表为空assignee 不一致、候选组没传、任务已被认领ACT_RU_TASK
完成任务报错taskId 错、任务已完成、无权限任务查询和业务日志
网关不走变量名错、类型错、表达式不匹配流程变量表
流程结束但业务未更新事务边界不一致、监听器失败业务表和流程历史
历史记录少history level 配置低Activiti 配置
新流程没生效启动用错 key 或旧实例仍走旧版本流程定义版本

面试标准回答

Activiti 是什么

text
Activiti 是基于 BPMN 的工作流引擎。它把流程图部署成流程定义,运行时根据定义创建流程实例、生成用户任务、判断网关条件、保存变量和历史记录。业务系统负责业务表、权限、表单、通知和业务状态,不能把所有业务逻辑都塞进流程引擎。

业务表和流程表怎么关联

text
通常业务表保存业务主键、业务状态、当前节点和 processInstanceId;启动流程时把业务主键作为 businessKey 传给流程实例。这样可以从业务单据查流程,也可以从流程实例反查业务。流程变量只保存网关判断需要的小字段,大业务对象仍放业务表。

待办和已办怎么查

text
待办查运行时任务,通常通过 assignee、candidate user 或 candidate group 过滤;已办查历史任务表,根据处理人、流程实例、业务 key 等条件查询。待办是正在等待处理的任务,已办是已经处理过的历史记录,不能混用同一张表理解。

流程卡住怎么排查

text
先看当前流程实例是否还存在,再查 ACT_RU_TASK 是否有用户任务。如果没有任务,要看 execution 卡在哪个活动,重点排查网关条件、并行分支是否全部到达、变量名和类型是否正确。如果业务状态和流程状态不一致,还要查启动流程、完成任务和更新业务表是否在同一事务边界内。

关联知识点

知识点继续学习
从零到精通验收Activiti 从零到精通验收清单
商业场景训练营Activiti 商业场景训练营
Activiti 总览从零学习路线、引擎架构与首个流程
流程设计BPMN可执行模型、事件、任务、子流程与会签
网关Execution、排他、并行、包容与汇聚原理
任务流转 API任务生命周期、认领、委派、并发与待办已办
业务一致性本地事务、跨库命令、Outbox、补偿与对账
部署与版本部署落表、版本、缓存、灰度与回退
Spring 事务Spring 事务
分布式事务分布式事务

本章小结

Activiti 学习的关键不是会画 BPMN,而是把“流程定义、流程实例、任务、变量、网关、历史、业务表、权限、事务、版本、排查”串起来。商业项目里流程引擎只负责流程运行,真正的业务正确性仍然依赖业务表设计、权限校验、事务边界和可观测性。