淘车湾项目实战六_集成Activiti7与审批流程定义页面实现
淘车湾项目实战(六):集成 Activiti7 与审批流程定义页面实现
本章位置:第三阶段 Java 企业项目 + AI 助手
对应课程:淘车湾项目实战——集成 Activiti7 及审批流程定义页面实现
前置知识:Activiti7/BPMN、若依脚手架、SpringBoot、Spring Security、MyBatis、Redis、事务、Vue3、Element Plus、淘车湾项目实战(一)~(五)
后续衔接:流程审核信息、我的待办/已办、流程图高亮、项目总结
学习目标:真正把 Activiti 工作流接入淘车湾结算审批业务,掌握流程定义、部署、流程实例、业务主键、UserTask、Assignee、CandidateGroup、TaskService、RuntimeService、RepositoryService、HistoryService、结算提交启动流程以及流程定义管理页面。
一、先说明版本问题
本课程名称是:
Activiti7
但实际开发时必须理解:
“课程知识版本”
和
“当前框架版本”
不是完全一回事。
Activiti 7 官方开发指南的 Spring Boot 示例主要面向:
Spring Boot 2.x
现代项目如果使用:
Spring Boot 3
JDK 17
Jakarta Servlet
不要直接复制:
多年以前 Activiti7 Starter 示例
然后只修改版本号。
本章重点学习:
Activiti7 经典流程引擎思想
BPMN
RepositoryService
RuntimeService
TaskService
HistoryService
ProcessDefinition
ProcessInstance
Task
businessKey
processInstanceId
这些是:
真正值得掌握的工作流核心
实际依赖:
必须以项目基线和课程提供版本为准
二、为什么项目需要工作流
没有工作流引擎时:
结算提交
↓
经理审批
↓
财务审批
↓
完成
很多初学者会写:
if (amount > 500) {
if (managerApproved) {
if (financeApproved) {
...
}
}
}
业务越来越复杂后:
if / else
越来越多
三、审批流程真正包含什么
例如:
提交申请
经理审批
财务审批
审批拒绝
审批通过
流程历史
待办任务
已办任务
当前节点
流程图高亮
这些如果全部:
自己写状态字段
会越来越复杂。
四、工作流引擎的价值
Activiti 帮我们管理:
流程当前走到哪里
当前任务是谁的
哪个任务已经完成
下一个任务是谁
流程变量是什么
历史审批经过哪些节点
流程什么时候结束
五、最重要的类比
把 BPMN 想象成:
流程蓝图
例如:
结算审批流程.bpmn20.xml
它只是:
“审批应该怎么走”
六、ProcessDefinition
流程部署以后:
BPMN 蓝图
↓
ProcessDefinition
可以理解:
已经发布的流程模板
七、ProcessInstance
某一张结算单提交:
启动一次流程
得到:
ProcessInstance
也就是:
这一张结算单自己的审批实例
八、Task
流程走到:
经理审批
Activiti 创建:
Task
表示:
当前有人需要处理的审批任务
九、businessKey
这是本项目最重要字段之一。
例如:
settlementId = 10086
启动流程:
runtimeService
.startProcessInstanceByKey(
"settlementApproval",
"10086"
);
这里:
10086
就是:
businessKey
十、businessKey 的作用
流程引擎:
知道 processInstanceId
业务系统:
知道 settlementId
businessKey:
把两边连接起来
十一、processInstanceId
启动流程后返回:
ProcessInstance instance
获得:
instance.getId()
保存到:
car_settlement.process_instance_id
十二、businessKey 和 processInstanceId 区别
businessKey
业务系统提供给流程引擎
例如:
10086
processInstanceId
流程引擎生成
例如:
3f6249f...
十三、为什么两个都值得保留
通过 businessKey:
流程 → 找业务
通过 processInstanceId:
业务 → 找流程
十四、本项目流程
第一版设计:
开始
↓
经理审批
↓
是否通过?
├─ 否
│ ↓
│ 结束:拒绝
│
└─ 是
↓
财务审批
↓
是否通过?
├─ 否
│ ↓
│ 结束:拒绝
│
└─ 是
↓
结束:通过
十五、是否所有结算都走 Activiti
不一定。
例如:
优惠 <= 500
可以:
无需审批
直接:
approvalStatus = NOT_REQUIRED
十六、需要审批时
例如:
优惠 > 500
结算提交:
启动 Activiti
十七、最终提交逻辑
Settlement DRAFT
↓
Submit
↓
Determine Approval
├─ 不需要审批
│ ↓
│ approvalStatus = NOT_REQUIRED
│ ↓
│ status = SUBMITTED
│
└─ 需要审批
↓
Start Activiti Process
↓
save processInstanceId
↓
approvalStatus = PROCESSING
↓
status = SUBMITTED
十八、BPMN 文件
推荐:
settlement-approval.bpmn20.xml
十九、流程 key
settlementApproval
二十、为什么 key 要稳定
代码会:
startProcessInstanceByKey(
"settlementApproval",
...
)
如果随便修改:
代码找不到流程定义
二十一、流程名称
可以:
淘车湾结算审批流程
二十二、简单 BPMN 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="CarSettlement">
<process
id="settlementApproval"
name="淘车湾结算审批流程"
isExecutable="true"
>
<startEvent
id="start"
name="开始"
/>
<sequenceFlow
id="flow1"
sourceRef="start"
targetRef="managerApprove"
/>
<userTask
id="managerApprove"
name="经理审批"
activiti:candidateGroups="store_manager"
/>
<sequenceFlow
id="flow2"
sourceRef="managerApprove"
targetRef="managerGateway"
/>
<exclusiveGateway
id="managerGateway"
name="经理审批结果"
/>
<sequenceFlow
id="managerPass"
sourceRef="managerGateway"
targetRef="financeApprove"
>
<conditionExpression
xsi:type="tFormalExpression"
>
<![CDATA[
${approved == true}
]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow
id="managerReject"
sourceRef="managerGateway"
targetRef="rejectEnd"
>
<conditionExpression
xsi:type="tFormalExpression"
>
<![CDATA[
${approved == false}
]]>
</conditionExpression>
</sequenceFlow>
<userTask
id="financeApprove"
name="财务审批"
activiti:candidateGroups="finance"
/>
<sequenceFlow
id="flow3"
sourceRef="financeApprove"
targetRef="financeGateway"
/>
<exclusiveGateway
id="financeGateway"
name="财务审批结果"
/>
<sequenceFlow
id="financePass"
sourceRef="financeGateway"
targetRef="approveEnd"
>
<conditionExpression
xsi:type="tFormalExpression"
>
<![CDATA[
${approved == true}
]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow
id="financeReject"
sourceRef="financeGateway"
targetRef="rejectEnd"
>
<conditionExpression
xsi:type="tFormalExpression"
>
<![CDATA[
${approved == false}
]]>
</conditionExpression>
</sequenceFlow>
<endEvent
id="approveEnd"
name="审批通过"
/>
<endEvent
id="rejectEnd"
name="审批拒绝"
/>
</process>
</definitions>
二十三、exclusiveGateway
这是:
排他网关
作用:
根据条件
选择一条分支
二十四、approved
这里:
approved
是:
流程变量
任务完成时:
Map<String, Object> variables =
new HashMap<>();
variables.put(
"approved",
true
);
taskService.complete(
taskId,
variables
);
二十五、一个严重问题
如果两个审批节点都使用:
approved
流程变量会:
被后一个节点覆盖
第一版:
可以工作
但更清晰建议:
managerApproved
financeApproved
二十六、改进变量
经理:
managerApproved
财务:
financeApproved
二十七、为什么变量名要语义明确
后面历史和排错:
更容易
二十八、UserTask
<userTask>
表示:
需要人工处理的任务
二十九、Assignee
例如:
activiti:assignee="${managerUserId}"
表示:
直接指定一个人
三十、CandidateUser
表示:
多个候选用户
其中某人:
claim
后成为真正 assignee。
三十一、CandidateGroup
表示:
某个候选组
例如:
finance
三十二、本项目推荐 CandidateGroup
原因:
若依本来就有角色
可以把:
RuoYi roleKey
映射为:
Activiti candidateGroup
三十三、例如
若依:
roleKey = store_manager
BPMN:
activiti:candidateGroups="store_manager"
三十四、财务
若依:
roleKey = finance
BPMN:
candidateGroups="finance"
三十五、但是 Activiti 会自动识别若依 sys_role 吗
不会。
这是关键。
Activiti:
不知道若依角色表
若依:
也不知道 Activiti candidateGroup
需要:
我们自己做映射
三十六、最简单映射思路
当前登录用户:
SecurityUtils.getLoginUser()
拿到:
roles
提取:
roleKey
查询任务:
candidateGroupIn(roleKeys)
三十七、不要复制若依用户到 Activiti Identity 表
课程项目:
不建议维护第二套用户体系
三十八、为什么
否则:
sys_user
一套
Activiti User
一套
需要:
同步用户名
同步角色
同步删除
同步密码
太复杂。
三十九、推荐
用户身份:
以若依为准
Activiti:
只负责流程和任务
四十、Activiti 核心 Service
你必须记住:
RepositoryService
RuntimeService
TaskService
HistoryService
ManagementService
四十一、RepositoryService
负责:
流程定义
流程部署
BPMN 资源
四十二、RuntimeService
负责:
正在运行的流程实例
流程变量
Execution
四十三、TaskService
负责:
当前人工任务
查询任务
领取任务
完成任务
审批意见
四十四、HistoryService
负责:
历史流程
历史任务
历史活动
历史变量
四十五、ManagementService
负责:
引擎管理
Job
数据库相关管理
日常业务:
使用较少
四十六、流程部署
Activiti 中:
BPMN 文件
要先:
Deployment
之后才有:
ProcessDefinition
四十七、手工部署示例
Deployment deployment =
repositoryService
.createDeployment()
.name(
"淘车湾结算审批"
)
.addClasspathResource(
"processes/settlement-approval.bpmn20.xml"
)
.deploy();
四十八、为什么流程文件目录要按实际 Starter 规则
不同 Activiti/Spring Boot 集成版本:
自动部署目录
可能存在差异。
很多教程会使用:
src/main/resources/processes
但项目必须:
看当前 Starter 配置
四十九、课程建议
即使自动部署可用,
也要先理解:
RepositoryService.deploy()
因为后面:
流程定义管理页面
会主动部署。
五十、Deployment
Deployment 表示:
一次流程资源部署行为
五十一、ProcessDefinition
部署后:
同一个 process key
会产生:
版本
例如:
settlementApproval
version 1
settlementApproval
version 2
settlementApproval
version 3
五十二、启动 by key 时
通常:
使用最新版本
五十三、为什么老流程不会突然改变
已经运行中的:
ProcessInstance
仍绑定:
启动时的 ProcessDefinition
五十四、这是流程版本管理核心
新业务:
用最新流程
旧业务:
继续旧流程
五十五、流程定义查询
List<ProcessDefinition> list =
repositoryService
.createProcessDefinitionQuery()
.latestVersion()
.orderByProcessDefinitionKey()
.asc()
.list();
五十六、流程定义页面应该显示什么
建议:
流程名称
流程 Key
版本
部署 ID
资源名称
是否挂起
部署时间
操作
五十七、页面操作
第一版:
查看流程
部署
挂起
激活
五十八、是否需要在线 BPMN 设计器
课程中如果重点是:
“流程定义页面实现”
可以先做:
流程定义管理页面
不一定必须:
浏览器在线拖拽 BPMN Designer
五十九、为什么不急着做复杂 Modeler
在线 BPMN 设计器涉及:
bpmn-js
XML 编辑
保存
版本
资源管理
会大幅增加工作量。
课程第一版:
BPMN 文件由开发者维护
+
后台查看/部署定义
已经能完成核心流程管理。
六十、如果课程明确要求模型器
可以后续:
接 bpmn-js
但先完成:
审批闭环
六十一、流程定义 VO
public class ProcessDefinitionVO {
private String id;
private String name;
private String key;
private Integer version;
private String deploymentId;
private String resourceName;
private boolean suspended;
}
六十二、为什么不要直接返回 ProcessDefinition 实现类
流程引擎对象:
内部字段多
序列化不一定友好
使用:
VO
更稳定。
六十三、流程定义 Controller
建议:
/car/workflow/definition
六十四、列表
GET /car/workflow/definition/list
六十五、部署
POST /car/workflow/definition/deploy
六十六、查看 XML
GET /car/workflow/definition/{id}/xml
六十七、查看图
GET /car/workflow/definition/{id}/diagram
后续:
流程图高亮章节深入
六十八、激活
POST /car/workflow/definition/{id}/activate
六十九、挂起
POST /car/workflow/definition/{id}/suspend
七十、为什么挂起流程定义
挂起后:
不允许启动新实例
适用于:
流程暂时停用
七十一、不要物理删除正在使用的流程定义
否则:
历史和运行实例
可能受影响
七十二、流程定义权限
建议:
car:workflow:definition:list
car:workflow:definition:deploy
car:workflow:definition:query
car:workflow:definition:suspend
七十三、谁有这些权限
通常:
系统管理员
流程管理员
普通服务顾问:
不应该能部署流程
七十四、部署接口 DTO
public class ProcessDeployDTO {
@NotBlank
private String resourceName;
}
如果流程来自:
classpath 预置文件
可以只允许:
白名单资源
七十五、为什么不要让前端传任意服务器文件路径
错误:
{
"path": "C:/xxx/xxx"
}
这会:
产生文件访问安全风险
七十六、更合理
上传:
BPMN XML 文件
或:
从应用内预置资源部署
七十七、课程第一版推荐预置资源
例如:
processes/settlement-approval.bpmn20.xml
后台按钮:
部署结算审批流程
七十八、部署 Service
@Transactional(
rollbackFor = Exception.class
)
public String deploySettlementProcess() {
Deployment deployment =
repositoryService
.createDeployment()
.name(
"淘车湾结算审批流程"
)
.addClasspathResource(
"processes/settlement-approval.bpmn20.xml"
)
.deploy();
return deployment.getId();
}
七十九、流程引擎自己的事务
Activiti 与 Spring 集成后:
通常可以参与 Spring 事务
但:
具体配置必须与数据源/事务管理器一致
八十、如果 Activiti 使用同一个数据库
常见:
业务表
+
ACT_*
共用:
MySQL
八十一、是否建议同一个数据库
课程项目:
建议
简单:
事务更容易理解
八十二、生产系统可以分库吗
可以,
但:
跨库事务和一致性更复杂
当前:
不需要
八十三、ACT_* 表
Activiti 常见前缀:
ACT_RE_*
ACT_RU_*
ACT_HI_*
ACT_GE_*
八十四、ACT_RE
Repository:
流程定义
部署资源
八十五、ACT_RU
Runtime:
运行中的流程
任务
执行实例
变量
流程结束后:
大量运行数据会清理
八十六、ACT_HI
History:
历史流程
历史任务
历史活动
八十七、ACT_GE
General:
通用资源
通用属性
八十八、常见表
不同版本表结构略有区别,但你常会遇到:
ACT_RE_DEPLOYMENT
ACT_RE_PROCDEF
ACT_RU_EXECUTION
ACT_RU_TASK
ACT_RU_VARIABLE
ACT_HI_PROCINST
ACT_HI_TASKINST
ACT_HI_ACTINST
八十九、不要直接操作 ACT_* 表
这是重要原则。
不要:
UPDATE ACT_RU_TASK
SET ASSIGNEE_ = ...
九十、正确
通过:
TaskService
RuntimeService
RepositoryService
操作。
九十一、为什么
Activiti 内部:
不只有一张表
直接改数据库:
容易破坏引擎状态
九十二、结算提交启动审批
这是本章最关键代码。
九十三、提交之前
必须:
Settlement = DRAFT
金额:
已经冻结
九十四、无需审批
status = SUBMITTED
approvalStatus = NOT_REQUIRED
九十五、需要审批
start process
九十六、启动流程代码
Map<String, Object> variables =
new HashMap<>();
variables.put(
"settlementId",
settlement.getSettlementId()
);
variables.put(
"deptId",
settlement.getDeptId()
);
variables.put(
"applicantUserId",
SecurityUtils.getUserId()
);
variables.put(
"discountAmount",
settlement.getDiscountAmount()
);
ProcessInstance processInstance =
runtimeService
.startProcessInstanceByKey(
"settlementApproval",
String.valueOf(
settlement
.getSettlementId()
),
variables
);
九十七、保存实例 ID
settlement.setProcessInstanceId(
processInstance.getId()
);
九十八、审批状态
settlement.setApprovalStatus(
ApprovalStatus
.PROCESSING
.getCode()
);
九十九、结算状态
settlement.setStatus(
SettlementStatus
.SUBMITTED
.getCode()
);
一百、为什么启动流程和更新业务表要同一事务
如果:
流程启动成功
但:
business table update 失败
系统会出现:
有流程
但结算单不知道流程 ID
一百零一、反过来
如果:
结算改成审批中
但:
流程启动失败
业务显示:
审批中
实际:
没有任务
一百零二、所以必须保证一致性
如果 Activiti 与业务:
同一个 Spring TransactionManager
+
同一个数据库
最好:
同一事务
一百零三、如果未来跨服务
需要:
事件
消息队列
补偿
最终一致性
当前:
不展开
一百零四、提交 Service 核心示例
@Transactional(
rollbackFor = Exception.class
)
public void submitSettlement(
Long settlementId
) {
CarSettlement settlement =
getAccessibleSettlement(
settlementId
);
if (
settlement == null
) {
throw new ServiceException(
"结算单不存在或无权限"
);
}
if (
!SettlementStatus
.DRAFT
.getCode()
.equals(
settlement.getStatus()
)
) {
throw new ServiceException(
"只有草稿结算单可以提交"
);
}
validateSettlementConsistency(
settlementId
);
boolean needApproval =
needApproval(
settlement
);
if (
!needApproval
) {
settlementMapper
.submitWithoutWorkflow(
settlementId,
SettlementStatus
.DRAFT
.getCode(),
SettlementStatus
.SUBMITTED
.getCode(),
ApprovalStatus
.NOT_REQUIRED
.getCode()
);
return;
}
Map<String, Object> variables =
buildProcessVariables(
settlement
);
ProcessInstance instance =
runtimeService
.startProcessInstanceByKey(
"settlementApproval",
String.valueOf(
settlementId
),
variables
);
int rows =
settlementMapper
.bindProcess(
settlementId,
SettlementStatus
.DRAFT
.getCode(),
SettlementStatus
.SUBMITTED
.getCode(),
ApprovalStatus
.PROCESSING
.getCode(),
instance.getId()
);
if (
rows == 0
) {
throw new ServiceException(
"结算单状态已变化,请刷新后重试"
);
}
}
一百零五、为什么先 validateSettlementConsistency
审批前:
必须确保主表和明细金额一致
审批的应该是:
稳定数据
一百零六、为什么不能启动流程后还允许 edit
审批中:
金额冻结
否则:
流程审批的金额
和最终支付金额
不一样
一百零七、流程变量放哪些
推荐只放:
流程路由所需
审批判断所需
轻量上下文
例如:
settlementId
deptId
applicantUserId
discountAmount
receivableAmount
一百零八、不要把整个 Settlement JSON 放流程变量
原因:
重复数据
序列化兼容问题
历史对象变更
数据库空间
难维护
一百零九、业务真实数据放哪里
仍然:
car_settlement
car_settlement_item
流程变量:
只是流程需要的上下文
一百一十、业务页面如何拿详情
审批任务:
拿 businessKey
得到:
settlementId
然后:
调用 SettlementService
查:
最新合法业务详情
一百一十一、为什么不是从 Activiti Variable 还原完整账单
Activiti:
不是业务数据库
一百一十二、Task 查询
当前用户待办:
TaskService
一百一十三、如果用 Assignee
taskService
.createTaskQuery()
.taskAssignee(
String.valueOf(
userId
)
)
.list();
一百一十四、如果用 CandidateGroup
若依当前用户角色:
store_manager
finance
一百一十五、提取 RoleKey
伪代码:
List<String> roleKeys =
SecurityUtils
.getLoginUser()
.getUser()
.getRoles()
.stream()
.map(
SysRole::getRoleKey
)
.toList();
一百一十六、查询候选任务
具体 Activiti 版本 API:
以当前依赖为准
经典思路:
taskCandidateGroup
taskCandidateGroupIn
一百一十七、为什么要查看实际 API
Activiti 不同版本:
TaskQuery
Runtime API
存在差异。
不要:
照抄不匹配版本代码
一百一十八、候选任务是否需要 claim
推荐:
需要
流程:
候选组任务
↓
某用户领取
↓
assignee = 当前用户
↓
完成
一百一十九、claim
taskService.claim(
taskId,
String.valueOf(
SecurityUtils.getUserId()
)
);
一百二十、为什么 claim 有价值
财务组有:
5 个人
某一任务:
谁先领取
谁处理
避免:
多人同时审批同一任务
一百二十一、unclaim
如果领取错:
可以取消领取
经典 API:
taskService.unclaim(
taskId
);
一百二十二、完成任务前必须检查任务归属
不能前端传:
taskId
后端直接:
taskService.complete(taskId);
一百二十三、否则
用户猜到别人 taskId:
可能越权审批
一百二十四、正确流程
查 Task
↓
确认存在
↓
确认当前用户是 assignee
或合法候选人
↓
确认业务 DataScope / 权限
↓
完成
一百二十五、任务权限与若依权限
例如:
car:settlement:approve
是:
功能权限
Activiti Task:
流程任务归属
两者都需要。
一百二十六、为什么两层
有审批按钮权限:
不代表可以审批所有任务
任务属于:
某候选组 / 某人
一百二十七、完整审批判断
@PreAuthorize approve
↓
Task belongs to current user/group
↓
Settlement belongs to allowed scope
↓
Current node valid
↓
Complete Task
一百二十八、审批 DTO
public class TaskApproveDTO {
@NotBlank
private String taskId;
@NotNull
private Boolean approved;
@Size(
max = 1000
)
private String comment;
}
一百二十九、为什么 approved 用 Boolean
前端:
true = 通过
false = 拒绝
比:
传任意字符串
更稳定。
一百三十、审批意见
建议:
通过可以选填
拒绝必须填写
一百三十一、后端校验
if (
Boolean.FALSE.equals(
dto.getApproved()
)
&&
StringUtils.isBlank(
dto.getComment()
)
) {
throw new ServiceException(
"审批拒绝时必须填写原因"
);
}
一百三十二、审批意见怎么存
Activiti 可以:
Task Comment
同时课程前面已经有:
car_approval_record
一百三十三、为什么业务表也保存 ApprovalRecord
Activiti 历史:
适合流程引擎查询
业务审批记录:
适合页面展示和业务报表
一百三十四、是否会重复
是有一定:
信息冗余
但属于:
业务快照
合理。
一百三十五、审批记录字段
businessType
businessId
processInstanceId
taskId
activityId
activityName
operatorUserId
action
comment
createTime
一百三十六、审批 Action
字典:
APPROVE
REJECT
CLAIM
UNCLAIM
第一版主要:
APPROVE
REJECT
一百三十七、完成任务代码
Map<String, Object> variables =
new HashMap<>();
variables.put(
variableName,
dto.getApproved()
);
taskService.complete(
taskId,
variables
);
一百三十八、variableName 怎么决定
如果当前任务:
managerApprove
使用:
managerApproved
如果:
financeApprove
使用:
financeApproved
一百三十九、不要前端传 variableName
否则前端可以:
随意改流程变量
一百四十、后端根据 taskDefinitionKey
task.getTaskDefinitionKey()
判断当前节点。
一百四十一、示例
String variableName;
switch (
task.getTaskDefinitionKey()
) {
case "managerApprove":
variableName =
"managerApproved";
break;
case "financeApprove":
variableName =
"financeApproved";
break;
default:
throw new ServiceException(
"当前任务节点不支持该审批操作"
);
}
一百四十二、为什么后端掌控流程变量
流程安全:
不能交给浏览器决定
一百四十三、Task complete 后怎么知道流程是否结束
可以查询:
ProcessInstance instance =
runtimeService
.createProcessInstanceQuery()
.processInstanceId(
processInstanceId
)
.singleResult();
一百四十四、如果 instance == null
通常说明:
流程运行实例已经结束
一百四十五、但是结束是通过还是拒绝
不能只看:
流程结束
需要知道:
审批结果
一百四十六、两种方式
方式一:
流程变量
保存:
finalApproved
一百四十七、方式二
通过:
业务逻辑根据当前任务 approved
以及:
是否还有下一任务
决定。
一百四十八、课程第一版推荐
完成任务后:
如果 approved == false
直接:
业务 approvalStatus = REJECTED
一百四十九、如果 approved == true
再查:
流程是否结束
如果结束:
APPROVED
如果未结束:
PROCESSING
一百五十、这样很好理解
经理通过:
流程没结束
→ PROCESSING
财务通过:
流程结束
→ APPROVED
任意节点拒绝:
→ REJECTED
一百五十一、一个重要事务问题
任务完成:
taskService.complete()
和:
car_settlement 更新
必须保持一致。
一百五十二、例如拒绝
如果 Activiti:
已经 complete
业务表:
update REJECTED 失败
系统会不一致。
一百五十三、同库同事务时
尽量:
@Transactional
把:
任务完成
审批记录
业务状态更新
放在:
同一个 Service 方法
一百五十四、审批 Service 流程
查 Task
↓
验证当前用户权限
↓
查 ProcessInstance businessKey
↓
查 Settlement
↓
检查业务归属
↓
记录 Activiti Comment
↓
complete Task
↓
保存 ApprovalRecord
↓
判断流程是否结束
↓
更新 approvalStatus
↓
Commit
一百五十五、为什么先拿 businessKey
Task:
属于一个 processInstance
通过:
processInstanceId
查询:
ProcessInstance
拿:
businessKey
一百五十六、不要前端同时传 settlementId 并直接相信
如果前端传:
taskId = A
settlementId = B
可能:
恶意错配
一百五十七、正确
业务 ID:
从 Task → ProcessInstance → businessKey
后端自己推导。
一百五十八、这是非常重要安全规则
前端只传:
taskId
approved
comment
一百五十九、审批业务 ID 后端获得
taskId
↓
processInstanceId
↓
businessKey
↓
settlementId
一百六十、待办页面
后面课程会深入,
本章先设计:
TaskVO
一百六十一、TaskVO
public class WorkflowTaskVO {
private String taskId;
private String taskName;
private String taskDefinitionKey;
private String processInstanceId;
private String processDefinitionId;
private Long businessId;
private String businessNo;
private LocalDateTime createTime;
private String assignee;
}
一百六十二、为什么待办里显示业务编号
用户更容易识别:
ST202609100001
而不是:
taskId
一百六十三、任务列表查询不能只显示 Activiti 数据
需要:
Task
+
Settlement
组合。
一百六十四、流程定义页面前端
建议:
src/views/car/workflow/definition/index.vue
一百六十五、表格
流程名称
Key
版本
部署 ID
状态
资源名称
操作
一百六十六、按钮
部署
查看 XML
查看流程图
挂起/激活
一百六十七、权限
使用:
v-hasPermi
例如:
<el-button
v-hasPermi="[
'car:workflow:definition:deploy'
]"
@click="handleDeploy"
>
部署流程
</el-button>
一百六十八、流程定义 API
export function listProcessDefinition(
query
) {
return request({
url:
'/car/workflow/definition/list',
method:
'get',
params:
query
})
}
一百六十九、部署 API
export function deploySettlementProcess() {
return request({
url:
'/car/workflow/definition/deploy/settlement',
method:
'post'
})
}
一百七十、挂起
export function suspendDefinition(
id
) {
return request({
url:
`/car/workflow/definition/${id}/suspend`,
method:
'post'
})
}
一百七十一、激活
export function activateDefinition(
id
) {
return request({
url:
`/car/workflow/definition/${id}/activate`,
method:
'post'
})
}
一百七十二、部署按钮是否可以重复点击
可以部署:
新版本
但快速双击:
不应该意外部署两次
可以:
@RepeatSubmit
一百七十三、每次部署都会产生新版本吗
通常:
相同 key
新的部署
→ 新的 process definition version
一百七十四、为什么版本号不是我们手填
流程引擎:
自动维护版本
一百七十五、启动流程前必须检查流程定义存在
否则:
no process definition deployed
一百七十六、可以预检查
ProcessDefinition definition =
repositoryService
.createProcessDefinitionQuery()
.processDefinitionKey(
"settlementApproval"
)
.latestVersion()
.active()
.singleResult();
if (
definition == null
) {
throw new ServiceException(
"结算审批流程尚未部署或已停用"
);
}
一百七十七、为什么这个提示比 Activiti 原始异常好
用户更容易理解:
先部署流程
一百七十八、流程定义挂起时
启动:
应该失败
所以:
提交结算前
检查 active
一百七十九、流程实例也能挂起吗
可以。
用于:
临时停止某个具体流程
当前课程:
不作为重点
一百八十、部署 XML 校验
BPMN XML 写错:
部署会失败
例如:
sequenceFlow targetRef 不存在
conditionExpression 错
process id 重复问题
一百八十一、常见错误 1
no processes deployed with key
原因:
BPMN 没部署
key 写错
流程被挂起
一百八十二、常见错误 2
部署成功但启动旧版本。
检查:
processDefinitionKey
版本
部署数据
一般 by key:
会启动最新有效版本
一百八十三、常见错误 3
任务查不到。
可能:
candidateGroup 和 roleKey 不一致
例如:
BPMN = finance
RuoYi roleKey = financial
一百八十四、解决
统一:
候选组编码规范
一百八十五、不要使用角色中文名作为流程组编码
错误:
财务管理员
更推荐:
finance
因为:
roleName 可以改
roleKey 应稳定
一百八十六、常见错误 4
任务列表查到了,
但任何人都能 complete。
原因:
后端没校验 Assignee/Candidate
一百八十七、常见错误 5
用户 claim 之后,
其他用户还能审批。
后端:
没有检查 task.assignee
一百八十八、常见错误 6
流程拒绝了,
业务仍 PROCESSING。
原因:
Task 完成后没同步 settlement
一百八十九、常见错误 7
业务 REJECTED,
Activiti 还有待办。
原因:
业务表先改状态
TaskService.complete 失败
需要:
事务一致性
一百九十、常见错误 8
流程启动成功,
processInstanceId 没保存。
原因:
业务更新失败
或代码漏写
一百九十一、常见错误 9
businessKey 是 null。
原因:
启动流程时
没有使用 businessKey 参数
一百九十二、常见错误 10
把整个 Settlement 对象塞流程变量。
后续类结构变化:
反序列化异常
一百九十三、常见错误 11
直接修改 ACT_RU_TASK。
后果:
引擎内部状态不一致
一百九十四、常见错误 12
Spring Boot 3 项目复制旧 Activiti7 Boot2 Starter。
可能出现:
javax/jakarta 冲突
Spring API 不兼容
启动失败
依赖冲突
一百九十五、版本排错
Maven:
mvn dependency:tree
重点看:
spring-boot
spring-framework
activiti-*
javax.servlet
jakarta.servlet
一百九十六、不要只看 Maven 下载成功
依赖能下载:
≠
运行时一定兼容
一百九十七、课程项目最稳做法
如果课程已有:
明确 Activiti7 + SpringBoot 基线
优先:
使用课程提供基线
一百九十八、如果你自己重新搭
必须先:
验证兼容性
再:
正式写业务
一百九十九、数据库表自动创建
Activiti 配置可能支持:
自动建表 / 更新表
具体配置名:
依版本而不同
二百、开发环境
可以:
自动创建/更新
生产:
更推荐可控 SQL / Schema 管理
二百零一、为什么
生产数据库:
不能每次启动随意变结构
二百零二、审批记录业务表
前面设计:
car_approval_record
本章正式使用。
二百零三、记录 manager approve
business_type = SETTLEMENT
business_id = settlementId
task_id = taskId
activity_id = managerApprove
activity_name = 经理审批
operator_user_id = currentUserId
action = APPROVE
comment = 同意
二百零四、拒绝
action = REJECT
二百零五、为什么不要只靠 Activiti Comment
业务页面经常需要:
统一格式
关联用户名
业务筛选
导出
独立业务表:
更方便
二百零六、审批操作 Service 示意
@Transactional(
rollbackFor = Exception.class
)
public void approveTask(
TaskApproveDTO dto
) {
Task task =
taskService
.createTaskQuery()
.taskId(
dto.getTaskId()
)
.singleResult();
if (
task == null
) {
throw new ServiceException(
"审批任务不存在或已处理"
);
}
validateTaskPermission(
task
);
ProcessInstance instance =
runtimeService
.createProcessInstanceQuery()
.processInstanceId(
task.getProcessInstanceId()
)
.singleResult();
if (
instance == null
) {
throw new ServiceException(
"流程实例不存在或已结束"
);
}
Long settlementId =
Long.valueOf(
instance.getBusinessKey()
);
CarSettlement settlement =
getAccessibleSettlement(
settlementId
);
if (
settlement == null
) {
throw new ServiceException(
"结算单不存在或无权限"
);
}
if (
StringUtils.isNotBlank(
dto.getComment()
)
) {
taskService.addComment(
task.getId(),
task.getProcessInstanceId(),
dto.getComment()
);
}
String variableName =
resolveApprovalVariable(
task.getTaskDefinitionKey()
);
Map<String, Object> variables =
Map.of(
variableName,
dto.getApproved()
);
taskService.complete(
task.getId(),
variables
);
approvalRecordService
.saveRecord(
settlement,
task,
dto
);
if (
Boolean.FALSE.equals(
dto.getApproved()
)
) {
settlementMapper
.updateApprovalStatus(
settlementId,
ApprovalStatus
.REJECTED
.getCode()
);
return;
}
ProcessInstance running =
runtimeService
.createProcessInstanceQuery()
.processInstanceId(
task.getProcessInstanceId()
)
.singleResult();
if (
running == null
) {
settlementMapper
.updateApprovalStatus(
settlementId,
ApprovalStatus
.APPROVED
.getCode()
);
} else {
settlementMapper
.updateApprovalStatus(
settlementId,
ApprovalStatus
.PROCESSING
.getCode()
);
}
}
二百零七、这里有什么潜在问题
ProcessInstance 查询:
在 complete 前存在
complete 后如果流程结束:
Runtime 查询返回 null
这个逻辑:
容易理解
二百零八、但如果拒绝分支流程还有“拒绝通知任务”
那么:
approved=false
并不一定:
立刻结束
所以:
业务状态同步要跟 BPMN 设计一致
二百零九、课程第一版 BPMN
拒绝分支:
直接 EndEvent
因此:
上述逻辑成立
二百一十、这说明什么
流程图:
不是 UI 装饰
它直接决定:
代码业务逻辑
二百一十一、任务领取接口
建议:
POST /car/workflow/task/{taskId}/claim
二百一十二、取消领取
POST /car/workflow/task/{taskId}/unclaim
二百一十三、审批
POST /car/workflow/task/{taskId}/approve
二百一十四、为什么 approve 路径虽然包含 taskId,DTO 也可以不再传 taskId
可以设计:
PathVariable taskId
Body:
approved
comment
更 REST 化。
二百一十五、ApproveDTO
public class ApprovalActionDTO {
@NotNull
private Boolean approved;
private String comment;
}
二百一十六、接口
@PostMapping(
"/{taskId}/approve"
)
public AjaxResult approve(
@PathVariable
String taskId,
@Valid
@RequestBody
ApprovalActionDTO dto
) {
}
二百一十七、权限码
car:workflow:task:list
car:workflow:task:claim
car:workflow:task:approve
二百一十八、待办查询
后面独立章节深入,
本章可以先实现:
当前用户候选/已领取任务
二百一十九、任务列表来源
Assignee tasks
+
CandidateGroup tasks
二百二十、去重
如果任务同时:
已被当前用户 claim
就不要:
候选列表重复显示
二百二十一、审批页面结构
左侧/上方:
任务信息
中间:
结算详情
底部:
审批意见
按钮:
通过
拒绝
二百二十二、审批页面不要让用户编辑结算金额
必须:
readonly
二百二十三、通过按钮
approved = true
二百二十四、拒绝按钮
approved = false
并:
comment 必填
二百二十五、流程定义页面 vs 审批页面
流程定义:
管理员使用
审批任务:
业务审批人使用
不要:
混在一个菜单
二百二十六、菜单建议
流程管理
├─ 流程定义
├─ 我的待办
└─ 我的已办
二百二十七、审批历史
后面会实现:
我的已办
业务审批记录
二百二十八、HistoryService
常用:
HistoricProcessInstance
HistoricTaskInstance
HistoricActivityInstance
二百二十九、为什么 Runtime 和 History 分开
Runtime:
当前还在跑
History:
已经发生过
二百三十、流程结束后为什么 TaskService 查不到旧任务
因为旧任务:
已经不是 Runtime Task
应该:
HistoryService
二百三十一、这就是常见错误
任务完成后
createTaskQuery()
查不到
是正常的。
二百三十二、流程历史查询
historyService
.createHistoricTaskInstanceQuery()
.processInstanceId(
processInstanceId
)
.finished()
.list();
二百三十三、审批记录优先显示什么
业务页面:
可以优先 car_approval_record
流程诊断:
HistoryService
二百三十四、为什么两者都保留价值
ApprovalRecord
业务友好
HistoryService
引擎完整
二百三十五、流程定义 XML 如何展示
后端:
RepositoryService
获取:
BPMN Resource InputStream
前端:
文本 / bpmn-js viewer
二百三十六、当前第一版
可以先:
下载/查看 XML
二百三十七、流程图显示
如果 BPMN 自带 DI 图形信息:
前端 bpmn-js
可以渲染。
二百三十八、后面“流程图高亮”章节
会进一步:
获取已执行 activityId
+
当前 activityId
然后:
高亮节点
二百三十九、现在先确保 BPMN 节点 id 稳定
例如:
managerApprove
financeApprove
不要:
每次随便改成 Task_1a2b3c
二百四十、为什么
后端:
可能根据 taskDefinitionKey
做业务映射
二百四十一、BPMN 节点 ID 规范
推荐:
start
managerApprove
managerGateway
financeApprove
financeGateway
approveEnd
rejectEnd
二百四十二、节点 name
可以中文:
经理审批
财务审批
二百四十三、ID 和 name 区别
ID:
代码稳定标识
name:
用户展示
二百四十四、为什么 ID 不建议中文
虽然部分场景:
可能支持
但:
英文稳定 ID
更方便开发。
二百四十五、审批组也用英文 key
store_manager
finance
二百四十六、若依角色配置
sys_role.role_key:
store_manager
finance
二百四十七、不要通过 role_id 写进 BPMN
错误:
candidateGroups="5"
因为:
不同环境 role_id
可能不同
二百四十八、roleKey 更稳定
开发环境:
role_id = 5
生产:
role_id = 19
但:
role_key = finance
可以保持一致。
二百四十九、这是业务编码思想
和:
itemCode
packageCode
processKey
一样。
二百五十、流程 Definition Key 也属于稳定业务编码
不要:
拿数据库 ID 当业务 key
二百五十一、数据库初始化顺序
建议:
1. 若依 sys_* 表
2. car_* 业务表
3. Activiti ACT_* 表
4. BPMN deployment
二百五十二、ACT_* 表是否手工建
看:
当前使用版本和部署策略
开发可以:
由引擎自动
生产:
建议使用官方匹配版本 SQL
二百五十三、绝对不要拿 Activiti6 SQL 配 Activiti7/8
版本:
必须匹配
二百五十四、流程定义 Deployment 是否加入初始化 SQL
不是普通 SQL。
部署:
通过引擎 API
更安全。
二百五十五、流程版本发布建议
每次 BPMN 改动:
Git Commit
+
新 Deployment
二百五十六、不要覆盖历史 BPMN 文件后假装没版本
流程:
需要可追踪
二百五十七、Git 文件
src/main/resources/processes/
settlement-approval.bpmn20.xml
二百五十八、提交建议
git commit -m "feat: add settlement approval BPMN"
二百五十九、集成引擎
git commit -m "feat: integrate Activiti workflow engine"
二百六十、流程定义页面
git commit -m "feat: add workflow definition management"
二百六十一、结算提交流程
git commit -m "feat: start approval workflow on settlement submit"
二百六十二、审批任务
git commit -m "feat: add settlement approval task actions"
二百六十三、为什么拆 Commit
工作流问题很多:
版本
BPMN
任务
权限
业务状态
拆分:
非常方便回退
二百六十四、Cursor 提示词:版本兼容检查
当前项目是 RuoYi-Vue Spring Boot 3 + JDK17。
课程要求 Activiti7。
请先只分析,不修改。
检查当前 pom.xml 和 dependency tree,
判断准备使用的 Activiti 依赖是否与:
Spring Boot 3
Spring Framework
Jakarta Servlet
JDK17
兼容。
不要直接复制旧 Spring Boot 2 Activiti7 示例。
输出:
1. 当前实际 Spring Boot 版本
2. Activiti 版本
3. 是否使用 javax
4. 是否使用 jakarta
5. 可能冲突的依赖
6. 最小可行集成方案
二百六十五、Cursor 提示词:BPMN
请创建结算审批 BPMN。
process id:
settlementApproval
节点:
start
managerApprove
managerGateway
financeApprove
financeGateway
approveEnd
rejectEnd
候选组:
managerApprove → store_manager
financeApprove → finance
变量:
managerApproved
financeApproved
要求:
1. 拒绝直接结束
2. 通过进入下一节点
3. 节点 ID 使用稳定英文
4. 节点 name 使用中文
5. 文件放在项目当前 Activiti 配置要求的流程资源目录
二百六十六、Cursor 提示词:结算提交
请修改 SettlementService.submitSettlement。
要求:
1. 只有 DRAFT 能提交
2. 提交前验证主表与明细金额一致
3. 判断是否需要审批
4. 无需审批:
approvalStatus = NOT_REQUIRED
5. 需要审批:
检查 settlementApproval 流程已部署且 active
使用 RuntimeService 启动流程
6. businessKey = settlementId
7. 流程变量只放轻量流程上下文
8. 保存 processInstanceId
9. settlement status 更新为 SUBMITTED
10. approvalStatus 更新为 PROCESSING
11. 启动流程和更新业务表必须同一事务
12. 不把整个 Settlement 对象塞进流程变量
二百六十七、Cursor 提示词:若依角色映射
请设计 Activiti candidateGroup 与若依角色映射。
要求:
1. 用户身份以 sys_user 为准
2. 不维护第二套 Activiti 用户密码体系
3. candidateGroup 使用 sys_role.role_key
4. manager = store_manager
5. finance = finance
6. 当前用户待办查询从 LoginUser 获取 roleKey
7. 领取/完成任务必须验证当前用户属于任务候选组或 assignee
8. 不使用 role_id 作为 BPMN group code
二百六十八、Cursor 提示词:审批动作
请实现工作流审批动作。
前端只传:
taskId
approved
comment
后端必须:
1. 根据 taskId 查询真实 Task
2. 检查当前用户任务权限
3. 从 Task 获取 processInstanceId
4. 从 ProcessInstance 获取 businessKey
5. businessKey 转 settlementId
6. 不信任前端传 businessId
7. 检查结算 DataScope
8. 根据 taskDefinitionKey 决定流程变量名
9. 拒绝必须填写 comment
10. TaskService.complete
11. 保存 car_approval_record
12. 同步 approvalStatus
13. 所有步骤同一事务
二百六十九、Cursor 提示词:流程定义页面
请基于 RuoYi-Vue3 实现流程定义管理页面。
页面:
src/views/car/workflow/definition/index.vue
显示:
名称
Key
版本
DeploymentId
ResourceName
状态
操作:
部署结算审批流程
查看 XML
查看流程图
挂起
激活
要求:
1. Element Plus
2. v-hasPermi
3. 调用后端 RepositoryService API
4. 不直接访问 ACT_* 表
5. 不做复杂在线拖拽 Modeler
6. 当前只做流程定义管理核心功能
二百七十、Cursor 提示词:安全审查
请审查 Activiti 结算审批集成。
重点检查:
1. 前端能否伪造 settlementId
2. taskId 是否验证归属
3. 是否只有 v-hasPermi 没有后端 @PreAuthorize
4. candidateGroup 是否和 roleKey 一致
5. 是否直接修改 ACT_* 表
6. 是否把整个业务对象塞流程变量
7. Task complete 和业务状态更新是否同一事务
8. 业务提交后是否还能改结算金额
9. businessKey 是否正确保存 settlementId
10. processInstanceId 是否回写业务表
11. 拒绝后是否仍残留运行任务
12. Spring Boot3 和旧 Activiti7 依赖是否冲突
二百七十一、IDEA 调试断点
重点:
submitSettlement
startProcessInstanceByKey
approveTask
taskService.claim
taskService.complete
resolveApprovalVariable
updateApprovalStatus
二百七十二、第一次调试流程
先:
部署 BPMN
数据库确认:
ACT_RE_DEPLOYMENT
ACT_RE_PROCDEF
有数据。
二百七十三、提交结算
确认:
car_settlement.process_instance_id
不为空。
二百七十四、运行表
查看:
ACT_RU_TASK
应该出现:
经理审批
二百七十五、经理领取并通过
任务:
managerApprove
完成后:
ACT_RU_TASK
应该变成:
财务审批
二百七十六、财务通过
流程结束:
ACT_RU_TASK
该流程:
没有运行任务
二百七十七、历史
ACT_HI_TASKINST
应该:
有经理和财务历史
二百七十八、业务表
car_settlement.approval_status
应该:
APPROVED
二百七十九、拒绝测试
经理:
Reject
预期:
流程结束
approvalStatus = REJECTED
无财务任务
二百八十、审批记录
car_approval_record
应该:
记录经理拒绝原因
二百八十一、数据库检查:流程定义
SELECT
ID_,
KEY_,
NAME_,
VERSION_,
DEPLOYMENT_ID_
FROM ACT_RE_PROCDEF
ORDER BY
KEY_,
VERSION_;
字段名:
以当前 Activiti 版本真实表结构为准
二百八十二、运行任务
SELECT
ID_,
NAME_,
TASK_DEF_KEY_,
ASSIGNEE_,
PROC_INST_ID_
FROM ACT_RU_TASK;
二百八十三、流程实例
SELECT *
FROM ACT_RU_EXECUTION
WHERE PROC_INST_ID_ = ?;
二百八十四、历史任务
SELECT
ID_,
NAME_,
ASSIGNEE_,
START_TIME_,
END_TIME_
FROM ACT_HI_TASKINST
WHERE PROC_INST_ID_ = ?
ORDER BY START_TIME_;
二百八十五、为什么 SQL 只用于观察
学习时:
查 ACT_* 表
有助理解。
但业务代码:
不要直接 UPDATE/DELETE
二百八十六、测试用例 1
流程未部署:
提交需审批结算
预期:
友好提示:
审批流程尚未部署
二百八十七、测试用例 2
部署流程。
列表:
出现 version 1
二百八十八、测试用例 3
再次部署修改后的同 key 流程:
出现 version 2
二百八十九、测试用例 4
新流程实例:
使用最新 active 版本
二百九十、测试用例 5
优惠未超过阈值:
不启动流程
二百九十一、测试用例 6
优惠超过阈值:
启动流程
二百九十二、测试用例 7
businessKey:
等于 settlementId
二百九十三、测试用例 8
processInstanceId:
回写 car_settlement
二百九十四、测试用例 9
经理角色:
能看到 managerApprove 候选任务
二百九十五、测试用例 10
普通服务顾问:
看不到该任务
二百九十六、测试用例 11
经理 claim:
assignee = 当前用户
二百九十七、测试用例 12
另一经理尝试完成已被领取任务:
拒绝
二百九十八、测试用例 13
经理通过:
生成 financeApprove
二百九十九、测试用例 14
经理拒绝:
流程结束
三百、测试用例 15
财务通过:
流程结束
approvalStatus = APPROVED
三百零一、测试用例 16
财务拒绝:
流程结束
approvalStatus = REJECTED
三百零二、测试用例 17
拒绝不填原因:
后端拒绝
三百零三、测试用例 18
伪造 taskId:
拒绝
三百零四、测试用例 19
伪造其他门店任务:
DataScope / Task 权限拒绝
三百零五、测试用例 20
审批进行中:
修改结算优惠
预期:
拒绝
三百零六、面试题 1:BPMN 和 Activiti 是什么关系
答:
BPMN 是业务流程建模标准,
用于描述开始事件、用户任务、网关、结束事件等流程结构。
Activiti 是工作流/BPM 平台,
可以部署和执行 BPMN 流程定义,
并管理流程实例、任务、变量和历史。
三百零七、面试题 2:ProcessDefinition 与 ProcessInstance 区别
答:
ProcessDefinition 是部署后的流程模板,
例如“结算审批流程 v2”。
ProcessInstance 是某一次实际运行,
例如“结算单 10086 的这次审批”。
一个流程定义可以启动很多流程实例。
三百零八、面试题 3:Task 是什么
答:
Task 表示流程运行过程中需要某个人处理的人工任务。
例如经理审批和财务审批都是 UserTask,
运行到节点时 Activiti 会产生 Task,
用户完成任务后流程继续向后执行。
三百零九、面试题 4:businessKey 有什么用
答:
businessKey 用来关联流程实例和业务数据。
例如启动结算审批时把 settlementId 作为 businessKey,
以后从 Task 找到 ProcessInstance,
再通过 businessKey 就能定位对应结算单。
三百一十、面试题 5:processInstanceId 为什么还要保存业务表
答:
businessKey 解决流程找业务,
processInstanceId 可以让业务快速找到自己的流程实例。
保存 processInstanceId 后,
结算详情可以直接查询流程进度、历史任务和流程图。
三百一十一、面试题 6:RepositoryService 做什么
答:
RepositoryService 主要负责流程定义和流程部署,
例如部署 BPMN、查询 ProcessDefinition、
读取流程资源以及挂起或激活流程定义。
三百一十二、面试题 7:RuntimeService 做什么
答:
RuntimeService 负责正在运行的流程,
包括启动 ProcessInstance、查询运行实例、
设置和读取流程变量等。
三百一十三、面试题 8:TaskService 做什么
答:
TaskService 负责人工任务,
例如查询待办、领取任务、取消领取、
添加审批意见和完成任务。
三百一十四、面试题 9:HistoryService 做什么
答:
HistoryService 用于查询已经发生过的流程历史,
例如历史流程实例、历史任务和历史活动节点。
流程任务完成后不会继续存在于 Runtime Task 查询中,
需要通过 HistoryService 查询历史。
三百一十五、面试题 10:Assignee 和 CandidateGroup 区别
答:
Assignee 表示任务已经明确属于某个具体用户。
CandidateGroup 表示某个用户组中的成员都有资格处理该任务,
通常需要其中一个人先 claim,
再成为真正 assignee。
三百一十六、面试题 11:为什么 candidateGroup 推荐用若依 roleKey
答:
roleKey 是稳定的角色业务编码,
例如 finance、store_manager。
roleId 在开发、测试、生产环境可能不同,
roleName 也可能被修改。
所以 BPMN 中使用 roleKey 作为候选组编码更稳定。
三百一十七、面试题 12:为什么不维护第二套 Activiti 用户体系
答:
若依已经通过 sys_user、sys_role 和 Spring Security
管理用户和角色。
如果 Activiti 再维护一套用户和密码,
就会产生同步和一致性问题。
课程项目更适合以若依为统一身份源,
Activiti 只负责流程和任务。
三百一十八、面试题 13:为什么 Task complete 前要校验当前用户
答:
taskId 只是一个请求参数,可以被伪造。
后端必须确认当前用户确实是该任务的 assignee,
或者属于允许领取/处理该任务的 candidateGroup。
否则会产生越权审批。
三百一十九、面试题 14:为什么不能信前端传 settlementId
答:
审批接口应该根据 taskId 找到 ProcessInstance,
再从流程实例 businessKey 推导 settlementId。
如果同时相信前端传 taskId 和 settlementId,
攻击者可以故意把一个任务和另一张业务单据组合,
产生业务越权。
三百二十、面试题 15:为什么 Task complete 和业务状态更新要同一事务
答:
流程任务完成和业务审批状态必须保持一致。
如果任务已经完成但业务状态更新失败,
或者业务状态已经修改但 Task complete 失败,
都会造成流程和业务数据不一致。
同数据源 Spring 集成场景下,
应该尽量把它们放进同一个事务。
三百二十一、面试题 16:为什么流程变量不能保存整个业务对象
答:
流程变量应该只保存流程路由需要的轻量上下文。
完整业务对象会造成重复数据、序列化兼容问题、
数据膨胀以及业务对象版本变化后的反序列化风险。
真正业务数据仍应该保存在业务表中。
三百二十二、面试题 17:为什么不要直接操作 ACT_* 表
答:
ACT_* 表是 Activiti 引擎内部持久化结构,
一个流程动作可能同时影响多张运行表和历史表。
直接修改其中某一张表很容易破坏引擎状态。
正确方式是通过 RepositoryService、
RuntimeService、TaskService 等官方 API 操作。
三百二十三、面试题 18:为什么提交后结算金额要冻结
答:
提交后审批流程已经针对当前金额启动。
如果审批过程中仍允许修改优惠和应收金额,
审批人看到的数据和最终结算会不一致。
所以提交是草稿和正式审批数据之间的冻结边界。
三百二十四、面试题 19:为什么流程定义有版本
答:
业务流程会迭代。
相同 process key 重新部署后,
Activiti 可以生成新的流程定义版本。
新流程实例通常使用最新版本,
已经运行的旧实例仍继续使用启动时的旧定义,
避免流程升级破坏正在审批的数据。
三百二十五、面试题 20:Activiti7 和 Spring Boot3 为什么需要关注兼容性
答:
Activiti7 官方早期 Spring Boot 文档主要面向 Spring Boot 2.x。
Spring Boot 3 带来了 Spring Framework 和
javax → jakarta 等生态迁移。
因此不能只把旧 Activiti7 示例依赖复制到 Boot3 项目,
应先检查实际 Activiti 版本、Starter、JDK、
Spring 和 Jakarta 兼容关系。
三百二十六、Activiti 工作流知识树
Workflow
│
├─ BPMN
│ ├─ StartEvent
│ ├─ UserTask
│ ├─ SequenceFlow
│ ├─ ExclusiveGateway
│ └─ EndEvent
│
├─ Repository
│ ├─ Deployment
│ ├─ ProcessDefinition
│ ├─ Version
│ └─ Resource
│
├─ Runtime
│ ├─ ProcessInstance
│ ├─ businessKey
│ ├─ processInstanceId
│ └─ Variables
│
├─ Task
│ ├─ Assignee
│ ├─ CandidateGroup
│ ├─ Claim
│ ├─ Unclaim
│ ├─ Comment
│ └─ Complete
│
├─ History
│ ├─ HistoricProcessInstance
│ ├─ HistoricTaskInstance
│ └─ HistoricActivityInstance
│
├─ RuoYi
│ ├─ sys_user
│ ├─ sys_role.role_key
│ ├─ @PreAuthorize
│ ├─ SecurityUtils
│ └─ DataScope
│
└─ Settlement
├─ settlementId
├─ approvalStatus
├─ processInstanceId
├─ car_approval_record
└─ SettlementDetailVO
三百二十七、完整流程部署图
settlement-approval.bpmn20.xml
↓
RepositoryService
↓
Deployment
↓
ProcessDefinition
↓
Key:
settlementApproval
↓
Version:
1 / 2 / 3
三百二十八、完整结算启动审批图
Settlement DRAFT
↓
Submit
↓
Validate Amounts
↓
Need Approval?
├─ No
│ ↓
│ NOT_REQUIRED
│
└─ Yes
↓
RuntimeService
↓
startProcessInstanceByKey
↓
businessKey = settlementId
↓
ProcessInstance
↓
save processInstanceId
↓
approvalStatus = PROCESSING
↓
First UserTask
三百二十九、完整任务审批安全图
Frontend
taskId + approved + comment
↓
@PreAuthorize
↓
TaskService query Task
↓
Validate Assignee / CandidateGroup
↓
ProcessInstance
↓
businessKey
↓
Settlement
↓
DataScope
↓
TaskService.complete
↓
ApprovalRecord
↓
Update approvalStatus
↓
Commit
三百三十、完整数据职责划分
RuoYi
负责:
用户
角色
权限
登录
部门
Activiti
负责:
流程定义
流程实例
任务
变量
流程历史
Car Business Tables
负责:
结算
金额
结算明细
业务审批状态
业务审批记录
三百三十一、本章最终验收
你应该能够独立完成:
1. 理解 BPMN
2. 编写结算审批 BPMN
3. 部署 BPMN
4. 查询 ProcessDefinition
5. 理解流程版本
6. 流程定义管理页面
7. 挂起/激活流程定义
8. 结算提交启动 ProcessInstance
9. businessKey = settlementId
10. 保存 processInstanceId
11. 配置经理 UserTask
12. 配置财务 UserTask
13. 使用 candidateGroup
14. candidateGroup 映射若依 roleKey
15. 当前用户查询候选任务
16. claim
17. unclaim
18. Task complete
19. 通过/拒绝变量
20. 拒绝意见必填
21. 保存 car_approval_record
22. 同步 approvalStatus
23. Task 权限校验
24. DataScope
25. 业务和流程事务一致性
26. 查询 HistoryService
27. 理解 ACT_RE / RU / HI / GE
28. 不直接修改 ACT_* 表
29. 识别 SpringBoot3 与旧 Activiti7 兼容问题
30. 为后续待办/已办/流程图高亮打基础
三百三十二、本章最重要的工程原则
1. BPMN 是流程蓝图,ProcessDefinition 是部署后的模板
2. ProcessInstance 是某一张业务单据的运行实例
3. Task 是当前需要人处理的节点
4. businessKey 用业务 ID 关联流程
5. processInstanceId 回写业务表
6. 若依负责用户身份,Activiti 负责流程
7. candidateGroup 推荐使用稳定 roleKey
8. 不维护第二套用户密码体系
9. taskId 不能直接信任,必须检查任务归属
10. settlementId 不由前端决定,应从 businessKey 推导
11. 流程变量只保存轻量上下文
12. 完整业务数据继续放 car_* 表
13. 提交后结算金额必须冻结
14. Task complete 和业务状态同步尽量同一事务
15. 不直接 UPDATE ACT_* 表
16. 流程版本升级不能破坏旧运行实例
17. Spring Boot3 项目不要机械复制旧 Activiti7 Boot2 依赖
三百三十三、下一篇
按照课程表,下一篇进入:
《淘车湾项目实战(七):流程审核信息分析及实现》
下一章会重点处理:
我的待办
候选任务
已领取任务
claim / unclaim
审批详情
审批意见
car_approval_record
HistoricTaskInstance
HistoricActivityInstance
审批时间轴
经理审批
财务审批
业务状态同步
拒绝回退策略
任务权限
若依角色和用户信息展示
Vue3 审批任务页面
审批记录组件
官方资料与版本提醒
Activiti 7 官方开发指南中的 Spring Boot Core 示例明确以:
Spring Boot 2
为集成背景,并展示:
activiti-spring-boot-starter
以及 Runtime/Task API 思路。
Activiti 官方项目目前仍持续演进,
GitHub 官方仓库已经存在更高主版本。
因此学习本章时:
重点掌握工作流模型和 API 关系
真正集成时:
务必先检查项目 Spring Boot、JDK、Jakarta 与 Activiti 版本兼容性
不要:
只因为课程写“Activiti7”
就把旧 Starter 强行放进任何现代 Spring Boot 项目。