
简介这是一套基于SpringBoot的企业级在线办公系统源码面向Java全栈开发者与企业信息化项目实践者聚焦工作流审批与协同办公场景完整实现请假、会议含腾讯TRTC视频会议、报销三大核心审批流程并集成支付宝沙箱支付功能。资源包共340个文件涵盖170个Java业务逻辑与Activiti7流程定义类、37个Vue前端组件、47个SVG图标资源、27个PNG界面素材及关键配置文件如application.yml、Dockerfile、SQL建表脚本等整体压缩后仅4.9MB结构清晰、模块解耦便于快速部署与二次开发。已有114人学习下载提供开箱即用的RBAC权限体系、WebSocket实时消息推送、Redis会议ID管理及Activiti7流程可视化支持配套代码注释详实适合中高级开发者深入理解企业级审批系统架构设计与主流技术栈MyBatisWebSocketRedisTRTC的工程化落地。1. 这不是又一个“待办列表”DemoSpringBoot企业级在线办公系统的真实落地切口你见过太多基于SpringBoot的“员工管理系统”——增删改查员工信息、导出Excel、带个登录页就叫“企业级”。但真实场景里审批流不是CRUD是状态机驱动的跨角色协作HR提交请假单后直属领导要批部门负责人要复核财务要同步扣薪系统还要自动归档并触发钉钉/企微通知。本项目源码正是从这个切口切入它把请假、会议申请、报销三类高频审批流程用可配置的状态流转数据库持久化前后端分离架构完整跑通且所有业务逻辑都落在SpringBoot主干上不依赖低代码平台或第三方BPM引擎。适合两类人一是正在用SpringBoot做OA模块开发的后端工程师需要可直接复用的审批模型设计二是技术负责人想评估一套开源办公系统能否作为内部系统基座——它不堆砌炫技功能如AI摘要、OCR识别但把事务一致性、审批节点权限控制、历史版本追溯这些企业刚需点扎扎实实写进了Mapper层和Service层。数据库脚本已预置MySQL 5.7兼容结构开箱即用。2. 审批流程如何在SpringBoot中建模从状态机到数据库表结构设计2.1 为什么不用Activiti或Flowable轻量级审批的取舍逻辑企业级审批系统常陷入“重引擎轻业务”的陷阱引入Activiti后流程定义XML分散、版本管理困难、调试需启动独立流程引擎、与SpringBoot事务难以统一。本项目选择纯Java状态机实现核心依据三点第一三类审批流程节点固定请假申请人→直属领导→HR报销申请人→财务初审→财务终审第二状态变更需强事务保障如审批通过时必须同时更新申请单状态、生成审批记录、扣减余额第三运维成本敏感——生产环境少一个Java进程就少一个监控盲区。因此所有状态流转逻辑收束在ApprovalService中用Transactional包裹状态变更业务操作避免分布式事务复杂度。提示若后续需支持动态流程编排如销售合同审批路径随金额浮动再引入Flowable是合理演进路径但初期硬编码状态机更可控。2.2 四张核心表的设计意图与字段约束数据库采用MySQL表结构刻意规避过度范式化以读写平衡为优先。关键表如下精简版含业务强相关字段表名主要字段设计要点approval_applyid,apply_type(1请假/2会议/3报销),applicant_id,status(0草稿/1待审/2通过/3驳回),create_time,update_timeapply_type用枚举值而非外键减少JOINstatus为tinyint便于SQL条件索引approval_nodeid,apply_id,node_order(1第一节点/2第二节点),approver_id,status(0未处理/1同意/2驳回),comment,handle_time每个审批节点独立记录支持多级审批回溯node_order隐含流程顺序无需额外流程定义表approval_leaveid,apply_id,start_date,end_date,reason,days请假专属扩展表避免主表字段膨胀days由end_date-start_date1计算后存入防前端篡改approval_reimburseid,apply_id,amount,receipt_images(JSON数组存储凭证URL),category(差旅/招待等)报销金额amount设为DECIMAL(10,2)杜绝浮点数精度问题凭证URL存JSON而非逗号分隔便于前端解析2.2.1 关键SQL验证如何确保同一申请单不被重复审批-- 更新审批节点状态时必须校验当前状态为未处理且申请单整体状态为待审 UPDATE approval_node SET status 1, comment 同意, handle_time NOW() WHERE apply_id ? AND node_order ? AND status 0 AND EXISTS ( SELECT 1 FROM approval_apply WHERE id ? AND status 1 );此SQL通过EXISTS子查询联动主表状态避免并发下审批人看到过期申请单仍能提交。执行后需检查ROW_COUNT()是否为1否则抛出OptimisticLockException提示“该申请已被他人处理”。2.3 SpringBoot中状态流转的代码骨架Service public class ApprovalService { Transactional public void approve(Long applyId, Long approverId, Integer nodeOrder, String comment, boolean isAgree) { // 1. 校验申请单是否存在且状态合法 ApprovalApply apply applyMapper.selectById(applyId); if (apply null || !apply.getStatus().equals(ApplyStatus.WAITING.getValue())) { throw new BusinessException(申请单不存在或不可审批); } // 2. 更新当前节点状态乐观锁 int updated nodeMapper.updateStatusByApplyIdAndOrder( applyId, nodeOrder, ApproveStatus.UNHANDLED.getValue(), isAgree ? ApproveStatus.APPROVED.getValue() : ApproveStatus.REJECTED.getValue(), comment, approverId ); if (updated ! 1) { throw new BusinessException(审批失败节点状态已变更请刷新页面); } // 3. 根据审批结果推进流程 if (isAgree) { moveNextNode(applyId, nodeOrder); // 触发下一节点 } else { rejectApply(applyId); // 驳回则终止流程 } } private void moveNextNode(Long applyId, Integer currentNodeOrder) { // 查询下一节点order按node_order升序取最小大于current的值 Integer nextOrder nodeMapper.selectNextNodeOrder(applyId, currentNodeOrder); if (nextOrder null) { // 无下一节点流程结束 applyMapper.updateStatus(applyId, ApplyStatus.APPROVED.getValue()); } else { // 向下一节点发送待办通知此处省略消息推送逻辑 } } }这段代码体现三个关键设计① 所有状态变更前必查主表状态防止越权操作② 节点更新用updateStatusByApplyIdAndOrder方法底层SQL含AND status 0条件实现乐观锁③ 流程推进逻辑与节点数据解耦moveNextNode只依赖node_order数值关系无需维护流程图元数据。3. 前后端分离下的审批交互Vue Axios企业级封装与SpringBoot接口对齐3.1 接口设计原则RESTful但不教条聚焦审批语义本系统接口命名放弃纯REST风格如/api/approvals/{id}/approve转而采用动词导向设计原因有二第一审批动作本质是命令式操作approve/reject非资源状态查询第二同一申请单需支持多角色不同操作申请人撤回、审批人同意、管理员强制通过URI路径难以承载全部语义。最终采用以下模式场景请求方式URI参数说明提交新申请POST/api/apply/submitBody含applyType,formData(JSON字符串不同类型字段不同)查询我的待办GET/api/todo/list?roleapproverstatuswaitingrole区分申请人/审批人/管理员status过滤状态审批操作POST/api/approval/handleBody含applyId,nodeOrder,isAgree,comment查看审批历史GET/api/approval/history/{applyId}PathVariable传申请单ID返回全节点记录注意所有接口统一返回ResultT包装体含code(200成功/500异常)、msg、data字段前端Axios拦截器据此统一处理loading和错误Toast。3.2 Vue Axios企业级封装拦截器如何解决审批场景痛点// api/request.js import axios from axios // 请求拦截器自动注入token与审批上下文 axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } // 关键为审批操作添加X-Approver-Id头后端据此校验审批人权限 if (config.url.includes(/api/approval/handle)) { const approverId localStorage.getItem(userId) config.headers[X-Approver-Id] approverId } return config }) // 响应拦截器统一处理审批业务异常 axios.interceptors.response.use( response response, error { if (error.response?.status 403) { // 403特指权限不足可能是审批人无权处理该节点或申请单已被他人处理 ElMessage.error(操作失败您无权处理此审批项或该申请已被处理) return Promise.reject(error) } if (error.response?.data?.code 5001) { // 自定义业务码审批冲突乐观锁失败 ElMessage.warning(检测到他人已处理该申请请刷新页面重试) return Promise.reject(error) } return Promise.reject(error) } )此封装解决两个审批高频问题①X-Approver-Id头让后端RequestHeader直接获取审批人ID避免每次从JWT解析提升性能② 对5001业务码的专项提示比泛化的“服务器错误”更精准引导用户操作。3.3 SpringBoot Controller层的关键参数校验RestController RequestMapping(/api/approval) public class ApprovalController { PostMapping(/handle) public Result? handleApproval(RequestBody Valid ApprovalHandleDTO dto, RequestHeader(X-Approver-Id) Long approverId) { // 1. 校验dto非空及字段约束Valid触发 // 2. 服务层校验approverId是否为当前节点指定审批人 approvalService.approve(dto.getApplyId(), approverId, dto.getNodeOrder(), dto.getComment(), dto.isAgree()); return Result.success(); } } // DTO校验注解确保基础安全 public class ApprovalHandleDTO { NotNull(message 申请单ID不能为空) private Long applyId; Min(value 1, message 节点序号至少为1) private Integer nodeOrder; NotBlank(message 审批意见不能为空) private String comment; private boolean isAgree; // 默认false避免前端不传导致逻辑错误 }Valid校验放在Controller层而非Service层符合分层职责Controller负责输入合法性空值、格式Service专注业务规则权限、状态。Min对nodeOrder的约束防止恶意请求传入负数触发SQL注入漏洞。4. 视频会议模块的轻量集成WebRTC信令服务与SpringBoot的协同方案4.1 为什么不用商业SDK自建信令服务的合理性边界视频会议功能常被误认为必须集成腾讯会议、Zoom SDK。但本系统仅需满足“审批通过后发起临时会议”这一场景核心诉求是① 会议链接一次性有效过期自动失效② 参会者身份与审批单绑定仅申请人、审批人可加入③ 无录像、无屏幕共享等高级功能。因此采用WebRTC 自建信令服务Signaling Server方案完全避开商业SDK的授权与流量费用。提示信令服务不处理音视频流由浏览器P2P直连仅负责交换SDP Offer/Answer和ICE CandidateSpringBoot作为信令中转站足够胜任。4.2 数据库中会议元数据的存储策略新增meeting_room表结构极简字段类型说明idBIGINT PK会议IDapply_idBIGINT NOT NULL关联审批单ID建立业务上下文room_codeVARCHAR(16) UNIQUE六位随机码如A7B9C2作为会议入口避免暴露内部IDexpire_timeDATETIME过期时间默认创建后2小时creator_idBIGINT创建人申请人statusTINYINT0未开始/1进行中/2已结束会议创建逻辑在审批通过后触发// ApprovalService.java 中 approve() 方法末尾追加 if (isAgree apply.getApplyType() ApplyType.MEETING.getValue()) { meetingService.createMeetingForApply(applyId); }createMeetingForApply方法生成room_code用SecureRandom避免碰撞、设置expire_timeLocalDateTime.now().plusHours(2)并插入meeting_room表。前端通过/api/meeting/join?codeA7B9C2获取会议信息。4.3 SpringBoot信令接口的WebSocket实现Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new SignalingWebSocketHandler(), /ws/signaling) .setAllowedOrigins(*); // 生产环境需限制域名 } } Component public class SignalingWebSocketHandler extends TextWebSocketHandler { // 存储房间码到Session映射生产环境应换Redis private final MapString, SetWebSocketSession roomSessions new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 从URL参数提取room_code验证有效性 String roomCode extractRoomCode(session.getUri()); if (isValidRoomCode(roomCode)) { roomSessions.computeIfAbsent(roomCode, k - ConcurrentHashMap.newKeySet()) .add(session); } else { session.close(); } } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); JsonNode json new ObjectMapper().readTree(payload); String type json.get(type).asText(); // offer, answer, candidate String roomCode json.get(roomCode).asText(); // 广播给同房间其他成员除发送者 SetWebSocketSession others roomSessions.getOrDefault(roomCode, Collections.emptySet()); for (WebSocketSession other : others) { if (!other.equals(session) other.isOpen()) { other.sendMessage(new TextMessage(payload)); } } } }此实现仅处理信令转发不涉及STUN/TURN服务器配置开发环境用公共STUN服务器即可。room_code作为房间标识天然隔离不同会议避免信令混淆。生产部署时roomSessions需替换为Redis的Set结构支持集群扩展。5. 企业级部署与参数调优从本地启动到生产环境的5个关键配置5.1 application.yml中必须调整的5个参数spring: datasource: url: jdbc:mysql://localhost:3306/office_system?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse # 关键连接池配置直接影响审批并发能力 hikari: maximum-pool-size: 20 # 高峰期审批请求密集需足够连接 minimum-idle: 5 # 避免空闲连接被MySQL断开 connection-timeout: 30000 # 30秒超时防数据库慢查询拖垮服务 validation-timeout: 3000 # 验证连接有效性超时 idle-timeout: 600000 # 空闲连接600秒后回收 server: port: 8080 servlet: context-path: /office # 统一上下文路径便于Nginx反向代理 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境开启SQL日志 global-config: db-config: id-type: assign_id # 使用雪花算法生成ID避免MySQL自增主键瓶颈 # 关键启用JVM参数优化应对审批系统内存波动 management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when_authorized5.2 MySQL生产环境必须执行的索引优化审批高频查询集中在approval_node表以下索引缺一不可-- 加速按申请单ID查询所有节点 ALTER TABLE approval_node ADD INDEX idx_apply_id (apply_id); -- 加速按审批人ID查询待办todo/list接口 ALTER TABLE approval_node ADD INDEX idx_approver_status (approver_id, status); -- 加速会议房间码查询meeting/join接口 ALTER TABLE meeting_room ADD INDEX idx_room_code_status (room_code, status);缺失idx_approver_status会导致SELECT * FROM approval_node WHERE approver_id ? AND status 0全表扫描在万级数据下响应超2秒。5.3 Nginx反向代理配置要点支持HTTPS与静态资源upstream office_backend { server 127.0.0.1:8080 weight1 max_fails3 fail_timeout30s; } server { listen 443 ssl; server_name office.yourcompany.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # 关键WebSocket连接需升级头 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; location /office/ { proxy_pass http://office_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源直接由Nginx服务减轻SpringBoot压力 location /static/ { alias /var/www/office/static/; expires 1h; } }proxy_http_version 1.1和Upgrade头是WebSocket正常工作的前提否则信令连接会降级为HTTP轮询延迟激增。5.4 日志分级与审批关键事件追踪在logback-spring.xml中配置审批操作专用Appenderappender nameAPPROVAL_LOG classch.qos.logback.core.rolling.RollingFileAppender filelogs/approval.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/approval.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 在ApprovalService中使用 -- logger namecom.example.office.service.ApprovalService levelINFO additivityfalse appender-ref refAPPROVAL_LOG/ /logger在ApprovalService.approve()方法开头添加日志log.info(审批操作开始: applyId{}, approverId{}, nodeOrder{}, isAgree{}, applyId, approverId, nodeOrder, isAgree);当出现审批状态不一致时直接grep applyId12345 logs/approval.log即可还原完整操作链无需翻查全量日志。5.5 数据库备份与审批数据一致性校验脚本每日凌晨执行校验确保审批状态逻辑自洽#!/bin/bash # check_approval_consistency.sh mysql -u root -p$PASSWORD office_system -e SELECT a.id as apply_id, a.status as apply_status, COUNT(n.id) as node_count, SUM(CASE WHEN n.status 1 THEN 1 ELSE 0 END) as approved_nodes, SUM(CASE WHEN n.status 2 THEN 1 ELSE 0 END) as rejected_nodes FROM approval_apply a LEFT JOIN approval_node n ON a.id n.apply_id GROUP BY a.id, a.status HAVING (a.status 2 AND approved_nodes 0) -- 已通过但无同意节点 OR (a.status 3 AND rejected_nodes 0) -- 已驳回但无驳回节点 OR (a.status 1 AND approved_nodes 0) -- 待审但已有同意节点 ;将此脚本加入crontab输出结果邮件告警。一旦发现状态矛盾立即触发人工核查避免审批结果错乱影响考勤或报销。本文还有配套的精品资源点击获取