ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Activiti7企业级审批流系统实战

SpringBoot+Activiti7企业级审批流系统实战 简介这是一套基于SpringBoot构建的企业级在线办公系统源码面向Java后端开发者与全栈学习者聚焦工作流审批与协同办公场景完整实现请假、会议含腾讯TRTC视频会议、报销三大核心审批流程并集成支付宝沙箱支付功能。资源包共340个文件涵盖170个Java业务逻辑与Activiti7流程配置类、37个Vue前端组件、47个SVG图标资源、27个PNG界面素材及关键配置文件如yml、sql、Dockerfile整体压缩后仅4.9MB结构清晰、模块解耦便于快速部署与二次开发。已有114人学习下载适合希望深入理解RBAC权限控制、WebSocket实时通知、Redis会议状态管理及Activiti7流程引擎集成的中高级开发者。源码中包含CustomProcessDiagramGenerator等定制化流程图生成组件以及TRTC音视频接入示例为工作流实时通信类项目提供可复用的技术范式。1. 这不是又一个“待办列表”DemoSpringBoot驱动的审批流系统直击企业级办公中流程割裂、状态不可溯、审批与业务脱钩三大痛点很多团队用SpringBoot搭个CRUD就敢叫“企业级办公系统”结果上线后请假单卡在二级审批三天没人点“通过”会议申请被行政部退回却查不到修改痕迹报销单金额改了三次但财务系统里还显示旧数据——根本原因不是功能少而是审批流没和业务实体深度耦合状态变更不触发下游动作日志只记“已提交”却不记录谁、何时、基于哪条规则做了决策。本文讲的是一套真实落地过制造业与SaaS服务商的SpringBoot在线办公系统源码它把请假、会议、报销三类高频审批抽象为统一工作流引擎数据库设计强制关联业务单据ID与审批实例ID每个审批节点自动写入操作快照含表单字段diff审批完成即触发对应业务更新如更新员工剩余假期天数、锁定会议室资源、生成财务应付凭证。适合已有Java开发能力、正从部门级工具向跨部门协同系统升级的团队尤其关注审批链路可审计、状态变更可回滚、与现有HR/财务系统能对接的场景。2. 审批流不是状态机模拟用SpringBootActiviti7构建可追溯、可干预、可扩展的流程引擎2.1 为什么选Activiti7而非自研状态机或Camunda企业级审批的核心诉求是“人在环路中可控”。自研状态机虽轻量但无法处理会签、加签、驳回重审等复杂分支Camunda功能强大但学习成本高且其BPMN设计器与SpringBoot生态集成需额外适配。Activiti7是Apache顶级项目2023年已全面拥抱Spring Boot 2.7和Spring Security 5.8其activiti-spring-boot-starter-process模块开箱支持流程定义热加载、REST API权限控制、以及与Spring Data JPA无缝联动。关键优势在于流程定义BPMN XML与业务代码物理分离运维人员可通过Web UI调整审批路径而无需重启应用每个流程实例自动绑定业务单据ID天然支持“查单据→看全流程→追溯每步操作人及时间戳”的审计要求。本系统选用Activiti7.2.0兼容SpringBoot 2.7.18避免使用已停止维护的Activiti6或尚不稳定的新版Activiti Cloud。提示Activiti7默认使用H2内存数据库演示生产环境必须切换为MySQL 8.0。切勿在application.yml中仅配置spring.datasource.url需显式声明activiti.database-schema-updatetrue并指定schema位置否则流程表初始化失败。2.2 数据库表结构设计让审批流与业务数据在物理层强关联Activiti7自动生成23张流程表如ACT_RU_EXECUTION、ACT_HI_PROCINST但这些表仅存流程元数据。本系统在原有数据库中新增4张核心业务表并通过外键与Activiti表建立双向映射表名关键字段关联逻辑作用leave_applyid, employee_id, start_time, end_time, reason,proc_inst_idproc_inst_id → ACT_RU_EXECUTION.ID_请假单主表proc_inst_id记录当前审批流程实例IDmeeting_applyid, title, room_id, start_time, participant_list,proc_inst_idproc_inst_id → ACT_HI_PROCINST.ID_会议申请表proc_inst_id指向历史流程实例因会议可能被取消reimburse_applyid, amount, category, receipt_images,proc_inst_id,finance_statusproc_inst_id → ACT_RU_EXECUTION.ID_; finance_status枚举值PENDING/PAID/REJECTED报销单表finance_status由审批完成事件监听器自动更新-- 创建请假单表含流程实例ID外键 CREATE TABLE leave_apply ( id bigint NOT NULL AUTO_INCREMENT, employee_id varchar(32) NOT NULL COMMENT 申请人工号, start_time datetime NOT NULL, end_time datetime NOT NULL, reason text, proc_inst_id varchar(64) DEFAULT NULL COMMENT 关联Activiti流程实例ID, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_proc_inst (proc_inst_id), CONSTRAINT fk_leave_proc_inst FOREIGN KEY (proc_inst_id) REFERENCES act_ru_execution (ID_) ON DELETE SET NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2.1 外键约束的取舍为什么用ON DELETE SET NULL而非CASCADEActiviti流程实例删除通常发生在流程归档后但业务单据需长期保留供审计。若设CASCADE删除流程实例将连带清空请假单违反数据主权原则。采用SET NULL既保证外键完整性又允许业务表独立存在。实际部署时需在Activiti配置中关闭自动清理activiti.history-levelfull确保历史流程数据永久留存。2.3 流程定义文件BPMN与SpringBoot的深度绑定本系统将BPMN文件存于src/main/resources/processes/目录下命名规范为{业务类型}_approval.bpmn20.xml如leave_approval.bpmn20.xml。关键设计点开始事件绑定业务单据ID在BPMN的Start Event属性中设置formKeyleaveApplyForm并在SpringBoot Controller中调用runtimeService.startProcessInstanceByKey(leaveApproval, variables)时将variablesMap注入applyId即leave_apply.id。用户任务动态分配不硬编码审批人而是用EL表达式${assigneeService.getApprover(employeeId, LEAVE)}该表达式调用Spring BeanassigneeService的getApprover方法根据申请人部门、职级、当前审批节点动态查询审批人如部门经理→HRBP→分管总监三级审批。结束事件触发业务更新在End Event的executionListener中配置classcom.example.office.listener.LeaveCompleteListener该监听器实现ExecutionListener接口在notify方法中执行leaveService.updateRemainingDays(applyId)。// LeaveCompleteListener.java Component public class LeaveCompleteListener implements ExecutionListener { Autowired private LeaveService leaveService; Override public void notify(DelegateExecution execution) { String applyId execution.getVariable(applyId).toString(); // 根据流程变量判断审批结果 String outcome (String) execution.getVariable(outcome); if (APPROVED.equals(outcome)) { leaveService.updateRemainingDays(Long.parseLong(applyId)); } } }2.3.1 BPMN文件如何热加载而不重启Activiti7提供ProcessEngineConfiguration的deploymentMode配置项。在Configuration类中注入Bean public SpringProcessEngineConfiguration processEngineConfiguration( DataSource dataSource, PlatformTransactionManager transactionManager, SpringSecurityContextProvider securityContextProvider) { SpringProcessEngineConfiguration config new SpringProcessEngineConfiguration(); config.setDataSource(dataSource); config.setTransactionManager(transactionManager); config.setSecurityContextProvider(securityContextProvider); // 关键启用自动部署扫描classpath下processes目录 config.setDeploymentMode(SpringProcessEngineConfiguration.DEPLOYMENT_MODE_REDEPLOY_ONLY); return config; }当修改BPMN文件并重新编译后Spring Boot DevTools会触发ProcessEngine重建新流程定义自动生效旧流程实例继续运行新实例使用新版流程。3. 三类审批场景的差异化实现从表单校验到业务闭环的完整链路3.1 请假审批时间冲突检测与假期余额实时计算请假单提交前需校验两个硬性条件时间不与已批准请假重叠、剩余假期天数足够。前端Vue组件调用/api/leave/check-conflict接口后端执行// LeaveController.java PostMapping(/check-conflict) public ResponseEntityCheckResult checkConflict(RequestBody LeaveCheckRequest request) { // 1. 查询申请人所有已批准请假状态为APPROVED ListLeaveApply approvedLeaves leaveApplyMapper.selectByEmployeeAndStatus( request.getEmployeeId(), APPROVED); // 2. 检测时间重叠将请求时间段与每条已批准记录比对 for (LeaveApply approved : approvedLeaves) { if (request.getStartTime().isBefore(approved.getEndTime()) request.getEndTime().isAfter(approved.getStartTime())) { return ResponseEntity.ok(new CheckResult(false, 与已批准请假时间冲突)); } } // 3. 查询剩余假期天数调用HR系统API或本地缓存 int remainingDays hrSystemService.getRemainingDays(request.getEmployeeId()); long requestedDays ChronoUnit.DAYS.between( request.getStartTime(), request.getEndTime()) 1; if (requestedDays remainingDays) { return ResponseEntity.ok(new CheckResult(false, 剩余假期不足)); } return ResponseEntity.ok(new CheckResult(true, 校验通过)); }注意时间重叠检测必须考虑时区问题。本系统所有datetime字段存储为UTC时间前端传参时需携带timezoneAsia/Shanghai后端用ZonedDateTime.parse()解析避免夏令时导致的1小时偏差。3.2 会议申请审批资源锁定与冲突预警的双重保障会议审批的核心是会议室资源独占性。系统在会议申请提交时非审批通过时即锁定资源防止多人同时预约同一时段。实现分两步预占阶段用户填写会议时间后前端调用/api/meeting/prelock后端执行INSERT INTO meeting_room_lock (room_id, start_time, end_time, apply_id, status) SELECT id, ?, ?, ?, PENDING FROM meeting_room WHERE id ? AND NOT EXISTS ( SELECT 1 FROM meeting_room_lock l WHERE l.room_id meeting_room.id AND l.status IN (PENDING,CONFIRMED) AND l.start_time ? AND l.end_time ? );若INSERT影响行数为0返回“会议室已被占用”。审批完成阶段Activiti监听器MeetingCompleteListener中若审批结果为APPROVED则将meeting_room_lock.status更新为CONFIRMED若REJECTED则删除该记录。3.3 报销审批多级金额阈值与财务系统对接报销单按金额自动分流审批路径≤5000元部门经理审批5001~20000元部门经理→财务总监20000元部门经理→财务总监→CFO此逻辑在BPMN的Exclusive Gateway中用EL表达式实现bpmn:conditionExpression xsi:typebpmn:tFormalExpression ${amount lt; 5000 ? level1 : (amount lt; 20000 ? level2 : level3)} /bpmn:conditionExpression审批完成后系统调用财务系统REST API生成应付凭证// ReimburseCompleteListener.java public void notify(DelegateExecution execution) { String applyId execution.getVariable(applyId).toString(); ReimburseApply apply reimburseApplyMapper.selectById(applyId); // 构造财务系统凭证JSON JSONObject voucher new JSONObject(); voucher.put(voucherNo, VOU- System.currentTimeMillis()); voucher.put(amount, apply.getAmount()); voucher.put(payee, apply.getEmployeeName()); voucher.put(category, apply.getCategory()); // 同步调用财务系统超时3秒失败降级为异步重试 try { restTemplate.postForObject(https://finance-api/v1/vouchers, voucher, Void.class); reimburseApplyMapper.updateFinanceStatus(applyId, PAID); } catch (ResourceAccessException e) { // 写入失败队列由定时任务重试 failedVoucherQueue.add(new FailedVoucher(applyId, voucher.toString())); } }3.3.1 财务系统对接的幂等性设计财务系统凭证号由本系统生成VOU-时间戳且每次调用附带唯一X-Request-ID头。财务系统收到重复ID时直接返回200避免重复记账。本系统在failedVoucherQueue中记录失败凭证时也保存该ID重试时优先校验ID是否已在财务系统存在。4. 视频会议模块轻量级WebRTC集成与审批流的深度耦合4.1 为什么不用第三方SDK而选择原生WebRTC企业级视频会议的核心诉求是可控性与审计合规。第三方SDK如Zoom、腾讯会议虽易集成但会议链接生成、参会者管理、录制文件存储均在厂商服务器无法满足金融、政务客户对数据不出域的要求。本系统采用WebRTC原生方案信令服务器用Spring Boot WebSocket实现媒体流通过TURN服务器中转避免P2P穿透失败所有会议元数据创建人、参会者、开始时间、结束时间、录制文件URL均存入meeting_record表并与审批流程实例ID关联。4.2 会议创建与审批状态的强绑定会议申请审批通过后系统自动创建会议并通知参会者。关键逻辑在MeetingCompleteListener中public void notify(DelegateExecution execution) { String applyId execution.getVariable(applyId).toString(); MeetingApply apply meetingApplyMapper.selectById(applyId); // 1. 生成唯一会议Room IDSHA256(applyId timestamp) String roomId DigestUtils.sha256Hex(applyId System.currentTimeMillis()); // 2. 插入meeting_record表关联proc_inst_id MeetingRecord record new MeetingRecord(); record.setRoomId(roomId); record.setApplyId(applyId); record.setProcInstId(execution.getProcessInstanceId()); record.setStartTime(apply.getStartTime()); record.setEndTime(apply.getEndTime()); meetingRecordMapper.insert(record); // 3. 通过WebSocket推送会议链接给所有参会者 String meetingUrl https://office.example.com/video?room roomId; for (String participant : apply.getParticipantList().split(,)) { webSocketTemplate.convertAndSend(/topic/meeting/ participant, new MeetingNotice(apply.getTitle(), meetingUrl, apply.getStartTime())); } }4.2.1 WebRTC信令服务器的Spring Boot实现要点信令服务器不处理音视频流仅转发SDP Offer/Answer和ICE Candidate。使用MessageMapping处理客户端消息Controller public class WebRtcSignalingController { // 存储房间-参与者映射生产环境应换为Redis private final MapString, SetString roomParticipants new ConcurrentHashMap(); MessageMapping(/webrtc/offer) SendTo(/topic/webrtc/{roomId}) public SignalingMessage handleOffer(DestinationVariable String roomId, Payload SignalingMessage message) { // 验证roomId是否有效查meeting_record表 if (!meetingRecordMapper.existsByRoomId(roomId)) { throw new IllegalArgumentException(Invalid room ID); } // 广播Offer给同房间其他参与者 return message; } }提示WebRTC在企业内网常因NAT类型导致连接失败。必须部署Coturn TURN服务器并在前端RTCPeerConnection配置中显式指定iceServersconst pc new RTCPeerConnection({ iceServers: [ { urls: stun:stun.l.google.com:19302 }, { urls: turn:turn.example.com:3478, username: user, credential: pass } ] });5. 生产环境关键参数调优与典型故障排查5.1 Activiti7数据库连接池与事务隔离级别配置Activiti7在高并发审批场景下易出现Lock wait timeout exceeded错误根源在于默认的READ_COMMITTED隔离级别下流程实例更新时对ACT_RU_EXECUTION表加行锁若多个审批同时操作同一业务单据如多人修改同一报销单将形成锁等待链。解决方案是提升隔离级别并优化连接池# application-prod.yml spring: datasource: hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 # 关键设置事务隔离级别为REPEATABLE_READ transaction-isolation: TRANSACTION_REPEATABLE_READ activiti: # 关键禁用自动SQL执行由DBA统一管理 database-schema-update: false # 关键历史数据全量保留避免审计缺失 history-level: full5.1.1 如何验证事务隔离级别生效在MySQL客户端执行SELECT global.transaction_isolation, session.transaction_isolation; -- 应返回 REPEATABLE-READ同时检查HikariCP连接池日志确认连接建立时执行了SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ。5.2 审批流程卡顿的三层定位法当用户反馈“审批按钮点击无响应”按以下顺序排查层级检查点命令/操作预期结果应用层Activiti流程实例状态SELECT ID_, PROC_INST_ID_, ACT_ID_, STATE_ FROM ACT_RU_TASK WHERE ASSIGNEE_ user123;查看任务是否处于CREATED状态未激活或SUSPENDED被挂起数据库层流程表锁竞争SHOW ENGINE INNODB STATUS\G搜索TRANSACTIONS部分查看是否有lock wait线程及阻塞源头网络层WebSocket连接健康度curl -i -N -H Connection: Upgrade -H Upgrade: websocket http://localhost:8080/ws返回101 Switching Protocols表示WebSocket服务正常5.3 视频会议黑屏问题的快速修复清单前端黑屏常见原因及对应命令摄像头权限未获取Chrome控制台执行navigator.mediaDevices.getUserMedia({video:true})若报NotAllowedError需检查浏览器地址栏锁图标点击“网站设置”→“摄像头”→“允许”。TURN服务器不可达在服务器执行telnet turn.example.com 3478若超时检查Coturn服务状态systemctl status coturn并确认防火墙放行UDP 3478端口。ICE候选者收集失败前端调试模式下pc.getStats()中检查candidate-pair状态若state为failed说明STUN/TURN服务器返回空候选者需检查Coturn配置中external-ip是否正确设置为公网IP。注意企业内网环境下必须在Coturn配置中显式指定external-ip和listening-ip否则客户端无法获取有效的中继地址。配置示例listening-port3478 external-ip203.205.128.45 listening-ip192.168.1.100 realmoffice.example.com5.4 数据库同步场景下的审批一致性保障当办公系统与HR系统数据库分离时员工信息变更如部门调整需实时同步。本系统采用CDCChange Data Capture方案在HR数据库开启binlog用Debezium监听employee表变更写入Kafka Topic办公系统消费该Topic更新本地缓存// EmployeeChangeConsumer.java KafkaListener(topics hr.employee.changes, groupId office-group) public void consume(ConsumerRecordString, String record) { JSONObject change new JSONObject(record.value()); String eventType change.getString(op); // ccreate, uupdate, ddelete JSONObject after change.optJSONObject(after); if (u.equals(eventType) after ! null) { String employeeId after.getString(id); // 更新本地缓存中的部门信息 employeeCache.put(employeeId, after.toMap()); // 同步更新待审批任务的审批人如部门经理变更 taskService.changeAssignee(departmentManager, employeeId, after.getString(deptId)); } }此方案确保审批路由逻辑始终基于最新组织架构避免因HR数据延迟导致审批人错误。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进