Skip to content

Activiti 商业场景训练营

这页把 Activiti 放到真实审批流和业务流程里学习。目标不是“会部署 BPMN 文件”,而是能解释流程定义、流程实例、业务单据、任务、候选人、网关、变量、历史表、版本、驳回、撤回、数据权限和业务状态如何协作。

训练目标

学完这页,你要能做到:

  1. 解释工作流引擎和普通状态机的区别。
  2. 解释 BPMN 文件部署后如何变成流程定义。
  3. 解释业务发起流程后如何创建流程实例和用户任务。
  4. 解释业务表和 Activiti 引擎表为什么要分开,又如何关联。
  5. 解释用户任务、候选人、候选组、认领、办理、转办、委派。
  6. 解释排他网关、并行网关、会签、多实例的使用边界。
  7. 设计报销、采购、资产变更、采集规则变更审批流程。
  8. 排查待办查不到、流程卡住、变量不生效、版本混乱、业务状态和流程状态不一致。

商业场景总览

以“资产变更审批”为例:

  1. 业务人员提交资产变更申请。
  2. 部门负责人审批。
  3. 金额或影响范围较大时,平台管理员复核。
  4. 通过后更新资产状态。
  5. 不通过则驳回修改或结束。
mermaid
flowchart TD
    A["提交资产变更申请"] --> B["启动流程实例"]
    B --> C["部门负责人审批"]
    C --> D{"是否高风险"}
    D -- "是" --> E["平台管理员复核"]
    D -- "否" --> F["更新资产状态"]
    E --> G{"是否通过"}
    G -- "通过" --> F
    G -- "驳回" --> H["退回申请人修改"]
    F --> I["流程结束并记录历史"]

Activiti 负责流程运行,但不负责你的业务正确性。资产状态、权限、表单、消息通知、审计字段仍要由业务系统设计。

工作流核心对象关系

mermaid
flowchart TD
    A["BPMN 文件"] --> B["部署 Deployment"]
    B --> C["流程定义 ProcessDefinition"]
    C --> D["流程实例 ProcessInstance"]
    D --> E["执行流 Execution"]
    D --> F["任务 Task"]
    F --> G["审批人完成任务"]
    G --> H["流程变量 Variables"]
    H --> I["网关判断分支"]
    D --> J["历史记录 History"]
对象含义商业理解
流程定义流程模板报销流程 v1、资产变更流程 v2
流程实例一次具体流程张三这次报销申请
业务单据业务系统自己的表报销单、资产变更单
Task当前待办需要某个审批人处理
Variables流程上下文金额、是否高风险、审批结果
History历史轨迹谁在什么时候做了什么

业务表和流程表怎么关联

不要把所有业务字段都塞进 Activiti 变量。推荐业务表保存事实,流程表保存流程运行状态。

mermaid
flowchart TD
    A["asset_change_apply 业务表"] --> B["business_key = applyId"]
    B --> C["ACT_RU_EXECUTION 流程实例"]
    C --> D["ACT_RU_TASK 当前任务"]
    C --> E["ACT_HI_* 历史记录"]

业务表:

sql
create table asset_change_apply (
  id bigint primary key,
  apply_no varchar(64) not null,
  asset_id bigint not null,
  applicant_id bigint not null,
  status varchar(32) not null,
  process_instance_id varchar(64),
  change_content text not null,
  created_at datetime not null,
  updated_at datetime not null,
  unique key uk_apply_no (apply_no)
);

状态建议:

text
DRAFT        草稿
APPROVING    审批中
REJECTED     已驳回
APPROVED     已通过
CANCELED     已撤回

流程变量只放必要判断字段:

text
applicantId
departmentManagerId
riskLevel
needAdminReview
businessKey

不要把大 JSON、完整表单、附件内容放进流程变量,否则历史表膨胀,查询和迁移都会变痛苦。

Demo 一:启动资产变更流程

java
@Service
public class AssetChangeProcessService {
    private final RuntimeService runtimeService;
    private final AssetChangeApplyRepository applyRepository;

