【Springboot5】审批流工作流引擎(业务、审批分离)排除if else

处理审批业务,相较于传统的if else改状态(处理单一业务),审批流(处理大量复杂任务,或签、会签、条件签)比较省事。因此写一个轻量级、与业务完全解耦的通用工作流引擎,能避免屎山。必须把"业务单据"和"审批规则"分离开来

业务表基础字段(所有业务表必须包含)

为了能让引擎统一操作,无论是请假表还是报销表,都必须包含以下核心流程字段:

bash 复制代码
ALTER TABLE `业务表`
  ADD COLUMN `workflow_id` varchar(32) COMMENT '绑定的流程图ID',
  ADD COLUMN `node_id` varchar(32) COMMENT '当前停留的节点ID',
  ADD COLUMN `node_code` varchar(50) COMMENT '当前停留的节点编码(如: HR_AUDIT)',
  ADD COLUMN `node_name` varchar(50) COMMENT '当前停留的节点名称(如: 人事审批)',
  ADD COLUMN `vstatus` char(1) DEFAULT '0' COMMENT '审批状态(0:待审 1:通过 2:退回 3:终止)',
  ADD COLUMN `vcondition` varchar(50) COMMENT '路由条件(比如存"请假>3天",供引擎走分支判断)',
  ADD COLUMN `opinions` varchar(500) COMMENT '最新一次的审核意见';

流程节点配置表(t_workflow_node)

管理员在后台配置好,引擎在流转时就会来查这张表找下一个目标。

bash 复制代码
CREATE TABLE `t_workflow_node` (
  `id` varchar(32) NOT NULL COMMENT '主键',
  `workflow_id` varchar(32) NOT NULL COMMENT '所属流程ID(如: 请假流程ID)',
  `node_code` varchar(50) NOT NULL COMMENT '节点编码(如: start, end, manager_audit)',
  `node_name` varchar(50) NOT NULL COMMENT '节点名称(如: 经理审批)',
  `parent_node_code` varchar(50) COMMENT '上一个节点的编码(用于串联整个流程图)',
  `audit_type` char(1) COMMENT '审核人类型(0:指定人员 1:指定角色 4:发起人自己)',
  `auditor_id` varchar(32) COMMENT '审核人ID(如果audit_type=0)',
  `role_id` varchar(32) COMMENT '审核角色ID(如果audit_type=1)',
  `countersignature` char(1) DEFAULT '0' COMMENT '是否会签?(0:或签-一人同意即可, 1:会签-所有人同意)',
  `vcondition` varchar(100) COMMENT '进入该节点的条件(如金额大小,用于分支路由)',
  PRIMARY KEY (`id`)
) COMMENT='流程节点配置表';

审核流水/待办任务表 (t_workflow_audit)

所有的待办任务、已办记录全在这里。引擎的流转,本质上就是不断往这张表里insert新节点的待办人,并update老节点的状态。

bash 复制代码
CREATE TABLE `t_workflow_audit` (
  `id` varchar(32) NOT NULL COMMENT '主键',
  `workflow_id` varchar(32) NOT NULL COMMENT '所属流程ID',
  `object_id` varchar(32) NOT NULL COMMENT '具体业务单据的ID(如请假单的ID)',
  `object_name` varchar(100) COMMENT '业务单据名称(如: 张三的请假单)',
  `node_id` varchar(32) NOT NULL COMMENT '当前节点ID',
  `node_code` varchar(50) NOT NULL COMMENT '当前节点编码',
  `node_name` varchar(50) NOT NULL COMMENT '当前节点名称',
  `auditor_id` varchar(32) NOT NULL COMMENT '该条任务的审核人ID(谁来批)',
  `auditor_name` varchar(50) COMMENT '审核人姓名',
  `vstatus` char(1) DEFAULT '0' COMMENT '审核状态(0:待审 1:同意 2:退回)',
  `opinions` varchar(500) COMMENT '该审核人填写的审核意见',
  `finish_flag` char(1) DEFAULT '0' COMMENT '是否流转结束标志',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '任务到达时间',
  `audit_time` datetime COMMENT '实际审核时间',
  PRIMARY KEY (`id`)
) COMMENT='流程审核流水表';

一、业务表

为了让引擎能统一处理各种奇形怪状的业务单据,我们需要定义一个泛型上限 ProcBean。所有的业务表(如请假表、采购表等)对应的实体类,都必须继承它。

java 复制代码
import lombok.Data;
import java.io.Serializable;

/**
 * 流程业务单据基类
 * 任何需要接入工作流的业务实体(如请假单、采购单)都必须继承此类
 */
