标题: 审批流还在写死代码?工作流引擎来救场!
副标题: 从简单审批到复杂流程,灵活配置一站式解决
🎬 开篇:一次审批流需求的噩梦
产品经理:我们要加一个请假审批功能
开发:好的(写死代码:员工 -> 主管 -> 经理)
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
愿每个流程都高效运转! ✨