审批流引擎实现方案:让审批流程自动化运转!📋

74 阅读9分钟

标题: 审批流还在写死代码?工作流引擎来救场!
副标题: 从简单审批到复杂流程,灵活配置一站式解决


🎬 开篇:一次审批流需求的噩梦

产品经理:我们要加一个请假审批功能
开发:好的(写死代码:员工 -> 主管 -> 经理)

1个月后...
产品:请假3天以上要总监审批
开发:改代码... 💦

2个月后...
产品:财务部门要走单独流程
开发:再改... 😰

3个月后...
产品:支持多人会签、条件分支、并行审批
开发:我裂开了... 💀

半年后...
产品:能不能让业务人员自己配置流程?
开发:早这样说啊!(重构工作流引擎)

引入工作流引擎后:
- 配置化流程(无需改代码)✅
- 支持复杂场景(会签、或签、条件)✅
- 可视化设计(拖拽配置)✅
- 审批时效监控 ✅

老板:这才对嘛! 😊

教训:审批流要做成引擎,而不是硬编码!

🤔 什么是审批流引擎?

想象一下:

  • 请假申请: 员工 -> 主管 -> 经理(串行审批)
  • 报销申请: 员工 -> 财务+主管(并行审批)
  • 合同审批: 法务+财务+老板全部通过(会签)
  • 采购审批: 金额>1万走总监,<=1万走经理(条件分支)

审批流引擎 = 工作流 + 状态机 + 规则引擎!


📚 知识地图

审批流引擎
├── 🏗️ 核心概念
│   ├── 流程定义(模板)
│   ├── 流程实例(运行中)
│   ├── 任务节点(待办)
│   ├── 流转规则(路由)
│   └── 审批人(处理人)
├── 🎯 功能模块
│   ├── 流程设计器
│   ├── 流程引擎
│   ├── 任务管理
│   ├── 历史记录
│   └── 监控统计
├── ⚡ 核心特性
│   ├── 串行审批
│   ├── 并行审批
│   ├── 会签(全部通过)
│   ├── 或签(一人通过)
│   ├── 条件分支
│   ├── 自动审批
│   ├── 审批撤回
│   └── 审批转交
└── 🔧 实现方案
    ├── Activiti/Flowable⭐⭐⭐⭐⭐
    ├── Camunda⭐⭐⭐⭐
    └── 自研工作流⭐⭐⭐

💾 数据库设计

