ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot+Vue前后端分离OA管理系统实战:从设计到部署全解析

SpringBoot+Vue前后端分离OA管理系统实战:从设计到部署全解析 先交代一个背景这套基于SpringBoot和Vue的OA管理系统是我近期完整跟下来的一套前后端分离项目。如果你正在做毕业设计、准备Java后端面试或者刚入职被分到维护内部OA系统的活那这篇文章应该能帮你省掉不少弯路。我尽量把项目里每一次踩坑、每一个关键设计决策背后的原因都摊开来说不吹技术多新只讲实际怎么落地。1. 整体设计与技术选型为什么是SpringBoot Vue而不是其他组合1.1 OA系统的核心功能边界先想清楚一个问题一个OA系统到底要管什么市面上的泛微、致远这类大厂产品功能铺得很广但绝大多数中小公司采购回来后真正高频使用的模块其实就那么几个。我这套项目里最终圈定的核心范围是员工管理、部门管理、公告发布、审批流请假/报销/用章申请、会议预定、任务分配。为什么圈定这个范围因为对一套以学习、参考或毕业设计为目标的系统来说边界清晰比功能丰富重要得多。你如果把工单、知识库、固定资产管理全塞进来光数据表设计就够你写一个月而且每个模块的深浅度都做不到位面试官一问就露馅。我见过太多人简历上写“精通OA系统开发”结果连审批流的状态机都没画过——这种大而全的伪需求害人不浅。1.2 前后端分离的架构收益与代价这套系统采用前后端分离不单纯为了跟风。分离架构在OA场景下有两个直接好处第一审批流这种交互复杂的模块前端需要独立控制状态变化。比如同一张请假单在“待审批”“已通过”“已驳回”三种状态下页面要展示的操作按钮完全不一样。如果你用Thymeleaf这类服务端渲染模板每次状态切换都要刷新页面用户在一个审批单上来回点几次浏览器就白屏了体验很差。第二公司里用OA的终端很杂。有人用Windows Chrome有人用Mac Safari还有人拿平板上。前后端只通过标准JSON交互后端完全不用关心前端跑在什么设备上。这个对后期扩展移动端非常关键——我们第二期上企业微信H5时后端接口一个都没改。但代价也必须说清楚部署成本变高了。原来一个jar包或者war包扔到Tomcat就跑现在你得管Node构建产物还要处理Nginx静态资源代理和接口转发多了一层要维护的东西。很多新手项目挂在部署这一步就是这个原因。1.3 技术栈的版本选型清单直接给清单都是这个月实操验证过的组合后端SpringBoot 2.7.14 JDK 8 MyBatis-Plus 3.5.3 MySQL 8.0前端Vue 2.6 Element UI 2.15 Axios 0.27 Vue Router 3.5鉴权JWTjjwt 0.9.1 Spring Security仅用核心认证部分构建部署前端Nginx 1.24后端jar包用systemd管理这里有一个很多人问我的点SpringBoot为什么不用3.xVue为什么不用3SpringBoot 3.x要求JDK 17起步很多公司的老系统还停留在JDK 8。更重要的是MyBatis-Plus对SpringBoot 3的官方适配到现在都还有边边角角的兼容问题。你如果只是自己学习用3.x没问题但你要把这套东西丢到公司生产环境JDK版本不是你说升就能升的。Vue这边同理Vue 3的生态已经成熟但Element UI的Vue 3版本Element Plus在表格、弹窗的API变化很大网上能找到的现成OA模板和教程更多还是Vue 2。考虑到这套项目要“源码文档调试”一条龙跑通稳定的社区生态比新版特性重要。2. 核心模块拆解从数据表到接口一步步怎么设计2.1 数据库设计9张表撑起一套OA很多人设计数据表时喜欢一上来就建几十张表我觉得没必要。这套系统的核心表一共9张恰好覆盖了所有核心流程表名用途关键字段sys_user员工账号id、dept_id、username、password、avatar、statussys_dept部门id、parent_id、name、leader_idsys_role角色id、role_key、namesys_user_role用户角色关联user_id、role_idsys_menu菜单/权限id、parent_id、name、path、permsoa_notice公告id、title、content、publisher_id、create_timeoa_leave请假审批id、user_id、days、reason、status、audit_remarkoa_meeting会议预定id、title、room、start_time、end_time、booker_idoa_task任务id、title、assignee_id、creator_id、deadline、status说几个设计时容易忽略的点审批状态字段不要用int存0/1/2一定要用varchar存带语义的值。比如statuspending、approved、rejected直接看数据库一眼就懂排查问题节省大量时间。用魔法数字的表时间久了没人能解释3到底代表什么。部门表里必须加parent_id不是所有公司都是树形组织但加了这一列就保留了扩展成树形的能力。同时leader_id直接指向sys_user表查“这个部门谁负责”就不用join两次。公告和审批单的时间字段要区分创建时间和更新时间这是MyBatis-Plus的AutoFill功能最容易演示的场景。insert时自动填create_timeupdate时自动填update_time不用在业务代码里手动set——这也是面试官爱问的一个点。2.2 三层权限模型如何用RBAC实现“谁能看到什么”OA系统的权限设计我强烈建议直接上RBAC用户-角色-菜单不要整复杂的ABAC或者数据权限。理由很简单OA系统的数据几乎都是全员可见的真正的权限差异只在于“你能点哪些按钮”。实现上有两种方案我最终选了“数据库动态菜单前端路由守卫”的组合。后端在登录时根据用户的角色查出该角色可见的菜单树以JSON形式返回前端。前端拿到菜单后动态添加路由。这样整个系统只有一个固定的路由入口但不同用户看到的内容不同从源头上避免“路由泄漏”。登录接口核心逻辑// 登录成功后构建菜单树 public LoginResult login(String username, String password) { // 1. 校验用户名密码 // 2. 查询用户角色 // 3. 根据角色查询菜单 // 4. 生成JWT包含用户ID和角色信息 // 5. 返回token 菜单树 }前端路由守卫的核心逻辑router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })这套组合拳打下来后端管数据校验前端管体验拦截。注意一个坑前端路由守卫只能做体验优化不能做安全边界。真正的数据权限必须在后端接口再查一次很多新手漏了这个拿Postman直接调接口就能越权。2.3 审批流的设计状态机比想象中简单请假、报销、用章申请这三个模块走到最后本质上都是一个状态机。我把它们抽象成一个通用的状态流转核心。以请假为例状态流是这样的pending - approved pending - rejected看起来简单但要注意“驳回后能否重新提交”这个需求。我见过很多系统把驳回直接判死刑用户只能重新建单这个体验很差。所以我在设计状态枚举时预留了一个字段allow_resubmit。驳回后如果这个字段为true用户在原有单据上点击“重新提交”状态回到pending审批人那边看到的是原单新审。实现这个逻辑时我更推荐用状态模式而不是一堆if-elsepublic interface LeaveState { boolean canOperate(LeaveOrder order, String action); void next(LeaveOrder order, String action); } public class PendingState implements LeaveState { public void next(LeaveOrder order, String action) { if (approve.equals(action)) { order.setStatus(approved); } else if (reject.equals(action)) { order.setStatus(rejected); } } }为什么要写成一个类而不是几个if因为后续加状态比如“已撤回”“审批中2级”时只需要新增状态类不用改旧代码。这是设计模式里典型的开闭原则也是面试加分项。3. 开发中必须知道的技术难点与解决方案3.1 MyBatis-Plus自动填充与逻辑删除的配置技巧MyBatis-Plus这套框架比JPA更贴近国内开发者的习惯。用它可以省掉大量单表CRUD的重复代码但有两个坑必须提前踩。第一个坑是自动填充。mybatis-plus的自动填充基于TableField(fill FieldFill.INSERT)注解但填充逻辑要写在MetaObjectHandler接口实现类里。很多人只加注解不写实现类结果create_time永远是null。正确写法是Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }第二个坑是逻辑删除配置。我建议逻辑删除字段用deleted类型tinyint默认0。在application.yml里配mybatis-plus: global-config: db-config: logic-delete-value: 1 logic-delete-not-value: 0配完之后所有Mapper接口自动带WHERE deleted 0不用你操心。但注意——如果你用原生SQL写复杂join逻辑删除条件不会自动加上需要手动补。我在这里栽过一次排查了半小时才发现是逻辑删除条件没生效。3.2 文件上传头像和附件如何统一处理OA系统里用户头像上传是最常见不过的需求。但如果直接写在业务代码里后面接OSS、云存储就要改一堆东西。我在这套系统里做了一个小的抽象定义StorageService接口目前用本地磁盘实现以后换云存储只需新增实现类。public interface StorageService { String store(MultipartFile file, String path); void delete(String path); } Service public class LocalStorageService implements StorageService { // 实现本地磁盘存储逻辑 }上传路径我建议放在和jar包同级的外部目录比如/opt/oa-upload/不要放在项目内部。因为项目重新部署时resources/upload会被覆盖头像就全丢了。这个坑公司老系统至少遇到过三次。另一个细节头像文件命名不能直接用用户本来的文件名。我实测中发现用户从微信传上来的图片名字全是乱七八糟的mmexport123456.jpg甚至还有中文和空格这些在URL里全是坑。我的做法是用UUID代替文件名原始文件名存数据库前端下载时再解析回来。3.3 JWT登录认证无状态方案在OA里的取舍OA系统用不用Spring Security我犹豫了很久。最后结论是用一部分不用全部。我只用Spring Security做密码加密BCrypt没有启用它的过滤器链和UserDetailsService流程——因为OA系统的认证流程就是简单的“查库签token前端存着”套太多Spring Security的概念反而更难维护。JWT这块我用的是jjwt 0.9.1版本注意这个版本的依赖jaxb-api需要手动引入否则高版本JDK下会报ClassNotFoundException。这是JDK 8以上一个经典坑好多帖子没提。生成token的核心逻辑String token Jwts.builder() .setSubject(userId.toString()) .claim(username, user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 30 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();token过期时间我设为30分钟这是个比较折中的值。设太长不安全有人XSS拿到你的token可以一直访问设太短用户烦动不动重登。30分钟前端拦截401后自动跳登录页实测体验可以接受。4. 前后端联调与Vue工程化实战4.1 前端项目的初始化与目录组织前端这块我用的是Vue CLI 4脚手架创建命令不提了网上都有。但目录结构倾向强烈推荐按模块组织而不是按文件类型组织。什么意思呢推荐这种结构src/ api/ # 按模块拆分的接口定义 leave.js notice.js user.js views/ # 页面组件 leave/ apply.vue audit.vue notice/ list.vue detail.vue router/ index.js store/ modules/ user.js utils/ request.js为什么按模块拆因为招聘模板“用户管理”一般涉及接口、页面、store三个位置。你按文件类型组织一个功能要横跨三个目录找按模块组织user这个功能相关的代码全在api/user.js、views/user/、store/modules/user.js三个文件里而且文件名完全一致一眼就找到了。4.2 Axios封装与请求拦截器的正确姿势Axios不能直接裸用一定要封装。我在这套系统里做了两层封装第一层统一处理token、错误码、超时第二层按模块导出API方法。第一层核心代码const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 系统异常) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { // token过期跳登录页 router.push(/login) } Message.error(网络异常请稍后重试) return Promise.reject(error) } )这一段有非常多的细节baseURL是/api而不是完整地址是为了配合Vue CLI的devServer代理和后端部署的Nginx转发不管在哪运行都不用改前端代码。response拦截器不直接返回response.data.data而是返回整个{code, msg, data}结构这样后端返回文件下载链接、导出任务这类自定义数据时有更大的灵活性。4.3 前端权限控制的两种实现路径页面级权限用Vue Router的meta.roles实现{ path: /meeting, component: () import(/views/meeting/index.vue), meta: { roles: [admin, manager] } }按钮级权限用自定义指令v-permission实现Vue.directive(permission, { inserted(el, binding) { const requiredPerms binding.value const userPerms store.state.user.perms if (!userPerms.some(p requiredPerms.includes(p))) { el.parentNode el.parentNode.removeChild(el) } } })为什么页面和按钮要分开控制因为页面隐藏了按钮还在。比如普通员工能看到“审批管理”页面里面有自己的申请记录但“通过/驳回”按钮只有审批人可见。只做页面级权限没法处理这种场景必须下到按钮级。5. 部署调试与常见问题排查5.1 前后端分离项目的部署编排与Nginx配置部署这块我把前端构建产物和后端jar包放在同一台服务器上用Nginx做统一入口。这个方案在低并发oa场景下完全够用而且省一台服务器钱。Nginx配置server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /opt/oa-frontend; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这段配置注意两个点try_files $uri $uri/ /index.html是vue router的history模式必需的否则用户刷新页面时Nginx会404。如果你用hash模式url里有#号这行可以不写。但hash模式不美观我的建议是直接用history模式配上这段try_files。location /api/里的proxy_pass后面带了一个斜杠这意味着把/api/替换成/转发给后端。也就是说前端请求/api/leave/list后端实际收到的是/leave/list。这样后端controller里的RequestMapping就不用统一加/api前缀了路径很干净。5.2 后端jar包运行与systemd守护进程配置SpringBoot的jar包不能裸跑一关终端就死。推荐用systemd管理[Unit] DescriptionOA System Backend Afternetwork.target mysql.service [Service] Userroot ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/oa-backend/oa-system.jar SuccessExitStatus143 Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后systemctl daemon-reload systemctl enable -now oa-backend。这个方案有一个重点JVM堆内存的-Xms512m -Xmx1024m默认和服务器物理内存一样大如果你的服务器内存只有2G会导致MySQL内存不足。设定最大值是一种显式可控的自保。数据库连接池我建议配在SpringBoot配置文件里并设置连接超时断开与重连机制spring: datasource: url: jdbc:mysql://localhost:3306/oa?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiconnectTimeout3000 hikari: maximum-pool-size: 10 minimum-idle: 5 idle-timeout: 300005.3 高频踩坑问题与排查思路速查表结合最近几天调试的实测把最常见的坑和排查方法整理成一张表直接用问题现象排查思路解决方案前端请求接口但出现404前端资源正常Nginx转发路径是否带斜杠后端context-path是否为/核对nginx的proxy_pass清空浏览器缓存重新加载资源前端构建时报内存溢出JavaScript heap out of memory前端依赖包过大webpack内存不够NODE_OPTIONS--max-old-space-size4096 npm run build登录时密码校验不通过但密码确实是对的数据库表密码是否是{bcrypt}前缀加密确认后端加密方式与数据库初始数据一致建议用自带初始化脚本重新导入数据MySQL 8连接报timezone错误JDBC驱动版本或timezone参数缺失url加serverTimezoneAsia/Shanghai驱动版本用8.0.33以上Vue页面空白无报错路由模式问题或Element UI按需引入遗漏检查控制台是否有[Vue warn] Failed to resolve component改用完整引入方式测试SpringBoot启动报端口被占用之前残留的jar进程未退出netstat -tlnp或lsof -i:8080找到进程kill5.4 一个意想不到的坑服务器时区导致的审批时间错误讲一个实际排了很久的坑。第一次部署后用户提交请假单后台显示的时间比实际晚了8小时。一开始以为是MySQL配置问题排查了一圈都不是。最终定位是JDK8的默认时区问题。MySQL连接串里写了serverTimezoneAsia/Shanghai但Java应用所在服务器的系统时区是UTC。Java通过JDBC读取MySQL的DateTime类型时会按系统时区去解释当前时间结果差了8小时。对治方法很直接在application.yml里显式配置Jackson的时区spring: jackson: time-zone: GMT8 date-format: yyyy-MM-dd HH:mm:ss同时启动命令加上-Duser.timezoneAsia/Shanghai参数双保险。6. 从这套OA系统里能学到什么一份给后来者的干货清单项目做完一轮复盘我梳理了五个这套系统里最有价值的“知识点”也是面试高频考点。6.1 JWT的过期与续期策略这套OA使用JWT方案的组合是token有效期30分钟 前端在登录后设置一个30分钟定时器 每次请求在axios拦截器里判断剩余有效时间低于5分钟且用户在用系统时手动刷新token。粗糙但可靠。面试时如果你能把这个逻辑讲清楚效果远好过死背原理。6.2 RBAC模型为什么能成为主流因为它的核心思想是“权限不与用户直接挂钩而是与角色挂钩”。公司新来一个实习生给他分配“员工”角色他就自动有了员工该有的权限离职了一个删掉这个人的用户记录即可不影响其他任何人的权限。在管理成本上它是目前性价比最高的方案。6.3 状态机模式在代码层面的落地审批流如果只用if-else做状态流转每个业务模块的表里都有状态字段流转逻辑一个审批模块或许还行一旦模块多了代码就成了“屎山”。状态机模式在OA这个场景是最好的设计模式解不单是为了面试更是为了卧槽式维护自己的项目。6.4 MyBatis-Plus为什么在中小企业这么火它最大的贡献不是在SQL层面而是帮开发者省掉了“单表CRUD”的时间。src从小到大的增速在OA这种字段固定、逻辑不复杂的场景下用MyBatis-Plus能让单表操作效率提升至少60%同时搭配SQL注入器可以做到零SQL。6.5 前后端分离联调的核心协作方法论前端开发时用mock数据后端开发时用Swagger。双方约定好接口字段早期就抛开页面debug。这套系统开发时前端的列表页、审批页和后端接口同步开发如果没有这套约定至少多半个月的联调周期。这是团队协作的实战远比技术栈更重要。结尾这套系统本身不是真理它只是一个参考。我在写代码时很多时候都在想一个问题如果三个月后的我自己来审这套代码会觉得哪里难维护于是特意在关键位置加了注释把常量集中管理把状态流转收敛到枚举里。如果你现在正准备动手写一套OA系统我的核心建议是先把数据库表和状态流转图画明白再去碰前后端代码。过程中你会遇到数不清的“小坑”每个坑后面都是对一个知识点更深的理解。做完之后你会有信心去写任何一套企业级管理系统。后面如果你们正在做OA系统里价值最高的审批流模块也遇到状态流转设计或者多级审批的问题欢迎随时交流我这边踩过的坑可以直接帮你绕开。
返回列表