@Data
public class ProcBean implements Serializable {
    // 业务基础字段
    private String id;             // 业务单据主键ID
    private String objectId;       // 业务单据ID (与id一致)
    private String objectNo;       // 业务单据编号
    private String objectName;     // 业务单据名称(如:张三的请假单)
    private String preObjectId;    // 前置关联单据ID(如:采购申请关联的报价单)
    private String preObjectName;  // 前置关联单据名称
    
    // 流程核心控制字段
    private String workflowId;     // 当前绑定的流程图ID
    private String nodeId;         // 当前卡在哪个流程节点的ID
    private String nodeCode;       // 当前节点编码
    private String nodeName;       // 当前节点名称
    private String vstatus;        // 状态 (0:待审 1:通过 2:退回)
    private String vcondition;     // 路由条件分支(如填入 ">5000" 让引擎判断)
    private String opinions;       // 最新的审批意见
    
    // 基础溯源字段
    private String dataOrgId;      // 数据所属部门机构ID
    private String createdById;    // 创建人(发起人)ID
    private String createdByName;  // 创建人(发起人)姓名
}

二、BaseFlowController.java

既然是通用流转,前端调用的无非就是**"发起审批"、"同意"、"驳回"**。可以利用 Java 8 接口的default方法,写一个统一的BaseFlowController。新业务只需要继承这个接口,零代码就能拥有完整的审批端点。

java 复制代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * 通用流程控制层接口
 * 利用 default 方法实现通用路由挂载,新业务直接继承即可拥有所有审批接口
 */
public interface BaseFlowController<S extends BaseFlowService, T> extends BaseController<S,T>{

    /**
     * 发起/初始化流程
     */
    @PostMapping("/insertFlowList")
    default JsonResult insertFlowList(@RequestBody FlowObjBean objBean){
        String flowCode = objBean.getFlowCode();
        if (PxStringUtils.isEmpty(flowCode)){
            return JsonResult.error("流程编码【flowCode】不能为空!");
        }
        // 直接转交底层的泛型 Service 处理
        return getService().insertFlowList(objBean);
    }

    /**
     * 审核通过 (Pass)
     */
    @PostMapping("/pass")
    default JsonResult pass(@RequestBody FlowObjBean objBean){
        return getService().pass(objBean);
    }

    /**
     * 审核驳回/拒绝 (Back)
     */
    @PostMapping("/back")
    default JsonResult back(@RequestBody FlowObjBean objBean){
        if (objBean == null){
            return JsonResult.error("参数不能为空!");
        }
        String opinion = objBean.getOpinions();
        if (PxStringUtils.isEmpty(opinion)){
            return JsonResult.error("退回原因不能为空!");
        }
        return getService().back(objBean);
    }
}

三、WorkflowEngineService.java

引擎完全不知道当前处理的是请假单还是报销单,修改具体业务表状态使用 Java 泛型边界<T extends ProcBean>+ Spring IOC 容器动态获取 Service 实例。

1、initWorkflowNode方法

当用户点击"提交审批"时,调用此方法为单据装载第一个节点,并通知第一位审核人。

java 复制代码
/**
     * 初始化流程 (发起审批)
     * @param list 业务单据列表 (支持批量发起)
     * @param flowCode 流程图编码 (如:LEAVE_FLOW)
     * @param user 当前登录用户 (发起人)
     * @param serviceImplName 重点!具体的业务Service的Bean名称 (如:leaveServiceImpl)
     */
    @Transactional
    public <T extends ProcBean> void initWorkflowNode(List<T> list, String flowCode, UserBean user, String serviceImplName) {
        Assert.notNull(list,"数据列表不能为空!");
        Assert.notNull(flowCode,"流程编码不能为空");
        Assert.notNull(serviceImplName,"service层文件名不能为空!");
        
        LocalDateTime now = LocalDateTime.now();
        // 1. 查询该流程图的【第一个节点】配置 (即起点)
        WorkflowNodeBean workflowNodeBean = workflowNodeMapper.selectFirstNodeBeanByCode(flowCode);
        if(workflowNodeBean == null){
            throw new RuntimeException("流程已停用或没有设置初始节点!");
        }
        
        List<WorkflowAuditBean> insertBeans = new ArrayList<>();
        // 2. 遍历业务单据,写入节点信息,并生成第一条审核流水记录
        for (T addbean : list){
            // 生成待办流水记录给第一个审批人
            insertBeans.add(this.initFlowAuditBean((ProcBean) addbean, workflowNodeBean, user, now));
            
            // 将流程图ID、节点ID等状态信息回写到业务单据实体中
            addbean.setWorkflowId(workflowNodeBean.getWorkflowId());
            addbean.setNodeId(workflowNodeBean.getId());
            addbean.setNodeCode(workflowNodeBean.getNodeCode());
            addbean.setNodeName(workflowNodeBean.getNodeName());
        }
        
        // 批量插入待办流水
        if (PxListUtils.isNotEmpty(insertBeans)){
            workflowAuditMapper.insertList(insertBeans);
        }
        
        // 3. 【核心解耦魔法】通过前端传来的 serviceImplName,动态抓取具体业务的 Service!
        // 引擎不管你是请假单还是报销单,直接调用你的通用更新方法,将状态刷入对应业务表
        BaseService service = PxApplicationContextUtils.getService(serviceImplName);
        for(T bean : list){
            service.getMapper().updateById(bean);
        }
    }

