引言:为什么我们需要审批流程引擎?

在企业级应用开发中,审批流程无处不在:员工请假需要审批,费用报销需要审批,合同签订需要审批,项目立项也需要审批。这些流程往往涉及多部门、多角色协作,规则复杂且经常变动。

如果每个审批场景都从零开发,不仅会造成大量重复劳动,更会导致系统维护成本激增。想象一下:当公司的请假审批规则从 "部门经理审批" 改为 "部门经理 + HR 审批" 时,你需要修改多少段代码?当报销审批的金额阈值调整时,又需要重新测试多少功能点?

审批流程引擎正是为解决这些问题而生。它将流程逻辑与业务代码解耦,通过配置化、可视化的方式定义和管理审批流程,使业务人员也能参与流程设计,大幅提升系统的灵活性和可维护性。

本文将带你深入理解审批流程引擎的核心原理,并用 Java 实现一个企业级的审批流程引擎,包含流程定义、实例运行、任务分配、规则引擎等关键组件。无论你是想从零构建引擎,还是想更好地使用 Flowable、Activiti 等开源框架,本文都能为你提供扎实的理论基础和实践指导。

一、审批流程引擎核心概念与架构设计

1.1 核心概念解析

在深入代码实现之前,我们需要先理解审批流程引擎的核心概念,这些概念大多源自 BPMN 2.0(Business Process Model and Notation)规范,是流程引擎的理论基础。

  1. 流程定义(Process Definition)

    • 流程的静态描述,相当于 "模板"
    • 包含节点、连线、规则等元素
    • 通常用 XML 或 JSON 格式存储
  2. 流程实例(Process Instance)

    • 流程定义的一次运行实例
    • 对应一个具体的审批单(如 "张三的请假申请")
    • 包含当前运行状态、变量等信息
  3. 流程节点(Flow Node)

    • 流程中的步骤,如 "部门经理审批"、"HR 审批"
    • 主要类型:开始节点、结束节点、审批节点、网关节点、分支节点等
  4. 连线(Sequence Flow)

    • 连接两个节点的线段,表示流程的走向
    • 可以包含条件表达式,决定流程分支
  5. 审批任务(Task)

    • 流程实例运行到审批节点时产生的待办任务
    • 包含执行人、处理状态、截止时间等信息
  6. 流程变量(Process Variable)

    • 存储流程运行过程中的动态数据
    • 如 "请假天数"、"报销金额" 等,用于条件判断
  7. 参与者(Participant)

    • 参与流程的用户或角色
    • 如 "部门经理"、"财务主管"、"张三" 等

1.2 审批流程引擎架构设计

一个完整的审批流程引擎应包含以下核心模块:

  1. 流程定义模块:负责流程模板的解析、存储和管理
  2. 流程运行时模块:处理流程实例的创建、推进和终止
  3. 任务管理模块:管理审批任务的创建、分配、处理和查询
  4. 规则引擎模块:解析和执行流程中的条件判断和分配规则
  5. 历史记录模块:记录流程和任务的运行历史,支持审计和追溯
  6. API 接口层:提供对外的 RESTful API,供前端调用

1.3 核心业务流程

审批流程引擎的典型工作流程如下:

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

二、数据库设计:存储流程的骨架

审批流程引擎的数据库设计是整个系统的基础,需要合理存储流程定义、实例、任务等核心数据。以下是基于 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();
        }
    }
}

六、实战案例:实现一个请假审批流程

现在,我们通过一个实际案例来演示如何使用上面实现的审批流程引擎。我们将创建一个请假审批流程,包含以下节点:

  1. 员工提交请假申请(开始节点)
  2. 部门经理审批(如果请假天数≤3 天,审批通过后流程结束)
  3. HR 审批(如果请假天数 > 3 天,部门经理审批通过后需要 HR 审批)
  4. 流程结束

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 流程引擎高级特性

  1. 流程版本管理

    • 支持流程定义的多版本并存
    • 新流程实例使用最新版本,已有实例继续使用旧版本
    • 提供版本迁移工具,支持将旧实例迁移到新版本
  2. 流程暂停与恢复

    • 支持暂停运行中的流程实例
    • 暂停后所有任务将不可操作
    • 可恢复暂停的流程,继续执行
  3. 流程跳转与回退

    • 允许管理员手动调整流程走向
    • 支持将流程回退到之前的某个节点
    • 跳转时可携带新的流程变量
  4. 任务委托与转办

    • 支持将任务委托给其他用户处理
    • 委托后原处理人仍有查看权限
    • 可随时收回委托
  5. 流程通知

    • 任务创建、到期、被处理时发送通知
    • 支持多种通知渠道:站内信、邮件、短信
    • 可配置通知模板和触发条件

7.2 性能优化建议

  1. 数据库优化

    • 为常用查询字段建立索引
    • 对历史表进行分表处理,按时间或流程标识分表
    • 定期归档历史数据,减轻主表压力
  2. 缓存策略

    • 缓存流程定义信息,减少数据库查询
    • 缓存用户权限和角色信息,加速任务分配
    • 使用 Redis 缓存热点数据,如待办任务列表
  3. 异步处理

    • 流程实例创建、任务完成等核心操作同步执行
    • 通知发送、历史记录保存等非核心操作异步执行
    • 使用消息队列削峰填谷,提高系统稳定性
  4. 并发控制

    • 使用乐观锁控制流程实例的并发修改
    • 任务签收操作加锁,防止同一任务被多人签收
    • 高并发场景下考虑分布式锁

7.3 与开源流程引擎的对比

本文实现的流程引擎是一个简化版,主要用于演示核心原理。与 Flowable、Activiti 等成熟开源引擎相比,有以下差异:

特性 本文实现的引擎 Flowable
规范支持 简化的 BPMN 子集 完整支持 BPMN 2.0
功能完整性 基础功能 全功能(包括网关、事件、子流程等)
性能 一般 优化较好,支持高并发
易用性 简单直观 较复杂,学习曲线陡
扩展性 一般 高度可扩展,支持多种插件
社区支持 活跃的社区支持

在实际项目中,如果流程场景复杂,建议使用成熟的开源引擎;如果需求简单,或者需要深度定制,可以考虑基于本文的思路构建自己的引擎。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