    @Transactional
    public String submit(Long applyId, CurrentUser user) {
        AssetChangeApply apply = applyRepository.findById(applyId)
                .orElseThrow(() -> new BizException("申请不存在"));

        if (!"DRAFT".equals(apply.getStatus())) {
            throw new BizException("只有草稿可以提交");
        }

        Map<String, Object> variables = new HashMap<>();
        variables.put("applicantId", user.userId());
        variables.put("departmentManagerId", user.managerId());
        variables.put("riskLevel", apply.getRiskLevel());
        variables.put("needAdminReview", "HIGH".equals(apply.getRiskLevel()));

        ProcessInstance instance = runtimeService.startProcessInstanceByKey(
                "asset_change_process",
                apply.getId().toString(),
                variables
        );

        apply.setProcessInstanceId(instance.getProcessInstanceId());
        apply.setStatus("APPROVING");
        applyRepository.save(apply);

        return instance.getProcessInstanceId();
    }
}

为什么启动流程和更新业务状态放在一个事务里?

  1. 防止流程启动了但业务单据仍是草稿。
  2. 防止业务单据变审批中但没有流程实例。
  3. 后续待办、业务详情、流程状态才能互相查到。

如果引擎和业务库不是同一个数据源,就要额外设计补偿或对账。

Demo 二:查询我的待办

java
public List<TaskVO> myTodo(CurrentUser user) {
    List<Task> tasks = taskService.createTaskQuery()
            .taskCandidateOrAssigned(user.userId().toString())
            .orderByTaskCreateTime()
            .desc()
            .list();

    List<String> processInstanceIds = tasks.stream()
            .map(Task::getProcessInstanceId)
            .toList();

    Map<String, AssetChangeApply> applyMap =
            applyRepository.findByProcessInstanceIdIn(processInstanceIds)
                    .stream()
                    .collect(Collectors.toMap(
                            AssetChangeApply::getProcessInstanceId,
                            Function.identity()
                    ));

    return tasks.stream()
            .map(task -> TaskVO.from(task, applyMap.get(task.getProcessInstanceId())))
            .toList();
}

待办列表一般不能只查 Activiti Task,因为业务页面需要展示申请标题、金额、资产名称、申请人、业务状态等字段。这些来自业务表。

Demo 三:审批任务

java
@Transactional
public void approve(String taskId, ApproveCommand command, CurrentUser user) {
    Task task = taskService.createTaskQuery()
            .taskId(taskId)
            .taskCandidateOrAssigned(user.userId().toString())
            .singleResult();

    if (task == null) {
        throw new AccessDeniedException("无权处理该任务");
    }

    String processInstanceId = task.getProcessInstanceId();
    AssetChangeApply apply = applyRepository.findByProcessInstanceId(processInstanceId)
            .orElseThrow(() -> new BizException("业务单据不存在"));

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

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

    if (!command.approved()) {
        apply.setStatus("REJECTED");
        applyRepository.save(apply);
    }
}

注意:

  1. 办理任务前必须校验当前用户是否有权处理。
  2. 审批意见要写评论或业务审批记录。
  3. taskService.complete 后流程可能继续到下一个节点,也可能结束。
  4. 业务状态是否立即变更,要看流程是否真正结束。

如果要在流程结束时统一更新业务状态,可以使用流程监听器或业务层查询流程状态后更新。

网关怎么选

网关作用场景
排他网关多选一金额大于 5000 走经理审批
并行网关多条分支同时执行法务和财务都要审批
包容网关满足条件的分支都走多条件组合审批

排他网关示例:

mermaid
flowchart TD
    A["提交报销"] --> B{"金额 > 5000"}
    B -- "是" --> C["部门经理审批"]
    B -- "否" --> D["财务审批"]

并行网关示例:

mermaid
flowchart TD
    A["提交合同"] --> B["并行网关拆分"]
    B --> C["法务审批"]
    B --> D["财务审批"]
    C --> E["并行网关汇聚"]
    D --> E
    E --> F["流程继续"]

