Java案例如何实现审批流程?从零搭建企业级工作流引擎
目录导读
为什么需要审批流程系统?
在企业级应用中,审批流程是业务流转的“高速公路”,无论是OA系统的请假审批、财务系统的报销审批,还是ERP系统的采购审批,都需要一套灵活、可扩展的Java架构来承载。

问:为什么不用现成的工作流引擎(如Activiti)?
答: 轻量级场景下,自研审批流能避免Activiti的数据库表结构复杂、学习成本高、部署臃肿等问题,通过”一张审批表+状态机模式“,可快速实现95%的审批场景。
Java实现审批流程的三大核心机制
| 机制 | 核心要素 | 类比 |
|---|---|---|
| 状态机 | 待审批→审批中→已通过/已驳回 | 电梯的向上/向下/停止 |
| 审批链 | 申请人→直属上级→部门经理→HR | 多米诺骨牌逐个倒下 |
| 回滚补偿 | 驳回→退回上一级/重新提交 | 游戏里的存档读档 |
关键设计原则:
- 每一个审批节点只关心当前操作
- 通过数据库字段记录状态而非动态流程引擎
- 使用策略模式处理不同审批类型的业务逻辑
实战案例:基于Spring Boot的请假审批系统
业务需求
员工提交请假申请 → 直属上级审批 → 部门经理审批 → 人事备案(通过后)
支持:退回重新提交、会签(多人同时审批)、条件分支(请假>3天需总监审批)
技术栈选型
- Spring Boot 2.7.x
- MyBatis-Plus
- MySQL 8.0
- Redis(用于并发控制)
数据库设计:状态机与审批链
核心表结构:approval_request
CREATE TABLE `approval_request` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `type` varchar(50) DEFAULT 'LEAVE' COMMENT '审批类型:LEAVE, EXPENSE, PURCHASE', `applicant_id` bigint(20) DEFAULT NULL COMMENT '申请人ID', `current_node` tinyint(4) DEFAULT '0' COMMENT '当前节点编号:0-待提交,1-直属上级,2-部门经理,3-人事备案', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0-进行中,1-通过,2-驳回,3-撤回', varchar(200) DEFAULT NULL COMMENT '标题', `content` text, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
审批记录表:approval_log
CREATE TABLE `approval_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `request_id` bigint(20) NOT NULL, `operator_id` bigint(20) DEFAULT NULL, `node` tinyint(4) DEFAULT NULL COMMENT '处理节点', `action` tinyint(4) DEFAULT NULL COMMENT '操作:1-通过,2-驳回', `comment` text, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计亮点:
current_node字段替代了复杂的流程引擎表- 通过
status字段组合current_node实现完整状态机 - 利用
approval_log实现全链路审计
核心代码实现逐行解析
Step 1:定义审批节点枚举
public enum ApprovalNode {
DRAFT(0, "草稿"),
DIRECT_LEADER(1, "直属上级"),
DEPT_MANAGER(2, "部门经理"),
HR(3, "人事备案"),
FINISHED(4, "已完成");
private int code;
private String desc;
// getter...
}
Step 2:实现审批服务核心逻辑
@Service
public class ApprovalService {
public void approve(Long requestId, Long operatorId, boolean approved, String comment) {
// 1. 通过Redis分布式锁防止并发
String lockKey = "approval:lock:" + requestId;
boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
try {
// 2. 查询当前审批单
ApprovalRequest request = approvalMapper.selectById(requestId);
// 3. 权限校验:必须是当前节点的审批人
if (!checkApprover(request.getCurrentNode(), operatorId)) {
throw new BusinessException("您不是当前环节的审批人");
}
// 4. 处理审批结果
if (approved) {
// 通过:推进到下一节点
request.setCurrentNode(request.getCurrentNode() + 1);
// 如果已经是最后一个节点,标记为通过
if (request.getCurrentNode() == ApprovalNode.FINISHED.getCode()) {
request.setStatus(1); // 审批完成
}
} else {
// 驳回:退回上一级(或直接退回申请人)
request.setCurrentNode(ApprovalNode.DIRECT_LEADER.getCode());
request.setStatus(2); // 驳回状态
}
// 5. 记录审批日志
ApprovalLog log = new ApprovalLog();
log.setRequestId(requestId);
log.setOperatorId(operatorId);
log.setAction(approved ? 1 : 2);
log.setComment(comment);
approvalLogMapper.insert(log);
// 6. 更新主表
request.setUpdateTime(new Date());
approvalMapper.updateById(request);
} finally {
redisLock.unlock(lockKey);
}
}
}
Step 3:动态审批人策略实现
@Component
public class ApproverStrategy {
// 根据不同节点动态获取审批人ID
public Long getApproverByNode(ApprovalRequest request, int node) {
// 实际项目可以从部门表、组织架构表中动态查询
switch (node) {
case 1: // 直属上级
return userService.getDirectLeader(request.getApplicantId());
case 2: // 部门经理
return userService.getDeptManager(request.getApplicantId());
case 3: // 人事
return userService.getHRUserId();
default:
return null;
}
}
// 条件分支:请假>3天需要总监审批
public boolean needAdditionalNode(ApprovalRequest request) {
if ("LEAVE".equals(request.getType())) {
JSONObject content = JSONObject.parseObject(request.getContent());
int days = content.getIntValue("days");
return days > 3;
}
return false;
}
}
关键设计要点总结
- 单一职责原则:每个审批节点只更新
current_node,不关注全局流程 - 幂等性:通过分布式锁防止同一条记录被并发审批
- 可回溯:所有操作落日志,支持撤销/追踪
- 扩展性:通过策略模式支持不同审批类型的差异化逻辑
常见问题FAQ
Q1:多人会签(如需3人同时审批)怎么实现?
A:可以在approval_request增加sign_count字段,每次通过加1,当达到阈值时自动推进节点,拒绝则立即驳回。
Q2:如何实现催办/超时自动审批?
A:使用Spring Task定时扫描超过24小时未处理的审批单,发送钉钉/邮件通知。
@Scheduled(cron = "0 0 8 * * ?") // 每天上午8点执行
public void handleTimeoutApprovals() {
List<ApprovalRequest> timeoutList = approvalMapper.selectTimeoutList();
timeoutList.forEach(request -> {
// 发送提醒通知
notificationService.sendUrge(request);
});
}
Q3:审批过程中业务数据变动怎么处理?
A:建议将业务数据(如请假天数)与审批状态解耦,审批通过后,通过事件监听机制更新业务表:
@EventListener
public void handleApprovalPassed(ApprovalPassedEvent event) {
if ("LEAVE".equals(event.getType())) {
// 更新考勤记录
attendanceService.updateLeave(event);
}
}
注意:本文所有代码示例已过滤敏感域名信息,可直接复制用于学习。
通过本文的案例,你应该已经掌握了用Java实现审批流程的最小可行方案,实际生产中,可根据业务复杂度选择是否引入Activiti/Flowable等专业引擎,但对于80%的企业场景,这种”一张表+状态机“的轻量方案已经足够高效且易于维护。