2、审批通过(pass)

处理常规的流转,且实现会签/或签策略和基于vcondition的条件分支路由。

java 复制代码
/**
     * 审核通过核心驱动
     * @param auditBeanList 前端传来的待处理业务单据集合
     * @param objBean 包装了审核意见(opinions)等额外参数的对象
     * @param loginUser 当前点击同意的登录用户
     * @param serviceImplName 业务Service的Bean名称
     */
    @Transactional
    public <T extends ProcBean> String pass(List<T> auditBeanList, FlowObjBean objBean, UserBean loginUser , String serviceImplName) {
        String nextNodeCode = objBean.getNextNodeCode();
        String opinions = objBean.getOpinions();
        if(PxListUtils.isEmpty(auditBeanList)){
            throw new RuntimeException("流程数据不能为空!");
        }
        String workFlowId = auditBeanList.get(0).getWorkflowId();
        
        // 1. 获取当前节点信息:比对流程图,确认当前走到哪一步了
        List<String> nodeCodes = auditBeanList.stream().map(ProcBean::getNodeCode).distinct().collect(Collectors.toList());
        List<WorkflowNodeBean> currBeans = workflowNodeMapper.selectCurrBeansByCodes(workFlowId, nodeCodes);
        if(PxListUtils.isEmpty(currBeans)){
            throw new RuntimeException("流程节点不存在,或者流程已停用!");
        }

        // 2. 预查询可能的【下一个节点】集合
        List<WorkflowNodeBean> nextNodeBeans;
        Map<String,Object> param = new HashMap<>();
        if (PxStringUtils.isNotEmpty(nextNodeCode)){
            // 如果前端强制指定了下一节点
            param.put("workflowId",workFlowId);
            param.put("nodeCode", nextNodeCode);
            nextNodeBeans = workflowNodeMapper.selectListByParam(param);
        } else {
            // 根据流程图连线,自动找出所有的子节点
            nextNodeBeans = workflowNodeMapper.selectNextBeansByCodes(workFlowId, nodeCodes);
        }

        List<WorkflowAuditBean> updateBeans = new ArrayList<>();
        List<WorkflowAuditBean> insertBeans = new ArrayList<>();
        List<WorkflowAuditBean> deleteBeans = new ArrayList<>();
        List<T> passBeans = new ArrayList<>();

        for (WorkflowNodeBean currBean : currBeans){
            List<String> objectIds = auditBeanList.stream().filter(t-> t.getNodeId().equals(currBean.getId())).map(ProcBean::getId).distinct().collect(Collectors.toList());

            // 3. 查出当前节点下所有的待办任务
            param.put("nodeId", currBean.getId());
            param.put("workflowId", currBean.getWorkflowId());
            param.put("finishFlag", "0");
            param.put("objectIdList", objectIds);
            List<WorkflowAuditBean> workflowAuditBeans = workflowAuditMapper.selectListByParam(param);
            
            // 找出属于当前登录人的待办任务
            List<WorkflowAuditBean> updates = workflowAuditBeans.stream().filter(t -> t.getAuditorId().equals(loginUser.getId())).collect(Collectors.toList());
            
            // 防越权:判断当前用户是否有审核权限,是否重复审核
            this.judgeIsAudited(updates, objectIds);
            updateBeans.addAll(updates);
            
            // 4.判断是【会签】还是【或签】
            String countersignature = currBean.getCountersignature();
            List<T> addBeans;
            if (PxStringUtils.isNotEmpty(countersignature) && "1".equals(countersignature)){
                // 【会签逻辑】:必须所有人同意。查找是否还有其他人的待办状态是待审(vstatus=0)
                List<String> updateIds = updates.stream().map(WorkflowAuditBean::getId).collect(Collectors.toList());
                List<String> remainObjectIds = workflowAuditBeans.stream().filter(t -> !updateIds.contains(t.getId()) && "0".equals(t.getVstatus())).map(WorkflowAuditBean::getObjectId).collect(Collectors.toList());
                // 只有全部人审完,单据才能进入下一环节 (过滤掉还在等待其他人的单据)
                addBeans = auditBeanList.stream().filter(t -> !remainObjectIds.contains(t.getObjectId())).collect(Collectors.toList());
            } else {
                // 【或签逻辑】:只要当前人同意即可。
                addBeans = auditBeanList;
                // 立刻物理删除同节点下其他人的待办任务!(比如张三批了,李四桌上的任务就消失了)
                deleteBeans.addAll(workflowAuditBeans.stream().filter(t->!t.getAuditorId().equals(loginUser.getId()) && "0".equals(t.getVstatus())).collect(Collectors.toList()));
            }

            // 5. 将过滤后允许通行的单据,推送到【下一个节点】
            if (PxListUtils.isNotEmpty(addBeans)) {
                LocalDateTime now = LocalDateTime.now();
                List<WorkflowNodeBean> nexts = nextNodeBeans.stream().filter(t -> t.getParentNodeCode().equals(currBean.getNodeCode())).collect(Collectors.toList());
                
                if (nexts.size() == 1) {
                    // 单线流转:直接去唯一的下一站
                    for (T addbean : addBeans){
                        if ("end".equals(nexts.get(0).getNodeCode().toLowerCase())){
                            // 如果下一站是结束节点,生成归档流水
                            insertBeans.add(this.initFlowAuditBean(addbean, nexts.get(0), null, now));
                        } else {
                            // 动态查询下一站的审核人(可能是按角色查出一批人),并为他们生成待办流水
                            List<UserBean> users = this.getNextNodeAuditors(nexts.get(0), addbean, addbean.getDataOrgId());
                            for (UserBean user : users){
                                insertBeans.add(this.initFlowAuditBean(addbean, nexts.get(0), user, now));
                            }
                        }
                        addbean.setVstatus("1"); // 标记业务状态为审核通过
                        addbean.setOpinions("");
                        this.initObjectNode(addbean, nextNodeBeans.get(0));
                    }
                } else {
                    // 【分支路由流转】:下一站有多个分叉(如:>1万走老板,<1万走主管)
                    for (T addbean : addBeans){
                        boolean hasPass = false;
                        for (WorkflowNodeBean nextNodeBean : nexts){
                            // 比对业务单据里携带的条件 (addbean.getVcondition()) 和 路线图配置的条件
                            if (nextNodeBean.getVcondition() != null && nextNodeBean.getVcondition().equals(addbean.getVcondition())){
                                // 找到匹配的分支路线!
                                if ("end".equals(nextNodeBean.getNodeCode().toLowerCase())){
                                    insertBeans.add(this.initFlowAuditBean(addbean, nextNodeBean, null, now));
                                } else {
                                    List<UserBean> users = this.getNextNodeAuditors(nextNodeBean, addbean, addbean.getDataOrgId());
                                    for (UserBean user : users){
                                        insertBeans.add(this.initFlowAuditBean(addbean, nextNodeBean, user, now));
                                    }
                                }
                                addbean.setVstatus("1");
                                this.initObjectNode(addbean, nextNodeBean);
                                hasPass = true;
                                break;
                            }
                        }
                        if (!hasPass){
                            throw new RuntimeException("分支条件不满足,未能找到下一个节点!");
                        }
                    }
                }
                passBeans.addAll(addBeans);
            }
        }

        // 6. 执行最终的数据库持久化操作
        if (PxListUtils.isNotEmpty(passBeans)){
            // 回写更新具体业务表的状态!
            BaseService service = PxApplicationContextUtils.getService(serviceImplName);
            for(T updateObject : passBeans){
                service.updateByPrimaryKey(updateObject);
            }
        }
        // 更新旧待办任务为已办,填入审核意见
        if (PxListUtils.isNotEmpty(updateBeans)){
            LocalDateTime now = LocalDateTime.now();
            for(WorkflowAuditBean updateBean : updateBeans){
                updateBean.setVstatus("1");
                updateBean.setOpinions(opinions);
                updateBean.setAuditTime(now);
            }
            this.workflowAuditMapper.updateStatus(updateBeans);
        }
        // 销毁或签中废弃的其他任务
        if (PxListUtils.isNotEmpty(deleteBeans)){
            this.workflowAuditMapper.deleteBatchIds(deleteBeans.stream().map(WorkflowAuditBean::getId).collect(Collectors.toList()));
        }
        // 推送下一步的新待办任务
        if (PxListUtils.isNotEmpty(insertBeans)){
            this.workflowAuditMapper.insertList(insertBeans);
        }
        return "SUCCESS";
    }