并行网关要注意:所有分支都完成后才会继续。如果某个分支没有人处理,流程会一直卡住。

驳回、撤回、转办怎么理解

操作含义风险
驳回当前审批人退回申请人或上一步需要明确退回到哪里
撤回发起人取消自己发起的流程已审批节点是否允许撤回要定义
转办当前处理人把任务转给别人要记录审计
委派委托别人处理后回到自己流程语义更复杂

不要用一个“万能跳转 API”乱跳节点,否则历史轨迹、并行分支、业务状态很容易混乱。驳回和撤回应该先定义业务规则,再选择引擎 API 或自定义状态处理。

版本管理

BPMN 部署后形成流程定义版本。旧实例通常继续按旧定义走,新实例使用新定义。

mermaid
flowchart TD
    A["asset_change v1"] --> B["旧流程实例继续运行"]
    C["部署 asset_change v2"] --> D["新流程实例使用 v2"]

生产注意:

  1. 不要随便删除旧流程定义。
  2. 修改节点 ID 会影响历史和跳转。
  3. 旧实例是否迁移到新版本要单独评估。
  4. 流程版本和业务版本要能追踪。

生产排查流程

待办查不到

mermaid
flowchart TD
    A["待办查不到"] --> B["流程实例是否启动"]
    B --> C["当前是否存在 ACT_RU_TASK"]
    C --> D["assignee/candidate 是否正确"]
    D --> E["用户和组是否匹配"]
    E --> F["业务过滤条件是否过严"]

流程卡住

mermaid
flowchart TD
    A["流程卡住"] --> B["查当前执行节点"]
    B --> C["查任务是否有人处理"]
    C --> D["查网关条件是否满足"]
    D --> E["查并行分支是否全部完成"]
    E --> F["查监听器和异步 Job 是否失败"]

业务状态和流程状态不一致

mermaid
flowchart TD
    A["状态不一致"] --> B["查业务表状态"]
    B --> C["查流程实例状态"]
    C --> D["查历史任务和评论"]
    D --> E["查事务是否一起提交"]
    E --> F["补偿修复并加对账任务"]

变量不生效

mermaid
flowchart TD
    A["变量不生效"] --> B["变量名是否一致"]
    B --> C["变量作用域是否正确"]
    C --> D["类型是否匹配"]
    D --> E["网关表达式是否正确"]
    E --> F["是否在 complete 时传入"]

商业落地清单

  1. 业务表必须有 process_instance_id 或 businessKey。
  2. 业务状态和流程状态要有一致性策略。
  3. 待办查询要结合流程任务和业务表。
  4. 审批权限必须后端校验。
  5. 审批意见、处理人、处理时间要留痕。
  6. 流程变量只放流程判断必要字段。
  7. 流程版本要可追踪。
  8. 驳回、撤回、转办要先定义业务规则。
  9. 流程结束后业务状态要可靠更新。
  10. 定期对账业务表和流程实例状态。

面试标准回答

Activiti 是什么

text
Activiti 是 BPMN 工作流引擎,用来把审批流和业务流程从硬编码中抽出来。BPMN 部署后形成流程定义,业务发起时创建流程实例,引擎根据用户任务、网关、变量生成待办并推动流程流转,同时记录历史轨迹。它适合多角色审批、条件分支、留痕审计、流程版本管理这类场景。

业务表和工作流表怎么设计

text
业务表保存业务事实和业务状态,例如报销单、资产变更单;Activiti 表保存流程运行状态,例如流程实例、任务、变量和历史。两者通过 businessKey 或 processInstanceId 关联。不要把完整业务数据都放进流程变量,流程变量只保存分支判断和办理人等必要字段。

工作流和状态机区别

text
状态机适合简单状态流转,例如订单待支付、已支付、已取消;工作流适合人工任务、多角色审批、条件分支、并行审批、流程留痕和版本管理。工作流能力更强但也更重,如果只是简单订单状态,不一定需要引擎。

关联知识点