Activiti 商业场景训练营
这页把 Activiti 放到真实审批流和业务流程里学习。目标不是“会部署 BPMN 文件”,而是能解释流程定义、流程实例、业务单据、任务、候选人、网关、变量、历史表、版本、驳回、撤回、数据权限和业务状态如何协作。
训练目标
学完这页,你要能做到:
- 解释工作流引擎和普通状态机的区别。
- 解释 BPMN 文件部署后如何变成流程定义。
- 解释业务发起流程后如何创建流程实例和用户任务。
- 解释业务表和 Activiti 引擎表为什么要分开,又如何关联。
- 解释用户任务、候选人、候选组、认领、办理、转办、委派。
- 解释排他网关、并行网关、会签、多实例的使用边界。
- 设计报销、采购、资产变更、采集规则变更审批流程。
- 排查待办查不到、流程卡住、变量不生效、版本混乱、业务状态和流程状态不一致。
商业场景总览
以“资产变更审批”为例:
- 业务人员提交资产变更申请。
- 部门负责人审批。
- 金额或影响范围较大时,平台管理员复核。
- 通过后更新资产状态。
- 不通过则驳回修改或结束。
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();
}
}为什么启动流程和更新业务状态放在一个事务里?
- 防止流程启动了但业务单据仍是草稿。
- 防止业务单据变审批中但没有流程实例。
- 后续待办、业务详情、流程状态才能互相查到。
如果引擎和业务库不是同一个数据源,就要额外设计补偿或对账。
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);
}
}注意:
- 办理任务前必须校验当前用户是否有权处理。
- 审批意见要写评论或业务审批记录。
taskService.complete后流程可能继续到下一个节点,也可能结束。- 业务状态是否立即变更,要看流程是否真正结束。
如果要在流程结束时统一更新业务状态,可以使用流程监听器或业务层查询流程状态后更新。
网关怎么选
| 网关 | 作用 | 场景 |
|---|---|---|
| 排他网关 | 多选一 | 金额大于 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"]生产注意:
- 不要随便删除旧流程定义。
- 修改节点 ID 会影响历史和跳转。
- 旧实例是否迁移到新版本要单独评估。
- 流程版本和业务版本要能追踪。
生产排查流程
待办查不到
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 时传入"]商业落地清单
- 业务表必须有
process_instance_id或 businessKey。 - 业务状态和流程状态要有一致性策略。
- 待办查询要结合流程任务和业务表。
- 审批权限必须后端校验。
- 审批意见、处理人、处理时间要留痕。
- 流程变量只放流程判断必要字段。
- 流程版本要可追踪。
- 驳回、撤回、转办要先定义业务规则。
- 流程结束后业务状态要可靠更新。
- 定期对账业务表和流程实例状态。
面试标准回答
Activiti 是什么
text
Activiti 是 BPMN 工作流引擎,用来把审批流和业务流程从硬编码中抽出来。BPMN 部署后形成流程定义,业务发起时创建流程实例,引擎根据用户任务、网关、变量生成待办并推动流程流转,同时记录历史轨迹。它适合多角色审批、条件分支、留痕审计、流程版本管理这类场景。业务表和工作流表怎么设计
text
业务表保存业务事实和业务状态,例如报销单、资产变更单;Activiti 表保存流程运行状态,例如流程实例、任务、变量和历史。两者通过 businessKey 或 processInstanceId 关联。不要把完整业务数据都放进流程变量,流程变量只保存分支判断和办理人等必要字段。工作流和状态机区别
text
状态机适合简单状态流转,例如订单待支付、已支付、已取消;工作流适合人工任务、多角色审批、条件分支、并行审批、流程留痕和版本管理。工作流能力更强但也更重,如果只是简单订单状态,不一定需要引擎。