# scaffold 项目之工作流的流程发起、取消、重新发起
# 简介
可以看 scaffold 项目之工作流 文章
# 开始使用
- 流程发起:开始一个流程任务
- 取消:可以取消或终止在走的流程任务
- 重新发起:对于完成的流程或者取消的流程可以在次重新发起(是一个全新的流程)
功能在:[审批中心菜单] -> [我的流程] -> [发起流程] 下
# 发起流程
# 表结构
先了解对应的表结构
① 流程实例表,由 Flowable 提供的 ACT_RU_EXECUTION 表实现,如下所示:
| 字段 | 类型 | 主键 | 说明 | 备注 |
|---|---|---|---|---|
| ID_ | NVARCHAR2(64) | Y | 主键 | |
| REV_ | INTEGER | N | 数据版本 | |
| PROC_INST_ID_ | NVARCHAR2(64) | N | 流程实例 ID | |
| BUSINESS_KEY_ | NVARCHAR2(255) | N | 业务主键 ID | |
| PARENT_ID_ | NVARCHAR2(64) | N | 父执行流的 ID | |
| PROC_DEF_ID_ | NVARCHAR2(64) | N | 流程定义的数据 ID | |
| SUPER_EXEC_ | NVARCHAR2(64) | N | ||
| ROOT_PROC_INST_ID_ | NVARCHAR2(64) | N | ||
| ACT_ID_ | NVARCHAR2(255) | N | 节点实例 ID | |
| IS_ACTIVE_ | NUMBER(1) | N | 是否存活 | |
| IS_CONCURRENT_ | NUMBER(1) | N | 执行流是否正在并行 | |
| IS_SCOPE_ | NUMBER(1) | N | ||
| IS_EVENT_SCOPE_ | NUMBER(1) | N | ||
| IS_MI_ROOT_ | NUMBER(1) | N | ||
| SUSPENSION_STATE_ | INTEGER | N | 流程终端状态 | |
| CACHED_ENT_STATE_ | INTEGER | N | ||
| TENANT_ID_ | NVARCHAR2(255) | N | ||
| NAME_ | NVARCHAR2(255) | N | ||
| START_TIME_ | TIMESTAMP(6) | N | 开始时间 | |
| START_USER_ID_ | NVARCHAR2(255) | N | ||
| LOCK_TIME_ | TIMESTAMP(6) | N | ||
| IS_COUNT_ENABLED_ | NUMBER(1) | N | ||
| EVT_SUBSCR_COUNT_ | INTEGER | N | ||
| TASK_COUNT_ | INTEGER | N | ||
| JOB_COUNT_ | INTEGER | N | ||
| TIMER_JOB_COUNT_ | INTEGER | N | ||
| SUSP_JOB_COUNT_ | INTEGER | N | ||
| DEADLETTER_JOB_COUNT_ | INTEGER | N | ||
| VAR_COUNT_ | INTEGER | N | ||
| ID_LINK_COUNT_ | INTEGER | N |
② 流程参数表,由 Flowable 提供的 ACT_RU_VARIABLE 表实现,如下所示:
| 字段 | 类型 | 主键 | 说明 | 备注 |
|---|---|---|---|---|
| ID_ | NVARCHAR2(64) | Y | 主键 | |
| REV_ | INTEGER | N | 数据版本 | |
| TYPE_ | NVARCHAR2(255) | N | 参数类型 | 可以是基本的类型,也可以用户自行扩展 |
| NAME_ | NVARCHAR2(255) | N | 参数名称 | |
| EXECUTION_ID_ | NVARCHAR2(64) | N | 参数执行 ID | |
| PROC_INST_ID_ | NVARCHAR2(64) | N | 流程实例 ID | |
| TASK_ID_ | NVARCHAR2(64) | N | 任务 ID | |
| BYTEARRAY_ID_ | NVARCHAR2(64) | N | 资源 ID | |
| DOUBLE_ | NUMBER(*,10) | N | 参数为 double,则保存在该字段中 | |
| LONG_ | NUMBER(19) | N | 参数为 long,则保存在该字段中 | |
| TEXT_ | NVARCHAR2(2000) | N | 用户保存文本类型的参数值 | |
| TEXT2_ | NVARCHAR2(2000) | N | 用户保存文本类型的参数值 |
在 Flowable 中,如果想给 ProcessInstance 增加拓展字段,无法通过 ACT_RU_EXECUTION 实现,而是通过 ACT_RU_VARIABLE 表实现。
该表是一种 Key-Value 的形式,可以存储任意类型的数据。例如说,项目中给 ProcessInstance 增加了一个 PROCESS_STATUS 字段,表示流程状态,如下所示:
| TYPE_(数据类型) | NAME_(key 的名称) | PROC_INST_ID_(流程实例的编号) | DOUBLE_(value 值) | LONG_(value 值) | TEXT_(value 值) |
|---|---|---|---|---|---|
| string | PROCESS_STATUS | ee01d8d3-9b72-11ef-b087-0242ac110004 | 1 |
# 流程状态
流程状态,由 BpmProcessInstanceStatusEnum 目前有 4 种
NOT_START(-1, "未开始"), | |
RUNNING(1, "审批中"), | |
APPROVE(2, "审批通过"), | |
REJECT(3, "审批不通过"), | |
CANCEL(4, "已取消"); |
# 实现原理
/** | |
* 创建流程实例(提供给前端) | |
* | |
* @param userId 用户编号 | |
* @param createReqVO 创建信息 | |
* @return 实例的编号 | |
*/ | |
String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO); | |
// 1. 通过流程定义 id 查询流程定义 | |
// 2. 根据流程定义来发起流程 | |
@Override | |
@Transactional(rollbackFor = Exception.class) | |
public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { | |
// 获得流程定义 | |
ProcessDefinition definition = processDefinitionService | |
.getProcessDefinition(createReqVO.getProcessDefinitionId()); | |
// 发起流程 | |
return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, | |
createReqVO.getStartUserSelectAssignees()); | |
} |
创建流程实例
private String createProcessInstance0(Long userId, ProcessDefinition definition, | |
Map<String, Object> variables, String businessKey, | |
Map<String, List<Long>> startUserSelectAssignees) { | |
// 1.1 校验流程定义 | |
if (definition == null) { | |
throw exception(PROCESS_DEFINITION_NOT_EXISTS); | |
} | |
if (definition.isSuspended()) { | |
throw exception(PROCESS_DEFINITION_IS_SUSPENDED); | |
} | |
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService | |
.getProcessDefinitionInfo(definition.getId()); | |
if (processDefinitionInfo == null) { | |
throw exception(PROCESS_DEFINITION_NOT_EXISTS); | |
} | |
// 1.2 校验是否能够发起 | |
if (!processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId)) { | |
throw exception(PROCESS_INSTANCE_START_USER_CAN_START); | |
} | |
// 1.3 校验发起人自选审批人 | |
validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables); | |
// 2. 创建流程实例 | |
if (variables == null) { | |
variables = new HashMap<>(); | |
} | |
FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 | |
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量,发起人 ID | |
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 | |
BpmProcessInstanceStatusEnum.RUNNING.getStatus()); | |
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true,不影响没配置 skipExpression 的节点 | |
if (CollUtil.isNotEmpty(startUserSelectAssignees)) { | |
// 设置流程变量,发起人自选审批人 | |
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, | |
startUserSelectAssignees); | |
} | |
// 3. 创建流程 | |
ProcessInstanceBuilder processInstanceBuilder = runtimeService.createProcessInstanceBuilder() | |
.processDefinitionId(definition.getId()) | |
.businessKey(businessKey) | |
.variables(variables); | |
// 3.1 创建流程 ID | |
BpmModelMetaInfoVO.ProcessIdRule processIdRule = processDefinitionInfo.getProcessIdRule(); | |
if (processIdRule != null && Boolean.TRUE.equals(processIdRule.getEnable())) { | |
processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule)); | |
} | |
// 3.2 流程名称 | |
processInstanceBuilder.name(generateProcessInstanceName(userId, definition, processDefinitionInfo, variables)); | |
// 3.3 发起流程实例 | |
ProcessInstance instance = processInstanceBuilder.start(); | |
return instance.getId(); | |
} |
核心是:
// 3. 创建流程 绑定流程定义、业务键和变量
ProcessInstanceBuilder processInstanceBuilder = runtimeService.createProcessInstanceBuilder ()
.processDefinitionId(definition.getId())
.businessKey(businessKey)
.variables(variables);就是调用 Flowable 的
RuntimeService#createProcessInstanceBuilder().start()方法,创建流程实例。同时因为 Flowable 自身没有流程状态,所以需要我们自己维护任务状态。所以状态就存在流程变量中 也就是 variables, 其中还维护了审批相关的变量
# 查看我的流程
我的流程,对应 [审批中心 -> 我的流程] 菜单
# 表结构
先了解表结构
① 历史流程实例表,由 Flowable 提供的 ACT_HI_PROCINST 表实现,如下所示:
| 字段 | 类型 | 主键 | 说明 | 备注 |
|---|---|---|---|---|
| ID_ | NVARCHAR2(64) | Y | 主键 | |
| PROC_INST_ID_ | NVARCHAR2(64) | N | 流程实例 ID | |
| BUSINESS_KEY_ | NVARCHAR2(255) | N | 业务主键 | |
| PROC_DEF_ID_ | NVARCHAR2(64) | N | 属性 ID | |
| START_TIME_ | TIMESTAMP(6) | N | 开始时间 | |
| END_TIME_ | TIMESTAMP(6) | N | 结束时间 | |
| DURATION_ | NUMBER(19) | N | 耗时 | |
| START_USER_ID_ | NVARCHAR2(255) | N | 起始人 | |
| START_ACT_ID_ | NVARCHAR2(255) | N | 起始节点 | |
| END_ACT_ID_ | NVARCHAR2(255) | N | 结束节点 | |
| SUPER_PROCESS_INSTANCE_ID_ | NVARCHAR2(64) | N | 父流程实例 ID | |
| DELETE_REASON_ | NVARCHAR2(2000) | N | 删除原因 | |
| TENANT_ID_ | NVARCHAR2(255) | N | ||
| NAME_ | NVARCHAR2(255) | N | 名称 |
在 Flowable 中,如果 ProcessInstance 被完成(全部审批通过、不通过、取消等)时候,会从 ACT_RU_EXECUTION 表中删除,只能在 ACT_HI_PROCINST 表查询到。这是一种 “冷热分离” 的设计思想,因为进行的任务访问比较频繁,数据量越小,性能会越好。
而 [我的流程] 需要查询进行中、已完成的流程,所以需要查询 ACT_HI_PROCINST 表,而不能使用 ACT_RU_EXECUTION 表。
冷热分离数据思想:
一个直观的生活类比:
- 热:你口袋里的手机、钥匙 —— 每天用无数次,随时取用。
- 冷:你床底下的大学课本、几年前的相册 —— 一年可能也翻不了一次,但丢了又可惜,收在箱子里就好。
# 典型应用场景
- 数据库(如 MySQL、MongoDB):
- 近期订单(热)存主表,用高性能存储;已发货 / 完成的旧订单(冷)定期迁移到历史表或归档数据库。
- 日志系统(如 ELK):
- 最近 7 天的日志(热)存放在 SSD 上,用于实时排查。7 天前的日志(冷)自动滚动到普通 HDD 或 S3 对象存储,甚至压缩后存入廉价存储。
- 消息队列(如 Kafka):
- Topic 中的最新消息(热)保留在快速磁盘中。旧消息(冷)按策略(如 7 天)自动删除或迁移到慢速存储。
- 电商 / 内容平台:
- 用户最近 3 个月的浏览记录、购物车(热)放在 Redis 缓存。3 个月前的历史记录(冷)存入 MySQL 或数仓,仅支持低频查询。
② 流程历史参数表,由 Flowable 提供的 ACT_HI_VARINST 表实现,如下所示:
| 字段 | 类型 | 主键 | 说明 | 备注 |
|---|---|---|---|---|
| ID_ | NVARCHAR2(64) | Y | 主键 | |
| PROC_INST_ID_ | NVARCHAR2(64) | N | 流程实例 ID | |
| EXECUTION_ID_ | NVARCHAR2(64) | N | 指定 ID | |
| TASK_ID_ | NVARCHAR2(64) | N | 任务 ID | |
| NAME_ | NVARCHAR2(255) | N | 名称 | |
| VAR_TYPE_ | NVARCHAR2(100) | N | 参数类型 | |
| REV_ | INTEGER | N | 数据版本 | |
| BYTEARRAY_ID_ | NVARCHAR2(64) | N | 字节表 ID | |
| DOUBLE_ | NUMBER(*,10) | N | 存储 double 类型数据 | |
| LONG_ | NUMBER(*,10) | N | 存储 long 类型数据 | |
| TEXT_ | NVARCHAR2(2000) | N | ||
| TEXT2_ | NVARCHAR2(2000) | N | ||
| CREATE_TIME_ | TIMESTAMP(6)(2000) | N | ||
| LAST_UPDATED_TIME_ | TIMESTAMP(6)(2000) | N |
在 Flowable 中,如果 ProcessInstance 被完成(全部审批通过、不通过、取消等)时候,会从 ACT_RU_VARIABLE 表中删除,只能在 ACT_HI_VARINST 表查询到。这当然也是是一种 “冷热分离” 的设计思想~
# 具体实现
@GetMapping("/my-page") | |
@Operation(summary = "获得我的实例分页列表", description = "在【我的流程】菜单中,进行调用") | |
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") | |
public CommonResult<PageResult<BpmProcessInstanceRespVO>> getProcessInstanceMyPage( | |
@Valid BpmProcessInstancePageReqVO pageReqVO) { | |
PageResult<HistoricProcessInstance> pageResult = processInstanceService.getProcessInstancePage( | |
getLoginUserId(), pageReqVO); | |
if (CollUtil.isEmpty(pageResult.getList())) { | |
return success(PageResult.empty(pageResult.getTotal())); | |
} | |
// 省略... | |
} | |
@Override | |
@SuppressWarnings("unchecked") | |
public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId, | |
BpmProcessInstancePageReqVO pageReqVO) { | |
// 1. 构建查询条件 | |
HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery() | |
.includeProcessVariables() | |
.processInstanceTenantId(FlowableUtils.getTenantId()) | |
.orderByProcessInstanceStartTime().desc(); | |
if (userId != null) { // 【我的流程】菜单时,需要传递该字段 | |
processInstanceQuery.startedBy(String.valueOf(userId)); | |
} else if (pageReqVO.getStartUserId() != null) { // 【管理流程】菜单时,才会传递该字段 | |
processInstanceQuery.startedBy(String.valueOf(pageReqVO.getStartUserId())); | |
} | |
// 省略.... | |
} |
关键在:
processInstanceService.getProcessInstancePage(getLoginUserId(), pageReqVO);
processInstanceQuery.startedBy(String.valueOf(userId));查询是指定用户 id 的流程实例
# 取消流程
可点击某个流程的「取消」按钮,进行流程的取消
# 具体实现
后端由 BpmProcessInstanceController 的 #cancelProcessInstance(...) 提供接口
@DeleteMapping("/cancel-by-start-user") | |
@Operation(summary = "用户取消流程实例", description = "取消发起的流程") | |
@PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')") | |
public CommonResult<Boolean> cancelProcessInstanceByStartUser( | |
@Valid @RequestBody BpmProcessInstanceCancelReqVO cancelReqVO) { | |
processInstanceService.cancelProcessInstanceByStartUser(getLoginUserId(), cancelReqVO); | |
return success(true); | |
} | |
@Override | |
public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { | |
// 1.1 校验流程实例存在 | |
ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); | |
if (instance == null) { | |
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); | |
} | |
// 1.2 只能取消自己的 | |
if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { | |
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); | |
} | |
// 1.3 校验允许撤销审批中的申请 | |
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService | |
.getProcessDefinitionInfo(instance.getProcessDefinitionId()); | |
Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo); | |
if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消 | |
&& BooleanUtil.isFalse(processDefinitionInfo.getAllowCancelRunningProcess())) { | |
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW); | |
} | |
// 1.4 子流程不允许取消 | |
if (StrUtil.isNotBlank(instance.getSuperExecutionId())) { | |
throw exception(PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW); | |
} | |
// 2. 取消流程 | |
updateProcessInstanceCancel(cancelReqVO.getId(), | |
BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); | |
} | |
private void updateProcessInstanceCancel(String id, String reason) { | |
// 1. 更新流程实例 status | |
runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, | |
BpmProcessInstanceStatusEnum.CANCEL.getStatus()); | |
runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); | |
// 2. 取消所有子流程 | |
List<ProcessInstance> childProcessInstances = runtimeService.createProcessInstanceQuery() | |
.superProcessInstanceId(id).list(); | |
childProcessInstances.forEach(processInstance -> updateProcessInstanceCancel( | |
processInstance.getProcessInstanceId(), BpmReasonEnum.CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS.getReason())); | |
// 3. 结束流程 | |
taskService.moveTaskToEnd(id, reason); | |
} |
关键在于:
updateProcessInstanceCancel (cancelReqVO.getId (), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format (cancelReqVO.getReason ())); 方法的:
taskService.moveTaskToEnd(id, reason);
# 重新发起流程
获取已经结束流程的历史信息作为基础信息【重新发起流程】
和发起流程类似,只是初始加多了旧流程的基础信息