首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 0 到 1 打造智能审批引擎:状态机模式如何优雅破解复杂流转难题

从 0 到 1 打造智能审批引擎:状态机模式如何优雅破解复杂流转难题

作者头像
果酱带你啃java
发布2026-04-14 13:02:48
发布2026-04-14 13:02:48
460
举报

引言:审批系统的 "熵增困境" 与状态机的破局之道

在企业级应用开发中,审批系统犹如神经中枢,支撑着从请假流程、费用报销到合同签署等核心业务运转。随着业务复杂度提升,审批流程往往会陷入 "熵增困境":单一流程中嵌套动态分支、多角色会签、并行审批等场景,代码逐渐演变成充斥着 if-else 的 "面条式" 逻辑,维护成本呈指数级增长。

状态机模式(State Pattern)为这种困境提供了优雅的解决方案。它通过将对象的状态封装成独立状态类,将状态转换逻辑集中管理,实现了状态、行为与转换规则的解耦。在审批系统中,这意味着我们可以:

  • 用清晰的状态定义替代模糊的状态标识
  • 用可配置的转换规则替代硬编码的条件判断
  • 用模块化的设计轻松扩展新的审批场景

本文将深入剖析如何基于状态机模式构建支持动态分支、会签、并行等复杂场景的审批引擎,从理论模型到实战代码,全方位展示这一设计模式在企业级应用中的强大威力。

一、审批系统的核心挑战与状态机的适配性分析

1.1 现代审批系统的典型场景与技术痛点

企业级审批系统通常面临以下复杂场景,这些场景直接考验着系统的设计合理性:

  1. 动态分支流转:审批路径并非固定不变,而是根据表单内容动态决定。例如,金额超过 10 万的采购单需要总经理审批,否则部门经理即可审批。
  2. 会签审批:需要多个角色或岗位全部同意才能进入下一环节。例如,项目立项需技术部、财务部、业务部共同审批通过。
  3. 并行审批:多个审批节点可以同时进行,互不影响,全部完成后再进入下一阶段。例如,合同审批中法务审核与财务审核可以并行处理。
  4. 审批回退与撤回:审批人可以将申请退回至前序节点,申请人也可以在审批完成前撤回申请。
  5. 代理审批:审批人可以将自己的审批权限临时委托给他人。

这些场景如果采用传统的硬编码方式实现,会导致:

  • 代码可读性差:大量的条件判断语句交织在一起
  • 可维护性低:修改一个流程细节可能影响多个地方
  • 扩展性弱:新增审批类型或修改流程需要大量代码改动
  • 测试困难:难以覆盖所有分支条件组合

1.2 状态机模式的核心原理与适配优势

状态机(Finite State Machine, FSM)是一种数学模型,由以下要素构成:

  • 状态(State):系统在某一时刻的条件或状况
  • 事件(Event):引起系统状态变化的外界刺激
  • 转换(Transition):从一个状态到另一个状态的过程
  • 动作(Action):在状态转换时执行的操作

状态机模式将这些要素映射到面向对象的设计中,使系统状态管理更加清晰。对于审批系统,这种模式的适配优势体现在:

  1. 状态可视化:每个审批节点都是一个明确的状态,便于理解和管理
  2. 转换规则集中化:状态之间的转换条件被单独管理,避免分散在代码各处
  3. 行为与状态解耦:不同状态下的行为被封装在对应的状态类中
  4. 易于扩展:新增状态或转换规则只需添加新的类,无需修改现有代码

1.3 状态机与审批系统的核心映射关系

为了更好地理解状态机如何应用于审批系统,我们可以建立如下映射关系:

状态机概念

审批系统对应概念

说明

状态(State)

审批节点

如 "部门经理审批"、"财务审核" 等

事件(Event)

审批操作

如 "同意"、"驳回"、"撤回" 等

转换(Transition)

流程流转

从一个审批节点到另一个审批节点的过程

动作(Action)

审批行为

如记录审批意见、通知下一审批人等

初始状态

申请提交

审批流程的起点

终止状态

审批完成 / 终止

审批流程的终点

这种清晰的映射关系为我们设计审批引擎奠定了基础。

二、基于状态机的审批引擎架构设计

2.1 整体架构概览

我们设计的审批引擎采用分层架构,从上到下依次为:

