# scaffold 项目之工作流的流程发起、取消、重新发起

# 简介

可以看 scaffold 项目之工作流 文章

# 开始使用

  • 流程发起:开始一个流程任务
  • 取消:可以取消或终止在走的流程任务
  • 重新发起:对于完成的流程或者取消的流程可以在次重新发起(是一个全新的流程)

功能在:[审批中心菜单] -> [我的流程] -> [发起流程] 下

# 发起流程

# 表结构

先了解对应的表结构

① 流程实例表,由 Flowable 提供的 ACT_RU_EXECUTION 表实现,如下所示:

字段类型主键说明备注
ID_NVARCHAR2(64)Y主键
REV_INTEGERN数据版本
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_INTEGERN流程终端状态
CACHED_ENT_STATE_INTEGERN
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_INTEGERN
TASK_COUNT_INTEGERN
JOB_COUNT_INTEGERN
TIMER_JOB_COUNT_INTEGERN
SUSP_JOB_COUNT_INTEGERN
DEADLETTER_JOB_COUNT_INTEGERN
VAR_COUNT_INTEGERN
ID_LINK_COUNT_INTEGERN

② 流程参数表,由 Flowable 提供的 ACT_RU_VARIABLE 表实现,如下所示:

字段类型主键说明备注
ID_NVARCHAR2(64)Y主键
REV_INTEGERN数据版本
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 值)
stringPROCESS_STATUSee01d8d3-9b72-11ef-b087-0242ac1100041

# 流程状态

流程状态,由 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_INTEGERN数据版本
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);

# 重新发起流程

获取已经结束流程的历史信息作为基础信息【重新发起流程】

和发起流程类似,只是初始加多了旧流程的基础信息