# 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_ | 部署 ID | nvarchar(255) | √ | 部署 ID | |
| EDITOR_SOURCE_VALUE_ID_ | datetime | √ | |||
| EDITOR_SOURCE_EXTRA_VALUE_ID_ | datetime | √ |
我们可以通过 META_INFO 字段,额外拓展了 icon 图标、 description 描述、 formType 、 formId 、 formCustomCreatePath 、 formCustomViewPath 表单等信息。如下代码所示:
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_ | INTEGER | N | 数据版本号 | |
| CATEGORY_ | NVARCHAR2(255) | N | 流程定义分类 | 读取 xml 文件中程的 targetNamespace 值 |
| NAME_ | NVARCHAR2(255) | N | 流程定义的名称 | 读取流程文件中 process 元素的 name 属性 |
| KEY_ | NVARCHAR2(255) | N | 流程定义 key | 读取流程文件中 process 元素的 id 属性 |
| VERSION_ | INTEGER | N | 版本 | |
| DEPLOYMENT_ID_ | NVARCHAR2(64) | N | 部署 ID | 流程定义对应的部署数据 ID |
| RESOURCE_NAME_ | NVARCHAR2(2000) | N | bpmn 文件名称 | 一般为流程文件的相对路径 |
| DGRM_RESOURCE_NAME_ | VARCHAR2(4000) | N | 流程定义对应的流程图资源名称 | |
| DESCRIPTION_ | NVARCHAR2(2000) | N | 说明 | |
| HAS_START_FORM_KEY_ | NUMBER(1) | N | 是否存在开始节点 formKey | start 节点是否存在 formKey :0 - 否,1 - 是 |
| HAS_GRAPHICAL_NOTATION_ | NUMBER(1) | N | ||
| SUSPENSION_STATE_ | INTEGER | N | 流程定义状态 | 1 - 激活、2 中止 |
| TENANT_ID_ | NVARCHAR2(255) | N | ||
| ENGINE_VERSION_ | NVARCHAR2(255) | N | 引擎版本 |
② 由于 ACT_RE_PROCDEF 表没有类似 ACT_RE_MODEL 有 META_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_MODEL 的 META_INFO_ 字段存储到 bpm_process_definition_info 表中。
因此,最终每次流程模型在部署时,会往 Flowable 插入一条 ACT_RE_PROCDEF 记录,也会往 bpm_process_definition_info 表中插入一条记录。
# 流程定义列表(可发起流程)
注意!一个流程模型,有且仅有一个【激活】状态的流程定义。最终,用户发起流程时,选择的是【激活】状态的流程定义。