代码语言:javascript
复制
  • API 层:提供 RESTful 接口,接收前端审批操作请求
  • 业务逻辑层:处理审批相关的业务逻辑,如权限校验、通知触发等
  • 状态机核心层:实现状态机模式的核心逻辑,包括状态管理、转换控制等
  • 数据访问层:负责审批数据的持久化操作,基于 MyBatis-Plus 实现
  • 规则引擎层:处理复杂的分支流转规则,决定下一步审批节点
  • 数据库:存储审批单、审批历史、流程定义等数据
  • 通知服务:负责审批通知的发送(如邮件、消息等)
  • 权限服务:提供审批权限校验功能

2.2 核心领域模型设计

基于领域驱动设计(DDD)思想,我们定义以下核心领域模型:

  1. ApprovalInstance(审批实例):代表一个具体的审批流程实例
  2. ApprovalNode(审批节点):代表审批流程中的一个节点
  3. ApprovalTask(审批任务):分配给具体用户的审批任务
  4. ApprovalAction(审批操作):用户执行的审批动作(同意 / 驳回等)
  5. ApprovalHistory(审批历史):记录审批流程的历史轨迹
  6. ProcessDefinition(流程定义):定义审批流程的模板,包括节点、流转规则等

这些模型之间的关系如下:

代码语言:javascript
复制

2.3 状态机核心组件设计

状态机核心层包含以下关键组件:

  1. StateManager(状态管理器):负责状态的注册、查询和管理
  2. StateMachine(状态机):核心控制器,协调状态转换
  3. State(状态接口):定义状态的基本行为
  4. AbstractState(抽象状态类):实现状态接口的通用方法
  5. Transition(转换规则):定义状态转换的条件和目标
  6. Event(事件枚举):定义触发状态转换的事件类型

组件协作流程如下:

代码语言:javascript
复制

三、核心代码实现:构建状态机审批引擎

3.1 项目依赖配置

首先,我们需要配置 Maven 依赖,确保使用最新的稳定版本:

代码语言:javascript
复制
<?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>
代码语言:javascript
复制

3.2 数据库设计

我们使用 MySQL 8.0 作为数据库,创建以下核心表结构:

代码语言:javascript
复制
-- 流程定义表
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='会签审批结果表';
代码语言:javascript
复制

3.3 核心领域模型实现

下面实现核心的领域模型类:

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

代码语言:javascript
复制
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;
}
代码语言:javascript
复制
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;
}
代码语言:javascript
复制
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;
}
代码语言:javascript
复制
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;
}
代码语言:javascript
复制
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;
}

3.4 状态机核心组件实现

首先定义事件枚举:

代码语言:javascript
复制
package com.example.approvalengine.state.event;

/**
 * 审批事件枚举
 *
 * @author ken
 */
public enum ApprovalEvent {
    /**
     * 提交审批
     */
    SUBMIT,
    /**
     * 同意
     */
    AGREE,
    /**
     * 驳回
     */
    REJECT,
    /**
     * 撤回
     */
    WITHDRAW,
    /**
     * 转交
     */
    TRANSFER,
    /**
     * 跳过
     */
    SKIP,
    /**
     * 终止
     */
    TERMINATE
}
代码语言:javascript
复制

定义状态接口:

代码语言:javascript
复制
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);
}
代码语言:javascript
复制

实现抽象状态类:

代码语言:javascript
复制
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());
    }
}
代码语言:javascript
复制

定义状态上下文:

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

定义状态处理结果:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

实现状态管理器:

代码语言:javascript
复制
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();
    }
}
代码语言:javascript
复制

实现核心状态机类:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

3.5 具体状态实现

实现开始状态:

代码语言:javascript
复制
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");
    }
}
代码语言:javascript
复制

实现普通审批状态:

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

实现会签状态:

代码语言:javascript
复制
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());
    }
}
代码语言:javascript
复制

实现并行审批状态:

代码语言:javascript
复制
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());
    }
}
代码语言:javascript
复制

实现结束状态:

代码语言:javascript
复制
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());
    }
}
代码语言:javascript
复制

3.6 服务层实现

实现节点转换服务:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

实现规则评估器(简化版):

代码语言:javascript
复制
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;
        }
    }
}
代码语言:javascript
复制

