前言
在 Flowable 中,挂起(suspend)与激活(activate)是通过 RepositoryService 或 REST API 对流程定义(Process Definition)和流程实例(Process Instance)执行生命周期管理的关键功能,挂起时将流程置于暂停状态,防止任何新的实例启动或现有实例中的任务、定时器及异步作业执行;激活则是相反操作,可将已挂起的流程恢复到可执行状态,重新允许流程操作与任务完成。
一、挂起(Suspend)的用途与影响
1.1 用途场景
-
维护与升级:在对流程定义或底层数据模型进行升级、维护时,可先挂起相关流程,以防止新实例启动及现有实例继续执行造成数据不一致。
-
资源管理:当系统资源紧张或高并发测试时,挂起非关键流程实例可释放 CPU/内存用于更重要的任务。
-
业务合规:在合规审计或外部审批待决场景下,需暂停流程执行直至审核结束,保障审计过程的中断一致性。
1.2 技术影响
-
新实例禁用:对于挂起的流程定义,不允许再启动新的流程实例,尝试启动将抛出异常或返回错误。
-
暂停现有任务:当挂起流程实例时,其下所有活动的用户任务会自动置为挂起状态,完成操作时会抛出异常,保障任务状态不被误操作。
-
定时/异步作业停调度:所有与该定义或实例关联的定时器(Timer Jobs)及异步执行作业(Async Jobs)将被移入
ACT_RU_SUSPENDED_JOB
(或相应表)中,不再被作业执行器(Job Executor)获取与执行。 -
数据库标记:Flowable 会在
ACT_RE_PROCDEF
(流程定义表)或ACT_RU_EXECUTION
(运行时执行表)的SUSPENSION_STATE_
字段中设置值2
,代表挂起状态。
二、激活(Activate)的用途与影响
2.1 恢复执行
-
重新启用新启动:对已激活的流程定义,恢复允许使用
RuntimeService.startProcessInstanceByKey()
等 API 启动新实例。 -
唤醒挂起任务:激活流程实例后,之前被挂起的用户任务、脚本任务等将恢复为可执行状态,允许流程继续推进。
-
重新调度作业:所有原先移入挂起表的定时器和异步作业将重新置为可运行状态,恢复正常调度与执行。
2.2 技术影响
影响类型 | 挂起状态 | 激活状态 |
---|---|---|
启动新实例 | 禁止,通过 API 或 RuntimeService 抛异常 | 允许,正常启动 |
用户任务 | 所有活动用户任务变为挂起,调用 complete() 抛异常 | 恢复为可完成状态,可调用 complete() |
定时器/异步 | 作业移入 ACT_RU_SUSPENDED_JOB 表,不再调度 | 恢复至正常作业表,Job Executor 可获取执行 |
历史查询 | 历史服务(HistoryService)仍可查询,但无新变更 | 恢复后历史数据继续记录 |
三、后端:完成激活和挂机功能
① 创建操作类型枚举值
这里的枚举值是用来表明当前流程的状态的,和数据库表中的数据对应;1是激活;2是挂起。
package com.ceair.enums;
import lombok.Getter;
/**
* @author wangbaohai
* @ClassName SuspensionState
* @description: 激活/挂起状态枚举
* @date 2025年04月21日
* @version: 1.0.0
*/
@Getter
public enum SuspensionState {
/**
* 1:激活
*/
ACTIVE(1, "激活"),
/**
* 2:挂起
*/
SUSPENDED(2, "挂起");
private final Integer code;
private final String description;
SuspensionState(Integer code, String description) {
this.code = code;
this.description = description;
}
}
② 创建操作参数
参数用来指明需要对具体哪个定义执行操作,同时指定需要做的是激活还是挂起操作。
package com.ceair.entity.request;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author wangbaohai
* @ClassName OperateActReProcdefReq
* @description: 激活/挂起流程定义请求对象
* @date 2025年04月21日
* @version: 1.0.0
*/
@Data
public class OperateActReProcdefReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程定义ID
*/
private String processDefinitionId;
/**
* 操作类型 1:激活 2:挂起
*/
private Integer operateType;
}
③ 创建服务
/**
* 操作流程定义的函数,根据传入的请求对象执行相应的操作。
*
* @param operateActReProcdefReq 包含操作流程定义所需信息的请求对象。
* 该对象通常包含流程定义的ID、操作类型(如启动、挂起、删除等)
* 以及其他与操作相关的参数。
* @return Boolean 返回操作结果。如果操作成功,则返回 true;否则返回 false。
* 返回值可以用于判断操作是否按预期完成。
*/
Boolean operateProcessDefinitionById(OperateActReProcdefReq operateActReProcdefReq);
④ 创建服务实现
具体是的实现我们需要使用Flowable的原生API,激活的API是【activateProcessDefinitionById】,第一个参数就是主键ID,第二个参数代表是否需要一起激活关联的实例,第三个参数是指定激活的时间,一般时间参数不指定,让生效动作立刻触发;挂起的API是【suspendProcessDefinitionById】,参数与激活API相同。
/**
* 激活或挂起指定的流程定义。
* <p>
* 该方法根据请求对象中的流程定义ID和操作类型,执行激活或挂起流程定义的操作。
* 支持级联操作相关流程实例,并可设置操作时间(立即或指定时间)。
* <p>
* 参数说明:
* - operateActReProcdefReq: 请求对象,包含流程定义ID和操作类型。
* 如果为 null 或其字段不符合要求,则会抛出异常。
* <p>
* 返回值:
* - Boolean: 操作成功时返回 true。如果发生异常,则不会返回值,而是抛出 BusinessException。
* <p>
* 异常说明:
* - BusinessException: 当参数校验失败、操作类型非法或发生其他异常时抛出。
*/
@Override
public Boolean operateProcessDefinitionById(OperateActReProcdefReq operateActReProcdefReq) {
try {
// 参数校验
if (operateActReProcdefReq == null) {
log.error("激活或者挂起流程失败,原因:请求对象不能为空");
throw new BusinessException("激活或者挂起流程失败,原因:请求对象不能为空");
}
String processDefinitionId = operateActReProcdefReq.getProcessDefinitionId();
Integer operateType = operateActReProcdefReq.getOperateType();
if (processDefinitionId == null || processDefinitionId.trim().isEmpty()) {
log.error("激活或者挂起流程失败,原因:流程定义ID不能为空或空字符串");
throw new BusinessException("激活或者挂起流程失败,原因:流程定义ID不能为空或空字符串");
}
if (operateType == null) {
log.error("激活或者挂起流程失败,原因:操作类型不能为空");
throw new BusinessException("激活或者挂起流程失败,原因:操作类型不能为空");
}
// 操作类型校验
if (!Objects.equals(SuspensionState.ACTIVE.getCode(), operateType)
&& !Objects.equals(SuspensionState.SUSPENDED.getCode(), operateType)) {
log.error("激活或者挂起流程失败,原因:操作类型错误,非法值为:{}", operateType);
throw new BusinessException("激活或者挂起流程失败,原因:操作类型错误");
}
// 根据操作类型执行相应逻辑
if (Objects.equals(SuspensionState.ACTIVE.getCode(), operateType)) {
log.info("尝试激活流程定义,流程定义ID:{}", processDefinitionId);
/*
* 激活指定的流程定义。
*
* 该方法通过调用 repositoryService 的 activateProcessDefinitionById 方法,
* 激活与给定流程定义 ID 对应的流程定义。支持级联激活相关实例,并可设置激活时间。
*
* 参数说明:
* - operateActReProcdefReq.getProcessDefinitionId(): 流程定义的唯一标识符,用于定位需要激活的流程定义。
* 如果为空,则无法定位目标流程定义,可能导致异常。
* - true: 表示是否级联激活相关流程实例。如果为 true,则所有关联的流程实例也会被激活;
* 如果为 false,则仅激活流程定义本身。
* - null: 表示激活时间。如果为 null,则立即激活;如果提供具体时间,则会在指定时间激活。
*/
repositoryService.activateProcessDefinitionById(processDefinitionId,
true, null);
} else if (Objects.equals(SuspensionState.SUSPENDED.getCode(), operateType)) {
log.info("尝试挂起流程定义,流程定义ID:{}", processDefinitionId);
/*
* 暂停指定的流程定义。
*
* 该方法通过调用 repositoryService 的 suspendProcessDefinitionById 方法,
* 暂停与给定流程定义 ID 对应的流程定义。支持级联暂停相关实例,并可设置暂停时间。
*
* 参数说明:
* - operateActReProcdefReq.getProcessDefinitionId(): 流程定义的唯一标识符,用于定位需要暂停的流程定义。
* 如果为空,则无法定位目标流程定义,可能导致异常。
* - true: 表示是否级联暂停相关流程实例。如果为 true,则所有关联的流程实例也会被暂停;
* 如果为 false,则仅暂停流程定义本身。
* - null: 表示暂停时间。如果为 null,则立即暂停;如果提供具体时间,则会在指定时间暂停。
*/
repositoryService.suspendProcessDefinitionById(processDefinitionId,
true, null);
}
} catch (IllegalArgumentException e) {
if (operateActReProcdefReq != null) {
log.error("激活或者挂起流程失败,原因:非法参数,流程定义ID:{}, 操作类型:{}",
operateActReProcdefReq.getProcessDefinitionId(), operateActReProcdefReq.getOperateType(), e);
}
throw new BusinessException("激活或者挂起流程失败,原因:非法参数", e);
} catch (Exception e) {
if (operateActReProcdefReq != null) {
log.error("激活或者挂起流程失败,原因:未知异常,流程定义ID:{}, 操作类型:{}",
operateActReProcdefReq.getProcessDefinitionId(), operateActReProcdefReq.getOperateType(), e);
}
throw new BusinessException("激活或者挂起流程失败,原因:未知异常", e);
}
return true;
}
⑤ 创建接口
/**
* 操作流程定义(激活/挂起)。
* <p>
* 该方法通过调用服务层的 `operateProcessDefinitionById` 方法,根据请求对象中的信息对流程定义进行操作(如激活或挂起)。
* 如果操作成功,则返回成功的响应结果;如果发生异常,则记录错误日志并返回失败的响应结果。
*
* @param operateActReProcdefReq 流程定义操作请求对象,包含操作所需的参数信息,不能为空。
* 该对象通过 HTTP 请求体传递,需符合 `OperateActReProcdefReq` 的结构定义。
* @return 返回一个 `Result<Boolean>` 对象:
* - 如果操作成功,返回 `Result.success(true)`;
* - 如果操作失败,返回 `Result.error`,并包含失败原因的描述信息。
*/
@PreAuthorize("hasAnyAuthority('/api/v1/actReProcdef/operate')")
@Parameter(name = "operateActReProcdefReq", description = "流程定义操作请求对象", required = true)
@Operation(summary = "操作流程定义(激活/挂起)")
@PostMapping("/operate")
public Result<Boolean> operateProcessDefinitionById(@RequestBody OperateActReProcdefReq operateActReProcdefReq) {
try {
// 调用服务层方法执行流程定义操作,传入请求对象并获取操作结果
Boolean operateResult = actReProcdefService.operateProcessDefinitionById(operateActReProcdefReq);
// 返回操作成功的响应结果
return Result.success(operateResult);
} catch (Exception e) {
// 捕获异常,记录详细的错误日志,并返回包含失败原因的响应结果
log.error("操作流程定义失败 具体原因为 : {}", e.getMessage());
return Result.error("操作流程定义失败,失败原因:" + e.getMessage());
}
}
二、前端:使用API
① 创建参数
与后端接口参数保持一致
// 激活/挂起流程定义请求对象
export interface OperateActReProcdefReq {
processDefinitionId: string // 流程定义ID
operateType: number // 操作类型 1:激活 2:挂起
}
② 创建API
// 激活/挂起流程定义
export function operateActReProcdef(data: OperateActReProcdefReq) {
return request.post<any>({
url: '/pm-process/api/v1/actReProcdef/operate',
data,
})
}
③ 修改 表格数据 区域增加操作列
操作列里增加激活/挂起按钮,按钮的名称需要根据当前流程定义的状态动态修改按钮名称
<!-- 表格数据 区域 -->
<el-table-column
v-for="(column, index) in tableColumns"
:key="index"
:type="column.type"
:label="column.label"
:prop="column.prop"
:align="column.align"
:width="column.width"
>
<!-- 使用单个 template 包裹所有条件 -->
<template #default="scope">
<!-- 翻译 suspensionState 状态 1 为 激活,2 为 挂起 -->
<template v-if="column.label === '定义状态'">
<template v-if="scope.row.suspensionState === 1">
激活
</template>
<template v-else>
挂起
</template>
</template>
<!-- 判断是否是操作列 -->
<div v-if="column.label === '操作'">
<el-button v-hasButton="`btn.actReProcdef.operate`" type="primary" @click="onOperate(scope.row)">
{{ scope.row.suspensionState === 1 ? '挂起' : '激活' }}
</el-button>
</div>
</template>
</el-table-column>
④ 创建按钮方法
/**
* 异步函数:onOperate
*
* 该函数用于对流程定义进行操作(如挂起或激活),并根据操作结果更新页面状态。
*
* @param {ActReProcdefVO} row - 流程定义对象,包含流程定义的详细信息,例如 ID 和挂起状态。
* - `id`: 流程定义的唯一标识符。
* - `suspensionState`: 流程定义的挂起状态,1 表示激活,2 表示挂起。
* @returns {Promise<void>} - 无返回值,函数执行完成后会根据操作结果更新页面状态或显示提示信息。
*/
async function onOperate(row: ActReProcdefVO) {
try {
// 组装操作参数,包括流程定义 ID 和操作类型
const param: OperateActReProcdefReq = {
processDefinitionId: row.id,
operateType: row.suspensionState === 1 ? 2 : 1,
}
// 调用后端接口进行流程定义的操作(挂起或激活)
const result: any = await operateActReProcdef(param)
// 判断操作结果是否成功
if (result.success && result.code === 200) {
// 操作成功时,显示成功提示信息
ElMessage({
message: '操作成功',
type: 'success',
})
// 初始化分页参数,设置当前页为第一页,每页显示 10 条数据
currentPage.value = 1
pageSize.value = 10
// 初始化流程名称为空字符串
processName.value = ''
// 刷新页面数据以反映最新的操作结果
getActReProcdefPageData()
}
}
catch (error) {
// 捕获异常并提取错误信息
let errorMessage = '未知错误'
if (error instanceof Error) {
errorMessage = error.message
}
// 显示操作失败的错误提示信息
ElMessage({
message: `操作失败: ${errorMessage || '未知错误'}`,
type: 'error',
})
}
}
三、配置权限
① 增加按钮
② 分配权限
四、界面操作验证
① 挂起
② 激活
后记
本篇文章的前后端仓库地址请查询专栏第一篇文章,后续打算把xml和流程图片展示出来
本文的后端分支是 process-6
本文的前端分支是 process-8