从 0 到 1 构建企业级 Java 审批流程引擎:架构设计与实战落地
本文介绍了如何从零开发一个企业级审批流程引擎。这种引擎通过将流程逻辑与业务代码解耦,实现流程配置化和可视化,能够显著提升系统的灵活性和可维护性。文章首先阐述了审批流程引擎的核心概念和架构设计,包括流程定义、实例、节点、连线等基本元素。随后详细展示了数据库设计和核心实体类实现,并基于MyBatis-Plus实现了基础CRUD功能。在核心实现部分,重点讲解了流程启动与任务生成、任务处理与流程推进等关键
引言:为什么我们需要审批流程引擎?
在企业级应用开发中,审批流程无处不在:员工请假需要审批,费用报销需要审批,合同签订需要审批,项目立项也需要审批。这些流程往往涉及多部门、多角色协作,规则复杂且经常变动。
如果每个审批场景都从零开发,不仅会造成大量重复劳动,更会导致系统维护成本激增。想象一下:当公司的请假审批规则从 "部门经理审批" 改为 "部门经理 + HR 审批" 时,你需要修改多少段代码?当报销审批的金额阈值调整时,又需要重新测试多少功能点?
审批流程引擎正是为解决这些问题而生。它将流程逻辑与业务代码解耦,通过配置化、可视化的方式定义和管理审批流程,使业务人员也能参与流程设计,大幅提升系统的灵活性和可维护性。
本文将带你深入理解审批流程引擎的核心原理,并用 Java 实现一个企业级的审批流程引擎,包含流程定义、实例运行、任务分配、规则引擎等关键组件。无论你是想从零构建引擎,还是想更好地使用 Flowable、Activiti 等开源框架,本文都能为你提供扎实的理论基础和实践指导。
一、审批流程引擎核心概念与架构设计
1.1 核心概念解析
在深入代码实现之前,我们需要先理解审批流程引擎的核心概念,这些概念大多源自 BPMN 2.0(Business Process Model and Notation)规范,是流程引擎的理论基础。
-
流程定义(Process Definition)
- 流程的静态描述,相当于 "模板"
- 包含节点、连线、规则等元素
- 通常用 XML 或 JSON 格式存储
-
流程实例(Process Instance)
- 流程定义的一次运行实例
- 对应一个具体的审批单(如 "张三的请假申请")
- 包含当前运行状态、变量等信息
-
流程节点(Flow Node)
- 流程中的步骤,如 "部门经理审批"、"HR 审批"
- 主要类型:开始节点、结束节点、审批节点、网关节点、分支节点等
-
连线(Sequence Flow)
- 连接两个节点的线段,表示流程的走向
- 可以包含条件表达式,决定流程分支
-
审批任务(Task)
- 流程实例运行到审批节点时产生的待办任务
- 包含执行人、处理状态、截止时间等信息
-
流程变量(Process Variable)
- 存储流程运行过程中的动态数据
- 如 "请假天数"、"报销金额" 等,用于条件判断
-
参与者(Participant)
- 参与流程的用户或角色
- 如 "部门经理"、"财务主管"、"张三" 等

1.2 审批流程引擎架构设计
一个完整的审批流程引擎应包含以下核心模块:
- 流程定义模块:负责流程模板的解析、存储和管理
- 流程运行时模块:处理流程实例的创建、推进和终止
- 任务管理模块:管理审批任务的创建、分配、处理和查询
- 规则引擎模块:解析和执行流程中的条件判断和分配规则
- 历史记录模块:记录流程和任务的运行历史,支持审计和追溯
- API 接口层:提供对外的 RESTful API,供前端调用

1.3 核心业务流程
审批流程引擎的典型工作流程如下:
- 管理员通过可视化编辑器定义审批流程(如请假流程:申请人提交→部门经理审批→HR 审批→结束)
- 系统将流程定义存储到数据库中
- 用户发起一个具体的审批(如张三申请请假 3 天),系统创建流程实例
- 引擎根据流程定义,生成第一个审批任务(如部门经理审批),并分配给相应人员
- 审批人处理任务(通过或拒绝)
- 引擎根据处理结果和流程规则,决定下一步走向:生成新任务或结束流程
- 重复步骤 4-6,直到流程结束
- 全程记录流程和任务的历史数据,支持查询和审计