3.7 API 层实现

代码语言:javascript
复制
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);
    }
}
代码语言:javascript
复制

四、复杂场景实战:动态分支、会签与并行审批

4.1 动态分支审批实现

动态分支是指审批流程根据业务数据的不同而走向不同的审批路径。例如,我们设计一个费用报销审批流程:

  • 金额 ≤ 1000 元:部门经理审批 → 结束
  • 1000 元 < 金额 ≤ 5000 元:部门经理审批 → 财务经理审批 → 结束
  • 金额 > 5000 元:部门经理审批 → 财务经理审批 → 总经理审批 → 结束

首先,我们需要在数据库中配置这个流程的定义:

代码语言:javascript
复制
-- 插入流程定义
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);
代码语言:javascript
复制

当部门经理同意审批后,规则引擎会根据金额判断下一步该走向哪个节点:

代码语言:javascript
复制
// 这部分逻辑已在NodeTransitionService中实现
// 当处理部门经理同意事件时,会调用以下代码
String nextNodeKey = transitionService.findNextNodeKey(
    instance.getProcessId(), "DEPT_MANAGER", "AGREE", businessData);
代码语言:javascript
复制

其中businessData包含了报销金额等信息,规则引擎会根据配置的条件表达式进行判断,返回正确的下一个节点标识。

4.2 会签审批实现

会签审批要求多个角色或用户都审批通过后才能进入下一环节。例如,项目立项审批需要技术部、财务部和业务部三个部门的经理全部同意。

配置会签流程:

代码语言:javascript
复制
-- 假设流程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);
代码语言:javascript
复制

当流程进入会签节点时,CountersignStateonEnter方法会创建多个审批任务,分别分配给技术部经理、财务部经理和业务部经理:

代码语言:javascript
复制
// 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);
    }
}
代码语言:javascript
复制

每个处理人完成审批后,系统会记录其审批结果。当最后一个处理人完成审批后,系统会判断所有结果:

代码语言:javascript
复制
// 检查是否所有任务都已完成
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;
    // ...
}
代码语言:javascript
复制

4.3 并行审批实现

并行审批允许多个审批节点同时进行,全部完成后再进入下一环节。例如,合同审批中,法务审核和财务审核可以并行处理。

配置并行审批流程:

代码语言:javascript
复制
-- 假设流程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);
代码语言:javascript
复制

当流程进入并行节点时,ParallelApprovalStateonEnter方法会创建并行子节点的任务:

代码语言:javascript
复制
// 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);
    }
}
代码语言:javascript
复制

每个子节点的审批完成后,都会回到并行节点。系统会检查是否所有并行子节点都已完成:

代码语言:javascript
复制
// 检查所有并行子节点是否都已完成
List<ParallelNodeRelation> allRelations = parallelNodeRelationService.listByInstanceAndParentNode(
    instance.getId(), getStateKey());

boolean allCompleted = allRelations.stream()
    .allMatch(r -> "COMPLETED".equals(r.getStatus()));

if (allCompleted) {
    // 所有并行节点都已完成,进入下一个节点
    // ...
}
代码语言:javascript
复制

五、状态机审批引擎的高级特性与扩展

5.1 审批回退与撤回机制

在实际业务中,审批回退和撤回是常见需求。我们可以通过状态机模式优雅地实现这些功能:

  1. 审批回退:审批人可以将审批单退回至前序节点。实现时需要:
    • 在状态转换规则中定义回退路径
    • 保存审批历史,以便回退时恢复前序状态
    • 处理回退时的任务重新分配
代码语言:javascript
复制
/**
 * 处理回退事件
 */
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);
}
代码语言:javascript
复制

  1. 审批撤回:申请人可以在审批完成前撤回申请。实现时需要:
    • 检查申请人权限
    • 终止当前所有审批任务
    • 将状态转换为已撤回
代码语言:javascript
复制
/**
 * 处理撤回事件
 */
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);
}
代码语言:javascript
复制

5.2 审批代理与权限控制

审批代理功能允许用户将自己的审批权限临时委托给他人。实现这一功能需要:

  1. 设计代理规则表,存储代理关系
  2. 在任务分配时检查是否有有效的代理关系
  3. 在权限验证时考虑代理关系
