OA审批流开发避坑指南:从‘待我审批’查询到事务提交的五个实战细节
审批系统作为OA的核心模块,其稳定性直接影响企业运营效率。经历过三次完整OA系统迭代后,我整理了开发中最容易忽视却可能引发严重生产问题的技术细节。这些经验来自真实线上故障的复盘,将帮助你避开那些教科书上不会写的"坑"。
1. "待我审批"列表的性能陷阱
当用户打开待办列表时,系统需要毫秒级响应。某次上线后,我们监控发现列表查询平均耗时从200ms飙升到8s——问题出在N+1查询上。
典型错误实现:
-- 先查询主表 SELECT * FROM audit_flow WHERE status = 'PENDING'; -- 对每条主表记录循环查询明细 SELECT * FROM audit_flow_detail WHERE flow_no = ? AND status = 'WAIT_MY_APPROVE';优化方案:
SELECT f.*, d.* FROM audit_flow f JOIN audit_flow_detail d ON f.flow_no = d.flow_no WHERE f.status = 'PENDING' AND d.status = 'WAIT_MY_APPROVE' AND d.audit_user_no = ?; -- 当前用户关键改进点:
- 使用单次JOIN查询替代循环查询
- 添加复合索引:(audit_user_no, status)
- 采用DTO投影只返回必要字段
注意:当审批单附件较多时,建议单独分页查询附件信息,避免大字段拖慢主查询
2. 多级状态流转的竞争条件
某次生产环境出现诡异现象:两个审批人同时操作时,最终状态竟变成"部分通过"。根本原因是状态更新没有做并发控制。
问题复现场景:
| 时间 | 用户A操作 | 用户B操作 | 数据库状态变化 |
|---|---|---|---|
| T1 | 查询到状态为"审核中" | 查询到状态为"审核中" | 状态仍为"审核中" |
| T2 | 更新为"通过" | 更新为"驳回" | 最终取决于最后执行的SQL |
解决方案:
// 使用乐观锁控制 @Transactional public void approve(String flowNo, String userNo) { // 1. 带版本号查询 AuditFlowDetail detail = detailRepo.findByFlowNoAndUserNoWithLock( flowNo, userNo); // 2. 检查状态是否已被修改 if (!"WAIT_MY_APPROVE".equals(detail.getStatus())) { throw new IllegalStateException("状态已变更"); } // 3. 带版本号更新 int rows = detailRepo.updateStatus( flowNo, userNo, detail.getVersion(), "APPROVED"); if (rows == 0) { throw new OptimisticLockException(); } }3. 事务边界的隐蔽漏洞
曾有一个BUG导致审批通过后,主表状态更新成功但通知未发送。问题出在事务配置不当:
错误配置:
// 伪代码 public void approve() { transactionTemplate.execute(() -> { updateDetailStatus(); // 明细表更新 updateMainStatus(); // 主表更新 }); // 事务在此提交 sendNotification(); // 不在事务内 }正确做法:
// 使用声明式事务 @Transactional(rollbackFor = Exception.class) public void completeApproval() { // 1. 更新明细状态 detailRepo.updateStatus(...); // 2. 更新主流程状态 flowRepo.updateStatus(...); // 3. 发送通知(同步) notificationService.send(...); // 4. 记录操作日志 auditLogService.log(...); }事务设计原则:
- 所有数据库操作和依赖操作放在同一事务
- 外部调用要考虑幂等性
- 大事务要拆分为多个小事务
4. 审批链断裂的预防策略
当审批人离职时,系统必须自动转移待审批项。我们曾因未处理这个场景导致流程卡死。
健壮性设计方案:
graph TD A[检查审批人状态] -->|在职| B[正常审批] A -->|离职| C{是否有备用审批人} C -->|是| D[转交给备用审批人] C -->|否| E[升级给上级主管] E --> F[记录转交日志]实现代码:
public void validateApprover(String userNo) { User approver = userRepo.findById(userNo) .orElseThrow(() -> new ApproverNotFoundException()); if ("INACTIVE".equals(approver.getStatus())) { // 查找备用审批人 User backup = approver.getBackupUser(); if (backup == null) { backup = departmentService .getSupervisor(approver.getDept()); } throw new ApproverInactiveException(backup); } }5. 全链路追踪的实现技巧
当用户反馈"我明明点了同意为什么没通过"时,完整的操作日志至关重要。
日志记录方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 数据库日志表 | 查询方便 | 影响主业务性能 | 关键操作记录 |
| ELK日志系统 | 支持复杂分析 | 有延迟 | 行为分析 |
| 消息队列 | 完全异步 | 可能丢失消息 | 高并发场景 |
推荐混合方案:
// 使用AOP统一记录 @Around("@annotation(com.xxx.AuditLog)") public Object logAudit(ProceedingJoinPoint pjp) { // 1. 记录基础信息 AuditLogEntry entry = new AuditLogEntry(); entry.setOperation(pjp.getSignature().getName()); entry.setParams(JsonUtils.toJson(pjp.getArgs())); try { // 2. 执行原方法 Object result = pjp.proceed(); // 3. 记录结果 entry.setSuccess(true); return result; } catch (Exception e) { entry.setSuccess(false); entry.setErrorMsg(e.getMessage()); throw e; } finally { // 4. 异步保存 logQueue.add(entry); } }在审批系统的开发中,这些细节处理往往决定了系统的稳定性。最近一次系统升级中,我们通过优化这些关键点,使审批失败率从0.3%降至0.02%。实际编码时,建议在本地环境使用Jmeter模拟并发审批场景,提前暴露潜在问题。