# scaffold 项目之工作流的流程设计器(bpm)

# 简介

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

# 开始使用

本文,我们将进一步讲解【流程模型】、【流程定义】,特别是如何使用 BPMN 流程设计器。

# 流程模型

流程模型,对应 [工作流程 -> 流程管理 -> 流程模型] 菜单

  • 后端,由 BpmModelController 提供接口
  • 前端,由 /views/bpm/model/index.vue 实现界面

# 表结构

流程设计模型部署表,由 Flowable 提供的 ACT_RE_MODEL 表实现,如下所示:

字段名称字段描述数据类型主键为空取值说明
ID_ID_nvarchar(64)ID_
REV_乐观锁int乐观锁
NAME_名称nvarchar(255)名称
KEY_KEY_nvarchar(255)key
CATEGORY_分类nvarchar(255)分类
CREATE_TIME_创建时间datetime创建时间
LAST_UPDATE_TIME_最新修改时间datetime最新修改时间
VERSION_版本int版本
META_INFO_META_INFO_nvarchar(255)以 json 格式保存流程定义的信息
DEPLOYMENT_ID_部署 IDnvarchar(255)部署 ID
EDITOR_SOURCE_VALUE_ID_datetime
EDITOR_SOURCE_EXTRA_VALUE_ID_datetime

我们可以通过 META_INFO 字段,额外拓展了 icon 图标、 description 描述、 formTypeformIdformCustomCreatePathformCustomViewPath 表单等信息。如下代码所示:

package cn.tzzfj.scaffold.module.bpm.controller.admin.definition.vo.model;
/**
 * <p> Project: scaffold - BpmModelMetaInfoVO  </p>
 *
 * 流程图标
 * 
 * @author Tz
 * @date 2025/10/25 15:26
 * @version 1.0.0
 * @since 1.0.0
 */