代码语言:javascript
复制
/**
 * 获取实际处理人(考虑代理关系)
 */
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;
}
代码语言:javascript
复制

权限控制则需要在处理审批操作前验证当前用户是否有权限:

代码语言:javascript
复制
/**
 * 验证审批权限
 */
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);
}
代码语言:javascript
复制

5.3 与工作流引擎的对比与集成

本文实现的状态机审批引擎与主流工作流引擎(如 Activiti、Flowable)相比,有以下特点:

特性

状态机审批引擎

主流工作流引擎

复杂度

简单,轻量级

复杂,全功能

学习曲线

平缓

陡峭

灵活性

高,易于定制

中等,受规范约束

性能

中等

适用场景

中小型审批场景

复杂流程场景

在实际项目中,可以根据需求选择:

  1. 简单场景:直接使用本文实现的状态机审批引擎
  2. 复杂场景:集成主流工作流引擎
  3. 混合场景:核心审批逻辑使用状态机,复杂流程部分集成工作流引擎

集成示例(与 Flowable 集成):

代码语言:javascript
复制
/**
 * 与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;
    }
}
代码语言:javascript
复制

六、性能优化与最佳实践

6.1 状态机引擎的性能优化

为了提高状态机审批引擎的性能,可以采用以下优化策略:

  1. 状态缓存:将常用的状态定义和转换规则缓存到内存中,减少数据库访问
代码语言:javascript
复制
/**
 * 带缓存的节点转换服务
 */
@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);
        }
    }

    // 其他方法...
}
代码语言:javascript
复制

  1. 异步处理:将审批通知、日志记录等非核心操作异步化
代码语言:javascript
复制
/**
 * 状态机测试示例
 */
@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() {
        // 并行流程测试...
    }
}
代码语言:javascript
复制

  1. 数据库优化:为常用查询创建合适的索引,使用分页查询等

6.2 最佳实践与设计原则

在使用状态机模式开发审批系统时,应遵循以下最佳实践:

  1. 单一职责原则:每个状态类只负责处理与其相关的事件和行为
  2. 开闭原则:新增状态或转换规则时,应通过添加新类实现,而非修改现有代码
  3. 状态可视化:使用工具将状态转换关系可视化,便于理解和维护
  4. 异常处理:在状态转换过程中妥善处理异常,确保状态一致性
  5. 事务管理:状态转换操作应在事务中执行,确保数据一致性
  6. 审计日志:记录所有状态转换和审批操作,便于追溯
  7. 可测试性:设计时考虑可测试性,为每个状态和转换路径编写单元测试
代码语言:javascript
复制
/**
 * 状态机测试示例
 */
@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() {
        // 并行流程测试...
    }
}
代码语言:javascript
复制

七、总结与展望

状态机模式为复杂审批系统提供了一种优雅而强大的解决方案。通过将审批节点建模为状态,将审批操作建模为事件,我们可以清晰地管理审批流程中的各种复杂场景,包括动态分支、会签、并行等。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言:审批系统的 "熵增困境" 与状态机的破局之道
  • 一、审批系统的核心挑战与状态机的适配性分析
    • 1.1 现代审批系统的典型场景与技术痛点
    • 1.2 状态机模式的核心原理与适配优势
    • 1.3 状态机与审批系统的核心映射关系
  • 二、基于状态机的审批引擎架构设计
    • 2.1 整体架构概览
    • 2.2 核心领域模型设计
    • 2.3 状态机核心组件设计
  • 三、核心代码实现:构建状态机审批引擎
    • 3.1 项目依赖配置
    • 3.2 数据库设计
    • 3.3 核心领域模型实现
    • 3.4 状态机核心组件实现
    • 3.5 具体状态实现
    • 3.6 服务层实现
    • 3.7 API 层实现
  • 四、复杂场景实战:动态分支、会签与并行审批
    • 4.1 动态分支审批实现
    • 4.2 会签审批实现
    • 4.3 并行审批实现
  • 五、状态机审批引擎的高级特性与扩展
    • 5.1 审批回退与撤回机制
    • 5.2 审批代理与权限控制
    • 5.3 与工作流引擎的对比与集成
  • 六、性能优化与最佳实践
    • 6.1 状态机引擎的性能优化
    • 6.2 最佳实践与设计原则
  • 七、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档