-- 流程定义表
CREATE TABLE workflow_definition (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    code VARCHAR(50) NOT NULL COMMENT '流程编码',
    name VARCHAR(100) NOT NULL COMMENT '流程名称',
    category VARCHAR(50) COMMENT '流程分类',
    version INT NOT NULL DEFAULT 1 COMMENT '版本号',
    content TEXT NOT NULL COMMENT '流程定义内容(JSON)',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0草稿 1发布 2停用',
    remark VARCHAR(500) COMMENT '备注',
    creator_id BIGINT NOT NULL COMMENT '创建人ID',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_code_version (code, version),
    INDEX idx_code (code),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程定义表';

-- 流程实例表
CREATE TABLE workflow_instance (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    definition_id BIGINT NOT NULL COMMENT '流程定义ID',
    definition_code VARCHAR(50) NOT NULL COMMENT '流程编码',
    definition_version INT NOT NULL COMMENT '流程版本',
    business_key VARCHAR(100) NOT NULL COMMENT '业务关联KEY',
    business_type VARCHAR(50) NOT NULL COMMENT '业务类型:leave/expense',
    title VARCHAR(200) NOT NULL COMMENT '流程标题',
    current_node_id BIGINT COMMENT '当前节点ID',
    current_node_name VARCHAR(100) COMMENT '当前节点名称',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1进行中 2已完成 3已拒绝 4已撤回',
    initiator_id BIGINT NOT NULL COMMENT '发起人ID',
    start_time DATETIME NOT NULL COMMENT '开始时间',
    end_time DATETIME COMMENT '结束时间',
    duration INT COMMENT '耗时(秒)',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_definition (definition_id),
    INDEX idx_business (business_type, business_key),
    INDEX idx_initiator (initiator_id),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程实例表';

-- 任务表
CREATE TABLE workflow_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    instance_id BIGINT NOT NULL COMMENT '流程实例ID',
    node_id VARCHAR(50) NOT NULL COMMENT '节点ID',
    node_name VARCHAR(100) NOT NULL COMMENT '节点名称',
    node_type TINYINT NOT NULL COMMENT '节点类型:1串行 2并行 3会签 4或签',
    assignee_id BIGINT NOT NULL COMMENT '处理人ID',
    assignee_name VARCHAR(50) NOT NULL COMMENT '处理人姓名',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1待处理 2已同意 3已拒绝 4已转交',
    comment VARCHAR(500) COMMENT '审批意见',
    claim_time DATETIME COMMENT '签收时间',
    complete_time DATETIME COMMENT '完成时间',
    duration INT COMMENT '耗时(秒)',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_instance (instance_id),
    INDEX idx_assignee_status (assignee_id, status),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务表';

-- 流转历史表
CREATE TABLE workflow_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    instance_id BIGINT NOT NULL COMMENT '流程实例ID',
    task_id BIGINT COMMENT '任务ID',
    from_node VARCHAR(50) COMMENT '源节点',
    to_node VARCHAR(50) COMMENT '目标节点',
    action TINYINT NOT NULL COMMENT '动作:1提交 2同意 3拒绝 4撤回 5转交',
    operator_id BIGINT NOT NULL COMMENT '操作人ID',
    operator_name VARCHAR(50) NOT NULL COMMENT '操作人姓名',
    comment VARCHAR(500) COMMENT '意见',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_instance (instance_id),
    INDEX idx_task (task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流转历史表';

🎯 流程定义(JSON格式)

{
  "code": "leave_approval",
  "name": "请假审批流程",
  "version": 1,
  "nodes": [
    {
      "id": "start",
      "name": "开始",
      "type": "start"
    },
    {
      "id": "apply",
      "name": "填写申请",
      "type": "apply"
    },
    {
      "id": "manager_approve",
      "name": "主管审批",
      "type": "approve",
      "assigneeType": "role",
      "assigneeValue": "manager",
      "dueDate": 24
    },
    {
      "id": "director_approve",
      "name": "总监审批",
      "type": "approve",
      "assigneeType": "role",
      "assigneeValue": "director",
      "dueDate": 48,
      "condition": "${days > 3}"
    },
    {
      "id": "end",
      "name": "结束",
      "type": "end"
    }
  ],
  "edges": [
    {
      "from": "start",
      "to": "apply"
    },
    {
      "from": "apply",
      "to": "manager_approve"
    },
    {
      "from": "manager_approve",
      "to": "director_approve",
      "condition": "${days > 3}"
    },
    {
      "from": "manager_approve",
      "to": "end",
      "condition": "${days <= 3}"
    },
    {
      "from": "director_approve",
      "to": "end"
    }
  ]
}

💻 核心代码实现

1. 流程定义实体

/**
 * 流程定义
 */
@Data
public class WorkflowDefinition {
    
    /**
     * 流程编码
     */
    private String code;
    
    /**
     * 流程名称
     */
    private String name;
    
    /**
     * 版本号
     */
    private Integer version;
    
    /**
     * 节点列表
     */
    private List<WorkflowNode> nodes;
    
    /**
     * 连线列表
     */
    private List<WorkflowEdge> edges;
}

/**
 * 流程节点
 */
@Data
public class WorkflowNode {
    
    /**
     * 节点ID
     */
    private String id;
    
    /**
     * 节点名称
     */
    private String name;
    
    /**
     * 节点类型
     */
    private NodeType type;
    
    /**
     * 审批人类型:role角色 / user用户 / dept部门
     */
    private String assigneeType;
    
    /**
     * 审批人值
     */
    private String assigneeValue;
    
    /**
     * 超时时间(小时)
     */
    private Integer dueDate;
    
    /**
     * 条件表达式(SpEL)
     */
    private String condition;
}

/**
 * 流程连线
 */
@Data
public class WorkflowEdge {
    
    /**
     * 源节点ID
     */
    private String from;
    
    /**
     * 目标节点ID
     */
    private String to;
    
    /**
     * 条件表达式
     */
    private String condition;
}

/**
 * 节点类型
 */
public enum NodeType {
    
    /**
     * 开始节点
     */
    START,
    
    /**
     * 申请节点
     */
    APPLY,
    
    /**
     * 审批节点
     */
    APPROVE,
    
    /**
     * 会签节点(所有人都要通过)
     */
    COUNTERSIGN,
    
    /**
     * 或签节点(一人通过即可)
     */
    OR_SIGN,
    
    /**
     * 条件节点
     */
    CONDITION,
    
    /**
     * 结束节点
     */
    END
}

2. 流程引擎核心

/**
 * 工作流引擎
 */
@Service
@Slf4j
public class WorkflowEngine {
    
    @Autowired
    private WorkflowDefinitionMapper definitionMapper;
    
    @Autowired
    private WorkflowInstanceMapper instanceMapper;
    
    @Autowired
    private WorkflowTaskMapper taskMapper;
    
    @Autowired
    private WorkflowHistoryMapper historyMapper;
    
    @Autowired
    private SpelExpressionParser expressionParser;
    
    /**
     * 启动流程
     */
    @Transactional(rollbackFor = Exception.class)
    public Long startProcess(ProcessStartDTO dto) {
        // 1. 查询流程定义
        WorkflowDefinition definition = getLatestDefinition(dto.getDefinitionCode());
        
        if (definition == null) {
            throw new BusinessException("流程定义不存在");
        }
        
        // 2. 创建流程实例
        WorkflowInstance instance = createInstance(definition, dto);
        instanceMapper.insert(instance);
        
        // 3. 获取第一个任务节点
        WorkflowNode firstNode = getFirstNode(definition);
        
        // 4. 创建任务
        createTasks(instance, firstNode, dto.getVariables());
        
        // 5. 记录历史
        recordHistory(instance.getId(), null, "start", firstNode.getId(), 
            dto.getInitiatorId(), dto.getInitiatorName(), "发起流程");
        
        log.info("流程启动成功:instanceId={}, definition={}", 
            instance.getId(), definition.getCode());
        
        return instance.getId();
    }
    
    /**
     * 完成任务
     */
    @Transactional(rollbackFor = Exception.class)
    public void completeTask(TaskCompleteDTO dto) {
        // 1. 查询任务
        WorkflowTask task = taskMapper.selectById(dto.getTaskId());
        
        if (task == null || task.getStatus() != TaskStatus.PENDING) {
            throw new BusinessException("任务不存在或已处理");
        }
        
        // 2. 权限校验
        if (!task.getAssigneeId().equals(dto.getOperatorId())) {
            throw new BusinessException("无权处理该任务");
        }
        
        // 3. 更新任务状态
        task.setStatus(dto.isApproved() ? TaskStatus.APPROVED : TaskStatus.REJECTED);
        task.setComment(dto.getComment());
        task.setCompleteTime(new Date());
        task.setDuration((int) ((System.currentTimeMillis() - 
            task.getCreateTime().getTime()) / 1000));
        taskMapper.updateById(task);
        
        // 4. 查询流程实例
        WorkflowInstance instance = instanceMapper.selectById(task.getInstanceId());
        
        // 5. 查询流程定义
        WorkflowDefinition definition = getDefinition(instance.getDefinitionId());
        
        // 6. 判断是否需要流转
        if (dto.isApproved()) {
            // 同意:流转到下一节点
            moveToNextNode(instance, definition, task, dto.getVariables());
        } else {
            // 拒绝:流程结束
            instance.setStatus(InstanceStatus.REJECTED);
            instance.setEndTime(new Date());
            instance.setDuration((int) ((System.currentTimeMillis() - 
                instance.getStartTime().getTime()) / 1000));
            instanceMapper.updateById(instance);
        }
        
        // 7. 记录历史
        recordHistory(instance.getId(), task.getId(), task.getNodeId(), 
            null, dto.getOperatorId(), dto.getOperatorName(), dto.getComment());
        
        log.info("任务完成:taskId={}, approved={}", dto.getTaskId(), dto.isApproved());
    }
    
    /**
     * 流转到下一节点
     */
    private void moveToNextNode(WorkflowInstance instance, 
                               WorkflowDefinition definition,
                               WorkflowTask currentTask,
                               Map<String, Object> variables) {
        
        // 1. 查找下一节点
        List<WorkflowNode> nextNodes = findNextNodes(definition, 
            currentTask.getNodeId(), variables);
        
        if (nextNodes.isEmpty()) {
            // 没有下一节点,流程结束
            instance.setStatus(InstanceStatus.COMPLETED);
            instance.setEndTime(new Date());
            instance.setDuration((int) ((System.currentTimeMillis() - 
                instance.getStartTime().getTime()) / 1000));
            instanceMapper.updateById(instance);
            return;
        }
        
        // 2. 创建下一节点的任务
        for (WorkflowNode nextNode : nextNodes) {
            if (nextNode.getType() == NodeType.END) {
                // 结束节点
                instance.setStatus(InstanceStatus.COMPLETED);
                instance.setEndTime(new Date());
                instance.setDuration((int) ((System.currentTimeMillis() - 
                    instance.getStartTime().getTime()) / 1000));
                instanceMapper.updateById(instance);
            } else {
                // 普通节点,创建任务
                createTasks(instance, nextNode, variables);
                
                // 更新当前节点
                instance.setCurrentNodeId(Long.parseLong(nextNode.getId()));
                instance.setCurrentNodeName(nextNode.getName());
                instanceMapper.updateById(instance);
            }
        }
    }
    
    /**
     * 查找下一节点
     */
    private List<WorkflowNode> findNextNodes(WorkflowDefinition definition, 
                                            String currentNodeId,
                                            Map<String, Object> variables) {
        List<WorkflowNode> nextNodes = new ArrayList<>();
        
        // 查找所有从当前节点出发的连线
        List<WorkflowEdge> edges = definition.getEdges().stream()
            .filter(edge -> edge.getFrom().equals(currentNodeId))
            .collect(Collectors.toList());
        
        for (WorkflowEdge edge : edges) {
            // 判断条件
            if (evaluateCondition(edge.getCondition(), variables)) {
                // 查找目标节点
                WorkflowNode targetNode = definition.getNodes().stream()
                    .filter(node -> node.getId().equals(edge.getTo()))
                    .findFirst()
                    .orElse(null);
                
                if (targetNode != null) {
                    nextNodes.add(targetNode);
                }
            }
        }
        
        return nextNodes;
    }
    
    /**
     * 创建任务
     */
    private void createTasks(WorkflowInstance instance, 
                            WorkflowNode node,
                            Map<String, Object> variables) {
        
        // 解析审批人
        List<Long> assigneeIds = resolveAssignees(node, instance, variables);
        
        if (assigneeIds.isEmpty()) {
            throw new BusinessException("未找到审批人:" + node.getName());
        }
        
        // 创建任务
        for (Long assigneeId : assigneeIds) {
            WorkflowTask task = new WorkflowTask();
            task.setInstanceId(instance.getId());
            task.setNodeId(node.getId());
            task.setNodeName(node.getName());
            task.setNodeType(node.getType());
            task.setAssigneeId(assigneeId);
            // task.setAssigneeName(getUserName(assigneeId));  // 查询用户姓名
            task.setStatus(TaskStatus.PENDING);
            task.setCreateTime(new Date());
            
            taskMapper.insert(task);
            
            // TODO: 发送待办通知
            sendTaskNotification(task);
        }
    }
    
    /**
     * 解析审批人
     */
    private List<Long> resolveAssignees(WorkflowNode node, 
                                       WorkflowInstance instance,
                                       Map<String, Object> variables) {
        List<Long> assigneeIds = new ArrayList<>();
        
        String assigneeType = node.getAssigneeType();
        String assigneeValue = node.getAssigneeValue();
        
        if ("user".equals(assigneeType)) {
            // 指定用户
            assigneeIds.add(Long.parseLong(assigneeValue));
            
        } else if ("role".equals(assigneeType)) {
            // 角色:查询拥有该角色的用户
            // assigneeIds = userService.getUserIdsByRole(assigneeValue);
            
        } else if ("dept".equals(assigneeType)) {
            // 部门:查询该部门的负责人
            // assigneeIds.add(deptService.getDeptLeader(assigneeValue));
            
        } else if ("initiator_manager".equals(assigneeType)) {
            // 发起人的主管
            // Long managerId = userService.getManagerId(instance.getInitiatorId());
            // assigneeIds.add(managerId);
        }
        
        return assigneeIds;
    }
    
    /**
     * 条件判断(SpEL表达式)
     */
    private boolean evaluateCondition(String condition, Map<String, Object> variables) {
        if (StringUtils.isBlank(condition)) {
            return true;  // 没有条件,默认通过
        }
        
        try {
            Expression expression = expressionParser.parseExpression(condition);
            StandardEvaluationContext context = new StandardEvaluationContext();
            context.setVariables(variables);
            
            Boolean result = expression.getValue(context, Boolean.class);
            return result != null && result;
            
        } catch (Exception e) {
            log.error("条件表达式计算失败:condition={}", condition, e);
            return false;
        }
    }
    
    /**
     * 获取最新流程定义
     */
    private WorkflowDefinition getLatestDefinition(String code) {
        // 查询最新版本的流程定义
        return definitionMapper.selectLatestByCode(code);
    }
    
    /**
     * 获取流程定义
     */
    private WorkflowDefinition getDefinition(Long definitionId) {
        return definitionMapper.selectById(definitionId);
    }
    
    /**
     * 获取第一个任务节点
     */
    private WorkflowNode getFirstNode(WorkflowDefinition definition) {
        // 找到开始节点
        WorkflowNode startNode = definition.getNodes().stream()
            .filter(node -> node.getType() == NodeType.START)
            .findFirst()
            .orElse(null);
        
        if (startNode == null) {
            throw new BusinessException("流程定义错误:缺少开始节点");
        }
        
        // 找到开始节点的下一个节点
        return findNextNodes(definition, startNode.getId(), new HashMap<>())
            .stream()
            .findFirst()
            .orElse(null);
    }
    
    /**
     * 创建流程实例
     */
    private WorkflowInstance createInstance(WorkflowDefinition definition, 
                                           ProcessStartDTO dto) {
        WorkflowInstance instance = new WorkflowInstance();
        instance.setDefinitionId(definition.getId());
        instance.setDefinitionCode(definition.getCode());
        instance.setDefinitionVersion(definition.getVersion());
        instance.setBusinessKey(dto.getBusinessKey());
        instance.setBusinessType(dto.getBusinessType());
        instance.setTitle(dto.getTitle());
        instance.setStatus(InstanceStatus.RUNNING);
        instance.setInitiatorId(dto.getInitiatorId());
        instance.setStartTime(new Date());
        
        return instance;
    }
    
    /**
     * 记录历史
     */
    private void recordHistory(Long instanceId, Long taskId, 
                              String fromNode, String toNode,
                              Long operatorId, String operatorName, 
                              String comment) {
        WorkflowHistory history = new WorkflowHistory();
        history.setInstanceId(instanceId);
        history.setTaskId(taskId);
        history.setFromNode(fromNode);
        history.setToNode(toNode);
        history.setOperatorId(operatorId);
        history.setOperatorName(operatorName);
        history.setComment(comment);
        history.setCreateTime(new Date());
        
        historyMapper.insert(history);
    }
    
    /**
     * 发送待办通知
     */
    private void sendTaskNotification(WorkflowTask task) {
        // TODO: 发送站内信、短信、邮件通知
        log.info("发送待办通知:taskId={}, assigneeId={}", task.getId(), task.getAssigneeId());
    }
}

⚡ 高级特性:会签

/**
 * 会签处理(所有人都要通过)
 */
@Service
public class CountersignHandler {
    
    @Autowired
    private WorkflowTaskMapper taskMapper;
    
    /**
     * 判断会签节点是否全部通过
     */
    public boolean isCountersignCompleted(Long instanceId, String nodeId) {
        // 查询该节点的所有任务
        List<WorkflowTask> tasks = taskMapper.selectByInstanceAndNode(instanceId, nodeId);
        
        // 判断是否全部完成
        return tasks.stream()
            .allMatch(task -> task.getStatus() == TaskStatus.APPROVED);
    }
    
    /**
     * 判断是否有拒绝
     */
    public boolean hasRejected(Long instanceId, String nodeId) {
        List<WorkflowTask> tasks = taskMapper.selectByInstanceAndNode(instanceId, nodeId);
        
        return tasks.stream()
            .anyMatch(task -> task.getStatus() == TaskStatus.REJECTED);
    }
}

✅ 最佳实践

审批流引擎设计要点:

1️⃣ 流程定义:
   □ JSON格式存储
   □ 支持版本管理
   □ 可视化设计器
   
2️⃣ 任务管理:
   □ 待办列表
   □ 已办列表
   □ 超时提醒
   □ 任务转交
   
3️⃣ 审批人配置:
   □ 指定用户
   □ 指定角色
   □ 指定部门
   □ 发起人主管
   □ 动态表达式
   
4️⃣ 流程控制:
   □ 串行审批
   □ 并行审批
   □ 会签(全部通过)
   □ 或签(一人通过)
   □ 条件分支
   
5️⃣ 高级功能:
   □ 审批撤回
   □ 审批转交
   □ 审批加签
   □ 审批跳转
   □ 超时自动审批
   
6️⃣ 监控统计:
   □ 流程耗时
   □ 节点耗时
   □ 超时告警
   □ 审批效率

🎉 总结

审批流引擎核心:

1️⃣ 配置化:流程定义JSON化,无需改代码
2️⃣ 灵活性:支持各种复杂审批场景
3️⃣ 可扩展:支持自定义节点类型
4️⃣ 可视化:拖拽设计流程图
5️⃣ 监控:实时监控流程运行状态

记住:好的审批流引擎能提升10倍开发效率! 📋


文档编写时间:2025年10月24日
作者:热爱流程自动化的工作流工程师
版本:v1.0
愿每个流程都高效运转!