@Data
public class BpmModelMetaInfoVO {
    @Schema(description = "流程图标", example = "https://www.tzzfj.cn/scaffold.jpg")
    @URL(message = "流程图标格式不正确")
    private String icon;
    @Schema(description = "流程描述", example = "我是描述")
    private String description;
    @Schema(description = "流程类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @InEnum(BpmModelTypeEnum.class)
    @NotNull(message = "流程类型不能为空")
    private Integer type;
    @Schema(description = "表单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @InEnum(BpmModelFormTypeEnum.class)
    @NotNull(message = "表单类型不能为空")
    private Integer formType;
    @Schema(description = "表单编号", example = "1024")
    private Long formId; //formType 为 NORMAL 使用,必须非空
    @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create")
    private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空
    @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view")
    private String formCustomViewPath; // 表单类型为 CUSTOM 时,必须非空
    @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
    @NotNull(message = "是否可见不能为空")
    private Boolean visible;
    @Schema(description = "可发起用户编号数组", example = "[1,2,3]")
    private List<Long> startUserIds;
    @Schema(description = "可发起部门编号数组", example = "[2,4,6]")
    private List<Long> startDeptIds;
    @Schema(description = "可管理用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2,4,6]")
    @NotEmpty(message = "可管理用户编号数组不能为空")
    private List<Long> managerUserIds;
    @Schema(description = "排序", example = "1")
    private Long sort; // 创建时,后端自动生成
    @Schema(description = "允许撤销审批中的申请", example = "true")
    private Boolean allowCancelRunningProcess;
    @Schema(description = "允许允许审批人撤回任务", example = "false")
    private Boolean allowWithdrawTask;
    @Schema(description = "流程 ID 规则", example = "{}")
    private ProcessIdRule processIdRule;
    @Schema(description = "自动去重类型", example = "1")
    @InEnum(BpmAutoApproveTypeEnum.class)
    private Integer autoApprovalType;
    @Schema(description = "标题设置", example = "{}")
    private TitleSetting titleSetting;
    @Schema(description = "摘要设置", example = "{}")
    private SummarySetting summarySetting;
    @Schema(description = "流程前置通知设置", example = "{}")
    private HttpRequestSetting processBeforeTriggerSetting;
    @Schema(description = "流程后置通知设置", example = "{}")
    private HttpRequestSetting processAfterTriggerSetting;
    @Schema(description = "任务前置通知设置", example = "{}")
    private HttpRequestSetting taskBeforeTriggerSetting;
    @Schema(description = "任务后置通知设置", example = "{}")
    private HttpRequestSetting taskAfterTriggerSetting;
    @Schema(description = "自定义打印模板设置", example = "{}")
    @Valid
    private PrintTemplateSetting printTemplateSetting;
    @Schema(description = "流程 ID 规则")
    @Data
    @Valid
    public static class ProcessIdRule {
        @Schema(description = "是否启用", example = "false")
        @NotNull(message = "是否启用不能为空")
        private Boolean enable;
        @Schema(description = "前缀", example = "XX")
        private String prefix;
        @Schema(description = "中缀", example = "20250120")
        private String infix; // 精确到日、精确到时、精确到分、精确到秒
        @Schema(description = "后缀", example = "YY")
        private String postfix;
        @Schema(description = "序列长度", example = "5")
        @NotNull(message = "序列长度不能为空")
        private Integer length;
    }
    @Schema(description = "标题设置")
    @Data
    @Valid
    public static class TitleSetting {
        @Schema(description = "是否自定义", example = "false")
        @NotNull(message = "是否自定义不能为空")
        private Boolean enable;
        @Schema(description = "标题", example = "流程标题")
        private String title;
    }
    @Schema(description = "摘要设置")
    @Data
    @Valid
    public static class SummarySetting {
        @Schema(description = "是否自定义", example = "false")
        @NotNull(message = "是否自定义不能为空")
        private Boolean enable;
        @Schema(description = "摘要字段数组", example = "[]")
        private List<String> summary;
    }
    @Schema(description = "http 请求通知设置", example = "{}")
    @Data
    public static class HttpRequestSetting {
        @Schema(description = "请求路径", example = "http://127.0.0.1")
        @NotEmpty(message = "请求 URL 不能为空")
        @URL(message = "请求 URL 格式不正确")
        private String url;
        @Schema(description = "请求头参数设置", example = "[]")
        @Valid
        private List<BpmSimpleModelNodeVO.HttpRequestParam> header;
        @Schema(description = "请求头参数设置", example = "[]")
        @Valid
        private List<BpmSimpleModelNodeVO.HttpRequestParam> body;
        /**
         * 请求返回处理设置,用于修改流程表单值
         * <p>
         * key:表示要修改的流程表单字段名 (name)
         * value:接口返回的字段名
         */
        @Schema(description = "请求返回处理设置", example = "[]")
        private List<KeyValue<String, String>> response;
    }
    @Schema(description = "自定义打印模板设置")
    @Data
    public static class PrintTemplateSetting {
        @Schema(description = "是否自定义打印模板", example = "false")
        @NotNull(message = "是否自定义打印模板不能为空")
        private Boolean enable;
        @Schema(description = "打印模板", example = "<p></p>")
        private String template;
    }
}

# 流程设计器

① BPMN 流程设计器,由项目的 [ProcessDesigner.vue] 实现。

它是基于 https://github.com/miyuesc/bpmn-process-designer 拓展,底层是 bpmn-js

补充说明:

bpmn-process-designer 提供 Vue2 + ElementUI、Vue3 + NaiveUI 两个版本,而我们是 Vue3 + ElementPlus,是通过 Vue2 + ElementUI 迁移适配实现。

② BPMN 预览,支持高亮,由 [ProcessViewer.vue] 实现。

它是直接基于 bpmn-js 拓展,没有基于 bpmn-process-designer


下面,我们将详细讲解 BPMN 流程设计器的各个配置项:任务(表单)、任务(审批人)、多实例(会签配置)、执行监听器、任务监听器等等。

# 任务(表单)

# 表单配置

每个任务节点,有个 [表单] 配置项,用于配置任务审批时,补充填写表单信息。

拓展知识:

① 问题:配置的表单,最终是怎么存储的?

回答:在 BPMN 的 UserTask 节点上,有个 formKey 属性,用于存储表单的 key,这里我们就存了【流程表单】的编号。

② 问题:为什么只支持【流程表单】,不支持【业务表单】呢?

回答:【业务表单】暂时没想到比较优雅的二次修改方案,因为它属于业务系统,无法在审批通过时,一起进行提交。

③ 问题:表单设计器,怎么使用远程数据?

回答:参见 https://docs.qq.com/doc/DZlNIVkZSTlVJVEd2 文档。

# 表单效果

在审批任务通过时,需要额外填写表单信息,如下图所示:

填写的表单数据,会存储到 Flowable 任务的 variables

@Override
    @Transactional(rollbackFor = Exception.class)
    public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) {
        // 1.1 校验任务存在
        Task task = validateTask(userId, reqVO.getId());
        // 1.2 校验流程实例存在
        ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_NOT_EXISTS);
        }
        // 1.3 校验签名
        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
        Boolean signEnable = parseSignEnable(bpmnModel, task.getTaskDefinitionKey());
        if (signEnable && StrUtil.isEmpty(reqVO.getSignPicUrl())) {
            throw exception(TASK_SIGNATURE_NOT_EXISTS);
        }
        // 1.4 校验审批意见
        Boolean reasonRequire = parseReasonRequire(bpmnModel, task.getTaskDefinitionKey());
        if (reasonRequire && StrUtil.isEmpty(reqVO.getReason())) {
            throw exception(TASK_REASON_REQUIRE);
        }
        // 情况一:被委派的任务,不调用 complete 去完成任务
        if (DelegationState.PENDING.equals(task.getDelegationState())) {
            approveDelegateTask(reqVO, task);
            return;
        }
        // 情况二:审批有【后】加签的任务
        if (BpmTaskSignTypeEnum.AFTER.getType().equals(task.getScopeType())) {
            approveAfterSignTask(task, reqVO);
            return;
        }
        // 情况三:审批普通的任务。大多数情况下,都是这样
        // 2.1 更新 task 状态、原因、签字
        updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason());
        if (signEnable) {
            taskService.setVariableLocal(task.getId(), BpmnVariableConstants.TASK_SIGN_PIC_URL, reqVO.getSignPicUrl());
        }
        // 2.2 添加评论
        taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
                BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
        // 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空
        // 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。
        // 场景二:A 节点发起,B 节点只有某一个字段可编辑(比如 day),但 C 节点需要多个节点。
        //       (比如 work + day 变量,在发起时填写,因为 B 节点只有 day 的编辑权限,在审批后,variables 会缺少 work 的值)
        Map<String, Object> processVariables = new HashMap<>();
        if (CollUtil.isNotEmpty(instance.getProcessVariables())) { // 获取历史中流程变量
            processVariables.putAll(instance.getProcessVariables());
        }
        if (CollUtil.isNotEmpty(reqVO.getVariables())) { // 合并前端传递的流程变量,以前端为准
            processVariables.putAll(reqVO.getVariables());
        }
        // 4. 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑
        Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), processVariables,
                bpmnModel, reqVO.getNextAssignees(), instance);
        runtimeService.setVariables(task.getProcessInstanceId(), variables);
        // 5. 移除辅助预测的流程变量,这些变量在回退操作中设置
        //todo @jason:可以直接 + 拼接哈
        String simulateVariableName = StrUtil.concat(false,
                BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, task.getTaskDefinitionKey());
        runtimeService.removeVariable(task.getProcessInstanceId(), simulateVariableName);
        // 6. 调用 BPM complete 去完成任务
        taskService.complete(task.getId(), variables, true);
        // 【加签专属】处理加签任务
        handleParentTaskIfSign(task.getParentTaskId());
    }