二、数据库设计:存储流程的骨架
审批流程引擎的数据库设计是整个系统的基础,需要合理存储流程定义、实例、任务等核心数据。以下是基于 MySQL 8.0 的数据库设计。
2.1 核心表结构设计
流程定义相关表
-- 流程定义表:存储流程模板的基本信息
CREATE TABLE `proc_definition` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`proc_key` varchar(64) NOT NULL COMMENT '流程标识(唯一)',
`name` varchar(128) NOT NULL COMMENT '流程名称',
`description` varchar(512) DEFAULT NULL COMMENT '流程描述',
`version` int NOT NULL COMMENT '流程版本号',
`category` varchar(64) DEFAULT NULL COMMENT '流程分类',
`resource_xml` longtext NOT NULL COMMENT '流程定义XML',
`deploy_time` datetime NOT NULL COMMENT '部署时间',
`deploy_user_id` bigint NOT NULL COMMENT '部署人ID',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
`tenant_id` varchar(32) DEFAULT '0' COMMENT '租户ID(多租户支持)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_proc_key_version` (`proc_key`,`version`,`tenant_id`),
KEY `idx_deploy_time` (`deploy_time`),
KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程定义表';
-- 流程节点表:存储流程中的节点信息
CREATE TABLE `proc_node` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`def_id` bigint NOT NULL COMMENT '流程定义ID(关联proc_definition.id)',
`node_id` varchar(64) NOT NULL COMMENT '节点标识(流程内唯一)',
`node_name` varchar(128) NOT NULL COMMENT '节点名称',
`node_type` tinyint NOT NULL COMMENT '节点类型:1-开始节点,2-结束节点,3-审批节点,4-分支节点,5-合并节点',
`assignee_type` tinyint DEFAULT NULL COMMENT '审批人类型:1-指定用户,2-指定角色,3-表达式',
`assignee_value` varchar(512) DEFAULT NULL COMMENT '审批人值:用户ID、角色ID或表达式',
`form_key` varchar(128) DEFAULT NULL COMMENT '关联表单标识',
`is_skip` tinyint DEFAULT '0' COMMENT '是否可跳过:0-否,1-是',
`is_multi_instance` tinyint DEFAULT '0' COMMENT '是否多实例:0-否,1-是',
`multi_type` tinyint DEFAULT NULL COMMENT '多实例类型:1-并行,2-串行',
`condition_expression` varchar(1024) DEFAULT NULL COMMENT '条件表达式(用于分支节点)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_def_node_id` (`def_id`,`node_id`),
KEY `idx_node_type` (`node_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程节点表';
-- 流程连线表:存储节点之间的连线信息
CREATE TABLE `proc_sequence` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`def_id` bigint NOT NULL COMMENT '流程定义ID(关联proc_definition.id)',
`source_node_id` varchar(64) NOT NULL COMMENT '源节点标识',
`target_node_id` varchar(64) NOT NULL COMMENT '目标节点标识',
`condition_expression` varchar(1024) DEFAULT NULL COMMENT '连线条件表达式',
`priority` int DEFAULT '1' COMMENT '优先级(条件相同情况下)',
PRIMARY KEY (`id`),
KEY `idx_def_source` (`def_id`,`source_node_id`),
KEY `idx_def_target` (`def_id`,`target_node_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程连线表';
流程运行时相关表
-- 流程实例表:存储流程运行实例
CREATE TABLE `proc_instance` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识(唯一)',
`proc_key` varchar(64) NOT NULL COMMENT '流程标识',
`def_id` bigint NOT NULL COMMENT '流程定义ID',
`business_key` varchar(64) DEFAULT NULL COMMENT '业务关联键(如订单ID)',
`title` varchar(256) NOT NULL COMMENT '流程实例标题',
`start_user_id` bigint NOT NULL COMMENT '发起人ID',
`start_time` datetime NOT NULL COMMENT '发起时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`status` tinyint NOT NULL COMMENT '状态:1-运行中,2-已完成,3-已终止,4-已暂停',
`current_node_ids` varchar(512) DEFAULT NULL COMMENT '当前活动节点ID(多个用逗号分隔)',
`tenant_id` varchar(32) DEFAULT '0' COMMENT '租户ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_proc_inst_id` (`proc_inst_id`),
KEY `idx_proc_key` (`proc_key`),
KEY `idx_business_key` (`business_key`),
KEY `idx_start_user` (`start_user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程实例表';
-- 流程变量表:存储流程实例的变量
CREATE TABLE `proc_variable` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识',
`var_name` varchar(64) NOT NULL COMMENT '变量名称',
`var_type` varchar(32) NOT NULL COMMENT '变量类型:string, integer, long, boolean, date等',
`var_value` text COMMENT '变量值(序列化存储)',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_inst_var_name` (`proc_inst_id`,`var_name`),
KEY `idx_var_name` (`var_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程变量表';
-- 任务表:存储审批任务
CREATE TABLE `proc_task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`task_id` varchar(64) NOT NULL COMMENT '任务标识(唯一)',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识',
`def_id` bigint NOT NULL COMMENT '流程定义ID',
`node_id` varchar(64) NOT NULL COMMENT '节点标识',
`node_name` varchar(128) NOT NULL COMMENT '节点名称',
`assignee_id` bigint DEFAULT NULL COMMENT '处理人ID',
`assignee_name` varchar(64) DEFAULT NULL COMMENT '处理人姓名',
`owner_id` bigint DEFAULT NULL COMMENT '拥有人ID(用于委托)',
`title` varchar(256) NOT NULL COMMENT '任务标题',
`status` tinyint NOT NULL COMMENT '状态:1-待处理,2-处理中,3-已完成,4-已取消',
`priority` tinyint DEFAULT '3' COMMENT '优先级:1-低,2-中,3-高,4-紧急',
`create_time` datetime NOT NULL COMMENT '创建时间',
`claim_time` datetime DEFAULT NULL COMMENT '签收时间',
`end_time` datetime DEFAULT NULL COMMENT '完成时间',
`due_date` datetime DEFAULT NULL COMMENT '截止时间',
`form_key` varchar(128) DEFAULT NULL COMMENT '关联表单标识',
`tenant_id` varchar(32) DEFAULT '0' COMMENT '租户ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_id` (`task_id`),
KEY `idx_proc_inst_id` (`proc_inst_id`),
KEY `idx_assignee_id` (`assignee_id`,`status`),
KEY `idx_node_id` (`node_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_due_date` (`due_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务表';
-- 任务参与者表:存储任务的多个参与者(如会签)
CREATE TABLE `proc_task_participant` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`task_id` varchar(64) NOT NULL COMMENT '任务标识',
`user_id` bigint NOT NULL COMMENT '用户ID',
`user_name` varchar(64) NOT NULL COMMENT '用户姓名',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-待处理,2-已处理',
`opinion` varchar(1024) DEFAULT NULL COMMENT '审批意见',
`handle_time` datetime DEFAULT NULL COMMENT '处理时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_user` (`task_id`,`user_id`),
KEY `idx_user_id` (`user_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务参与者表';
-- 任务操作记录表:存储任务的处理记录
CREATE TABLE `proc_task_operation` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`task_id` varchar(64) NOT NULL COMMENT '任务标识',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识',
`user_id` bigint NOT NULL COMMENT '操作人ID',
`user_name` varchar(64) NOT NULL COMMENT '操作人姓名',
`operation` tinyint NOT NULL COMMENT '操作类型:1-创建,2-签收,3-提交,4-退回,5-取消,6-委托',
`opinion` varchar(1024) DEFAULT NULL COMMENT '操作意见',
`operation_time` datetime NOT NULL COMMENT '操作时间',
`extension_data` text COMMENT '扩展数据(JSON格式)',
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`),
KEY `idx_proc_inst_id` (`proc_inst_id`),
KEY `idx_operation_time` (`operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务操作记录表';
历史记录相关表
-- 流程实例历史表
CREATE TABLE `hist_proc_instance` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识',
`proc_key` varchar(64) NOT NULL COMMENT '流程标识',
`def_id` bigint NOT NULL COMMENT '流程定义ID',
`business_key` varchar(64) DEFAULT NULL COMMENT '业务关联键',
`title` varchar(256) NOT NULL COMMENT '流程实例标题',
`start_user_id` bigint NOT NULL COMMENT '发起人ID',
`start_user_name` varchar(64) NOT NULL COMMENT '发起人姓名',
`start_time` datetime NOT NULL COMMENT '发起时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`duration` bigint NOT NULL COMMENT '持续时间(毫秒)',
`end_status` tinyint NOT NULL COMMENT '结束状态:1-正常完成,2-被终止,3-被驳回',
`delete_reason` varchar(256) DEFAULT NULL COMMENT '删除原因',
`tenant_id` varchar(32) DEFAULT '0' COMMENT '租户ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_proc_inst_id` (`proc_inst_id`),
KEY `idx_proc_key` (`proc_key`),
KEY `idx_business_key` (`business_key`),
KEY `idx_start_user` (`start_user_id`),
KEY `idx_start_end_time` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程实例历史表';
-- 任务历史表
CREATE TABLE `hist_task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`task_id` varchar(64) NOT NULL COMMENT '任务标识',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识',
`def_id` bigint NOT NULL COMMENT '流程定义ID',
`node_id` varchar(64) NOT NULL COMMENT '节点标识',
`node_name` varchar(128) NOT NULL COMMENT '节点名称',
`assignee_id` bigint DEFAULT NULL COMMENT '处理人ID',
`assignee_name` varchar(64) DEFAULT NULL COMMENT '处理人姓名',
`title` varchar(256) NOT NULL COMMENT '任务标题',
`priority` tinyint DEFAULT '3' COMMENT '优先级',
`create_time` datetime NOT NULL COMMENT '创建时间',
`claim_time` datetime DEFAULT NULL COMMENT '签收时间',
`end_time` datetime NOT NULL COMMENT '完成时间',
`duration` bigint NOT NULL COMMENT '持续时间(毫秒)',
`due_date` datetime DEFAULT NULL COMMENT '截止时间',
`delete_reason` varchar(256) DEFAULT NULL COMMENT '删除原因',
`tenant_id` varchar(32) DEFAULT '0' COMMENT '租户ID',
PRIMARY KEY (`id`),
KEY `idx_proc_inst_id` (`proc_inst_id`),
KEY `idx_assignee_id` (`assignee_id`),
KEY `idx_node_id` (`node_id`),
KEY `idx_create_end_time` (`create_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务历史表';
-- 流程变量历史表
CREATE TABLE `hist_proc_variable` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`proc_inst_id` varchar(64) NOT NULL COMMENT '流程实例标识',
`var_name` varchar(64) NOT NULL COMMENT '变量名称',
`var_type` varchar(32) NOT NULL COMMENT '变量类型',
`var_value` text COMMENT '变量值',
`operation_time` datetime NOT NULL COMMENT '操作时间',
`operation_type` tinyint NOT NULL COMMENT '操作类型:1-创建,2-更新,3-删除',
PRIMARY KEY (`id`),
KEY `idx_proc_inst_id` (`proc_inst_id`),
KEY `idx_var_name` (`var_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程变量历史表';
2.2 表关系说明
各表之间的关系如下:
- 一个流程定义(proc_definition)包含多个节点(proc_node)和多条连线(proc_sequence)
- 一个流程定义可以有多个版本,通过 proc_key 和 version 区分
- 一个流程实例(proc_instance)关联一个流程定义
- 一个流程实例可以有多个任务(proc_task)
- 一个任务可以有多个参与者(proc_task_participant)
- 一个任务有多个操作记录(proc_task_operation)
- 流程实例、任务、变量都有对应的历史表,用于存储已完成的数据
三、核心实体类设计:映射业务概念
基于上述数据库设计,我们来实现对应的 Java 实体类。这些类将作为整个引擎的核心数据载体。
3.1 流程定义相关实体
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 流程定义实体类
* 对应数据库表:proc_definition
* @author ken
*/
@Data
@TableName("proc_definition")
@Schema(description = "流程定义实体")
public class ProcDefinition {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程标识(唯一)", required = true)
private String procKey;
@Schema(description = "流程名称", required = true)
private String name;
@Schema(description = "流程描述")
private String description;
@Schema(description = "流程版本号", required = true)
private Integer version;
@Schema(description = "流程分类")
private String category;
@Schema(description = "流程定义XML", required = true)
private String resourceXml;
@Schema(description = "部署时间", required = true)
private LocalDateTime deployTime;
@Schema(description = "部署人ID", required = true)
private Long deployUserId;
@Schema(description = "状态:0-禁用,1-启用", required = true)
private Integer status;
@Schema(description = "租户ID(多租户支持)", defaultValue = "0")
private String tenantId;
}
/**
* 流程节点实体类
* 对应数据库表:proc_node
* @author ken
*/
@Data
@TableName("proc_node")
@Schema(description = "流程节点实体")
public class ProcNode {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程定义ID", required = true)
private Long defId;
@Schema(description = "节点标识(流程内唯一)", required = true)
private String nodeId;
@Schema(description = "节点名称", required = true)
private String nodeName;
@Schema(description = "节点类型:1-开始节点,2-结束节点,3-审批节点,4-分支节点,5-合并节点",
required = true)
private Integer nodeType;
@Schema(description = "审批人类型:1-指定用户,2-指定角色,3-表达式")
private Integer assigneeType;
@Schema(description = "审批人值:用户ID、角色ID或表达式")
private String assigneeValue;
@Schema(description = "关联表单标识")
private String formKey;
@Schema(description = "是否可跳过:0-否,1-是", defaultValue = "0")
private Integer isSkip;
@Schema(description = "是否多实例:0-否,1-是", defaultValue = "0")
private Integer isMultiInstance;
@Schema(description = "多实例类型:1-并行,2-串行")
private Integer multiType;
@Schema(description = "条件表达式(用于分支节点)")
private String conditionExpression;
}
/**
* 流程连线实体类
* 对应数据库表:proc_sequence
* @author ken
*/
@Data
@TableName("proc_sequence")
@Schema(description = "流程连线实体")
public class ProcSequence {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程定义ID", required = true)
private Long defId;
@Schema(description = "源节点标识", required = true)
private String sourceNodeId;
@Schema(description = "目标节点标识", required = true)
private String targetNodeId;
@Schema(description = "连线条件表达式")
private String conditionExpression;
@Schema(description = "优先级(条件相同情况下)", defaultValue = "1")
private Integer priority;
}
3.2 流程运行时相关实体
/**
* 流程实例实体类
* 对应数据库表:proc_instance
* @author ken
*/
@Data
@TableName("proc_instance")
@Schema(description = "流程实例实体")
public class ProcInstance {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程实例标识(唯一)", required = true)
private String procInstId;
@Schema(description = "流程标识", required = true)
private String procKey;
@Schema(description = "流程定义ID", required = true)
private Long defId;
@Schema(description = "业务关联键(如订单ID)")
private String businessKey;
@Schema(description = "流程实例标题", required = true)
private String title;
@Schema(description = "发起人ID", required = true)
private Long startUserId;
@Schema(description = "发起时间", required = true)
private LocalDateTime startTime;
@Schema(description = "结束时间")
private LocalDateTime endTime;
@Schema(description = "状态:1-运行中,2-已完成,3-已终止,4-已暂停", required = true)
private Integer status;
@Schema(description = "当前活动节点ID(多个用逗号分隔)")
private String currentNodeIds;
@Schema(description = "租户ID", defaultValue = "0")
private String tenantId;
}
/**
* 流程变量实体类
* 对应数据库表:proc_variable
* @author ken
*/
@Data
@TableName("proc_variable")
@Schema(description = "流程变量实体")
public class ProcVariable {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程实例标识", required = true)
private String procInstId;
@Schema(description = "变量名称", required = true)
private String varName;
@Schema(description = "变量类型:string, integer, long, boolean, date等", required = true)
private String varType;
@Schema(description = "变量值(序列化存储)")
private String varValue;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
@Schema(description = "更新时间", required = true)
private LocalDateTime updateTime;
}
/**
* 任务实体类
* 对应数据库表:proc_task
* @author ken
*/
@Data
@TableName("proc_task")
@Schema(description = "任务实体")
public class ProcTask {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "任务标识(唯一)", required = true)
private String taskId;
@Schema(description = "流程实例标识", required = true)
private String procInstId;
@Schema(description = "流程定义ID", required = true)
private Long defId;
@Schema(description = "节点标识", required = true)
private String nodeId;
@Schema(description = "节点名称", required = true)
private String nodeName;
@Schema(description = "处理人ID")
private Long assigneeId;
@Schema(description = "处理人姓名")
private String assigneeName;
@Schema(description = "拥有人ID(用于委托)")
private Long ownerId;
@Schema(description = "任务标题", required = true)
private String title;
@Schema(description = "状态:1-待处理,2-处理中,3-已完成,4-已取消", required = true)
private Integer status;
@Schema(description = "优先级:1-低,2-中,3-高,4-紧急", defaultValue = "3")
private Integer priority;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
@Schema(description = "签收时间")
private LocalDateTime claimTime;
@Schema(description = "完成时间")
private LocalDateTime endTime;
@Schema(description = "截止时间")
private LocalDateTime dueDate;
@Schema(description = "关联表单标识")
private String formKey;
@Schema(description = "租户ID", defaultValue = "0")
private String tenantId;
}
/**
* 任务参与者实体类
* 对应数据库表:proc_task_participant
* @author ken
*/
@Data
@TableName("proc_task_participant")
@Schema(description = "任务参与者实体")
public class ProcTaskParticipant {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "任务标识", required = true)
private String taskId;
@Schema(description = "用户ID", required = true)
private Long userId;
@Schema(description = "用户姓名", required = true)
private String userName;
@Schema(description = "状态:1-待处理,2-已处理", required = true, defaultValue = "1")
private Integer status;
@Schema(description = "审批意见")
private String opinion;
@Schema(description = "处理时间")
private LocalDateTime handleTime;
}
/**
* 任务操作记录实体类
* 对应数据库表:proc_task_operation
* @author ken
*/
@Data
@TableName("proc_task_operation")
@Schema(description = "任务操作记录实体")
public class ProcTaskOperation {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "任务标识", required = true)
private String taskId;
@Schema(description = "流程实例标识", required = true)
private String procInstId;
@Schema(description = "操作人ID", required = true)
private Long userId;
@Schema(description = "操作人姓名", required = true)
private String userName;
@Schema(description = "操作类型:1-创建,2-签收,3-提交,4-退回,5-取消,6-委托", required = true)
private Integer operation;
@Schema(description = "操作意见")
private String opinion;
@Schema(description = "操作时间", required = true)
private LocalDateTime operationTime;
@Schema(description = "扩展数据(JSON格式)")
private String extensionData;
}
3.3 历史记录相关实体
/**
* 流程实例历史实体类
* 对应数据库表:hist_proc_instance
* @author ken
*/
@Data
@TableName("hist_proc_instance")
@Schema(description = "流程实例历史实体")
public class HistProcInstance {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程实例标识", required = true)
private String procInstId;
@Schema(description = "流程标识", required = true)
private String procKey;
@Schema(description = "流程定义ID", required = true)
private Long defId;
@Schema(description = "业务关联键")
private String businessKey;
@Schema(description = "流程实例标题", required = true)
private String title;
@Schema(description = "发起人ID", required = true)
private Long startUserId;
@Schema(description = "发起人姓名", required = true)
private String startUserName;
@Schema(description = "发起时间", required = true)
private LocalDateTime startTime;
@Schema(description = "结束时间", required = true)
private LocalDateTime endTime;
@Schema(description = "持续时间(毫秒)", required = true)
private Long duration;
@Schema(description = "结束状态:1-正常完成,2-被终止,3-被驳回", required = true)
private Integer endStatus;
@Schema(description = "删除原因")
private String deleteReason;
@Schema(description = "租户ID", defaultValue = "0")
private String tenantId;
}
/**
* 任务历史实体类
* 对应数据库表:hist_task
* @author ken
*/
@Data
@TableName("hist_task")
@Schema(description = "任务历史实体")
public class HistTask {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "任务标识", required = true)
private String taskId;
@Schema(description = "流程实例标识", required = true)
private String procInstId;
@Schema(description = "流程定义ID", required = true)
private Long defId;
@Schema(description = "节点标识", required = true)
private String nodeId;
@Schema(description = "节点名称", required = true)
private String nodeName;
@Schema(description = "处理人ID")
private Long assigneeId;
@Schema(description = "处理人姓名")
private String assigneeName;
@Schema(description = "任务标题", required = true)
private String title;
@Schema(description = "优先级", defaultValue = "3")
private Integer priority;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
@Schema(description = "签收时间")
private LocalDateTime claimTime;
@Schema(description = "完成时间", required = true)
private LocalDateTime endTime;
@Schema(description = "持续时间(毫秒)", required = true)
private Long duration;
@Schema(description = "截止时间")
private LocalDateTime dueDate;
@Schema(description = "删除原因")
private String deleteReason;
@Schema(description = "租户ID", defaultValue = "0")
private String tenantId;
}
/**
* 流程变量历史实体类
* 对应数据库表:hist_proc_variable
* @author ken
*/
@Data
@TableName("hist_proc_variable")
@Schema(description = "流程变量历史实体")
public class HistProcVariable {
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "流程实例标识", required = true)
private String procInstId;
@Schema(description = "变量名称", required = true)
private String varName;
@Schema(description = "变量类型", required = true)
private String varType;
@Schema(description = "变量值")
private String varValue;
@Schema(description = "操作时间", required = true)
private LocalDateTime operationTime;
@Schema(description = "操作类型:1-创建,2-更新,3-删除", required = true)
private Integer operationType;
}
四、MyBatis-Plus 映射与基础 CRUD
使用 MyBatis-Plus 实现数据库访问层,简化 CRUD 操作。我们以流程定义和流程实例为例,实现对应的 Mapper 接口。
4.1 依赖配置
首先,在 pom.xml 中添加必要的依赖:
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.45</version>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
4.2 Mapper 接口实现
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
/**
* 流程定义Mapper接口
* @author ken
*/
public interface ProcDefinitionMapper extends BaseMapper<ProcDefinition> {
/**
* 根据流程标识查询最新版本的流程定义
* @param procKey 流程标识
* @param tenantId 租户ID
* @return 最新版本的流程定义
*/
ProcDefinition selectLatestVersionByKey(
@Param("procKey") String procKey,
@Param("tenantId") String tenantId);
/**
* 分页查询流程定义
* @param page 分页参数
* @param procKey 流程标识(模糊查询)
* @param name 流程名称(模糊查询)
* @param category 流程分类
* @param status 状态
* @param tenantId 租户ID
* @return 分页结果
*/
IPage<ProcDefinition> selectPage(
Page<ProcDefinition> page,
@Param("procKey") String procKey,
@Param("name") String name,
@Param("category") String category,
@Param("status") Integer status,
@Param("tenantId") String tenantId);
}
/**
* 流程节点Mapper接口
* @author ken
*/
public interface ProcNodeMapper extends BaseMapper<ProcNode> {
/**
* 根据流程定义ID查询所有节点
* @param defId 流程定义ID
* @return 节点列表
*/
List<ProcNode> selectByDefId(@Param("defId") Long defId);
/**
* 根据流程定义ID和节点ID查询节点
* @param defId 流程定义ID
* @param nodeId 节点ID
* @return 节点信息
*/
ProcNode selectByDefIdAndNodeId(
@Param("defId") Long defId,
@Param("nodeId") String nodeId);
}
/**
* 流程连线Mapper接口
* @author ken
*/
public interface ProcSequenceMapper extends BaseMapper<ProcSequence> {
/**
* 根据流程定义ID和源节点ID查询连线
* @param defId 流程定义ID
* @param sourceNodeId 源节点ID
* @return 连线列表
*/
List<ProcSequence> selectByDefIdAndSourceNodeId(
@Param("defId") Long defId,
@Param("sourceNodeId") String sourceNodeId);
}
/**
* 流程实例Mapper接口
* @author ken
*/
public interface ProcInstanceMapper extends BaseMapper<ProcInstance> {
/**
* 根据业务键查询流程实例
* @param businessKey 业务键
* @param procKey 流程标识
* @param tenantId 租户ID
* @return 流程实例
*/
ProcInstance selectByBusinessKey(
@Param("businessKey") String businessKey,
@Param("procKey") String procKey,
@Param("tenantId") String tenantId);
/**
* 分页查询流程实例
* @param page 分页参数
* @param procKey 流程标识
* @param status 状态
* @param startUserId 发起人ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param tenantId 租户ID
* @return 分页结果
*/
IPage<ProcInstance> selectPage(
Page<ProcInstance> page,
@Param("procKey") String procKey,
@Param("status") Integer status,
@Param("startUserId") Long startUserId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("tenantId") String tenantId);
}
/**
* 任务Mapper接口
* @author ken
*/
public interface ProcTaskMapper extends BaseMapper<ProcTask> {
/**
* 根据流程实例ID查询活跃任务
* @param procInstId 流程实例ID
* @param status 任务状态
* @return 任务列表
*/
List<ProcTask> selectActiveByProcInstId(
@Param("procInstId") String procInstId,
@Param("status") Integer status);
/**
* 查询用户的待办任务
* @param page 分页参数
* @param userId 用户ID
* @param procKey 流程标识
* @param status 任务状态
* @param tenantId 租户ID
* @return 分页结果
*/
IPage<ProcTask> selectTodoTasks(
Page<ProcTask> page,
@Param("userId") Long userId,
@Param("procKey") String procKey,
@Param("status") Integer status,
@Param("tenantId") String tenantId);
}
4.3 Service 层实现
基于上述 Mapper 接口,实现 Service 层:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 流程定义服务实现类
* @author ken
*/
@Slf4j
@Service
public class ProcDefinitionServiceImpl
extends ServiceImpl<ProcDefinitionMapper, ProcDefinition>
implements ProcDefinitionService {
@Autowired
private ProcNodeMapper procNodeMapper;
@Autowired
private ProcSequenceMapper procSequenceMapper;
/**
* 部署流程定义
* @param procDefinition 流程定义
* @param nodes 节点列表
* @param sequences 连线列表
* @return 部署的流程定义
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ProcDefinition deploy(
ProcDefinition procDefinition,
List<ProcNode> nodes,
List<ProcSequence> sequences) {
// 参数校验
StringUtils.hasText(procDefinition.getProcKey(), "流程标识不能为空");
StringUtils.hasText(procDefinition.getName(), "流程名称不能为空");
StringUtils.hasText(procDefinition.getResourceXml(), "流程定义XML不能为空");
// 确定版本号:查询当前最大版本号+1
ProcDefinition latest = baseMapper.selectLatestVersionByKey(
procDefinition.getProcKey(), procDefinition.getTenantId());
int newVersion = latest != null ? latest.getVersion() + 1 : 1;
procDefinition.setVersion(newVersion);
// 设置部署时间和部署人
procDefinition.setDeployTime(LocalDateTime.now());
// 保存流程定义
baseMapper.insert(procDefinition);
log.info("部署流程定义成功,procKey: {}, version: {}",
procDefinition.getProcKey(), procDefinition.getVersion());
// 保存节点
if (!CollectionUtils.isEmpty(nodes)) {
for (ProcNode node : nodes) {
node.setDefId(procDefinition.getId());
procNodeMapper.insert(node);
}
log.info("保存流程节点成功,数量: {}", nodes.size());
}
// 保存连线
if (!CollectionUtils.isEmpty(sequences)) {
for (ProcSequence sequence : sequences) {
sequence.setDefId(procDefinition.getId());
procSequenceMapper.insert(sequence);
}
log.info("保存流程连线成功,数量: {}", sequences.size());
}
return procDefinition;
}
/**
* 根据流程标识查询最新版本的流程定义
* @param procKey 流程标识
* @param tenantId 租户ID
* @return 最新版本的流程定义
*/
@Override
public ProcDefinition getLatestByKey(String procKey, String tenantId) {
StringUtils.hasText(procKey, "流程标识不能为空");
String actualTenantId = StringUtils.hasText(tenantId) ? tenantId : "0";
return baseMapper.selectLatestVersionByKey(procKey, actualTenantId);
}
/**
* 查询流程定义的所有节点
* @param defId 流程定义ID
* @return 节点列表
*/
@Override
public List<ProcNode> getNodesByDefId(Long defId) {
if (ObjectUtils.isEmpty(defId)) {
return Lists.newArrayList();
}
return procNodeMapper.selectByDefId(defId);
}
/**
* 查询流程定义的所有连线
* @param defId 流程定义ID
* @return 连线列表
*/
@Override
public List<ProcSequence> getSequencesByDefId(Long defId) {
if (ObjectUtils.isEmpty(defId)) {
return Lists.newArrayList();
}
// 这里简化处理,实际应查询所有源节点的连线
// 更完善的实现应该是查询该流程定义的所有连线
LambdaQueryWrapper<ProcSequence> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ProcSequence::getDefId, defId);
return procSequenceMapper.selectList(queryWrapper);
}
/**
* 分页查询流程定义
* @param pageNum 页码
* @param pageSize 每页条数
* @param procKey 流程标识(模糊查询)
* @param name 流程名称(模糊查询)
* @param category 流程分类
* @param status 状态
* @param tenantId 租户ID
* @return 分页结果
*/
@Override
public IPage<ProcDefinition> queryPage(
int pageNum, int pageSize,
String procKey, String name,
String category, Integer status,
String tenantId) {
Page<ProcDefinition> page = new Page<>(pageNum, pageSize);
return baseMapper.selectPage(
page, procKey, name, category, status,
StringUtils.hasText(tenantId) ? tenantId : "0");
}
}
/**
* 流程实例服务实现类
* @author ken
*/
@Slf4j
@Service
public class ProcInstanceServiceImpl
extends ServiceImpl<ProcInstanceMapper, ProcInstance>
implements ProcInstanceService {
@Autowired
private ProcDefinitionService procDefinitionService;
@Autowired
private ProcVariableMapper procVariableMapper;
@Autowired
private IdGenerator idGenerator;
/**
* 创建流程实例
* @param createDTO 流程实例创建参数
* @return 创建的流程实例
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ProcInstance create(ProcInstanceCreateDTO createDTO) {
// 参数校验
StringUtils.hasText(createDTO.getProcKey(), "流程标识不能为空");
ObjectUtils.isEmpty(createDTO.getStartUserId(), "发起人ID不能为空");
StringUtils.hasText(createDTO.getTitle(), "流程标题不能为空");
// 获取最新版本的流程定义
ProcDefinition procDefinition = procDefinitionService.getLatestByKey(
createDTO.getProcKey(), createDTO.getTenantId());
if (ObjectUtils.isEmpty(procDefinition)) {
throw new BusinessException("流程定义不存在,procKey: " + createDTO.getProcKey());
}
if (procDefinition.getStatus() != 1) {
throw new BusinessException("流程定义未启用,procKey: " + createDTO.getProcKey());
}
// 创建流程实例
ProcInstance procInstance = new ProcInstance();
procInstance.setProcInstId(idGenerator.generateId()); // 生成唯一实例ID
procInstance.setProcKey(procDefinition.getProcKey());
procInstance.setDefId(procDefinition.getId());
procInstance.setBusinessKey(createDTO.getBusinessKey());
procInstance.setTitle(createDTO.getTitle());
procInstance.setStartUserId(createDTO.getStartUserId());
procInstance.setStartTime(LocalDateTime.now());
procInstance.setStatus(1); // 1-运行中
procInstance.setTenantId(StringUtils.hasText(createDTO.getTenantId()) ?
createDTO.getTenantId() : "0");
// 保存流程实例
baseMapper.insert(procInstance);
log.info("创建流程实例成功,procInstId: {}, procKey: {}",
procInstance.getProcInstId(), procInstance.getProcKey());
// 保存流程变量
if (!CollectionUtils.isEmpty(createDTO.getVariables())) {
LocalDateTime now = LocalDateTime.now();
for (ProcVariable variable : createDTO.getVariables()) {
variable.setProcInstId(procInstance.getProcInstId());
variable.setCreateTime(now);
variable.setUpdateTime(now);
procVariableMapper.insert(variable);
}
log.info("保存流程变量成功,数量: {}", createDTO.getVariables().size());
}
return procInstance;
}
/**
* 根据流程实例ID查询流程实例
* @param procInstId 流程实例ID
* @return 流程实例
*/
@Override
public ProcInstance getByInstId(String procInstId) {
StringUtils.hasText(procInstId, "流程实例ID不能为空");
LambdaQueryWrapper<ProcInstance> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ProcInstance::getProcInstId, procInstId);
return baseMapper.selectOne(queryWrapper);
}
/**
* 根据业务键查询流程实例
* @param businessKey 业务键
* @param procKey 流程标识
* @param tenantId 租户ID
* @return 流程实例
*/
@Override
public ProcInstance getByBusinessKey(String businessKey, String procKey, String tenantId) {
StringUtils.hasText(businessKey, "业务键不能为空");
StringUtils.hasText(procKey, "流程标识不能为空");
return baseMapper.selectByBusinessKey(
businessKey, procKey,
StringUtils.hasText(tenantId) ? tenantId : "0");
}
/**
* 更新流程实例状态
* @param procInstId 流程实例ID
* @param status 新状态
* @param endTime 结束时间(状态为结束时必填)
* @return 是否更新成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStatus(
String procInstId, int status, LocalDateTime endTime) {
StringUtils.hasText(procInstId, "流程实例ID不能为空");
ProcInstance procInstance = new ProcInstance();
procInstance.setStatus(status);
// 如果是结束状态,设置结束时间
if (status == 2 || status == 3) { // 2-已完成,3-已终止
procInstance.setEndTime(endTime != null ? endTime : LocalDateTime.now());
}
LambdaQueryWrapper<ProcInstance> updateWrapper = new LambdaQueryWrapper<>();
updateWrapper.eq(ProcInstance::getProcInstId, procInstId);
int rows = baseMapper.update(procInstance, updateWrapper);
log.info("更新流程实例状态,procInstId: {}, status: {}, 影响行数: {}",
procInstId, status, rows);
return rows > 0;
}
/**
* 分页查询流程实例
* @param pageNum 页码
* @param pageSize 每页条数
* @param queryDTO 查询参数
* @return 分页结果
*/
@Override
public IPage<ProcInstance> queryPage(int pageNum, int pageSize, ProcInstanceQueryDTO queryDTO) {
Page<ProcInstance> page = new Page<>(pageNum, pageSize);
return baseMapper.selectPage(
page,
queryDTO.getProcKey(),
queryDTO.getStatus(),
queryDTO.getStartUserId(),
queryDTO.getStartTime(),
queryDTO.getEndTime(),
StringUtils.hasText(queryDTO.getTenantId()) ? queryDTO.getTenantId() : "0");
}
}
五、流程引擎核心实现:驱动流程运转
流程引擎的核心功能是驱动流程实例按照流程定义运行,包括生成任务、处理任务、推进流程等。我们来实现这部分核心逻辑。
5.1 流程引擎接口定义
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 流程引擎API接口
* 提供流程实例创建、任务处理等核心功能
* @author ken
*/
@Tag(name = "流程引擎接口", description = "提供流程实例创建、任务处理等核心功能")
@RequestMapping("/api/v1/process-engine")
public interface ProcessEngineApi {
@Operation(summary = "发起流程实例")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(schema = @Schema(implementation = ProcInstance.class))),
@ApiResponse(responseCode = "400", description = "参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/instances")
ProcInstance startProcessInstance(
@Parameter(description = "流程实例创建参数", required = true)
@RequestBody ProcInstanceCreateDTO createDTO);
@Operation(summary = "获取流程实例详情")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(schema = @Schema(implementation = ProcInstance.class))),
@ApiResponse(responseCode = "404", description = "流程实例不存在")
})
@GetMapping("/instances/{procInstId}")
ProcInstance getProcessInstance(
@Parameter(description = "流程实例ID", required = true)
@PathVariable String procInstId);
@Operation(summary = "获取任务详情")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(schema = @Schema(implementation = ProcTask.class))),
@ApiResponse(responseCode = "404", description = "任务不存在")
})
@GetMapping("/tasks/{taskId}")
ProcTask getTask(
@Parameter(description = "任务ID", required = true)
@PathVariable String taskId);
@Operation(summary = "签收任务")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功"),
@ApiResponse(responseCode = "400", description = "参数错误或任务已被签收"),
@ApiResponse(responseCode = "404", description = "任务不存在")
})
@PostMapping("/tasks/{taskId}/claim")
void claimTask(
@Parameter(description = "任务ID", required = true)
@PathVariable String taskId,
@Parameter(description = "签收人ID", required = true)
@RequestParam Long userId,
@Parameter(description = "签收人姓名", required = true)
@RequestParam String userName);
@Operation(summary = "完成任务")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功"),
@ApiResponse(responseCode = "400", description = "参数错误或任务状态不正确"),
@ApiResponse(responseCode = "404", description = "任务不存在")
})
@PostMapping("/tasks/{taskId}/complete")
void completeTask(
@Parameter(description = "任务ID", required = true)
@PathVariable String taskId,
@Parameter(description = "任务完成参数", required = true)
@RequestBody TaskCompleteDTO completeDTO);
@Operation(summary = "获取用户待办任务")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(schema = @Schema(implementation = ProcTask.class)))
})
@GetMapping("/tasks/todo")
IPage<ProcTask> getTodoTasks(
@Parameter(description = "用户ID", required = true)
@RequestParam Long userId,
@Parameter(description = "流程标识")
@RequestParam(required = false) String procKey,
@Parameter(description = "页码", defaultValue = "1")
@RequestParam(defaultValue = "1") int pageNum,
@Parameter(description = "每页条数", defaultValue = "10")
@RequestParam(defaultValue = "10") int pageSize);
@Operation(summary = "获取流程实例的历史记录")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(schema = @Schema(implementation = HistProcInstance.class)))
})
@GetMapping("/instances/{procInstId}/history")
HistProcInstance getProcessInstanceHistory(
@Parameter(description = "流程实例ID", required = true)
@PathVariable String procInstId);
}
5.2 流程启动与任务生成
流程启动是流程引擎的第一个关键步骤,需要根据流程定义创建流程实例,并生成第一个任务。
/**
* 流程引擎服务实现类
* 实现流程实例创建、任务处理等核心功能
* @author ken
*/
@Slf4j
@Service
public class ProcessEngineServiceImpl implements ProcessEngineService {
@Autowired
private ProcDefinitionService procDefinitionService;
@Autowired
private ProcInstanceService procInstanceService;
@Autowired
private ProcTaskService procTaskService;
@Autowired
private ProcTaskOperationService taskOperationService;
@Autowired
private RuleEngine ruleEngine;
@Autowired
private IdGenerator idGenerator;
/**
* 启动流程实例
* @param createDTO 流程实例创建参数
* @return 流程实例
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ProcInstance startProcessInstance(ProcInstanceCreateDTO createDTO) {
// 1. 创建流程实例
ProcInstance procInstance = procInstanceService.create(createDTO);
Long defId = procInstance.getDefId();
// 2. 获取流程定义的节点信息
List<ProcNode> nodes = procDefinitionService.getNodesByDefId(defId);
if (CollectionUtils.isEmpty(nodes)) {
throw new BusinessException("流程定义没有节点信息,defId: " + defId);
}
// 3. 找到开始节点
ProcNode startNode = nodes.stream()
.filter(node -> node.getNodeType() == 1) // 1-开始节点
.findFirst()
.orElseThrow(() -> new BusinessException("流程定义没有开始节点,defId: " + defId));
// 4. 从开始节点推进流程
this推进流程(procInstance, startNode, null);
return procInstance;
}
/**
* 推进流程
* @param procInstance 流程实例
* @param currentNode 当前节点
* @param completeDTO 任务完成参数(为null时表示从开始节点启动)
*/
private void 推进流程(
ProcInstance procInstance,
ProcNode currentNode,
TaskCompleteDTO completeDTO) {
log.info("推进流程,procInstId: {}, currentNodeId: {}",
procInstance.getProcInstId(), currentNode.getNodeId());
// 1. 如果当前节点是结束节点,流程结束
if (currentNode.getNodeType() == 2) { // 2-结束节点
this.endProcessInstance(procInstance, 1); // 1-正常完成
return;
}
// 2. 获取当前节点的所有出口连线
List<ProcSequence> sequences = procDefinitionService.getSequencesByDefId(
procInstance.getDefId()).stream()
.filter(seq -> seq.getSourceNodeId().equals(currentNode.getNodeId()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(sequences)) {
// 如果没有出口连线,检查是否是结束节点
if (currentNode.getNodeType() != 2) {
throw new BusinessException("节点没有出口连线,nodeId: " + currentNode.getNodeId());
}
return;
}
// 3. 计算需要流转的目标节点
List<String> targetNodeIds = this.calculateTargetNodes(
procInstance, currentNode, sequences, completeDTO);
if (CollectionUtils.isEmpty(targetNodeIds)) {
throw new BusinessException("未找到符合条件的目标节点,nodeId: " + currentNode.getNodeId());
}
// 4. 更新流程实例的当前节点
this.updateProcessInstanceCurrentNodes(procInstance, targetNodeIds);
// 5. 为每个目标节点创建任务
for (String targetNodeId : targetNodeIds) {
ProcNode targetNode = procDefinitionService.getNodesByDefId(procInstance.getDefId()).stream()
.filter(node -> node.getNodeId().equals(targetNodeId))
.findFirst()
.orElseThrow(() -> new BusinessException("目标节点不存在,nodeId: " + targetNodeId));
// 创建任务
this.createTask(procInstance, targetNode);
}
}
/**
* 计算目标节点
* @param procInstance 流程实例
* @param currentNode 当前节点
* @param sequences 出口连线
* @param completeDTO 任务完成参数
* @return 目标节点ID列表
*/
private List<String> calculateTargetNodes(
ProcInstance procInstance,
ProcNode currentNode,
List<ProcSequence> sequences,
TaskCompleteDTO completeDTO) {
// 获取流程变量
Map<String, Object> variables = this.getProcessVariables(procInstance.getProcInstId());
// 如果有任务完成参数,添加到变量中
if (completeDTO != null && !CollectionUtils.isEmpty(completeDTO.getVariables())) {
variables.putAll(completeDTO.getVariables());
}
// 分支节点需要计算条件
if (currentNode.getNodeType() == 4) { // 4-分支节点
List<String> targetNodeIds = Lists.newArrayList();
for (ProcSequence sequence : sequences) {
// 如果没有条件表达式,默认通过
if (!StringUtils.hasText(sequence.getConditionExpression())) {
targetNodeIds.add(sequence.getTargetNodeId());
continue;
}
// 执行条件表达式
boolean conditionMet = ruleEngine.evaluate(
sequence.getConditionExpression(), variables);
if (conditionMet) {
targetNodeIds.add(sequence.getTargetNodeId());
}
}
return targetNodeIds;
} else {
// 非分支节点,默认取第一条连线
return Lists.newArrayList(sequences.get(0).getTargetNodeId());
}
}
/**
* 创建任务
* @param procInstance 流程实例
* @param targetNode 目标节点
* @return 创建的任务
*/
private ProcTask createTask(ProcInstance procInstance, ProcNode targetNode) {
// 审批节点才需要创建任务
if (targetNode.getNodeType() != 3) { // 3-审批节点
log.info("跳过非审批节点的任务创建,nodeId: {}", targetNode.getNodeId());
// 对于非审批节点,直接推进流程
this推进流程(procInstance, targetNode, null);
return null;
}
// 生成任务ID
String taskId = idGenerator.generateId();
// 创建任务对象
ProcTask task = new ProcTask();
task.setTaskId(taskId);
task.setProcInstId(procInstance.getProcInstId());
task.setDefId(procInstance.getDefId());
task.setNodeId(targetNode.getNodeId());
task.setNodeName(targetNode.getNodeName());
task.setTitle(procInstance.getTitle() + "-" + targetNode.getNodeName());
task.setStatus(1); // 1-待处理
task.setPriority(3); // 默认优先级:高
task.setCreateTime(LocalDateTime.now());
task.setFormKey(targetNode.getFormKey());
task.setTenantId(procInstance.getTenantId());
// 计算任务处理人
List<Long> assigneeIds = this.calculateAssignees(
procInstance, targetNode, taskId);
// 保存任务
procTaskService.save(task);
log.info("创建任务成功,taskId: {}, nodeId: {}", taskId, targetNode.getNodeId());
// 记录任务操作
taskOperationService.recordOperation(
taskId, procInstance.getProcInstId(),
null, null, 1, // 1-创建
"系统自动创建任务", null);
// 如果是多实例节点,创建多个参与者
if (targetNode.getIsMultiInstance() == 1) {
procTaskService.createParticipants(taskId, assigneeIds);
} else if (!CollectionUtils.isEmpty(assigneeIds)) {
// 单实例节点,设置处理人
Long assigneeId = assigneeIds.get(0);
String assigneeName = this.getUserNameById(assigneeId);
task.setAssigneeId(assigneeId);
task.setAssigneeName(assigneeName);
procTaskService.updateById(task);
}
return task;
}
/**
* 计算任务处理人
* @param procInstance 流程实例
* @param node 节点信息
* @param taskId 任务ID
* @return 处理人ID列表
*/
private List<Long> calculateAssignees(
ProcInstance procInstance, ProcNode node, String taskId) {
// 获取流程变量
Map<String, Object> variables = this.getProcessVariables(procInstance.getProcInstId());
// 添加流程实例信息到变量中
variables.put("procInstId", procInstance.getProcInstId());
variables.put("startUserId", procInstance.getStartUserId());
variables.put("taskId", taskId);
// 根据节点的处理人类型计算处理人
if (node.getAssigneeType() == 1) { // 1-指定用户
// 格式:1001,1002,1003
String[] userIdStrs = node.getAssigneeValue().split(",");
return Arrays.stream(userIdStrs)
.map(Long::valueOf)
.collect(Collectors.toList());
} else if (node.getAssigneeType() == 2) { // 2-指定角色
// 格式:role1,role2
String[] roleIds = node.getAssigneeValue().split(",");
return userService.getUserIdsByRoles(roleIds);
} else if (node.getAssigneeType() == 3) { // 3-表达式
// 表达式示例:${managerService.getManagerByUserId(startUserId)}
Object result = ruleEngine.execute(node.getAssigneeValue(), variables);
if (result instanceof List) {
return (List<Long>) result;
} else if (result instanceof Long) {
return Lists.newArrayList((Long) result);
} else {
throw new BusinessException("表达式返回类型不支持,nodeId: " + node.getNodeId());
}
} else {
throw new BusinessException("未设置处理人类型,nodeId: " + node.getNodeId());
}
}
/**
* 更新流程实例的当前节点
* @param procInstance 流程实例
* @param nodeIds 节点ID列表
*/
private void updateProcessInstanceCurrentNodes(ProcInstance procInstance, List<String> nodeIds) {
String currentNodeIds = String.join(",", nodeIds);
ProcInstance updateInstance = new ProcInstance();
updateInstance.setId(procInstance.getId());
updateInstance.setCurrentNodeIds(currentNodeIds);
procInstanceService.updateById(updateInstance);
log.info("更新流程实例当前节点,procInstId: {}, currentNodeIds: {}",
procInstance.getProcInstId(), currentNodeIds);
// 更新内存中的当前节点信息
procInstance.setCurrentNodeIds(currentNodeIds);
}
/**
* 结束流程实例
* @param procInstance 流程实例
* @param endStatus 结束状态:1-正常完成,2-被终止,3-被驳回
*/
private void endProcessInstance(ProcInstance procInstance, int endStatus) {
LocalDateTime endTime = LocalDateTime.now();
// 更新流程实例状态
procInstanceService.updateStatus(
procInstance.getProcInstId(), 2, endTime); // 2-已完成
// 记录流程历史
histProcInstanceService.createHistInstance(procInstance, endStatus, endTime);
log.info("流程实例结束,procInstId: {}, endStatus: {}",
procInstance.getProcInstId(), endStatus);
}
// 其他方法实现...
}
5.3 任务处理与流程推进
任务处理是流程引擎的另一个核心功能,包括任务签收、完成、退回等操作。
/**
* 任务处理相关实现
* @author ken
*/
@Slf4j
@Service
public class TaskServiceImpl implements TaskService {
@Autowired
private ProcTaskMapper procTaskMapper;
@Autowired
private ProcInstanceService procInstanceService;
@Autowired
private ProcDefinitionService procDefinitionService;
@Autowired
private ProcTaskParticipantMapper taskParticipantMapper;
@Autowired
private ProcessEngineService processEngineService;
/**
* 签收任务
* @param taskId 任务ID
* @param userId 用户ID
* @param userName 用户名
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void claimTask(String taskId, Long userId, String userName) {
// 参数校验
StringUtils.hasText(taskId, "任务ID不能为空");
ObjectUtils.isEmpty(userId, "用户ID不能为空");
StringUtils.hasText(userName, "用户名不能为空");
// 查询任务
ProcTask task = this.getById(taskId);
if (ObjectUtils.isEmpty(task)) {
throw new BusinessException("任务不存在,taskId: " + taskId);
}
// 检查任务状态
if (task.getStatus() != 1) { // 1-待处理
throw new BusinessException("任务状态不正确,无法签收,taskId: " + taskId + ", status: " + task.getStatus());
}
// 检查是否已经有处理人
if (!ObjectUtils.isEmpty(task.getAssigneeId())) {
if (task.getAssigneeId().equals(userId)) {
log.info("用户已经签收过该任务,taskId: {}, userId: {}", taskId, userId);
return;
} else {
throw new BusinessException("任务已被其他人签收,taskId: " + taskId);
}
}
// 更新任务
task.setAssigneeId(userId);
task.setAssigneeName(userName);
task.setStatus(2); // 2-处理中
task.setClaimTime(LocalDateTime.now());
procTaskMapper.updateById(task);
// 记录操作
taskOperationService.recordOperation(
taskId, task.getProcInstId(),
userId, userName, 2, // 2-签收
"签收任务", null);
log.info("任务签收成功,taskId: {}, userId: {}", taskId, userId);
}
/**
* 完成任务
* @param taskId 任务ID
* @param completeDTO 任务完成参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void completeTask(String taskId, TaskCompleteDTO completeDTO) {
// 参数校验
StringUtils.hasText(taskId, "任务ID不能为空");
ObjectUtils.isEmpty(completeDTO, "任务完成参数不能为空");
ObjectUtils.isEmpty(completeDTO.getUserId(), "操作人ID不能为空");
// 查询任务
ProcTask task = this.getById(taskId);
if (ObjectUtils.isEmpty(task)) {
throw new BusinessException("任务不存在,taskId: " + taskId);
}
// 检查任务状态
if (task.getStatus() == 3 || task.getStatus() == 4) { // 3-已完成,4-已取消
throw new BusinessException("任务已处理,无法重复操作,taskId: " + taskId);
}
// 检查处理权限
if (!ObjectUtils.isEmpty(task.getAssigneeId()) &&
!task.getAssigneeId().equals(completeDTO.getUserId())) {
throw new BusinessException("没有权限处理该任务,taskId: " + taskId);
}
// 处理多实例任务
if (this.handleMultiInstanceTask(task, completeDTO)) {
return;
}
// 更新任务状态为已完成
LocalDateTime now = LocalDateTime.now();
task.setStatus(3); // 3-已完成
task.setEndTime(now);
procTaskMapper.updateById(task);
// 记录任务操作
taskOperationService.recordOperation(
taskId, task.getProcInstId(),
completeDTO.getUserId(), completeDTO.getUserName(),
3, // 3-提交
completeDTO.getOpinion(),
completeDTO.getVariables() != null ?
JSON.toJSONString(completeDTO.getVariables()) : null);
// 记录任务历史
histTaskService.createHistTask(task, now);
log.info("任务完成,taskId: {}, procInstId: {}", taskId, task.getProcInstId());
// 获取当前节点信息
ProcNode currentNode = procDefinitionService.getNodesByDefId(task.getDefId()).stream()
.filter(node -> node.getNodeId().equals(task.getNodeId()))
.findFirst()
.orElseThrow(() -> new BusinessException("节点不存在,nodeId: " + task.getNodeId()));
// 获取流程实例
ProcInstance procInstance = procInstanceService.getByInstId(task.getProcInstId());
if (ObjectUtils.isEmpty(procInstance)) {
throw new BusinessException("流程实例不存在,procInstId: " + task.getProcInstId());
}
// 推进流程
processEngineService.推进流程(procInstance, currentNode, completeDTO);
}
/**
* 处理多实例任务
* @param task 任务
* @param completeDTO 任务完成参数
* @return 是否为多实例且未全部完成
*/
private boolean handleMultiInstanceTask(ProcTask task, TaskCompleteDTO completeDTO) {
// 查询该任务的所有参与者
LambdaQueryWrapper<ProcTaskParticipant> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ProcTaskParticipant::getTaskId, task.getTaskId());
List<ProcTaskParticipant> participants = taskParticipantMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(participants)) {
return false;
}
// 更新当前参与者的状态
ProcTaskParticipant currentParticipant = participants.stream()
.filter(p -> p.getUserId().equals(completeDTO.getUserId()))
.findFirst()
.orElseThrow(() -> new BusinessException("不是任务参与者,无法处理任务"));
currentParticipant.setStatus(2); // 2-已处理
currentParticipant.setOpinion(completeDTO.getOpinion());
currentParticipant.setHandleTime(LocalDateTime.now());
taskParticipantMapper.updateById(currentParticipant);
// 记录任务操作
taskOperationService.recordOperation(
task.getTaskId(), task.getProcInstId(),
completeDTO.getUserId(), completeDTO.getUserName(),
3, // 3-提交
completeDTO.getOpinion(), null);
// 检查是否所有参与者都已处理
boolean allCompleted = participants.stream()
.allMatch(p -> p.getStatus() == 2);
if (!allCompleted) {
// 还有未完成的参与者,任务继续
log.info("多实例任务部分完成,等待其他参与者处理,taskId: {}", task.getTaskId());
return true;
}
// 所有参与者都已处理,完成整个任务
LocalDateTime now = LocalDateTime.now();
task.setStatus(3); // 3-已完成
task.setEndTime(now);
procTaskMapper.updateById(task);
// 记录任务历史
histTaskService.createHistTask(task, now);
log.info("多实例任务全部完成,taskId: {}", task.getTaskId());
return false;
}
/**
* 获取用户待办任务
* @param userId 用户ID
* @param procKey 流程标识
* @param pageNum 页码
* @param pageSize 每页条数
* @return 分页结果
*/
@Override
public IPage<ProcTask> getTodoTasks(
Long userId, String procKey, int pageNum, int pageSize) {
ObjectUtils.isEmpty(userId, "用户ID不能为空");
return procTaskMapper.selectTodoTasks(
new Page<>(pageNum, pageSize),
userId, procKey, 1, // 1-待处理
"0"); // 默认租户
}
// 其他方法实现...
}
5.4 规则引擎:处理流程中的条件判断
规则引擎用于解析和执行流程中的条件表达式和分配规则,是实现流程灵活性的关键组件。
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.util.Map;
/**
* 规则引擎实现类
* 用于解析和执行流程中的条件表达式和分配规则
* @author ken
*/
@Slf4j
@Component
public class RuleEngine {
private static final ScriptEngine ENGINE = new ScriptEngineManager().getEngineByName("nashorn");
/**
* 评估条件表达式
* @param expression 条件表达式,如:${amount > 1000 && department == 'IT'}
* @param variables 变量映射
* @return 表达式结果(true/false)
*/
public boolean evaluate(String expression, Map<String, Object> variables) {
if (!StringUtils.hasText(expression)) {
return true;
}
// 处理表达式格式,去掉${}
String actualExpression = expression.trim();
if (actualExpression.startsWith("${") && actualExpression.endsWith("}")) {
actualExpression = actualExpression.substring(2, actualExpression.length() - 1).trim();
}
try {
// 设置变量
Bindings bindings = ENGINE.createBindings();
if (variables != null) {
bindings.putAll(variables);
}
// 执行表达式
Object result = ENGINE.eval(actualExpression, bindings);
// 转换为boolean
if (result instanceof Boolean) {
return (Boolean) result;
} else {
log.warn("条件表达式返回非布尔值,expression: {}, result: {}", expression, result);
return false;
}
} catch (ScriptException e) {
log.error("条件表达式执行错误,expression: {}", expression, e);
throw new BusinessException("条件表达式执行错误: " + e.getMessage());
}
}
/**
* 执行表达式并返回结果
* @param expression 表达式,如:${userService.getManager(userId)}
* @param variables 变量映射
* @return 表达式执行结果
*/
public Object execute(String expression, Map<String, Object> variables) {
if (!StringUtils.hasText(expression)) {
return null;
}
// 处理表达式格式,去掉${}
String actualExpression = expression.trim();
if (actualExpression.startsWith("${") && actualExpression.endsWith("}")) {
actualExpression = actualExpression.substring(2, actualExpression.length() - 1).trim();
}
try {
// 设置变量
Bindings bindings = ENGINE.createBindings();
if (variables != null) {
bindings.putAll(variables);
}
// 添加常用服务
bindings.put("jsonUtils", new JsonUtils());
bindings.put("dateUtils", new DateUtils());
// 执行表达式
return ENGINE.eval(actualExpression, bindings);
} catch (ScriptException e) {
log.error("表达式执行错误,expression: {}", expression, e);
throw new BusinessException("表达式执行错误: " + e.getMessage());
}
}
/**
* JSON工具类,提供给表达式使用
*/
public static class JsonUtils {
public String toJson(Object obj) {
return JSON.toJSONString(obj);
}
public <T> T parseJson(String json, Class<T> clazz) {
return JSON.parseObject(json, clazz);
}
}
/**
* 日期工具类,提供给表达式使用
*/
public static class DateUtils {
public long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
}
}
六、实战案例:实现一个请假审批流程
现在,我们通过一个实际案例来演示如何使用上面实现的审批流程引擎。我们将创建一个请假审批流程,包含以下节点:
- 员工提交请假申请(开始节点)
- 部门经理审批(如果请假天数≤3 天,审批通过后流程结束)
- HR 审批(如果请假天数 > 3 天,部门经理审批通过后需要 HR 审批)
- 流程结束
6.1 流程定义与部署
首先,我们需要定义并部署这个请假流程:
/**
* 请假流程部署示例
* @author ken
*/
@Slf4j
@Component
public class LeaveProcessDeployer implements CommandLineRunner {
@Autowired
private ProcDefinitionService procDefinitionService;
@Override
public void run(String... args) throws Exception {
// 检查流程是否已部署
ProcDefinition existing = procDefinitionService.getLatestByKey("leave-process", "0");
if (existing != null) {
log.info("请假流程已部署,无需重复部署");
return;
}
// 创建流程定义
ProcDefinition procDefinition = new ProcDefinition();
procDefinition.setProcKey("leave-process");
procDefinition.setName("请假审批流程");
procDefinition.setDescription("员工请假审批流程,3天及以下由部门经理审批,3天以上需HR审批");
procDefinition.setDeployUserId(1L); // 系统管理员
procDefinition.setStatus(1); // 启用
procDefinition.setTenantId("0");
// 模拟XML内容
procDefinition.setResourceXml("<process id=\"leave-process\" name=\"请假审批流程\"><!-- XML内容省略 --></process>");
// 定义节点
List<ProcNode> nodes = Lists.newArrayList();
// 1. 开始节点
ProcNode startNode = new ProcNode();
startNode.setNodeId("start");
startNode.setNodeName("开始");
startNode.setNodeType(1); // 1-开始节点
nodes.add(startNode);
// 2. 部门经理审批节点
ProcNode deptManagerNode = new ProcNode();
deptManagerNode.setNodeId("dept-manager-approval");
deptManagerNode.setNodeName("部门经理审批");
deptManagerNode.setNodeType(3); // 3-审批节点
deptManagerNode.setAssigneeType(3); // 3-表达式
deptManagerNode.setAssigneeValue("${userService.getDepartmentManager(startUserId)}"); // 动态获取部门经理
deptManagerNode.setFormKey("leave-approval-form");
nodes.add(deptManagerNode);
// 3. 分支节点(判断请假天数)
ProcNode gatewayNode = new ProcNode();
gatewayNode.setNodeId("duration-gateway");
gatewayNode.setNodeName("请假天数判断");
gatewayNode.setNodeType(4); // 4-分支节点
nodes.add(gatewayNode);
// 4. HR审批节点
ProcNode hrNode = new ProcNode();
hrNode.setNodeId("hr-approval");
hrNode.setNodeName("HR审批");
hrNode.setNodeType(3); // 3-审批节点
hrNode.setAssigneeType(2); // 2-指定角色
hrNode.setAssigneeValue("HR_ROLE"); // HR角色
hrNode.setFormKey("leave-approval-form");
nodes.add(hrNode);
// 5. 结束节点
ProcNode endNode = new ProcNode();
endNode.setNodeId("end");
endNode.setNodeName("结束");
endNode.setNodeType(2); // 2-结束节点
nodes.add(endNode);
// 定义连线
List<ProcSequence> sequences = Lists.newArrayList();
// 开始节点 -> 部门经理审批
ProcSequence seq1 = new ProcSequence();
seq1.setSourceNodeId("start");
seq1.setTargetNodeId("dept-manager-approval");
sequences.add(seq1);
// 部门经理审批 -> 分支节点
ProcSequence seq2 = new ProcSequence();
seq2.setSourceNodeId("dept-manager-approval");
seq2.setTargetNodeId("duration-gateway");
sequences.add(seq2);
// 分支节点 -> 结束节点(请假天数≤3天)
ProcSequence seq3 = new ProcSequence();
seq3.setSourceNodeId("duration-gateway");
seq3.setTargetNodeId("end");
seq3.setConditionExpression("${leaveDays <= 3}");
sequences.add(seq3);
// 分支节点 -> HR审批(请假天数>3天)
ProcSequence seq4 = new ProcSequence();
seq4.setSourceNodeId("duration-gateway");
seq4.setTargetNodeId("hr-approval");
seq4.setConditionExpression("${leaveDays > 3}");
sequences.add(seq4);
// HR审批 -> 结束节点
ProcSequence seq5 = new ProcSequence();
seq5.setSourceNodeId("hr-approval");
seq5.setTargetNodeId("end");
sequences.add(seq5);
// 部署流程
procDefinitionService.deploy(procDefinition, nodes, sequences);
log.info("请假流程部署成功");
}
}
6.2 发起请假流程
员工发起一个请假申请:
/**
* 请假流程使用示例
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/leave")
public class LeaveController {
@Autowired
private ProcessEngineService processEngineService;
@Autowired
private TaskService taskService;
/**
* 提交请假申请
* @param leaveDTO 请假信息
* @return 流程实例
*/
@PostMapping
public ProcInstance submitLeave(@RequestBody LeaveDTO leaveDTO) {
// 参数校验
ObjectUtils.isEmpty(leaveDTO.getUserId(), "用户ID不能为空");
StringUtils.hasText(leaveDTO.getUserName(), "用户名不能为空");
ObjectUtils.isEmpty(leaveDTO.getLeaveDays(), "请假天数不能为空");
leaveDTO.getLeaveDays() > 0, "请假天数必须大于0");
ObjectUtils.isEmpty(leaveDTO.getStartDate(), "开始日期不能为空");
ObjectUtils.isEmpty(leaveDTO.getEndDate(), "结束日期不能为空");
// 构建流程实例创建参数
ProcInstanceCreateDTO createDTO = new ProcInstanceCreateDTO();
createDTO.setProcKey("leave-process");
createDTO.setStartUserId(leaveDTO.getUserId());
createDTO.setTitle(leaveDTO.getUserName() + "请假" + leaveDTO.getLeaveDays() + "天");
createDTO.setBusinessKey("LEAVE_" + System.currentTimeMillis());
createDTO.setTenantId("0");
// 设置流程变量
List<ProcVariable> variables = Lists.newArrayList();
variables.add(createVariable("leaveDays", leaveDTO.getLeaveDays(), "integer"));
variables.add(createVariable("startDate", leaveDTO.getStartDate().toString(), "string"));
variables.add(createVariable("endDate", leaveDTO.getEndDate().toString(), "string"));
variables.add(createVariable("reason", leaveDTO.getReason(), "string"));
variables.add(createVariable("applicantName", leaveDTO.getUserName(), "string"));
createDTO.setVariables(variables);
// 启动流程
return processEngineService.startProcessInstance(createDTO);
}
/**
* 部门经理审批
* @param taskId 任务ID
* @param approveDTO 审批信息
*/
@PostMapping("/dept-manager/approve")
public void deptManagerApprove(
@RequestParam String taskId,
@RequestBody ApproveDTO approveDTO) {
TaskCompleteDTO completeDTO = new TaskCompleteDTO();
completeDTO.setUserId(approveDTO.getUserId());
completeDTO.setUserName(approveDTO.getUserName());
completeDTO.setOpinion(approveDTO.getOpinion());
completeDTO.setVariables(Map.of("deptManagerApproved", approveDTO.isApproved()));
taskService.completeTask(taskId, completeDTO);
}
/**
* HR审批
* @param taskId 任务ID
* @param approveDTO 审批信息
*/
@PostMapping("/hr/approve")
public void hrApprove(
@RequestParam String taskId,
@RequestBody ApproveDTO approveDTO) {
TaskCompleteDTO completeDTO = new TaskCompleteDTO();
completeDTO.setUserId(approveDTO.getUserId());
completeDTO.setUserName(approveDTO.getUserName());
completeDTO.setOpinion(approveDTO.getOpinion());
completeDTO.setVariables(Map.of("hrApproved", approveDTO.isApproved()));
taskService.completeTask(taskId, completeDTO);
}
/**
* 获取用户的请假审批任务
* @param userId 用户ID
* @param pageNum 页码
* @param pageSize 每页条数
* @return 任务列表
*/
@GetMapping("/tasks")
public IPage<ProcTask> getLeaveTasks(
@RequestParam Long userId,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return taskService.getTodoTasks(userId, "leave-process", pageNum, pageSize);
}
/**
* 创建流程变量
*/
private ProcVariable createVariable(String name, Object value, String type) {
ProcVariable variable = new ProcVariable();
variable.setVarName(name);
variable.setVarType(type);
variable.setVarValue(value.toString());
return variable;
}
}
6.3 流程执行流程图
请假流程的执行流程如下:

七、高级特性与优化建议
7.1 流程引擎高级特性
-
流程版本管理
- 支持流程定义的多版本并存
- 新流程实例使用最新版本,已有实例继续使用旧版本
- 提供版本迁移工具,支持将旧实例迁移到新版本
-
流程暂停与恢复
- 支持暂停运行中的流程实例
- 暂停后所有任务将不可操作
- 可恢复暂停的流程,继续执行
-
流程跳转与回退
- 允许管理员手动调整流程走向
- 支持将流程回退到之前的某个节点
- 跳转时可携带新的流程变量
-
任务委托与转办
- 支持将任务委托给其他用户处理
- 委托后原处理人仍有查看权限
- 可随时收回委托
-
流程通知
- 任务创建、到期、被处理时发送通知
- 支持多种通知渠道:站内信、邮件、短信
- 可配置通知模板和触发条件
7.2 性能优化建议
-
数据库优化
- 为常用查询字段建立索引
- 对历史表进行分表处理,按时间或流程标识分表
- 定期归档历史数据,减轻主表压力
-
缓存策略
- 缓存流程定义信息,减少数据库查询
- 缓存用户权限和角色信息,加速任务分配
- 使用 Redis 缓存热点数据,如待办任务列表
-
异步处理
- 流程实例创建、任务完成等核心操作同步执行
- 通知发送、历史记录保存等非核心操作异步执行
- 使用消息队列削峰填谷,提高系统稳定性
-
并发控制
- 使用乐观锁控制流程实例的并发修改
- 任务签收操作加锁,防止同一任务被多人签收
- 高并发场景下考虑分布式锁
7.3 与开源流程引擎的对比
本文实现的流程引擎是一个简化版,主要用于演示核心原理。与 Flowable、Activiti 等成熟开源引擎相比,有以下差异:
| 特性 | 本文实现的引擎 | Flowable |
|---|---|---|
| 规范支持 | 简化的 BPMN 子集 | 完整支持 BPMN 2.0 |
| 功能完整性 | 基础功能 | 全功能(包括网关、事件、子流程等) |
| 性能 | 一般 | 优化较好,支持高并发 |
| 易用性 | 简单直观 | 较复杂,学习曲线陡 |
| 扩展性 | 一般 | 高度可扩展,支持多种插件 |
| 社区支持 | 无 | 活跃的社区支持 |
在实际项目中,如果流程场景复杂,建议使用成熟的开源引擎;如果需求简单,或者需要深度定制,可以考虑基于本文的思路构建自己的引擎。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)