ARTICLE DETAIL

资讯详情

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

SpringBoot OA系统实战:自动装配、权限模型与工作流引擎

SpringBoot OA系统实战:自动装配、权限模型与工作流引擎 SpringBoot OA 自动化办公系统这东西光看标题以为就是个“增删改查”的毕业设计实际做下来才发现它几乎能把后端开发要碰的坑都碰一遍。我自己的体会是OA 系统是所有业务系统的浓缩版有用户权限、有流程审批、有消息通知、有文件上传、有系统日志每一个模块拎出来都能单独开一篇。这篇文章我不打算写成一个从头到尾的保姆教程那样写出来也没人看。我更想把一个 SpringBoot OA 从零到落地的完整过程和关键决策点拆开讲清楚每一步为什么这么做以及我在真实项目里踩过的那些坑。这套系统针对的是典型的办公自动化场景公告发布、请假审批、报销流程、会议管理、部门组织结构管理。适合正在做毕设的同学也适合公司里需要快速搭一套内部管理后台的开发者。全文我会围绕 SpringBoot 的核心机制来展开结合自动装配原理、权限模型设计、工作流引擎选型、文件存储方案、部署上线这几个主线来讲。你能看到的不只是代码还包括我当时做技术选型时的脑回路以及哪些地方是文档里不会告诉你的。1. OA 系统的技术选型为什么 SpringBoot 几乎是唯一解先说选型。OA 系统不是新技术驱动的项目它讲究的是稳定、快速开发、容易维护。我在做这套系统之前也犹豫过要不要用 RuoYi 这种快速开发平台后来还是决定从 SpringBoot 手动搭建。原因很简单毕设也好中小型项目也好你都要能讲清楚每一个组件是干嘛的。用了快速开发平台答辩时候一问底层原理直接卡壳。1.1 SpringBoot 在 OA 场景下的核心优势SpringBoot 在 OA 系统里最大的价值不是“自动配置”这个花哨概念而是它把 Spring 生态里那些繁琐的 XML 配置全部收编了。你想想传统的 SSM 项目要做事务管理要配 DataSource、配置 SqlSessionFactory、配置 MapperScan一环扣一环配错一个地方整个项目启动就报错。SpringBoot 用spring-boot-starter-jdbc、spring-boot-starter-web这些起步依赖把常用场景的配置全部做成自动装配。自动装配原理这块我必须单独提一下。很多人面试会背“SpringBoot 通过EnableAutoConfiguration实现自动配置”但落到 OA 项目里你要真正理解的是spring.factories和META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这两个文件。SpringBoot 启动的时候会扫描这些配置文件里注册的所有AutoConfiguration类然后通过ConditionalOnClass、ConditionalOnMissingBean这类条件注解判断“当前环境里有没有这个类”“用户有没有自定义 Bean”来决定要不要执行默认配置。我记得最开始自己做了一个很蠢的调试项目里引入了 Redis 依赖但是一直没用到 Redis结果启动的时候日志里出现了 RedisAutoConfiguration 相关的配置。我当时以为是哪里写错了后来才知道 SpringBoot 自动装配就是这么设计的——只要 classpath 里有对应的类就会加载对应配置。这也是为什么说 OA 项目里用 SpringBoot 很省心你只管把依赖抽进来SpringBoot 帮你把基础配置全部兜底你只需要覆盖自己不满足的部分。1.2 技术栈组合的整体规划我最终定的技术组合是这样的组件版本选型选择理由SpringBoot2.7.x稳定、适配面广避免高版本带来的未知兼容问题MyBatis-Plus3.5.x单表 CRUD 开箱即用分页插件成熟适合 OA 这种大量单表操作MySQL8.x主流稳定InnoDB 事务支持完善Redis可选做验证码缓存、在线用户状态、部门树缓存MinIO最新稳定版兼容 S3 协议的对象存储OA 附件、头像存储Spring Security JWT5.x前后端分离场景的认证授权方案Activiti 77.x请假、报销等审批流引擎这里有一个很容易踩的坑SpringSecurity 的版本兼容性。SpringBoot 2.7.x 对应的 Spring Security 是 5.7.x而 SpringBoot 3.x 全面切到了 Jakarta 命名空间如果你用了 SpringBoot 3.x 再用老教程里的javax.servlet代码编译直接报错。这是 2024 年以来大家在毕设和项目重构里最容易遇到的坑很多人升级 SpringBoot 版本之后发现一堆类找不到原因就是 javax 变成了 jakarta。所以我个人的建议是OA 系统如果没有特殊需求就老老实实用 SpringBoot 2.7.x不要去追新版本。1.3 为什么不用 RuoYi 这类脚手架直接改我见过太多人用 RuoYi 做 OA 毕设评论区问的问题都是“怎么去掉 RuoYi 自带的功能”。说实话RuoYi 是一个很好的快速开发框架代码生成器也确实强大。但拿它做 OA 有一个致命问题你很难把框架自带的东西和你自己写的东西区分开。答辩的时候老师问“这个用户管理模块的代码是你写的吗”你总不能说是我在 RuoYi 上改的吧。从学习角度讲手动搭建一次 SpringBoot OA你会对 Maven 依赖管理、拦截器注册、全局异常处理、MyBatis 分页插件配置这些基础组件有非常清晰的认识。这些东西才是真正面试要考的。2. 数据库模型设计OA 系统的核心是“关系”而不是“增删改查”OA 系统的表结构看起来很简单无非就是用户表、部门表、角色表、菜单表、公告表、流程表但真正设计起来你会发现最难的其实是梳理表之间的关系。很多新手上来就建表建到后面发现字段不够用、关联对不上只能推倒重来。2.1 用户、角色、部门、菜单的四角关系一个标准的 OA 权限模型通常是 RBAC基于角色的访问控制模型。我设计的核心表包括sys_user、sys_role、sys_menu、sys_dept以及三张关联表sys_user_role、sys_role_menu、sys_user_dept。这里最容易被忽略的是部门和用户的关系很多人会直接在sys_user表里加一个dept_id字段但实际业务里一个用户可能兼职多个部门或者需要跨部门查看数据。我最终的方案是保留了dept_id作为主部门同时用sys_user_dept表存储辅助部门关系。这样既能快速查出用户的主部门又能在 OA 系统里做跨部门的数据权限控制。部门表我建议用parent_id做树形结构不要用ancestors字段做冗余路径除非你确定层级非常深。OA 系统的部门层级一般不超过五层用递归查询树足够不需要引入闭包表这种复杂方案。菜单表的设计决定了前端的动态路由。sys_menu表里我刻意加了component_path和route_path两个字段前者对应 Vue 组件的实际文件路径后者对应浏览器 URL 的路由路径。这俩字段分开存的原因是一个菜单项可能映射到不同的布局组件比如同样是“审批管理”首页入口和详情页的路由差异很大。2.2 流程模块的表结构不要把审批流程做死在业务表里请假、报销、用章申请这些 OA 里的高频流程如果每个业务都单独建一套流程表后续维护会非常痛苦。我的做法是抽象出一个通用的流程设计oa_process流程实例表、oa_process_task任务节点表、oa_process_approval审批记录表三张核心表。oa_process表存储的是流程的基本信息包括流程发起人、当前节点、流程状态审批中/已通过/已驳回、关联的业务类型和业务主键。oa_process_task存储的是每个节点的处理情况一个流程实例对应多条任务记录当前任务用task_status标记。oa_process_approval存储的是审批意见和操作记录这个表也是审计追踪的核心依据。这样做的好处是新增一个审批类型比如离职审批的时候不需要重新建表只需要在业务类型字典里加一个枚举值然后写一个业务对象关联到oa_process表。我实测下来这种设计能把 OA 系统的流程模块扩展成本降到最低。2.3 消息通知模块的幂等设计OA 系统里的“待办提醒”“审批通知”“公告推送”是很容易被忽略的一个模块。我见过有人直接把通知记录做成业务表的附属字段结果消息重复插入、无法标记已读体验很糟糕。我的方案是建一张独立的sys_notification表字段包括接收人、标题、内容、类型、是否已读、跳转路由。这里有一个重点消息通知的写入要保证幂等。比如一个审批流里有三个审批人A 审批通过后系统要通知 B 和 C 去处理下一步如果消息推送失败消费者重试时可能会重复插入两条相同通知。解决办法是建一个业务唯一键用biz_id receiver_id type做联合唯一索引插入时捕获DuplicateKeyException忽略重复消息。这个设计看起来很简单但实际项目里九成的人没做后期线上就会看到大量重复通知。3. SpringBoot 核心机制在 OA 里的落地自动装配、事务与全局异常处理现在到了 SpringBoot 机制真正在 OA 系统里发挥作用的部分。这一章我会掰开揉碎讲自动装配机制、事务边界和全局异常处理是怎么在 OA 场景中落地的以及我踩过的坑。3.1 自动装配机制在项目里的实际配置先说 MyBatis-Plus 的自动装配。MyBatis-Plus 提供MybatisPlusAutoConfiguration它会根据SqlSessionFactory是否存在来自动配置 MyBatis 的核心组件。但有一个坑是它默认不会配置分页插件你必须手动注册一个PaginationInnerInterceptor否则selectPage方法返回的数据不会自动分页甚至可能把全表查出来。我当时就在这上面栽过写了一个用户列表查询接口测试的时候一页返回了全部用户。后来一查是 MyBatis-Plus 版本升级后分页插件的注册方式变了旧代码里用的是OptimisticLockerInnerInterceptor和PaginationInnerInterceptor一起注册的老写法。在高版本里如果只加了一个分页插件但没指定数据库类型多租户和分页插件同时使用时也会出现分页失效的问题。正确的做法是单独把PaginationInnerInterceptor配置成一个Bean并指定DbType.MYSQL。事务这块我还犯过一个比较经典的错误在一个发起审批的方法上加了Transactional方法里调用了短信通知接口短信接口是外部 HTTP 调用。如果短信接口响应慢或者超时整个事务会被拖住数据库连接也被占着不释放。后来我把外部调用挪到了事务提交之后用了TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT)来发布事件在事务真正成功后再做外部通知。这个改动看起来小但 OA 系统里大面积使用事务 外部接口组合场景时这个设计能省掉很多性能问题。3.2 全局异常处理要比你想的更细致在 OA 系统里异常处理是我见过初学者最随意的地方。最常见的做法是在 Controller 里写try-catch然后 return 一个 null。这样做的后果就是前端拿到 null 之后还要再做一层判断既啰嗦又容易忽略错误码。我的建议是统一用RestControllerAdvice做全局异常处理并且要区分业务异常和系统异常。业务异常是指用户操作不当导致的比如审批人不是当前用户、报销金额超过预算、公告发布时间晚于当前时间等这类异常用自定义的ServiceException抛出全局处理器返回错误码和提示信息HTTP 状态码用 200 也行但更规范的是用 400。系统异常是指 NPE、SQL 异常、远程调用超时等这类异常要记录完整堆栈返回 500 和一句“系统繁忙”。这里有一个很关键的细节全局异常处理器里一定要处理好MethodArgumentNotValidException这是Validated参数校验失败时抛出的异常。如果你不单独处理这个异常前端拿到的错误信息会是 Spring 默认的一长串英文体验极差。我在实际项目里是把所有字段校验的错误信息拼成中文提示返回的。另外OA 系统如果一个模块比较复杂比如审批流引擎回调异常信息就特别容易丢失。千万要在全局异常处理器里加上日志埋点并且把 traceId 通过日志关联起来。没有 traceId 的情况下生产环境排查一个审批失败的问题可能要翻半小时日志。3.3 MyBatis-Plus 的代码生成策略OA 系统的表很多手写实体类、Mapper、Service 太浪费时间。MyBatis-Plus 官方提供了代码生成器但默认生成出来的代码有个问题它会把所有字段塞进实体类包括create_time、update_time这类基础字段导致前端传值的时候可能覆盖后端字段。我的做法是生成器生成完之后手动在实体类的公共字段上加上TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)注解配合MetaObjectHandler实现 createTime 和 updateTime 的自动填充。这样写代码的速度快很多而且不担心时间字段被前端传值覆盖。同样逻辑删除字段deleted也建议在生成后手动统一加上TableLogic避免每次查询都要手写deleted 0。4. 认证与权限设计JWT Spring Security 的正确打开方式OA 系统最核心的模块就是权限。我在做认证设计之前纠结了很久到底用 Session 还是 JWT。OA 系统是前后端分离用 Vue3 做前端所以我最终选了 JWT。这里有一个原则权限改成动态而不是写死。很多人做 RBAC结果权限判断写成了if (user.getRole().equals(admin))这样的代码虽然能跑但是完全丧失了 RBAC 的意义。4.1 JWT 无状态认证在 OA 场景的边界JWT 放在 Header 里做认证是无状态的这是它的优点也是它的缺点。优点是服务端不用存 Session水平扩展的时候不用考虑 Session 同步。缺点是服务端不能主动让某个 token 失效。在 OA 系统里管理员封禁一个账号之后如果 token 还没过期用户依然能访问接口。这是 JWT 方案的一个天然短板。我的解法是引入 Redis 做 token 黑名单。当管理员封禁用户、用户退出登录或修改密码时把该 token 的jtiJWT ID写入 Redis设置过期时间为 token 剩余有效时间。然后在 JWT 过滤器里先校验 token 签名再查 Redis 黑名单如果存在就直接拒绝访问。只有两步校验都通过才放行这样才能做到“伪无状态 可控注销”。这个方案在 OA 这种需要精细化权限控制的系统里是必要的纯 JWT 无状态只适合开放接口场景。4.2 权限表达用权限点而不是角色判断我的权限设计里sys_menu表中有一个perms字段存的是权限点名称比如system:user:list、oa:leave:approve。登录成功后后端把当前用户的所有权限点集合返回给前端前端用v-hasPermi这样的自定义指令控制按钮显隐后端每个接口用PreAuthorize(ss.hasPermi(oa:leave:approve))注解做二次校验。为什么要这么设计因为 OA 系统里的角色很多有部门主管、人事、财务、管理员等不同角色之间的权限是交叉的一个部门主管可能同时有审批权限和公告发布权限。如果你在代码里写“部门主管可以审批”那当新人入职被授予主管角色时所有权限都要重新检查一遍。而用权限点设计只需要在给新角色分配菜单权限时勾选对应的权限点即可代码里根本不需要知道“谁是什么角色”。这是 OA 系统权限设计的精髓。Spring Security 配置里还有一个很重要的细节放行路径的配置。我见过很多 OA 项目因为配置了.antMatchers(/**).permitAll()把 Swagger 文档路径、登录接口、验证码接口、静态资源直接放行了结果后端接口相当于裸露。正确做法是只放行/login、/captcha、/doc.html这些明确不需要认证的路径其余全部走 JWT 过滤器。前端路由守卫里也要配置白名单两个白名单信息一致避免出现“后端放行了前端却跳转登录页”的割裂情况。4.3 验证码与登录防爆破登录接口我加了 Kaptcha 验证码验证码的生成和校验逻辑很简单后端生成图片和 UUID把 UUID 作为 key 存入 Redis过期时间 2 分钟。前端登录时提交captchaKey和captchaCode后端比对成功后删除验证码。这种“一次一用”的设计能防住很小的重放攻击但对真正有规模的暴力破解还是不够。我当时的加强方案是限制 IP 维度同一个 IP 连续输错 5 次密码锁定 10 分钟。这个逻辑在登录失败的时候用 Redis 做计数器注意不要用INCR到 5 之后就无脑锁 10 分钟应该做滑动窗口或者把固定窗口时间缩短否则很容易被恶意请求刷持久锁。OA 系统是内部系统不会像公开网站那样有大规模攻击但基础的防爆破逻辑还是得有。5. 工作流引擎选型自研状态机还是 Activiti工作流是 OA 系统里最有技术含量、也是最容易被人轻视的模块。很多毕设和初级项目的做法是“写死流程”请假申请提交后状态改成 1待审批领导审批通过后改成 2已通过领导驳回后改成 3已驳回。这种状态机设计在单一固定流程场景下完全够用但一旦流程多了比如请假走主管审批报销走主管 财务 总经理审批不同流程之间还可能有会签、或签状态机就会写出一堆 if-else代码会变得非常不可控。5.1 自研轻量状态引擎的适用条件我自己最初是用状态机做的为的就是避免引入 Activiti 的重型依赖。但后续发现状态机适合“流程简单、节点固定、无并发审批”的场景比如 OA 里的请假流程完全可以状态机搞定审批节点就两级不会出现一个节点同时要三个人审批的情况。轻量状态机的核心是一个设计良好的状态流转表process_id、current_status、event、next_status、action_bean这几个字段用一条事件驱动流转移表替代散落的 if-else。这样做的好处是流程变更只需要改表数据不需要改 Java 代码。我在做通用审批流时用到过类似设计可以在不新增代码的情况下支持“审批通过”“审批驳回”“撤回”“转办”等事件只需要往表里插入新的流转记录就行。但状态机有个大问题它默认当前节点只有一个处理人如果你要做“会签”多个审批人必须全部通过、或签任一审批人通过即可状态机的编码复杂度成倍增加。我后来把这两个场景单独写了一个“并行审批”模块用子任务表拆分每个审批人的独立任务最后汇总判断。如果你只做一个简版 OA建议第一版就用状态机不要一上来就搞 Activiti工作量差异巨大。5.2 Activiti 与 Flowable 在 OA 里的取舍如果流程确实复杂比如报销流程里有条件分支金额大于 5000 走总经理审批小于 5000 走部门主管审批用 Activiti 或 Flowable 是更正规的方案。Activiti 7 和 Flowable 6 都是从老 Activiti 5 演化过来的两者核心概念相似用 BPMN 2.0 XML 描述流程模型运行时把流程实例、任务实例持久化到对应表中。选型上的建议是如果是新项目直接选 Flowable社区活跃度高文档完善和 SpringBoot 集成也比较丝滑。Activiti 7 的架构变化较大很多老教程已经失效。我调研下来Flowable 的中文资料质量相对更好遇到问题更容易搜到解决方案。用 Flowable 做 OA 还有一个容易踩的坑Flowable 自带大量历史表ACT_HI_和运行时表ACT_RU_如果不在配置文件里开启历史清理数据量很快会膨胀。OA 系统跑个三五年历史流程数据会有几十万条表膨胀会影响查询性能。建议在配置里加上历史数据自动清理策略或者在业务低峰期定时删除已归档的历史数据。5.3 业务表与流程表的数据关联方式无论用自研状态机还是 Flowable都必须解决一个问题流程引擎里的流程实例和业务表里的单据怎么关联。我用的方案是在业务表里加process_instance_id字段流程发起成功后把返回值写入。查询“我发起的流程”时先查业务表拿到 process_instance_id再查流程引擎的表获取当前节点和审批状态。这里有个实际经验不要把流程引擎的表直接当成业务表来查。有一段时间图省事前端查审批列表直接查了 Flowable 的 ACT_HI_TASKINST结果发现字段语义和业务对不上连“请假天数”都没有只能反查业务表。正确的做法是列表接口也用业务表作为主表通过状态和 process_instance_id 关联流程信息避免跨表大查询。6. 文件存储MinIO 接入 SpringBoot 的正确姿势OA 系统里文件上传是刚需用户头像、公告附件、报销单据照片、审批附件。初学者一般会把文件存到本地磁盘然后nginx代理一个静态路径访问。这个方案单机跑没问题但一旦未来部署到多个实例文件在不同机器上就会不一致。尤其我们这种带 MinIO 的 OA 项目直接把 MinIO 接进来不仅解决了分布式问题也符合生产环境常用的对象存储方案。这里要注意MinIO 只是对象存储跟“网盘”不是一回事但在 OA 场景里把它当附件仓库用非常顺手。6.1 MinIO 与 SpringBoot 的整合要点MinIO 的 Java SDK 集成到 SpringBoot 很简单核心是要配置 Endpoint、AccessKey、SecretKey、Bucket 等参数。这里最关键的一个细节是文件访问 URL 一定要处理权限问题。MinIO 的 Bucket 有两种策略私有和公有。OA 系统里我建议把 Bucket 设为私有上传时拿到文件路径生成一个带签名的临时 URL 返回给前端。这样文件不会直接暴露在公网访问时通过预签名 URL 控制有效期。官方 SDK 里presignedGetObject可以生成一个 7 天内有效的临时链接但 OA 系统里建议有效期设成几分钟到几小时即可不用太长。有一个小坑是预签名 URL 里的文件名可能包含中文或特殊字符前端拿到后直接展示没问题但下载时会解析异常。最好统一用 UUID 重命名文件后再存储展示名单独存数据库字段。6.2 上传进度的处理浏览器上传文件如果附件比较大用户很着急前端希望显示进度条。MinIO SDK 支持putObject时传入一个Progress监听器但它的进度回调单位是字节而且回调频率很高直接把进度写入数据库会非常消耗资源。我的方案是上传请求通过 WebSocket 或 SSE 把进度推送到前端后端在监听器里做节流比如每 500ms 推送一次进度而不是每次回调都推送。这样用户体验好后端也不会有太大压力。如果你想做得更轻一点也可以直接采用前端分片上传 MinIO 合并的方式但 OA 场景一般用不到那么复杂几十 MB 的附件一次性上传没太大问题。6.3 文件清理与重复上传问题OA 系统里用户经常会发生“上传了附件但没提交表单”的场景这些孤儿文件如果没人清理会慢慢占满存储空间。我当时的方案是做一个定时任务扫描上传表中超过 24 小时未关联业务记录的临时文件调用 MinIO 的删除接口清理。同时每次上传前都先对比文件的 MD5如果数据库里已有相同 MD5 且业务关联一致的文件就直接复用已有地址不重复上传。这个优化在实际使用中的效果很明显重复提交表单高频场景下可以省不少存储。7. 前后端联调Vue3 与 SpringBoot 的接口约定我说 OA 系统是一个完整体前后端联调如果有问题整个系统的体验就非常拉胯。前端这块我选了 Vue3 Element Plus Pinia Vue Router前后端分离部署。这里分享我在联调阶段沉淀下来的一套接口规范能大幅减少沟通成本。7.1 统一响应体与错误码设计后端所有的接口返回统一使用ResultT结构包括code、message、data三个字段。成功时code200业务失败用code500或自定义错误码未登录code401无权限code403。前端在封装 Axios 拦截器的时候统一处理401跳转登录页和403提示无权限不要每个页面单独去判断。这里有一个比较微妙的地方HTTP 状态码和业务 code 的关系。我见过很多团队做了双重状态码导致前端要同时判断 HTTP 状态码和业务 code非常繁琐。我个人的做法是HTTP 状态码只表示请求是否成功业务错误一律用 HTTP 200 业务 code 来表达。这样前端拦截器只需要看业务 code 即可逻辑清晰很多。唯一的例外是文件下载发生错误时没有办法用 JSON body 表达只能靠 HTTP 状态码来判断。7.2 动态路由的加载时机前端根据用户权限动态生成菜单必须在用户登录后、进入首页前完成。我的做法是登录成功后调用/auth/info拿到用户信息和权限点集合然后前端调用getUserMenus获取菜单树用router.addRoute动态注入路由。这里有一个坑是刷新页面时 Pinia 里的状态会丢失必须重新拉取用户信息。如果后端不提供“根据 token 获取用户信息”的接口前端刷新后就没法恢复登录态整个系统就废了。这个接口在 OA 系统里必不可少。7.3 跨域问题与开发代理开发环境下前端和后端分别跑在 5173 和 8080 端口跨域是必然的。我在 vite.config.ts 里配置了 proxy把/api开头的请求转发到后端服务。这个方案在生产环境也有继承性Nginx 配置里同样做/api反向代理后端服务不直接暴露公网。这里要提一个易错点跨域请求默认不带 Cookie所以如果你用了 Session 和 Cookie 的认证方案要设置withCredentials: true。用 JWT 方案没有这个烦恼Token 在 Header 里天然跨域友好这也是 JWT 被广泛使用的原因之一。8. 部署上线Docker 容器化落地实操OA 系统做完之后部署是最后一道坎。我在部署阶段踩过不少坑比如配置文件里数据库地址写错、MinIO 的 Endpoint 指向了内网 IP、Redis 没设密码导致连接被拒都是些小问题但排查起来确实费时间。8.1 Docker Compose 编排我选择用 Docker Compose 统一编排 MySQL、Redis、MinIO 和后端服务、前端 Nginx。直接用docker-compose.yml管理所有服务比在服务器上挨个手动安装省很多时间。一个精简的docker-compose.yml包含以下服务mysql指定 8.0 镜像挂载数据卷、redis带密码和持久化配置、minio两个端口控制台 9001API 9000、backendDockerfile 构建的 SpringBoot 应用、frontendNginx 容器。这里有一个容易被忽略的坑SpringBoot 应用在容器里连接数据库和 Redis 时host 不能写localhost或127.0.0.1必须写 Docker Compose 里的服务名比如jdbc:mysql://mysql:3306/oa_db。如果写成 localhost应用容器访问的是它自己的回环地址连不到数据库容器。这是每个 Docker 部署新手必踩的坑。8.2 SpringBoot 应用镜像构建的优化SpringBoot 的 Dockerfile 构建有一个很常见的效率问题每次更新代码都要重新依赖下载、重新打包、重新构建镜像非常慢。我的优化是用多阶段构建并在构建阶段把 Maven 依赖和项目代码分开利用 Docker 的层缓存机制让依赖层不变时直接命中缓存。一个可直接用的 Dockerfile 结构是FROM maven:3.8-openjdk-11 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline # 先把依赖下载完成并缓存 COPY src ./src RUN mvn package -DskipTests FROM openjdk:11-jre-slim COPY --frombuild /app/target/oa-system.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar]如果服务器的内存比较吃紧JVM 启动参数可以加上-Xms256m -Xmx512m防止容器因为内存超限被 OOM Kill。我之前在一台 2G 内存的服务器上没加这个参数结果部署后第三天内存就爆了。8.3 Nginx 配置的细节前端路由与后端接口分离前端是 Vue3 应用用的createWebHistory路由模式直接部署到 Nginx 时必须配置try_files $uri $uri/ /index.html不然刷新页面会 404。这个坑 90% 的人都会遇到。接口反向代理配置如下Nginx 把/api路径转发到后端服务的 8080 端口后端返回的静态资源路径如果是 MinIO 的预签名 URL前端直接跳转到 MinIO 的地址下载文件这里需要注意后端返回的 URL 必须是公网可访问的地址也就是说 MinIO 的 Endpoint 不能写内网 IP。我当时在服务器上 MinIO 配的是localhost:9000本地测试没问题部署到服务器后前端访问预签名 URL 全部失败改成公网 IP 后才恢复正常。9. 性能优化与生产环境注意事项OA 系统作为内部系统并发量不会特别大但要保障长时间运行稳定。数据库慢查询、文件存储占用、日志膨胀、定时任务堆积都是运行一段时间后才会暴露的问题。9.1 数据库索引与慢查询OA 系统的高频查询是“我发起/待办/已办”的列表接口筛选条件是用户 ID 加状态。如果不在oa_process表的starter_id和process_status字段上建联合索引数据量过万之后查询会肉眼可见变慢。另一个容易漏索引的是sys_notification表的receiver_id和is_read字段消息列表、未读数统计都靠它。我建议在开发阶段就把常见的查询字段索引建好不要等线上慢了再补。可以开启 MySQL 的慢查询日志阈值设 1 秒上线后跑一周把慢日志里的 SQL 拿出来逐一分析。这是成本最低的性能排查手段。9.2 定时任务与异步处理OA 系统里的定时任务不少待办提醒、会议提醒、流程超时自动处理、日志定时清理。我用的方案是 SpringBoot 自带的Scheduled。这里有一个坑如果项目是多实例部署SpringBoot 自带的定时任务会在每个实例上都触发一遍导致重复发通知、重复执行清理任务。生产上要么改成单实例部署内部系统这样就够了要么引入 XXL-Job 或 Quartz 集群模式。异步处理方面OA 系统里最典型的是审批通过之后给发起人发通知。这个通知操作绝对不能同步阻塞在审批接口里我推荐用Async注解或者 Spring 的事件机制把通知调用挪到异步线程池。要注意Async注解默认使用的是SimpleAsyncTaskExecutor每次调用都会新建线程不适合高频率场景最好定义一个自己的线程池 Bean设置核心线程数、队列容量和拒绝策略。9.3 日志规范最后聊一下日志。OA 系统在线上出了“审批失败”“用户看不到菜单”“附件下载失败”这些问题时如果日志里没有任何上下文信息排查起来就像大海捞针。我自己的日志习惯是每个关键业务操作都要打日志用户发起审批、审批人处理任务、通知推送结果每个环节都记录 userId、processId、业务类型和结果。使用 Lombok 的Slf4j按模块区分日志文件比如oa-process.log、oa-auth.log、oa-file.log别把所有日志都堆到一个文件里。10. 实测效果与改进空间我做完这套 OA 系统之后用一个测试用户跑了完整的业务链路登录、发起请假申请、部门主管审批、人事归档、消息通知、文件上传下载功能链路都能跑通。性能上没有做大并发压测但这套架构让我对 SpringBoot 自动装配、权限模型、工作流这三大块有了很深的实战理解。后来我又在系统里加了一个小功能把 HanLP 分词用起来对公告标题做关键词提取。最初觉得 OA 系统里搞自然语言处理有点硬凑但在公告搜索和文档检索的场景里分词搜索确实比 MySQL 的LIKE %关键词%搜索体验好很多。集成的时候发现 HanLP 的模型文件很大放在 SpringBoot 的 classpath 里会拖慢启动速度后来改成外部路径加载启动才恢复正常。这个经历让我意识到OA 系统不是只有增删改查把合适的技术点适当地引入核心场景会有额外的亮点。关于下一步改进我觉得最值得做的是数据权限细化和移动端适配。目前的数据权限只做到了部门维度每个角色能看哪些数据是通过手动分配的。理想情况是支持“仅本人”“本部门”“本部门及以下”“全部”四种数据范围。移动端适配这块OA 系统最常用的场景是审批和查看公告我的前端虽然做了响应式但真正放到手机浏览器上交互体验还是很吃力。如果有时间应该把审批流程简化成一个小程序版本而不是直接套用 PC 端的布局让审批人用手指点几下就能完成待办处理。如果你也要做一个 SpringBoot OA 系统我的建议是先把权限模型想清楚再去处理工作流。权限模型的成败决定了 OA 系统的骨架是否稳固工作流决定了它能不能真正被人用起来。代码可以从参考源码开始但一定要理解每一行的意图把需求拆解成表结构和接口设计再把接口实现和核心机制结合起来。等到你真的把一套 OA 系统从零搭完你收获的就不只是“会写增删改查”而是对整个 SpringBoot 生态有一个非常立体的认识——这种地基打牢之后今后要转向任何 Java 业务系统都会轻松很多。
返回列表