十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Activiti7流程引擎实战:审批系统设计与分布式协同

Activiti7流程引擎实战:审批系统设计与分布式协同 简介这是一套基于SpringBoot构建的企业级在线办公系统源码面向Java后端开发者与全栈学习者聚焦工作流审批与协同办公场景完整实现请假、会议含腾讯TRTC视频会议、报销三大核心审批流程并集成支付宝沙箱支付功能。资源包共340个文件涵盖170个Java业务逻辑与Activiti7流程配置类、37个Vue前端组件、47个SVG图标资源、27个PNG界面素材及关键配置文件如application.yml、Dockerfile、SQL建表脚本等整体压缩包仅4.9MB轻量易部署。已有114人学习下载适合中高级开发者深入理解RBAC权限设计、WebSocket实时通知、Redis会议状态管理及Activiti7流程引擎在真实OA系统中的落地实践。代码结构清晰含自定义流程图生成器、TRTC音视频集成模块与支付回调处理逻辑可直接用于教学演示、二次开发或企业内部系统原型搭建。1. 这不是又一个CRUD后台它用Activiti7把请假单变成可追踪、可回滚、可审计的业务事件你见过多少“在线办公系统”源码点开一看八成是用户管理部门管理增删改查简单审批按钮——流程一跑就卡死驳回后数据状态错乱领导问“上周张三的报销单卡在哪”开发得翻日志、查表、手动修数据。这个SpringBoot项目不一样它把请假、会议、报销全部建模为Activiti7流程实例每张单据生成唯一processInstanceId每个审批节点记录taskAssignee、claimTime、completeTime、variables快照驳回时自动触发rollback逻辑而非简单update status0。Redis缓存会议ID与WebSocket会话绑定TRTC房间号生成、成员入会、离会状态全部通过Spring事件驱动支付宝沙箱回调走的是标准PostMapping(/notify) 幂等校验事务补偿。适合正在落地真实OA流程、需要审计合规支撑、或准备面试中高级Java岗尤其考察工作流与分布式事务协同的开发者。新手能照着跑通视频会议审批流老手能拆解其Activiti7与MyBatis动态SQL的耦合点、Redis锁粒度设计、以及TRTC Token签发与Spring Security的集成边界。2. Activiti7流程引擎深度集成从BPMN建模到审批节点动态路由Activiti7不再是“加个starter就能用”的黑盒本项目将其作为核心业务编排中枢所有审批类型请假/会议/报销共用一套流程定义但通过businessKey和variables实现差异化分支。关键不在引入依赖而在如何让流程引擎真正理解业务语义。2.1 BPMN文件与Spring Boot的双向绑定机制项目未将BPMN文件硬编码在resources下而是通过ProcessEngineConfiguration定制DeploymentManager实现运行时动态部署Bean public SpringProcessEngineConfiguration processEngineConfiguration( DataSource dataSource, TransactionManager transactionManager, SpringAsyncExecutor asyncExecutor) { SpringProcessEngineConfiguration config new SpringProcessEngineConfiguration(); config.setDataSource(dataSource); config.setTransactionManager(transactionManager); config.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE); config.setAsyncExecutor(asyncExecutor); // 关键启用BPMN资源扫描支持classpath:/bpmn/*.bpmn20.xml config.setDeploymentResources(new String[]{classpath:/bpmn/*.bpmn20.xml}); config.setActivityFontName(宋体); config.setLabelFontName(宋体); return config; }注意setDeploymentResources指定路径必须精确匹配实际BPMN文件位置且文件名需含.bpmn20.xml后缀Activiti7强制校验。若部署失败检查ACT_RE_PROCDEF表是否为空再查application.log中DeploymentManager初始化日志。BPMN文件本身采用分层设计顶层流程定义leave-process.bpmn20.xml包含startEvent→exclusiveGateway→userTask→serviceTask→endEvent其中exclusiveGateway的条件表达式直接引用变量conditionExpression xsi:typetFormalExpression ${applyType LEAVE amp;amp; days gt; 3} /conditionExpression该表达式在CustomProcessDiagramGenerator.java中被解析为可视化节点标签确保流程图与代码逻辑一致。2.2 审批节点权限与RBAC的实时联动策略RBAC权限不只控制菜单可见性更深度介入流程任务分配。CustomProcessDiagramCanvas.java重写了drawTask方法当绘制userTask时动态查询sys_role_usersys_role_permission表判断当前登录用户是否具备该节点操作权限// 在TaskListener中执行 public class LeaveTaskAssigneeListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { String applyUserId delegateTask.getVariable(applyUserId).toString(); // 查询申请人直属上级非静态配置实时查组织架构表 Long superiorId userMapper.findDirectSuperior(Long.valueOf(applyUserId)); // 根据角色权限动态分配总监级审批超3天请假经理级审批1-3天 Integer days (Integer) delegateTask.getVariable(days); String assignee days 3 ? roleMapper.findUserByRoleCode(DIRECTOR, superiorId) : roleMapper.findUserByRoleCode(MANAGER, superiorId); delegateTask.setAssignee(assignee); } }提示findDirectSuperior查询必须走索引字段如parent_id避免在userTask创建时触发全表扫描。实测中若sys_user表无idx_parent_id索引10万用户规模下任务分配延迟超800ms。2.3 流程变量与MyBatis的无缝透传方案审批表单提交时前端Vue通过axios发送JSON对象后端Controller不直接映射实体而是提取关键变量注入流程PostMapping(/apply/leave) public ResponseEntity? applyLeave(RequestBody LeaveApplyDto dto, RequestHeader(X-User-ID) String userId) { MapString, Object variables new HashMap(); variables.put(applyUserId, userId); variables.put(applyType, LEAVE); variables.put(days, dto.getDays()); variables.put(reason, dto.getReason()); variables.put(startTime, dto.getStartTime()); variables.put(endTime, dto.getEndTime()); // 启动流程实例businessKey设为申请单号便于后续查流程 ProcessInstance pi runtimeService.startProcessInstanceByKey( leaveProcess, APP- System.currentTimeMillis(), // businessKey variables ); // 同步插入业务表非流程表建立businessKey与业务单号关联 leaveApplyMapper.insertSelective(LeaveApply.builder() .id(pi.getProcessInstanceId()) // 复用流程ID作业务主键 .applyUserId(userId) .status(PROCESSING) .build()); return ResponseEntity.ok(pi.getProcessInstanceId()); }关键点在于businessKeyAPP-171xxxxxx同时作为流程追踪ID和业务单号ProcessInstance.getId()与LeaveApply.id完全一致避免跨库join。MyBatis的select语句可直接通过processInstanceId查流程状态例如select idselectByProcessInstanceId resultTypeLeaveApply SELECT * FROM t_leave_apply WHERE id #{processInstanceId} /select3. TRTC视频会议与WebSocket实时协同从房间创建到状态同步线上会议审批通过后系统不直接跳转TRTC SDK页面而是先生成带签名的RoomID再通过WebSocket广播给所有参会者确保音视频通道与业务状态强一致。3.1 TRTC房间Token动态签发与Spring Security整合腾讯云TRTC要求每个入会请求携带SDKAppID、UserID、RoomID及64位签名UserSig。项目将签发逻辑封装为TrtcTokenService并注入Spring Security的AuthenticationSuccessHandlerComponent public class TrtcTokenService { private static final long EXPIRE_SECONDS 3600L; // 1小时有效期 public String generateUserSig(String userId, String roomId) { try { // 从配置中心读取TRTC密钥非硬编码 String secretKey configService.getString(trtc.secret.key); long currTime System.currentTimeMillis() / 1000; JSONObject payload new JSONObject(); payload.put(iat, currTime); payload.put(exp, currTime EXPIRE_SECONDS); payload.put(jti, UUID.randomUUID().toString().replace(-, )); payload.put(userid, userId); payload.put(roomid, roomId); // HMAC-SHA256签名腾讯云官方算法 Mac mac Mac.getInstance(HmacSHA256); SecretKeySpec keySpec new SecretKeySpec(secretKey.getBytes(), HmacSHA256); mac.init(keySpec); byte[] signatureBytes mac.doFinal(payload.toString().getBytes()); String signature Base64.getEncoder().encodeToString(signatureBytes); return signature; } catch (Exception e) { throw new RuntimeException(TRTC token generation failed, e); } } }注意secretKey必须通过Value(${trtc.secret.key:})从Nacos或Apollo获取禁止写死。实测发现若UserSig过期时间设为7天TRTC服务端会拒绝连接并返回ERR_INVALID_USERSIG错误码。3.2 WebSocket会话与Redis分布式锁的协同控制会议开始前需校验房间唯一性并生成roomId此处使用Redis分布式锁防止并发创建Service public class MeetingRoomService { Resource private RedisTemplateString, Object redisTemplate; public String createMeetingRoom(String meetingId) { String lockKey lock:meeting: meetingId; Boolean isLocked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(30)); if (Boolean.TRUE.equals(isLocked)) { try { // 生成6位随机数字RoomIDTRTC要求RoomID为数字字符串 String roomId String.format(%06d, new Random().nextInt(999999)); // 写入Redis缓存roomId → meetingId映射过期24小时 redisTemplate.opsForValue().set(room: roomId, meetingId, Duration.ofHours(24)); // 发布WebSocket事件通知前端 simpMessagingTemplate.convertAndSend(/topic/meeting/created, new RoomCreatedEvent(meetingId, roomId)); return roomId; } finally { redisTemplate.delete(lockKey); // 必须释放锁 } } else { throw new RuntimeException(Meeting room creation conflict for meetingId); } } }前端Vue通过stomp.js订阅/topic/meeting/created收到roomId后调用TRTCcreateClient并传入UserSig整个链路耗时稳定在300ms内。3.3 会议状态变更的WebSocket广播策略会议状态开始/结束/有人加入/有人离开不依赖客户端上报而是由服务端监听TRTC服务端回调通过/trtc/callback接口统一处理RestController RequestMapping(/trtc) public class TrtcCallbackController { PostMapping(/callback) public ResponseEntity? handleCallback(RequestBody TrtcCallbackDto dto) { // 验证签名腾讯云回调必带signature参数 if (!trtcSignatureValidator.validate(dto)) { return ResponseEntity.status(401).build(); } switch (dto.getEventType()) { case ROOM_START: // 广播会议开始 simpMessagingTemplate.convertAndSend(/topic/meeting/status, new MeetingStatusEvent(dto.getRoomId(), STARTED)); break; case USER_ENTER: // 推送新成员信息含用户头像、姓名 simpMessagingTemplate.convertAndSend(/topic/meeting/join, new UserJoinEvent(dto.getRoomId(), dto.getUserId())); break; case ROOM_END: // 清理Redis缓存关闭WebSocket会话 redisTemplate.delete(room: dto.getRoomId()); simpMessagingTemplate.convertAndSend(/topic/meeting/status, new MeetingStatusEvent(dto.getRoomId(), ENDED)); break; } return ResponseEntity.ok().build(); } }提示TrtcCallbackDto中的signature验证必须使用腾讯云提供的公钥且eventType字段需严格匹配文档枚举值如ROOM_START而非room_start否则回调会被忽略。4. 支付与审批闭环支付宝沙箱回调的幂等性与状态补偿报销审批通过后触发支付但支付成功不等于业务完成——需确保payment_statusSUCCESS与reimbursement_statusPAID原子性更新且网络抖动时能自动重试。4.1 支付回调接口的幂等性设计支付宝沙箱回调地址/pay/notify必须满足同一笔交易可能被重复推送多次接口需识别并丢弃重复请求PostMapping(/notify) public String handleAlipayNotify(HttpServletRequest request) { MapString, String params getParameterMap(request); // 1. 验证签名支付宝官方SDK boolean signVerified AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSignType()); if (!signVerified) { return fail; // 签名失败直接返回fail支付宝会停止重试 } // 2. 提取业务参数 String outTradeNo params.get(out_trade_no); // 业务订单号即reimbursement_id String tradeStatus params.get(trade_status); // 3. 幂等校验查数据库确认是否已处理 Reimbursement reimbursement reimbursementMapper.selectByPrimaryKey(outTradeNo); if (reimbursement null || PAID.equals(reimbursement.getStatus())) { return success; // 已处理过直接返回success } // 4. 更新状态关键开启事务 try { reimbursementMapper.updateStatusPaid(outTradeNo); // UPDATE ... SET statusPAID // 发送MQ通知财务系统可选 rabbitTemplate.convertAndSend(reimbursement.exchange, paid, outTradeNo); return success; } catch (Exception e) { log.error(Payment callback failed for {}, outTradeNo, e); return fail; // 异常返回fail支付宝继续重试 } }注意getPaymentMap(request)必须使用request.getParameterMap()而非RequestBody因支付宝回调是form-data格式。updateStatusPaid方法需在XML中明确写WHERE id #{id} AND status APPROVED防止状态被覆盖。4.2 支付超时的主动查询与补偿机制支付宝未回调时系统每5分钟扫描reimbursement表中statusAPPROVED AND create_time NOW()-300的记录调用支付宝query接口Scheduled(fixedRate 300_000) // 5分钟一次 public void checkPendingPayments() { ListReimbursement pendingList reimbursementMapper.selectPendingPayments(); for (Reimbursement r : pendingList) { try { AlipayTradeQueryResponse response alipayClient.query(r.getOutTradeNo()); if (TRADE_SUCCESS.equals(response.getTradeStatus())) { reimbursementMapper.updateStatusPaid(r.getId()); log.info(Compensated payment for reimbursement {}, r.getId()); } } catch (AlipayApiException e) { log.warn(Alipay query failed for {}, r.getId(), e); } } }该机制确保即使回调丢失300秒内也能完成状态同步避免财务对账差异。5. 数据库设计与性能优化从ER模型到高频查询索引策略本项目数据库并非简单三张表堆砌而是围绕流程实例生命周期设计t_process_instance与业务表通过business_key关联避免冗余字段。5.1 核心表结构与外键约束说明表名主键关键字段说明t_process_instanceidbusiness_key,process_definition_id,start_time,end_timeActiviti7原生表business_key存APP-xxx格式单号t_leave_applyidapply_user_id,status,days,start_time,end_timeidt_process_instance.business_key无外键但逻辑强关联t_meeting_applyidmeeting_type(ONLINE/OFFLINE),room_id,start_time,end_timeroom_id非空仅当meeting_typeONLINEt_reimbursementidamount,status,pay_time,out_trade_noout_trade_noid与支付宝订单号一致提示t_process_instance表不建议加外键指向业务表因Activiti7自身维护流程状态强行外键会导致流程引擎操作失败。关联逻辑由应用层保证。5.2 高频查询场景的索引优化清单针对生产环境慢SQL必须添加以下复合索引MySQL语法-- 查询某用户所有待审批任务Activiti7 RBAC CREATE INDEX idx_task_assignee_proc ON act_ru_task(assignee_, proc_inst_id_); -- 查询某流程实例的所有历史任务审计用 CREATE INDEX idx_hi_task_procinst ON act_hi_taskinst(proc_inst_id_); -- 报销单按状态时间范围查询财务月报 CREATE INDEX idx_reimburse_status_time ON t_reimbursement(status, create_time); -- 会议申请按类型开始时间查询日程视图 CREATE INDEX idx_meeting_type_start ON t_meeting_apply(meeting_type, start_time); -- 请假单按申请人状态日期范围HR统计 CREATE INDEX idx_leave_user_status_time ON t_leave_apply(apply_user_id, status, start_time);实测表明未加idx_reimburse_status_time时财务导出“本月已支付报销单”需12秒加索引后降至180ms。5.3 MySQL配置调优关键参数在my.cnf中调整以下参数以支撑高并发审批# 连接池相关 max_connections 500 wait_timeout 28800 interactive_timeout 28800 # InnoDB优化 innodb_buffer_pool_size 2G # 物理内存的70% innodb_log_file_size 512M # 日志文件大小需重启生效 innodb_flush_log_at_trx_commit 2 # 平衡性能与安全性1最安全2折中 innodb_lock_wait_timeout 50 # 锁等待超时避免长事务阻塞 # 查询缓存MySQL 8.0已废弃但需确认关闭 query_cache_type 0注意innodb_flush_log_at_trx_commit2表示事务提交时仅写入OS缓存非强制刷盘可提升3倍写入吞吐但极端断电场景可能丢失1秒内事务——对OA系统属可接受范围。6. 源码调试与流程验证用Actuator端点快速定位审批卡点当审批单据在某个节点停滞不必翻日志、查表、猜逻辑直接调用Spring Boot Actuator暴露的/actuator/activiti端点获取实时流程快照。6.1 启用Activiti7 Actuator端点在application.yml中添加management: endpoints: web: exposure: include: health,info,metrics,activiti,threaddump endpoint: activiti: show-details: always启动后访问http://localhost:8080/actuator/activiti返回JSON包含所有运行中流程实例及其当前任务{ processInstances: [ { id: c8a1e5b2-...-a1b2c3d4e5f6, businessKey: APP-1712345678901, processDefinitionId: leaveProcess:1:abc123, startTime: 2024-05-20T09:30:00Z, tasks: [ { id: task-789, name: 部门经理审批, assignee: zhangsan, createTime: 2024-05-20T09:30:05Z, dueDate: 2024-05-20T17:30:00Z } ] } ] }6.2 用H2 Console直连查看流程表状态开发阶段启用H2 Consolespring.h2.console.enabledtrue访问http://localhost:8080/h2-consoleJDBC URL填jdbc:h2:mem:activiti用户名密码均为sa。重点观察三张表ACT_RU_TASK运行中任务ASSIGNEE_为空表示未分配CLAIM_TIME_为空表示未认领ACT_HI_TASKINST历史任务DELETE_REASON_为completed表示正常结束deleted表示被取消ACT_RU_EXECUTION执行流IS_ACTIVE_ 0表示流程暂停需检查是否有boundaryEvent未触发。若发现ACT_RU_TASK中存在ASSIGNEE_为空但CLAIM_TIME_有值的任务说明TaskListener分配逻辑异常应检查LeaveTaskAssigneeListener中findDirectSuperior是否返回null。6.3 模拟审批驳回并验证数据一致性手动执行SQL模拟驳回操作验证businessKey关联是否健壮-- 1. 查找待驳回的流程实例ID SELECT ID_, BUSINESS_KEY_ FROM ACT_RU_EXECUTION WHERE PROC_DEF_ID_ leaveProcess:1:abc123 AND IS_ACTIVE_ 1; -- 2. 驳回当前任务假设任务ID为task-789 UPDATE ACT_RU_TASK SET ASSIGNEE_ NULL WHERE ID_ task-789; -- 3. 触发驳回逻辑实际应调用runtimeService.signal()此处仅演示数据状态 INSERT INTO ACT_HI_COMMENT (ID_, TYPE_, TIME_, USER_ID_, TASK_ID_, MESSAGE_) VALUES (UUID(), event, NOW(), admin, task-789, REJECTED); -- 4. 验证业务表状态是否同步更新 SELECT status FROM t_leave_apply WHERE id APP-1712345678901; -- 正确结果应为 REJECTED而非仍为 PROCESSING此验证确保流程引擎与业务表的状态机严格对齐杜绝“流程已结束但业务单还显示审批中”的经典缺陷。本文还有配套的精品资源点击获取
返回列表