# 任务(审批人)

详细见 [《选择审批人、发起人自选》] 文档。

# 多实例(会签配置)

详细见 [《会签、或签、依次审批》] 文档。

# 执行监听器

详细见 [《执行监听器、任务监听器》] 文档。

# 任务监听器

详细见 [《执行监听器、任务监听器》] 文档。

# 流程定义

流程模型在部署后,会创建一个新版本的流程定义,并挂起老版本的流程定义。最终,我们点击某个流程模型的「流程定义」按钮,可以看到它对应的流程定义

  • 后端,由 BpmProcessDefinitionController 提供接口
  • 前端,由 [ /views/bpm/definition/index.vue ] 实现界面

# 表结构

① 流程定义表,由 Flowable 提供的 ACT_RE_PROCDEF 表实现,如下所示:

字段类型主键说明备注
ID_NVARCHAR2(64)Y主键
REV_INTEGERN数据版本号
CATEGORY_NVARCHAR2(255)N流程定义分类读取 xml 文件中程的 targetNamespace
NAME_NVARCHAR2(255)N流程定义的名称读取流程文件中 process 元素的 name 属性
KEY_NVARCHAR2(255)N流程定义 key读取流程文件中 process 元素的 id 属性
VERSION_INTEGERN版本
DEPLOYMENT_ID_NVARCHAR2(64)N部署 ID流程定义对应的部署数据 ID
RESOURCE_NAME_NVARCHAR2(2000)Nbpmn 文件名称一般为流程文件的相对路径
DGRM_RESOURCE_NAME_VARCHAR2(4000)N流程定义对应的流程图资源名称
DESCRIPTION_NVARCHAR2(2000)N说明
HAS_START_FORM_KEY_NUMBER(1)N是否存在开始节点 formKeystart 节点是否存在 formKey :0 - 否,1 - 是
HAS_GRAPHICAL_NOTATION_NUMBER(1)N
SUSPENSION_STATE_INTEGERN流程定义状态1 - 激活、2 中止
TENANT_ID_NVARCHAR2(255)N
ENGINE_VERSION_NVARCHAR2(255)N引擎版本

② 由于 ACT_RE_PROCDEF 表没有类似 ACT_RE_MODELMETA_INFO_ 字段,所以我们额外创建了一个 BPM 流程定义的信息表,用于存储流程定义的额外信息。如下所示:

省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段

CREATE TABLE `bpm_process_definition_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `process_definition_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程定义的编号',
  `model_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流程模型的编号',
  `icon` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
  `form_type` tinyint NOT NULL COMMENT '表单类型',
  `form_id` bigint DEFAULT NULL COMMENT '表单编号',
  `form_conf` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '表单的配置',
  `form_fields` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '表单项的数组',
  `form_custom_create_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '自定义表单的提交路径',
  `form_custom_view_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '自定义表单的查看路径',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=246 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BPM 流程定义的信息表';

本质上,就是把 ACT_RE_MODELMETA_INFO_ 字段存储到 bpm_process_definition_info 表中。

因此,最终每次流程模型在部署时,会往 Flowable 插入一条 ACT_RE_PROCDEF 记录,也会往 bpm_process_definition_info 表中插入一条记录。

# 流程定义列表(可发起流程)

注意!一个流程模型,有且仅有一个【激活】状态的流程定义。最终,用户发起流程时,选择的是【激活】状态的流程定义。