
在企业级应用开发中,审批系统犹如神经中枢,支撑着从请假流程、费用报销到合同签署等核心业务运转。随着业务复杂度提升,审批流程往往会陷入 "熵增困境":单一流程中嵌套动态分支、多角色会签、并行审批等场景,代码逐渐演变成充斥着 if-else 的 "面条式" 逻辑,维护成本呈指数级增长。
状态机模式(State Pattern)为这种困境提供了优雅的解决方案。它通过将对象的状态封装成独立状态类,将状态转换逻辑集中管理,实现了状态、行为与转换规则的解耦。在审批系统中,这意味着我们可以:
本文将深入剖析如何基于状态机模式构建支持动态分支、会签、并行等复杂场景的审批引擎,从理论模型到实战代码,全方位展示这一设计模式在企业级应用中的强大威力。
企业级审批系统通常面临以下复杂场景,这些场景直接考验着系统的设计合理性:
这些场景如果采用传统的硬编码方式实现,会导致:
状态机(Finite State Machine, FSM)是一种数学模型,由以下要素构成:
状态机模式将这些要素映射到面向对象的设计中,使系统状态管理更加清晰。对于审批系统,这种模式的适配优势体现在:
为了更好地理解状态机如何应用于审批系统,我们可以建立如下映射关系:
状态机概念 | 审批系统对应概念 | 说明 |
|---|---|---|
状态(State) | 审批节点 | 如 "部门经理审批"、"财务审核" 等 |
事件(Event) | 审批操作 | 如 "同意"、"驳回"、"撤回" 等 |
转换(Transition) | 流程流转 | 从一个审批节点到另一个审批节点的过程 |
动作(Action) | 审批行为 | 如记录审批意见、通知下一审批人等 |
初始状态 | 申请提交 | 审批流程的起点 |
终止状态 | 审批完成 / 终止 | 审批流程的终点 |
这种清晰的映射关系为我们设计审批引擎奠定了基础。
我们设计的审批引擎采用分层架构,从上到下依次为:

基于领域驱动设计(DDD)思想,我们定义以下核心领域模型:
这些模型之间的关系如下:

状态机核心层包含以下关键组件:
组件协作流程如下:

首先,我们需要配置 Maven 依赖,确保使用最新的稳定版本:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>approval-engine</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>3.2.0</spring.boot.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<lombok.version>1.18.30</lombok.version>
<fastjson2.version>2.0.45</fastjson2.version>
<guava.version>33.1.0-jre</guava.version>
<springdoc.version>2.1.0</springdoc.version>
</properties>
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- 集合工具 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
</plugins>
</build>
</project>
我们使用 MySQL 8.0 作为数据库,创建以下核心表结构:
-- 流程定义表
CREATE TABLE `process_definition` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`process_key` varchar(64) NOT NULL COMMENT '流程标识',
`name` varchar(128) NOT NULL COMMENT '流程名称',
`description` varchar(512) DEFAULT NULL COMMENT '流程描述',
`category` varchar(64) DEFAULT NULL COMMENT '流程分类',
`is_active` tinyint NOT NULL DEFAULT '1' COMMENT '是否激活:0-否,1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_process_key` (`process_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程定义表';
-- 审批节点表
CREATE TABLE `approval_node` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`process_id` bigint NOT NULL COMMENT '流程定义ID',
`node_key` varchar(64) NOT NULL COMMENT '节点标识',
`name` varchar(128) NOT NULL COMMENT '节点名称',
`type` varchar(32) NOT NULL COMMENT '节点类型:START-开始,NORMAL-普通,PARALLEL-并行,Countersign-会签,END-结束',
`assignee_type` varchar(32) NOT NULL COMMENT '处理人类型:ROLE-角色,USER-用户,DEPT-部门',
`assignee_value` varchar(256) NOT NULL COMMENT '处理人值,多个用逗号分隔',
`sort` int NOT NULL COMMENT '排序号',
`is_skipable` tinyint NOT NULL DEFAULT '0' COMMENT '是否可跳过:0-否,1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_process_id` (`process_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批节点表';
-- 节点转换规则表
CREATE TABLE `node_transition` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`process_id` bigint NOT NULL COMMENT '流程定义ID',
`source_node_key` varchar(64) NOT NULL COMMENT '源节点标识',
`target_node_key` varchar(64) NOT NULL COMMENT '目标节点标识',
`event` varchar(32) NOT NULL COMMENT '触发事件:AGREE-同意,REJECT-驳回,SKIP-跳过',
`condition_expression` varchar(1024) DEFAULT NULL COMMENT '条件表达式,为空表示无条件',
`sort` int NOT NULL COMMENT '排序号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_process_source` (`process_id`,`source_node_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='节点转换规则表';
-- 审批实例表
CREATE TABLE `approval_instance` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`process_id` bigint NOT NULL COMMENT '流程定义ID',
`process_key` varchar(64) NOT NULL COMMENT '流程标识',
`business_id` varchar(64) DEFAULT NULL COMMENT '业务关联ID',
`title` varchar(256) NOT NULL COMMENT '审批标题',
`applicant_id` varchar(64) NOT NULL COMMENT '申请人ID',
`applicant_name` varchar(64) NOT NULL COMMENT '申请人姓名',
`current_node_key` varchar(64) NOT NULL COMMENT '当前节点标识',
`status` varchar(32) NOT NULL COMMENT '状态:RUNNING-运行中,COMPLETED-已完成,TERMINATED-已终止',
`priority` tinyint NOT NULL DEFAULT '2' COMMENT '优先级:1-低,2-中,3-高',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`complete_time` datetime DEFAULT NULL COMMENT '完成时间',
PRIMARY KEY (`id`),
KEY `idx_process_key` (`process_key`),
KEY `idx_applicant_id` (`applicant_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批实例表';
-- 审批任务表
CREATE TABLE `approval_task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`instance_id` bigint NOT NULL COMMENT '审批实例ID',
`node_key` varchar(64) NOT NULL COMMENT '节点标识',
`node_name` varchar(128) NOT NULL COMMENT '节点名称',
`assignee_id` varchar(64) NOT NULL COMMENT '处理人ID',
`assignee_name` varchar(64) NOT NULL COMMENT '处理人姓名',
`status` varchar(32) NOT NULL COMMENT '状态:PENDING-待处理,COMPLETED-已完成,CANCELED-已取消',
`priority` tinyint NOT NULL DEFAULT '2' COMMENT '优先级:1-低,2-中,3-高',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`complete_time` datetime DEFAULT NULL COMMENT '完成时间',
PRIMARY KEY (`id`),
KEY `idx_instance_id` (`instance_id`),
KEY `idx_assignee_id_status` (`assignee_id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批任务表';
-- 审批操作表
CREATE TABLE `approval_action` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`instance_id` bigint NOT NULL COMMENT '审批实例ID',
`task_id` bigint NOT NULL COMMENT '审批任务ID',
`node_key` varchar(64) NOT NULL COMMENT '节点标识',
`action` varchar(32) NOT NULL COMMENT '操作:AGREE-同意,REJECT-驳回,TRANSFER-转交,WITHDRAW-撤回',
`operator_id` varchar(64) NOT NULL COMMENT '操作人ID',
`operator_name` varchar(64) NOT NULL COMMENT '操作人姓名',
`comment` varchar(1024) DEFAULT NULL COMMENT '审批意见',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_instance_id` (`instance_id`),
KEY `idx_task_id` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批操作表';
-- 并行审批节点关联表
CREATE TABLE `parallel_node_relation` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`instance_id` bigint NOT NULL COMMENT '审批实例ID',
`parent_node_key` varchar(64) NOT NULL COMMENT '父节点标识(并行节点)',
`child_node_key` varchar(64) NOT NULL COMMENT '子节点标识',
`status` varchar(32) NOT NULL COMMENT '状态:RUNNING-运行中,COMPLETED-已完成',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_instance_parent` (`instance_id`,`parent_node_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='并行审批节点关联表';
-- 会签审批结果表
CREATE TABLE `countersign_result` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`instance_id` bigint NOT NULL COMMENT '审批实例ID',
`node_key` varchar(64) NOT NULL COMMENT '节点标识',
`task_id` bigint NOT NULL COMMENT '审批任务ID',
`assignee_id` varchar(64) NOT NULL COMMENT '处理人ID',
`result` varchar(32) NOT NULL COMMENT '结果:AGREE-同意,REJECT-驳回',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_instance_node` (`instance_id`,`node_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会签审批结果表';
下面实现核心的领域模型类:
package com.example.approvalengine.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 流程定义
*
* @author ken
*/
@Data
@TableName("process_definition")
public class ProcessDefinition {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 流程标识
*/
private String processKey;
/**
* 流程名称
*/
private String name;
/**
* 流程描述
*/
private String description;
/**
* 流程分类
*/
private String category;
/**
* 是否激活:0-否,1-是
*/
private Integer isActive;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.example.approvalengine.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批节点
*
* @author ken
*/
@Data
@TableName("approval_node")
public class ApprovalNode {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 流程定义ID
*/
private Long processId;
/**
* 节点标识
*/
private String nodeKey;
/**
* 节点名称
*/
private String name;
/**
* 节点类型:START-开始,NORMAL-普通,PARALLEL-并行,COUNTERSIGN-会签,END-结束
*/
private String type;
/**
* 处理人类型:ROLE-角色,USER-用户,DEPT-部门
*/
private String assigneeType;
/**
* 处理人值,多个用逗号分隔
*/
private String assigneeValue;
/**
* 排序号
*/
private Integer sort;
/**
* 是否可跳过:0-否,1-是
*/
private Integer isSkipable;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}package com.example.approvalengine.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 节点转换规则
*
* @author ken
*/
@Data
@TableName("node_transition")
public class NodeTransition {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 流程定义ID
*/
private Long processId;
/**
* 源节点标识
*/
private String sourceNodeKey;
/**
* 目标节点标识
*/
private String targetNodeKey;
/**
* 触发事件:AGREE-同意,REJECT-驳回,SKIP-跳过
*/
private String event;
/**
* 条件表达式,为空表示无条件
*/
private String conditionExpression;
/**
* 排序号
*/
private Integer sort;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}package com.example.approvalengine.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批实例
*
* @author ken
*/
@Data
@TableName("approval_instance")
public class ApprovalInstance {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 流程定义ID
*/
private Long processId;
/**
* 流程标识
*/
private String processKey;
/**
* 业务关联ID
*/
private String businessId;
/**
* 审批标题
*/
private String title;
/**
* 申请人ID
*/
private String applicantId;
/**
* 申请人姓名
*/
private String applicantName;
/**
* 当前节点标识
*/
private String currentNodeKey;
/**
* 状态:RUNNING-运行中,COMPLETED-已完成,TERMINATED-已终止
*/
private String status;
/**
* 优先级:1-低,2-中,3-高
*/
private Integer priority;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 完成时间
*/
private LocalDateTime completeTime;
}package com.example.approvalengine.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批任务
*
* @author ken
*/
@Data
@TableName("approval_task")
public class ApprovalTask {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 审批实例ID
*/
private Long instanceId;
/**
* 节点标识
*/
private String nodeKey;
/**
* 节点名称
*/
private String nodeName;
/**
* 处理人ID
*/
private String assigneeId;
/**
* 处理人姓名
*/
private String assigneeName;
/**
* 状态:PENDING-待处理,COMPLETED-已完成,CANCELED-已取消
*/
private String status;
/**
* 优先级:1-低,2-中,3-高
*/
private Integer priority;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 完成时间
*/
private LocalDateTime completeTime;
}package com.example.approvalengine.domain.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批操作
*
* @author ken
*/
@Data
@TableName("approval_action")
public class ApprovalAction {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 审批实例ID
*/
private Long instanceId;
/**
* 审批任务ID
*/
private Long taskId;
/**
* 节点标识
*/
private String nodeKey;
/**
* 操作:AGREE-同意,REJECT-驳回,TRANSFER-转交,WITHDRAW-撤回
*/
private String action;
/**
* 操作人ID
*/
private String operatorId;
/**
* 操作人姓名
*/
private String operatorName;
/**
* 审批意见
*/
private String comment;
/**
* 创建时间
*/
private LocalDateTime createTime;
}首先定义事件枚举:
package com.example.approvalengine.state.event;
/**
* 审批事件枚举
*
* @author ken
*/
public enum ApprovalEvent {
/**
* 提交审批
*/
SUBMIT,
/**
* 同意
*/
AGREE,
/**
* 驳回
*/
REJECT,
/**
* 撤回
*/
WITHDRAW,
/**
* 转交
*/
TRANSFER,
/**
* 跳过
*/
SKIP,
/**
* 终止
*/
TERMINATE
}
定义状态接口:
package com.example.approvalengine.state;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
/**
* 状态接口
*
* @author ken
*/
public interface State {
/**
* 获取状态标识
*
* @return 状态标识
*/
String getStateKey();
/**
* 处理事件
*
* @param context 状态上下文
* @param event 事件
* @return 处理结果
*/
StateHandleResult handleEvent(StateContext context, ApprovalEvent event);
/**
* 进入状态时执行的动作
*
* @param context 状态上下文
*/
void onEnter(StateContext context);
/**
* 离开状态时执行的动作
*
* @param context 状态上下文
*/
void onExit(StateContext context);
}
实现抽象状态类:
package com.example.approvalengine.state;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 抽象状态类
*
* @author ken
*/
public abstract class AbstractState implements State {
private static final Logger log = LoggerFactory.getLogger(AbstractState.class);
/**
* 状态标识
*/
@Getter
@Setter
private String stateKey;
/**
* 状态名称
*/
@Getter
@Setter
private String stateName;
public AbstractState(String stateKey, String stateName) {
this.stateKey = stateKey;
this.stateName = stateName;
}
@Override
public abstract StateHandleResult handleEvent(StateContext context, ApprovalEvent event);
@Override
public void onEnter(StateContext context) {
ApprovalInstance instance = context.getApprovalInstance();
log.info("Enter state: [{}], instanceId: [{}]", getStateKey(), instance.getId());
// 更新实例当前节点
instance.setCurrentNodeKey(getStateKey());
}
@Override
public void onExit(StateContext context) {
ApprovalInstance instance = context.getApprovalInstance();
log.info("Exit state: [{}], instanceId: [{}]", getStateKey(), instance.getId());
}
}
定义状态上下文:
package com.example.approvalengine.state.context;
import com.example.approvalengine.domain.model.ApprovalAction;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.domain.model.ApprovalTask;
import lombok.Data;
import java.util.Map;
/**
* 状态上下文
*
* @author ken
*/
@Data
public class StateContext {
/**
* 审批实例
*/
private ApprovalInstance approvalInstance;
/**
* 当前审批任务
*/
private ApprovalTask currentTask;
/**
* 审批操作
*/
private ApprovalAction approvalAction;
/**
* 业务数据
*/
private Map<String, Object> businessData;
/**
* 流程定义ID
*/
private Long processId;
/**
* 扩展参数
*/
private Map<String, Object> extraParams;
}
定义状态处理结果:
package com.example.approvalengine.state.result;
import com.example.approvalengine.domain.model.ApprovalInstance;
import lombok.Data;
/**
* 状态处理结果
*
* @author ken
*/
@Data
public class StateHandleResult {
/**
* 是否处理成功
*/
private boolean success;
/**
* 错误信息
*/
private String errorMessage;
/**
* 下一个状态标识
*/
private String nextStateKey;
/**
* 处理后的审批实例
*/
private ApprovalInstance approvalInstance;
/**
* 创建成功结果
*
* @param nextStateKey 下一个状态标识
* @param instance 审批实例
* @return 状态处理结果
*/
public static StateHandleResult success(String nextStateKey, ApprovalInstance instance) {
StateHandleResult result = new StateHandleResult();
result.setSuccess(true);
result.setNextStateKey(nextStateKey);
result.setApprovalInstance(instance);
return result;
}
/**
* 创建失败结果
*
* @param errorMessage 错误信息
* @return 状态处理结果
*/
public static StateHandleResult failure(String errorMessage) {
StateHandleResult result = new StateHandleResult();
result.setSuccess(false);
result.setErrorMessage(errorMessage);
return result;
}
}
实现状态管理器:
package com.example.approvalengine.state.manager;
import com.example.approvalengine.state.State;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 状态管理器
*
* @author ken
*/
public class StateManager {
/**
* 状态缓存
*/
private final Map<String, State> stateMap = new HashMap<>();
/**
* 注册状态
*
* @param state 状态对象
*/
public void registerState(State state) {
Assert.notNull(state, "State cannot be null");
Assert.hasText(state.getStateKey(), "State key cannot be empty");
stateMap.put(state.getStateKey(), state);
}
/**
* 获取状态
*
* @param stateKey 状态标识
* @return 状态对象
*/
public State getState(String stateKey) {
Assert.hasText(stateKey, "State key cannot be empty");
return stateMap.get(stateKey);
}
/**
* 获取所有状态标识
*
* @return 状态标识集合
*/
public Set<String> getAllStateKeys() {
return stateMap.keySet();
}
/**
* 清除所有状态
*/
public void clear() {
stateMap.clear();
}
}
实现核心状态机类:
package com.example.approvalengine.state.machine;
import com.example.approvalengine.state.State;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.manager.StateManager;
import com.example.approvalengine.state.result.StateHandleResult;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 状态机
*
* @author ken
*/
@Component
@RequiredArgsConstructor
public class StateMachine {
private static final Logger log = LoggerFactory.getLogger(StateMachine.class);
private final StateManager stateManager;
/**
* 处理事件
*
* @param context 状态上下文
* @param event 事件
* @return 处理结果
*/
public StateHandleResult handleEvent(StateContext context, ApprovalEvent event) {
String currentStateKey = context.getApprovalInstance().getCurrentNodeKey();
log.info("Handling event: [{}] in state: [{}], instanceId: [{}]",
event, currentStateKey, context.getApprovalInstance().getId());
// 获取当前状态
State currentState = stateManager.getState(currentStateKey);
if (currentState == null) {
String errorMsg = String.format("State not found: %s", currentStateKey);
log.error(errorMsg);
return StateHandleResult.failure(errorMsg);
}
// 处理事件
StateHandleResult result = currentState.handleEvent(context, event);
if (!result.isSuccess()) {
log.error("Handle event failed: {}", result.getErrorMessage());
return result;
}
// 如果有下一个状态,则执行状态转换
String nextStateKey = result.getNextStateKey();
if (nextStateKey != null) {
// 离开当前状态
currentState.onExit(context);
// 获取下一个状态
State nextState = stateManager.getState(nextStateKey);
if (nextState == null) {
String errorMsg = String.format("Next state not found: %s", nextStateKey);
log.error(errorMsg);
return StateHandleResult.failure(errorMsg);
}
// 进入下一个状态
nextState.onEnter(context);
log.info("Transition completed: [{}] -> [{}], instanceId: [{}]",
currentStateKey, nextStateKey, context.getApprovalInstance().getId());
}
return result;
}
}
实现开始状态:
package com.example.approvalengine.state.impl;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.domain.model.ApprovalTask;
import com.example.approvalengine.service.ApprovalTaskService;
import com.example.approvalengine.state.AbstractState;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 开始状态
*
* @author ken
*/
@Component
@RequiredArgsConstructor
public class StartState extends AbstractState {
private static final String STATE_KEY = "START";
private static final String STATE_NAME = "开始节点";
private final ApprovalTaskService approvalTaskService;
public StartState() {
super(STATE_KEY, STATE_NAME);
}
@Override
public StateHandleResult handleEvent(StateContext context, ApprovalEvent event) {
ApprovalInstance instance = context.getApprovalInstance();
// 只处理提交事件
if (event != ApprovalEvent.SUBMIT) {
return StateHandleResult.failure("Start state can only handle SUBMIT event");
}
// 创建第一个审批任务(通常是提交者的直接上级)
ApprovalTask firstTask = approvalTaskService.createFirstTask(instance);
context.setCurrentTask(firstTask);
// 流转到第一个审批节点
String firstNodeKey = firstTask.getNodeKey();
return StateHandleResult.success(firstNodeKey, instance);
}
@Override
public void onEnter(StateContext context) {
super.onEnter(context);
ApprovalInstance instance = context.getApprovalInstance();
// 设置实例状态为运行中
instance.setStatus("RUNNING");
}
}
实现普通审批状态:
package com.example.approvalengine.state.impl;
import com.example.approvalengine.domain.model.ApprovalAction;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.domain.model.ApprovalTask;
import com.example.approvalengine.service.ApprovalActionService;
import com.example.approvalengine.service.ApprovalTaskService;
import com.example.approvalengine.service.NodeTransitionService;
import com.example.approvalengine.state.AbstractState;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
/**
* 普通审批状态
*
* @author ken
*/
@Component
@RequiredArgsConstructor
public class NormalApprovalState extends AbstractState {
private final NodeTransitionService transitionService;
private final ApprovalTaskService taskService;
private final ApprovalActionService actionService;
public NormalApprovalState(String stateKey, String stateName) {
super(stateKey, stateName);
}
@Override
public StateHandleResult handleEvent(StateContext context, ApprovalEvent event) {
ApprovalInstance instance = context.getApprovalInstance();
ApprovalTask currentTask = context.getCurrentTask();
ApprovalAction action = context.getApprovalAction();
// 记录审批操作
action.setInstanceId(instance.getId());
action.setTaskId(currentTask.getId());
action.setNodeKey(getStateKey());
action.setAction(event.name());
actionService.save(action);
// 更新当前任务状态
currentTask.setStatus("COMPLETED");
currentTask.setCompleteTime(LocalDateTime.now());
taskService.updateById(currentTask);
// 根据事件类型获取下一个节点
String nextNodeKey = transitionService.findNextNodeKey(
instance.getProcessId(), getStateKey(), event.name(), context.getBusinessData());
if (!StringUtils.hasText(nextNodeKey)) {
return StateHandleResult.failure("No next node found for event: " + event);
}
// 如果下一个节点是结束节点,直接返回
if ("END".equals(nextNodeKey)) {
instance.setStatus("COMPLETED");
instance.setCompleteTime(LocalDateTime.now());
return StateHandleResult.success(nextNodeKey, instance);
}
// 创建下一个节点的审批任务
taskService.createNextTasks(instance, nextNodeKey);
return StateHandleResult.success(nextNodeKey, instance);
}
}
实现会签状态:
package com.example.approvalengine.state.impl;
import com.example.approvalengine.domain.model.ApprovalAction;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.domain.model.ApprovalTask;
import com.example.approvalengine.domain.model.CountersignResult;
import com.example.approvalengine.service.ApprovalActionService;
import com.example.approvalengine.service.ApprovalTaskService;
import com.example.approvalengine.service.CountersignResultService;
import com.example.approvalengine.service.NodeTransitionService;
import com.example.approvalengine.state.AbstractState;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 会签状态
*
* @author ken
*/
@Component
@RequiredArgsConstructor
public class CountersignState extends AbstractState {
private final NodeTransitionService transitionService;
private final ApprovalTaskService taskService;
private final ApprovalActionService actionService;
private final CountersignResultService countersignResultService;
public CountersignState(String stateKey, String stateName) {
super(stateKey, stateName);
}
@Override
public StateHandleResult handleEvent(StateContext context, ApprovalEvent event) {
ApprovalInstance instance = context.getApprovalInstance();
ApprovalTask currentTask = context.getCurrentTask();
ApprovalAction action = context.getApprovalAction();
// 记录审批操作
action.setInstanceId(instance.getId());
action.setTaskId(currentTask.getId());
action.setNodeKey(getStateKey());
action.setAction(event.name());
actionService.save(action);
// 记录会签结果
CountersignResult result = new CountersignResult();
result.setInstanceId(instance.getId());
result.setNodeKey(getStateKey());
result.setTaskId(currentTask.getId());
result.setAssigneeId(action.getOperatorId());
result.setResult(event.name());
countersignResultService.save(result);
// 更新当前任务状态
currentTask.setStatus("COMPLETED");
currentTask.setCompleteTime(LocalDateTime.now());
taskService.updateById(currentTask);
// 查询该节点的所有任务
List<ApprovalTask> allTasks = taskService.listByInstanceIdAndNodeKey(
instance.getId(), getStateKey());
// 检查是否所有任务都已完成
boolean allCompleted = allTasks.stream()
.allMatch(task -> "COMPLETED".equals(task.getStatus()));
if (!allCompleted) {
// 还有未完成的任务,保持当前状态
return StateHandleResult.success(getStateKey(), instance);
}
// 所有会签人都已处理,判断最终结果
List<CountersignResult> allResults = countersignResultService.listByInstanceIdAndNodeKey(
instance.getId(), getStateKey());
// 只要有一个人驳回,整个会签节点就视为驳回
boolean hasReject = allResults.stream()
.anyMatch(r -> ApprovalEvent.REJECT.name().equals(r.getResult()));
// 确定最终事件
ApprovalEvent finalEvent = hasReject ? ApprovalEvent.REJECT : ApprovalEvent.AGREE;
// 根据最终事件获取下一个节点
String nextNodeKey = transitionService.findNextNodeKey(
instance.getProcessId(), getStateKey(), finalEvent.name(), context.getBusinessData());
if (!StringUtils.hasText(nextNodeKey)) {
return StateHandleResult.failure("No next node found for event: " + finalEvent);
}
// 如果下一个节点是结束节点,更新实例状态
if ("END".equals(nextNodeKey)) {
instance.setStatus("COMPLETED");
instance.setCompleteTime(LocalDateTime.now());
return StateHandleResult.success(nextNodeKey, instance);
}
// 创建下一个节点的审批任务
taskService.createNextTasks(instance, nextNodeKey);
return StateHandleResult.success(nextNodeKey, instance);
}
@Override
public void onEnter(StateContext context) {
super.onEnter(context);
// 创建会签任务(多个处理人)
taskService.createCountersignTasks(context.getApprovalInstance(), getStateKey());
}
}
实现并行审批状态:
package com.example.approvalengine.state.impl;
import com.example.approvalengine.domain.model.ApprovalAction;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.domain.model.ApprovalTask;
import com.example.approvalengine.domain.model.ParallelNodeRelation;
import com.example.approvalengine.service.ApprovalActionService;
import com.example.approvalengine.service.ApprovalTaskService;
import com.example.approvalengine.service.NodeTransitionService;
import com.example.approvalengine.service.ParallelNodeRelationService;
import com.example.approvalengine.state.AbstractState;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 并行审批状态
*
* @author ken
*/
@Component
@RequiredArgsConstructor
public class ParallelApprovalState extends AbstractState {
private final NodeTransitionService transitionService;
private final ApprovalTaskService taskService;
private final ApprovalActionService actionService;
private final ParallelNodeRelationService parallelNodeRelationService;
public ParallelApprovalState(String stateKey, String stateName) {
super(stateKey, stateName);
}
@Override
public StateHandleResult handleEvent(StateContext context, ApprovalEvent event) {
ApprovalInstance instance = context.getApprovalInstance();
ApprovalTask currentTask = context.getCurrentTask();
ApprovalAction action = context.getApprovalAction();
// 记录审批操作
action.setInstanceId(instance.getId());
action.setTaskId(currentTask.getId());
action.setNodeKey(currentTask.getNodeKey()); // 注意这里是子节点的key
action.setAction(event.name());
actionService.save(action);
// 更新当前任务状态
currentTask.setStatus("COMPLETED");
currentTask.setCompleteTime(LocalDateTime.now());
taskService.updateById(currentTask);
// 更新并行节点关系状态
ParallelNodeRelation relation = parallelNodeRelationService.getByInstanceAndChildNode(
instance.getId(), currentTask.getNodeKey());
if (relation != null) {
relation.setStatus("COMPLETED");
parallelNodeRelationService.updateById(relation);
}
// 检查所有并行子节点是否都已完成
List<ParallelNodeRelation> allRelations = parallelNodeRelationService.listByInstanceAndParentNode(
instance.getId(), getStateKey());
boolean allCompleted = allRelations.stream()
.allMatch(r -> "COMPLETED".equals(r.getStatus()));
if (!allCompleted) {
// 还有未完成的并行节点,保持当前状态
return StateHandleResult.success(getStateKey(), instance);
}
// 所有并行节点都已完成,进入下一个节点
String nextNodeKey = transitionService.findNextNodeKey(
instance.getProcessId(), getStateKey(), ApprovalEvent.AGREE.name(), context.getBusinessData());
if (!StringUtils.hasText(nextNodeKey)) {
return StateHandleResult.failure("No next node found for parallel state completion");
}
// 如果下一个节点是结束节点,更新实例状态
if ("END".equals(nextNodeKey)) {
instance.setStatus("COMPLETED");
instance.setCompleteTime(LocalDateTime.now());
return StateHandleResult.success(nextNodeKey, instance);
}
// 创建下一个节点的审批任务
taskService.createNextTasks(instance, nextNodeKey);
return StateHandleResult.success(nextNodeKey, instance);
}
@Override
public void onEnter(StateContext context) {
super.onEnter(context);
// 创建并行子节点的审批任务
parallelNodeRelationService.createParallelRelationsAndTasks(
context.getApprovalInstance(), getStateKey());
}
}
实现结束状态:
package com.example.approvalengine.state.impl;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.state.AbstractState;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.result.StateHandleResult;
import org.springframework.stereotype.Component;
/**
* 结束状态
*
* @author ken
*/
@Component
public class EndState extends AbstractState {
private static final String STATE_KEY = "END";
private static final String STATE_NAME = "结束节点";
public EndState() {
super(STATE_KEY, STATE_NAME);
}
@Override
public StateHandleResult handleEvent(StateContext context, ApprovalEvent event) {
// 结束状态不处理任何事件
return StateHandleResult.failure("End state cannot handle any events");
}
@Override
public void onEnter(StateContext context) {
super.onEnter(context);
ApprovalInstance instance = context.getApprovalInstance();
// 设置实例状态为已完成
instance.setStatus("COMPLETED");
instance.setCompleteTime(java.time.LocalDateTime.now());
}
}
实现节点转换服务:
package com.example.approvalengine.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.approvalengine.domain.model.NodeTransition;
import com.example.approvalengine.mapper.NodeTransitionMapper;
import com.example.approvalengine.rule.RuleEvaluator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
/**
* 节点转换服务
*
* @author ken
*/
@Service
@RequiredArgsConstructor
public class NodeTransitionService extends ServiceImpl<NodeTransitionMapper, NodeTransition> {
private final RuleEvaluator ruleEvaluator;
/**
* 查找下一个节点标识
*
* @param processId 流程定义ID
* @param sourceNodeKey 源节点标识
* @param event 事件
* @param businessData 业务数据
* @return 下一个节点标识
*/
public String findNextNodeKey(Long processId, String sourceNodeKey, String event, Map<String, Object> businessData) {
// 查询符合条件的转换规则
LambdaQueryWrapper<NodeTransition> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(NodeTransition::getProcessId, processId)
.eq(NodeTransition::getSourceNodeKey, sourceNodeKey)
.eq(NodeTransition::getEvent, event)
.orderByAsc(NodeTransition::getSort);
List<NodeTransition> transitions = list(queryWrapper);
if (CollectionUtils.isEmpty(transitions)) {
return null;
}
// 检查转换条件,找到第一个满足条件的目标节点
for (NodeTransition transition : transitions) {
String condition = transition.getConditionExpression();
// 如果没有条件,直接返回目标节点
if (condition == null || condition.trim().isEmpty()) {
return transition.getTargetNodeKey();
}
// 评估条件表达式
boolean conditionMet = ruleEvaluator.evaluate(condition, businessData);
if (conditionMet) {
return transition.getTargetNodeKey();
}
}
// 没有找到满足条件的转换规则
return null;
}
}
实现规则评估器(简化版):
package com.example.approvalengine.rule;
import com.googlecode.aviator.AviatorEvaluator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 规则评估器
* 用于评估条件表达式是否成立
*
* @author ken
*/
@Slf4j
@Component
public class RuleEvaluator {
/**
* 评估表达式
*
* @param expression 表达式
* @param context 上下文数据
* @return 表达式结果是否为true
*/
public boolean evaluate(String expression, Map<String, Object> context) {
try {
// 使用Aviator表达式引擎评估
Object result = AviatorEvaluator.execute(expression, context);
if (result instanceof Boolean) {
return (Boolean) result;
} else {
log.warn("Expression evaluation result is not a boolean: {}", expression);
return false;
}
} catch (Exception e) {
log.error("Failed to evaluate expression: {}", expression, e);
return false;
}
}
}
package com.example.approvalengine.controller;
import com.example.approvalengine.domain.model.ApprovalAction;
import com.example.approvalengine.domain.model.ApprovalInstance;
import com.example.approvalengine.dto.request.ApprovalRequest;
import com.example.approvalengine.dto.request.CreateApprovalRequest;
import com.example.approvalengine.dto.response.ApiResponse;
import com.example.approvalengine.service.ApprovalInstanceService;
import com.example.approvalengine.state.context.StateContext;
import com.example.approvalengine.state.event.ApprovalEvent;
import com.example.approvalengine.state.machine.StateMachine;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 审批控制器
*
* @author ken
*/
@RestController
@RequestMapping("/api/v1/approvals")
@RequiredArgsConstructor
@Tag(name = "审批管理", description = "审批流程相关接口")
public class ApprovalController {
private final ApprovalInstanceService approvalInstanceService;
private final StateMachine stateMachine;
/**
* 创建审批实例
*
* @param request 创建审批请求
* @return 审批实例ID
*/
@PostMapping
@Operation(summary = "创建审批实例", description = "提交新的审批申请")
public ApiResponse<Long> createApproval(@RequestBody CreateApprovalRequest request) {
Long instanceId = approvalInstanceService.createApproval(request);
return ApiResponse.success(instanceId);
}
/**
* 同意审批
*
* @param instanceId 审批实例ID
* @param request 审批请求
* @return 处理结果
*/
@PostMapping("/{instanceId}/agree")
@Operation(summary = "同意审批", description = "审批人同意当前审批")
public ApiResponse<Boolean> agreeApproval(
@Parameter(description = "审批实例ID") @PathVariable Long instanceId,
@RequestBody ApprovalRequest request) {
approvalInstanceService.handleApproval(instanceId, request, ApprovalEvent.AGREE);
return ApiResponse.success(true);
}
/**
* 驳回审批
*
* @param instanceId 审批实例ID
* @param request 审批请求
* @return 处理结果
*/
@PostMapping("/{instanceId}/reject")
@Operation(summary = "驳回审批", description = "审批人驳回当前审批")
public ApiResponse<Boolean> rejectApproval(
@Parameter(description = "审批实例ID") @PathVariable Long instanceId,
@RequestBody ApprovalRequest request) {
approvalInstanceService.handleApproval(instanceId, request, ApprovalEvent.REJECT);
return ApiResponse.success(true);
}
/**
* 撤回审批
*
* @param instanceId 审批实例ID
* @param request 审批请求
* @return 处理结果
*/
@PostMapping("/{instanceId}/withdraw")
@Operation(summary = "撤回审批", description = "申请人撤回审批")
public ApiResponse<Boolean> withdrawApproval(
@Parameter(description = "审批实例ID") @PathVariable Long instanceId,
@RequestBody ApprovalRequest request) {
approvalInstanceService.handleApproval(instanceId, request, ApprovalEvent.WITHDRAW);
return ApiResponse.success(true);
}
/**
* 获取审批详情
*
* @param instanceId 审批实例ID
* @return 审批实例详情
*/
@GetMapping("/{instanceId}")
@Operation(summary = "获取审批详情", description = "查询审批实例的详细信息")
public ApiResponse<ApprovalInstance> getApprovalDetail(
@Parameter(description = "审批实例ID") @PathVariable Long instanceId) {
ApprovalInstance instance = approvalInstanceService.getById(instanceId);
return ApiResponse.success(instance);
}
}
动态分支是指审批流程根据业务数据的不同而走向不同的审批路径。例如,我们设计一个费用报销审批流程:
首先,我们需要在数据库中配置这个流程的定义:
-- 插入流程定义
INSERT INTO process_definition (process_key, name, description, category, is_active)
VALUES ('EXPENSE_REIMBURSEMENT', '费用报销流程', '员工费用报销审批流程', 'FINANCE', 1);
-- 获取流程ID(假设为1)
SET @process_id = 1;
-- 插入审批节点
INSERT INTO approval_node (process_id, node_key, name, type, assignee_type, assignee_value, sort, is_skipable)
VALUES
(@process_id, 'DEPT_MANAGER', '部门经理审批', 'NORMAL', 'ROLE', 'DEPT_MANAGER', 1, 0),
(@process_id, 'FINANCE_MANAGER', '财务经理审批', 'NORMAL', 'ROLE', 'FINANCE_MANAGER', 2, 0),
(@process_id, 'GENERAL_MANAGER', '总经理审批', 'NORMAL', 'ROLE', 'GENERAL_MANAGER', 3, 0),
(@process_id, 'END', '结束', 'END', 'NONE', '', 4, 0);
-- 插入节点转换规则
INSERT INTO node_transition (process_id, source_node_key, target_node_key, event, condition_expression, sort)
VALUES
-- 部门经理审批后的分支
(@process_id, 'DEPT_MANAGER', 'END', 'AGREE', 'amount <= 1000', 1),
(@process_id, 'DEPT_MANAGER', 'FINANCE_MANAGER', 'AGREE', 'amount > 1000 && amount <= 5000', 2),
(@process_id, 'DEPT_MANAGER', 'FINANCE_MANAGER', 'AGREE', 'amount > 5000', 3),
-- 财务经理审批后的分支
(@process_id, 'FINANCE_MANAGER', 'END', 'AGREE', 'amount <= 5000', 1),
(@process_id, 'FINANCE_MANAGER', 'GENERAL_MANAGER', 'AGREE', 'amount > 5000', 2),
-- 总经理审批后结束
(@process_id, 'GENERAL_MANAGER', 'END', 'AGREE', '', 1),
-- 各节点驳回规则(回到申请人)
(@process_id, 'DEPT_MANAGER', 'END', 'REJECT', '', 1),
(@process_id, 'FINANCE_MANAGER', 'END', 'REJECT', '', 1),
(@process_id, 'GENERAL_MANAGER', 'END', 'REJECT', '', 1);
当部门经理同意审批后,规则引擎会根据金额判断下一步该走向哪个节点:
// 这部分逻辑已在NodeTransitionService中实现
// 当处理部门经理同意事件时,会调用以下代码
String nextNodeKey = transitionService.findNextNodeKey(
instance.getProcessId(), "DEPT_MANAGER", "AGREE", businessData);
其中businessData包含了报销金额等信息,规则引擎会根据配置的条件表达式进行判断,返回正确的下一个节点标识。
会签审批要求多个角色或用户都审批通过后才能进入下一环节。例如,项目立项审批需要技术部、财务部和业务部三个部门的经理全部同意。
配置会签流程:
-- 假设流程ID为2
SET @process_id = 2;
-- 插入审批节点
INSERT INTO approval_node (process_id, node_key, name, type, assignee_type, assignee_value, sort, is_skipable)
VALUES
(@process_id, 'START', '开始', 'START', 'NONE', '', 1, 0),
(@process_id, 'DEPARTMENT_COUNTERSIGN', '部门会签', 'COUNTERSIGN', 'ROLE', 'TECH_MANAGER,FINANCE_MANAGER,BUSINESS_MANAGER', 2, 0),
(@process_id, 'GENERAL_MANAGER', '总经理审批', 'NORMAL', 'ROLE', 'GENERAL_MANAGER', 3, 0),
(@process_id, 'END', '结束', 'END', 'NONE', '', 4, 0);
-- 插入节点转换规则
INSERT INTO node_transition (process_id, source_node_key, target_node_key, event, condition_expression, sort)
VALUES
(@process_id, 'START', 'DEPARTMENT_COUNTERSIGN', 'SUBMIT', '', 1),
(@process_id, 'DEPARTMENT_COUNTERSIGN', 'GENERAL_MANAGER', 'AGREE', '', 1),
(@process_id, 'DEPARTMENT_COUNTERSIGN', 'END', 'REJECT', '', 1),
(@process_id, 'GENERAL_MANAGER', 'END', 'AGREE', '', 1),
(@process_id, 'GENERAL_MANAGER', 'END', 'REJECT', '', 1);
当流程进入会签节点时,CountersignState的onEnter方法会创建多个审批任务,分别分配给技术部经理、财务部经理和业务部经理:
// CountersignState的onEnter方法会调用以下服务方法
taskService.createCountersignTasks(instance, nodeKey);
// 该方法的实现逻辑大致如下
public void createCountersignTasks(ApprovalInstance instance, String nodeKey) {
// 查询节点定义
ApprovalNode node = approvalNodeService.getByProcessIdAndNodeKey(instance.getProcessId(), nodeKey);
// 解析处理人
String[] assigneeIds = node.getAssigneeValue().split(",");
// 为每个处理人创建任务
for (String assigneeId : assigneeIds) {
ApprovalTask task = new ApprovalTask();
task.setInstanceId(instance.getId());
task.setNodeKey(nodeKey);
task.setNodeName(node.getName());
task.setAssigneeId(assigneeId);
task.setAssigneeName(getUserName(assigneeId)); // 获取用户名
task.setStatus("PENDING");
task.setPriority(instance.getPriority());
save(task);
}
}
每个处理人完成审批后,系统会记录其审批结果。当最后一个处理人完成审批后,系统会判断所有结果:
// 检查是否所有任务都已完成
boolean allCompleted = allTasks.stream()
.allMatch(task -> "COMPLETED".equals(task.getStatus()));
if (allCompleted) {
// 判断是否有驳回
boolean hasReject = allResults.stream()
.anyMatch(r -> "REJECT".equals(r.getResult()));
// 根据结果决定下一步
ApprovalEvent finalEvent = hasReject ? ApprovalEvent.REJECT : ApprovalEvent.AGREE;
// ...
}
并行审批允许多个审批节点同时进行,全部完成后再进入下一环节。例如,合同审批中,法务审核和财务审核可以并行处理。
配置并行审批流程:
-- 假设流程ID为3
SET @process_id = 3;
-- 插入审批节点(包括并行节点和其子节点)
INSERT INTO approval_node (process_id, node_key, name, type, assignee_type, assignee_value, sort, is_skipable)
VALUES
(@process_id, 'START', '开始', 'START', 'NONE', '', 1, 0),
(@process_id, 'PARALLEL_REVIEW', '并行审核', 'PARALLEL', 'NONE', '', 2, 0),
(@process_id, 'LEGAL_REVIEW', '法务审核', 'NORMAL', 'ROLE', 'LEGAL_MANAGER', 3, 0),
(@process_id, 'FINANCE_REVIEW', '财务审核', 'NORMAL', 'ROLE', 'FINANCE_MANAGER', 3, 0),
(@process_id, 'EXECUTIVE_APPROVAL', '高管审批', 'NORMAL', 'ROLE', 'EXECUTIVE', 4, 0),
(@process_id, 'END', '结束', 'END', 'NONE', '', 5, 0);
-- 插入节点转换规则
INSERT INTO node_transition (process_id, source_node_key, target_node_key, event, condition_expression, sort)
VALUES
(@process_id, 'START', 'PARALLEL_REVIEW', 'SUBMIT', '', 1),
(@process_id, 'PARALLEL_REVIEW', 'EXECUTIVE_APPROVAL', 'AGREE', '', 1),
(@process_id, 'EXECUTIVE_APPROVAL', 'END', 'AGREE', '', 1),
(@process_id, 'LEGAL_REVIEW', 'PARALLEL_REVIEW', 'AGREE', '', 1),
(@process_id, 'LEGAL_REVIEW', 'END', 'REJECT', '', 1),
(@process_id, 'FINANCE_REVIEW', 'PARALLEL_REVIEW', 'AGREE', '', 1),
(@process_id, 'FINANCE_REVIEW', 'END', 'REJECT', '', 1),
(@process_id, 'EXECUTIVE_APPROVAL', 'END', 'REJECT', '', 1);
当流程进入并行节点时,ParallelApprovalState的onEnter方法会创建并行子节点的任务:
// ParallelApprovalState的onEnter方法会调用以下服务方法
parallelNodeRelationService.createParallelRelationsAndTasks(instance, getStateKey());
// 该方法的实现逻辑大致如下
public void createParallelRelationsAndTasks(ApprovalInstance instance, String parentNodeKey) {
// 查询并行节点的子节点(这里简化处理,实际应根据流程定义查询)
List<ApprovalNode> childNodes = approvalNodeService.getParallelChildNodes(instance.getProcessId(), parentNodeKey);
for (ApprovalNode childNode : childNodes) {
// 创建并行节点关系记录
ParallelNodeRelation relation = new ParallelNodeRelation();
relation.setInstanceId(instance.getId());
relation.setParentNodeKey(parentNodeKey);
relation.setChildNodeKey(childNode.getNodeKey());
relation.setStatus("RUNNING");
parallelNodeRelationMapper.insert(relation);
// 为子节点创建审批任务
approvalTaskService.createTaskForNode(instance, childNode);
}
}
每个子节点的审批完成后,都会回到并行节点。系统会检查是否所有并行子节点都已完成:
// 检查所有并行子节点是否都已完成
List<ParallelNodeRelation> allRelations = parallelNodeRelationService.listByInstanceAndParentNode(
instance.getId(), getStateKey());
boolean allCompleted = allRelations.stream()
.allMatch(r -> "COMPLETED".equals(r.getStatus()));
if (allCompleted) {
// 所有并行节点都已完成,进入下一个节点
// ...
}
在实际业务中,审批回退和撤回是常见需求。我们可以通过状态机模式优雅地实现这些功能:
/**
* 处理回退事件
*/
private StateHandleResult handleRollback(StateContext context) {
ApprovalInstance instance = context.getApprovalInstance();
ApprovalAction action = context.getApprovalAction();
// 获取要回退到的目标节点(可以从action的扩展参数中获取)
String targetNodeKey = (String) context.getExtraParams().get("targetNodeKey");
if (!StringUtils.hasText(targetNodeKey)) {
return StateHandleResult.failure("Target node is required for rollback");
}
// 记录回退操作
action.setAction("ROLLBACK");
actionService.save(action);
// 取消当前节点的所有未完成任务
taskService.cancelTasksByInstanceIdAndNodeKey(instance.getId(), getStateKey());
// 创建目标节点的审批任务
taskService.createNextTasks(instance, targetNodeKey);
return StateHandleResult.success(targetNodeKey, instance);
}
/**
* 处理撤回事件
*/
private StateHandleResult handleWithdraw(StateContext context) {
ApprovalInstance instance = context.getApprovalInstance();
ApprovalAction action = context.getApprovalAction();
// 检查是否为申请人操作
if (!instance.getApplicantId().equals(action.getOperatorId())) {
return StateHandleResult.failure("Only applicant can withdraw approval");
}
// 记录撤回操作
action.setAction("WITHDRAW");
actionService.save(action);
// 取消所有未完成任务
taskService.cancelAllPendingTasks(instance.getId());
// 更新实例状态
instance.setStatus("WITHDRAWN");
instance.setCompleteTime(LocalDateTime.now());
return StateHandleResult.success("END", instance);
}
审批代理功能允许用户将自己的审批权限临时委托给他人。实现这一功能需要:
/**
* 获取实际处理人(考虑代理关系)
*/
public String getActualAssignee(String originalAssigneeId, LocalDateTime taskCreateTime) {
// 查询有效的代理规则
LambdaQueryWrapper<ApprovalProxy> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ApprovalProxy::getOriginalUserId, originalAssigneeId)
.eq(ApprovalProxy::getIsActive, 1)
.le(ApprovalProxy::getStartDate, taskCreateTime)
.ge(ApprovalProxy::getEndDate, taskCreateTime);
ApprovalProxy proxy = approvalProxyService.getOne(queryWrapper);
// 如果有有效的代理,返回代理人ID,否则返回原处理人ID
return proxy != null ? proxy.getProxyUserId() : originalAssigneeId;
}
权限控制则需要在处理审批操作前验证当前用户是否有权限:
/**
* 验证审批权限
*/
public boolean validateApprovalPermission(Long taskId, String userId) {
ApprovalTask task = approvalTaskService.getById(taskId);
if (task == null) {
return false;
}
// 检查是否为任务的处理人或其代理人
if (task.getAssigneeId().equals(userId)) {
return true;
}
// 检查是否有代理关系
return hasProxyPermission(task.getAssigneeId(), userId);
}
本文实现的状态机审批引擎与主流工作流引擎(如 Activiti、Flowable)相比,有以下特点:
特性 | 状态机审批引擎 | 主流工作流引擎 |
|---|---|---|
复杂度 | 简单,轻量级 | 复杂,全功能 |
学习曲线 | 平缓 | 陡峭 |
灵活性 | 高,易于定制 | 中等,受规范约束 |
性能 | 高 | 中等 |
适用场景 | 中小型审批场景 | 复杂流程场景 |
在实际项目中,可以根据需求选择:
集成示例(与 Flowable 集成):
/**
* 与Flowable集成的适配器
*/
@Component
public class FlowableAdapter {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
/**
* 启动Flowable流程
*/
public String startProcess(String processDefinitionKey, Map<String, Object> variables) {
ProcessInstance instance = runtimeService.startProcessInstanceByKey(processDefinitionKey, variables);
return instance.getId();
}
/**
* 完成Flowable任务
*/
public void completeTask(String taskId, String userId, Map<String, Object> variables) {
// 设置处理人
taskService.setAssignee(taskId, userId);
// 完成任务
taskService.complete(taskId, variables);
}
/**
* 将Flowable事件转换为本地状态机事件
*/
public ApprovalEvent convertFlowableEventToLocalEvent(FlowableEvent event) {
// 根据Flowable事件类型转换为本地事件
if (event instanceof TaskCompletedEvent) {
return ApprovalEvent.AGREE;
} else if (event instanceof TaskRejectedEvent) {
return ApprovalEvent.REJECT;
}
// 其他事件转换...
return null;
}
}
为了提高状态机审批引擎的性能,可以采用以下优化策略:
/**
* 带缓存的节点转换服务
*/
@Service
public class CachedNodeTransitionService extends NodeTransitionService {
private final LoadingCache<String, List<NodeTransition>> transitionCache;
public CachedNodeTransitionService(NodeTransitionMapper mapper, RuleEvaluator evaluator) {
super(mapper, evaluator);
// 初始化缓存,过期时间设置为1小时
transitionCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(1000)
.build(new CacheLoader<>() {
@Override
public List<NodeTransition> load(String key) {
// 解析key,格式为"processId:sourceNodeKey:event"
String[] parts = key.split(":");
Long processId = Long.parseLong(parts[0]);
String sourceNodeKey = parts[1];
String event = parts[2];
// 从数据库加载转换规则
return queryTransitionsFromDb(processId, sourceNodeKey, event);
}
});
}
@Override
public String findNextNodeKey(Long processId, String sourceNodeKey, String event, Map<String, Object> businessData) {
try {
// 从缓存获取转换规则
String cacheKey = processId + ":" + sourceNodeKey + ":" + event;
List<NodeTransition> transitions = transitionCache.get(cacheKey);
// 评估转换条件(与之前逻辑相同)
// ...
return nextNodeKey;
} catch (ExecutionException e) {
log.error("Failed to get transitions from cache", e);
// 缓存获取失败时,直接从数据库查询
return super.findNextNodeKey(processId, sourceNodeKey, event, businessData);
}
}
// 其他方法...
}
/**
* 状态机测试示例
*/
@SpringBootTest
public class StateMachineTest {
@Autowired
private StateMachine stateMachine;
@Autowired
private StateManager stateManager;
@Test
public void testNormalApprovalFlow() {
// 创建测试用例
ApprovalInstance instance = createTestInstance();
StateContext context = createTestContext(instance);
// 测试提交事件
StateHandleResult result = stateMachine.handleEvent(context, ApprovalEvent.SUBMIT);
assertTrue(result.isSuccess());
assertEquals("DEPT_MANAGER", result.getNextStateKey());
// 测试部门经理同意
context = createTestContext(instance);
result = stateMachine.handleEvent(context, ApprovalEvent.AGREE);
assertTrue(result.isSuccess());
assertEquals("FINANCE_MANAGER", result.getNextStateKey());
// 其他测试步骤...
}
@Test
public void testCountersignFlow() {
// 会签流程测试...
}
@Test
public void testParallelFlow() {
// 并行流程测试...
}
}
在使用状态机模式开发审批系统时,应遵循以下最佳实践:
/**
* 状态机测试示例
*/
@SpringBootTest
public class StateMachineTest {
@Autowired
private StateMachine stateMachine;
@Autowired
private StateManager stateManager;
@Test
public void testNormalApprovalFlow() {
// 创建测试用例
ApprovalInstance instance = createTestInstance();
StateContext context = createTestContext(instance);
// 测试提交事件
StateHandleResult result = stateMachine.handleEvent(context, ApprovalEvent.SUBMIT);
assertTrue(result.isSuccess());
assertEquals("DEPT_MANAGER", result.getNextStateKey());
// 测试部门经理同意
context = createTestContext(instance);
result = stateMachine.handleEvent(context, ApprovalEvent.AGREE);
assertTrue(result.isSuccess());
assertEquals("FINANCE_MANAGER", result.getNextStateKey());
// 其他测试步骤...
}
@Test
public void testCountersignFlow() {
// 会签流程测试...
}
@Test
public void testParallelFlow() {
// 并行流程测试...
}
}
状态机模式为复杂审批系统提供了一种优雅而强大的解决方案。通过将审批节点建模为状态,将审批操作建模为事件,我们可以清晰地管理审批流程中的各种复杂场景,包括动态分支、会签、并行等。