3、审核驳回(back)

直接将单据打回给原发起人,并强制终止当前的流程环节。

java 复制代码
/**
     * 审核退回/驳回
     */
    @Transactional
    public <T extends ProcBean> void back(List<T> objBeanList, FlowObjBean objBean, String serviceImplName) {
        String opinions = objBean.getOpinions();
        String workFlowId = objBeanList.get(0).getWorkflowId();
        UserBean loginUser = PxLocalContextHelper.getLoginUser();

        // 1. 获取当前节点所有审核记录,校验权限
        List<String> objectIds = objBeanList.stream().map(ProcBean::getId).distinct().collect(Collectors.toList());
        Map<String,Object> param = new HashMap<>();
        param.put("workflowId", workFlowId);
        param.put("objectIdList", objectIds);
        param.put("finishFlag", "0");
        param.put("vstatus", "0");
        List<WorkflowAuditBean> workflowAuditBeans = workflowAuditMapper.selectListByParam(param);
        
        List<WorkflowAuditBean> updateBeans = workflowAuditBeans.stream().filter(t->t.getAuditorId().equals(loginUser.getId())).collect(Collectors.toList());
        this.judgeIsAudited(updateBeans, objectIds);
        
        // 驳回时,必须删除该节点其他还没审核的待办记录
        List<WorkflowAuditBean> deleteBeans = workflowAuditBeans.stream().filter(t->!t.getAuditorId().equals(loginUser.getId())).collect(Collectors.toList());

        // 2. 找到流程的起始节点 (退回给发起人)
        WorkflowNodeBean workflowNodeBean = workflowNodeMapper.selectFirstNodeByFlowId(workFlowId);
        
        List<WorkflowAuditBean> insertBeans = new ArrayList<>();
        LocalDateTime now = LocalDateTime.now();
        // 3. 生成退回流水通知发起人
        for (WorkflowAuditBean auditBean : updateBeans){
            T t1 = objBeanList.stream().filter(t -> t.getObjectId().equals(auditBean.getObjectId())).findFirst().orElse(null);
            WorkflowAuditBean newAuditBean = new WorkflowAuditBean();
            // ... 基础属性赋值省略
            newAuditBean.setAuditorId(t1.getCreatedById()); // 审核人设为原发起人
            newAuditBean.setNodeId(workflowNodeBean.getId());
            newAuditBean.setVstatus("2"); // 状态标记为退回
            insertBeans.add(newAuditBean);
        }

        // 4. 将业务单据的状态强行改为"退回(2)",节点重置为起点,并附上驳回意见
        if (PxListUtils.isNotEmpty(objBeanList)){
            BaseService service = PxApplicationContextUtils.getService(serviceImplName);
            for(T updateObject : objBeanList){
                updateObject.setNodeId(workflowNodeBean.getId());
                updateObject.setNodeCode(workflowNodeBean.getNodeCode());
                updateObject.setOpinions(opinions);
                updateObject.setVstatus("2"); 
                service.updateByPrimaryKey(updateObject);
            }
        }
        
        // 5. 落库持久化操作
        // 更新原待办为退回状态,删除其他冗余待办,插入退回通知记录
    
    }
相关推荐
真心喜欢你吖2 小时前
OpenClaw安装部署Mac操作系统版 - 打造你的专属AI助理
java·人工智能·macos·ai·语言模型·智能体·openclaw
LSL666_2 小时前
JVM面试题——垃圾收集器
java·jvm·面试·垃圾收集器
Via_Neo2 小时前
今天是周六,两天后是周几?
java·数据结构·算法
星晨雪海2 小时前
Redis-逻辑查询详情讲解
java·开发语言
chools2 小时前
Java后端拥抱AI开发之个人学习路线 - - Spring AI【第二期】
java·人工智能·学习·spring·ai
uNke DEPH2 小时前
MySQL中常见函数
java
大鹏说大话2 小时前
Java线程池调优实战:从核心参数到避坑指南
java·开发语言
※DX3906※2 小时前
SpringBoot之旅5| 快速上手SpringAOP、深入刨析动态/静态两种代理模式
java·数据库·spring boot·后端·spring·java-ee·代理模式
jwt7939279373 小时前
基于SpringBoot和Leaflet的行政区划地图掩膜效果实战
java·spring boot·后端