每年到了准备毕设的季节,总能看到不少人对着"SpringBoot+Vue 企业OA管理系统"这种题目发愁。要么是从网上拖下来一份前后端不分的烂摊子,要么是自己吭哧吭哧写了一半发现审批流程绕得下不来台。我前前后后整理过两套类似的Java Web项目,踩了不少坑,也总结出一套比较顺的思路。这篇就围绕"SpringBoot + Vue 企业OA管理系统"这个题目,从需求拆解、技术选型、数据库设计、后端接口、前端落地,一直讲到源码、SQL脚本和接口文档该怎么整理成一份拿得出手的完整交付物。不管是用来做毕设答辩,还是想在公司内部搭一套轻量级的办公管理工具,这篇文章都能给到一个可以直接照着走的方案。
1. OA系统的需求边界:先搞清楚你究竟在做什么系统
很多同学拿到题目第一步就去敲代码,这是大忌。OA系统看着简单,不就是请假审批、员工管理嘛,但真做起来,业务边界不划清楚,后面全是坑。
1.1 为什么OA是毕设里的"安全牌"
先说结论:企业OA管理系统是Java Web方向里最不容易翻车的选题。原因有三个。
第一,需求足够立体。一个OA系统天然会涉及用户的增删改查、角色的权限控制、流程审批的状态流转、公告的发布与可见性,这些都是面试官和答辩老师最爱问的技术点,覆盖面广但不至于深到失控。
第二,演示效果直观。学生管理系统、图书馆管理系统这类题目太单薄,答辩时三五分钟就讲完了。而OA系统有登录、有工作台、有待办审批、有统计图表,演示起来层次感很强,老师能看到的"功能"足够多。
第三,技术栈匹配度极高。SpringBoot负责接口,Vue负责页面,MySQL存数据,这套组合正好是当前Java Web开发的通用搭配,既有学习价值,又有实用价值,不会让人觉得是"过时的玩具"。
1.2 核心功能清单与模块划分
我在动手之前,会把整个系统拆成下面这张表。这里不是拍脑袋,而是根据OA系统的真实使用场景反推出来的。
| 模块 | 核心功能 | 在毕设中的优先级 |
|---|---|---|
| 登录认证 | 账号密码登录、Token鉴权、退出登录 | 必须 |
| 员工管理 | 员工信息的增删改查、部门归属 | 必须 |
| 部门管理 | 部门的层级结构、增删改查 | 必须 |
| 角色权限 | 角色分配、菜单权限控制 | 必须 |
| 请假审批 | 发起请假、待办审批、审批记录 | 必须 |
| 报销审批 | 报销单提交、财务审批 | 加分 |
| 公告管理 | 公告发布、可见范围、置顶 | 建议 |
| 考勤统计 | 签到签退、月度考勤报表 | 加分 |
我的建议是,时间不够的情况下,优先保证前五个模块,这已经能撑起一个完整的业务闭环。公告管理做出来之后演示效果提升明显,因为它是典型的"所有人都能看到"的功能。
1.3 千万别一上来就做"大而全"
我见过不少同学把OA系统做成了"全家桶",工作流引擎、在线聊天、日历日程、项目管理全往里塞。结果就是每个功能都半吊子,答辩时被老师随便追问一个细节就答不上来。
正确的姿势是MVP思维。第一版只要做到"员工能登录、能看到公告、能提请假、领导能审批"这个闭环就足够了。数据库里的表控制在十张以内,代码量控制在合理范围,反而更容易把每一块的质量打磨好。后面如果时间富余,再考虑加模块。
2. 技术栈选型:SpringBoot和Vue是怎么配合的
这个题目最核心的技术选型就是标题里的SpringBoot加Vue。但具体到版本、配套组件,很多人栽过跟头,我这里直接给一套实测稳定的组合。
2.1 后端:为什么是SpringBoot而不是SSH
早期的Java Web项目喜欢用SSH(Spring + Struts + Hibernate),但现在再做新项目,SpringBoot几乎是唯一解。原因很直白:SpringBoot把繁琐的XML配置全部变成了约定和自动配置,内置了Tomcat,打个jar包就能跑,开发效率不是一个量级的。
具体到这个OA项目,后端核心组件我建议这样选:
- SpringBoot 2.7.x版本,配合JDK 8。这里不推荐一上来就上SpringBoot 3.x,因为3.x强制要求JDK 17,很多老教程和老代码跑不起来,对于毕设来说没必要给自己添堵。
- MyBatis-Plus作为ORM框架。它比原生MyBatis好用太多,分页插件、代码生成器、LambdaQueryWrapper这些功能能省掉大量重复代码。
- MySQL 8.0作为数据库,5.7也完全够用,但8.0是目前的主流。
- JJWT或Hutool的JWT工具来做登录令牌。
- 至于工作流引擎,要克制,不要引入Activiti、Flowable这种东西,对OA毕设来说太重了。
2.2 前端:Vue加Element Plus,后台管理系统的黄金搭档
前端如果不考虑Vue,那标题就名不副实了。Vue的核心价值在于组件化和响应式,这特别适合后台管理系统这种"表格加表单"为主的界面形态。
我推荐的具体组合是:
- Vue 3 + Vite + Element Plus
- Pinia做状态管理
- Vue Router做路由
- Axios做HTTP请求
选Vite而不是Vue CLI,因为Vite启动速度快,而且现在官方主推Vite,网上能查到的坑和解决方案也更多。Element Plus这个组件库内置了大量的表格、表单、弹窗、菜单组件,基本就是给后台管理系统量身定制的,你不需要自己写任何复杂的样式,专注业务逻辑就行。
2.3 环境与版本匹配里最容易踩的坑
这套组合跑不起来,90%的情况不是代码问题,而是版本不匹配。我列几个亲测过的坑:
- JDK版本和SpringBoot版本不匹配,最常见的就是JDK 8配了SpringBoot 3.x,启动直接报错。
- Node.js版本太老,Vite 4以上版本要求Node 14.18+,如果你还是Node 12,npm install都会失败。
- Maven依赖下载慢,国内建议配置阿里云镜像,否则光等依赖就能等半小时。
- MySQL 8的驱动类名是
com.mysql.cj.jdbc.Driver,和MySQL 5的com.mysql.jdbc.Driver不一样,配置错了连数据库直接报ClassNotFound。
3. 数据库设计与权限模型:地基打牢,后面才不慌
数据库设计是整个OA系统的地基。地基打得歪,后面写代码全是别扭的。我设计这套表的时候,遵循的原则很简单:表宁少勿多,字段宁冗余勿过度范式化,权限模型能用经典RBAC就别自创。
3.1 核心表结构:从员工到部门再到审批
先给出一套核心表的清单,这套结构我实测下来接近一个完整OA的最小可运行集合。
- sys_user(用户表):id、username、password、real_name、dept_id、phone、email、avatar、status、create_time
- sys_role(角色表):id、role_name、role_code、description
- sys_menu(菜单表):id、parent_id、menu_name、path、component、icon、sort
- sys_user_role(用户角色关联表):id、user_id、role_id
- sys_role_menu(角色菜单关联表):id、role_id、menu_id
- sys_dept(部门表):id、parent_id、dept_name、sort
- oa_leave(请假表):id、user_id、start_time、end_time、days、reason、status、approver_id、approve_time、approve_comment
- oa_notice(公告表):id、title、content、publisher_id、publish_time、status
几个容易纠结的地方我直接给结论。用户表不要和员工表拆成两张,毕设场景下一张表搞定,还省得联查。部门表用parent_id做层级结构,不要用物理外键去约束,逻辑关联足够,插入数据时还能少踩外键顺序的坑。
3.2 RBAC权限设计:五张表搞定所有权限问题
RBAC(基于角色的访问控制)是后台管理系统最经典的权限模型。核心思想就一句话:用户不直接绑定权限,而是通过角色间接获得权限。
我实际采用的是"用户-角色-菜单"三层模型,一共五张表。用户表和角色表是多对多,通过sys_user_role关联;角色表和菜单表多对多,通过sys_role_menu关联。后端在用户登录成功后,通过用户ID查出全部角色,再用角色ID查出全部菜单,最后把菜单渲染成前端路由。
这里有个重要经验:菜单权限只是最基础的"你能看到什么页面",按钮级别的权限(比如"审批通过"按钮只有管理员能点)要不要做,我建议看时间而定。如果要做,在sys_menu表里加一个perms字段,存按钮权限标识,前端用自定义指令v-hasPermission来控制按钮显示隐藏,后端在接口上也做同样的权限校验。两段都做才算完整的权限控制,只做前端等于没做。
3.3 审批流的数据结构:一张表加状态字段就够了
很多同学一听到"审批流"就发怵,以为要设计复杂的工作流引擎。实际上,对于毕设场景下的OA系统,单据存表加状态流转就完全够用。
以请假审批为例,理想状态是这样的:员工提交请假申请,表中插入一条status为0的记录,直属领导登录后看到的待办列表就是status为0的记录。领导点击通过,status变为1,记录approver_id、approve_time和审批意见。点击驳回,status变为2。
这里有一个设计细节:审批意见字段要在单据表上直接加,而不是单独建一张审批记录表。原因是OA系统里最常见的审批是单级审批,一张表就能完整表达。如果你设计了多级审批(比如请假超过三天要总经理审批),建议再建一张"审批记录表"来存每一步的流转历史,否则单靠status字段无法表达中间过程。优先级上,单级审批永远是第一版,多级审批是加分项。
4. 后端接口设计:认证、返回体、异常处理器三板斧
后端接口这部分,决定了前端开发同学的体验,也决定了答辩时老师问你"接口怎么设计的"你能不能答得漂亮。我的经验是先把三件事做好:认证拦截、统一返回体、全局异常处理。这三板斧立住,后面的业务代码只是往框架里填内容。
4.1 JWT登录认证与拦截器的完整链路
JWT(JSON Web Token)是目前前后端分离项目最常用的认证方案。它的核心逻辑是:用户登录成功后,后端生成一个包含用户ID、用户名、过期时间的Token返回给前端。前端把Token存在本地,每次请求时放在请求头里带回来,后端解析Token就能知道请求是谁发的。
具体到SpringBoot里,我的实现分三步:
第一步,登录接口校验用户名密码,成功后用JWT工具类生成Token。这里要注意密码必须加密存储,用BCrypt格式,不能让明文密码直接躺在数据库里。
第二步,写一个JwtInterceptor拦截器,放行登录接口,拦截其余接口。一旦请求头里没有Token或者Token解析失败,直接返回401。
第三步,注册拦截器时把Swagger文档相关的路径、静态资源路径、错误页面路径都排除掉,否则会出现文档打不开、前端页面刷新后被拦截的问题。
核心代码可以是这样:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行跨域预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } try { Long userId = JwtUtil.parseToken(token); request.setAttribute("userId", userId); return true; } catch (Exception e) { response.setStatus(401); return false; } } }4.2 统一返回体和全局异常:让前后端联调不吵架
我见过最糟糕的接口设计是:有的接口返回{code: 200, data: ...},有的接口返回{success: true, result: ...},还有的直接返回一个字符串。前端每次请求都要先猜返回结构,这是联调效率低下的最大原因。
标准做法是定义统一返回体,我一般叫Result:
public class Result<T> { private Integer code; // 200成功,500业务错误,401未认证 private String message; private T data; // 省略getter/setter和静态方法success()、error() }配合全局异常处理器,业务代码里连try-catch都可以少写。只需要自定义业务异常类BusinessException,在Service层里遇到不正常情况就抛异常,Controller层不需要任何处理,@RestControllerAdvice会统一捕获并转换成Result返回。
这样做的好处是:Controller层代码极其干净,每个接口就三行,一行调service,一行返回Result.success。答辩时老师问起"怎么保证接口风格一致",你就能把这个设计说明白。
4.3 典型业务接口的设计规范:以请假审批为例
OA系统的接口不少,但核心模式是雷同的。我拿请假模块的四个接口举例,你掌握模式之后,报销、出差这些模块就是复制粘贴换字段。
- 提交请假申请:POST /api/leave,入参是请假表单,后端校验时间合法性后插入记录。
- 我的请假列表:GET /api/leave/my,根据当前登录用户的ID查出自己的全部记录。
- 待办审批列表:GET /api/leave/todo,查status为0的记录。
- 审批操作:PUT /api/leave/approve/{id},入参是approveResult和approveComment,后端更新status等字段。
接口设计里有几个细节值得反复强调。分页查询一定要封装PageQuery对象,统一接收pageNum、pageSize、keyword这些参数;列表接口默认按创建时间倒序,方便前端直接展示最新数据;审批操作里务必要做状态校验,防止重复审批把数据状态改乱。
5. 前端落地:路由、请求封装、组件复用
后端跑通了,前端如果写得稀碎,整个系统照样没法看。Vue这套体系里我重点讲三个东西:动态菜单、axios封装、审批组件的复用。这三个都是后台管理系统前端的通用套路。
5.1 动态菜单:不同角色登录看到的菜单不一样
权限控制在页面上最直观的体现就是"不同的人登录后菜单不同"。管理员能看到员工管理和角色管理,普通员工只能看到请假和公告,这就是动态菜单要做的事。
我的实现方案是:用户登录成功后,后端接口返回这个用户有权访问的菜单树,前端把这个菜单树渲染成侧边栏,同时动态注册路由。这里要特别叮嘱一句:前端隐藏菜单只是体验层面的处理,真正的权限校验必须依赖后端接口的拦截器,否则懂行的人改一下前端路由就能访问没有权限的页面。
用Vue Router的动态注册,核心思路是遍历后端返回的菜单数组,逐个调用router.addRoute()。这个逻辑在权限控制里非常关键,建议提前封装好。
5.2 axios封装:请求拦截器与401的统一处理
前端每个接口都裸调axios,会让代码非常难维护。我习惯在utils/request.js里做一层统一封装,两个拦截器就够:
请求拦截器里,每次发请求前从Pinia或本地存储中取Token,加到请求头:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config })响应拦截器里,统一处理后端返回的Result格式。code为200时直接返回data,业务错误码弹出错误提示,401则清空Token并跳转回登录页:
service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )这套封装做完之后,业务代码里的接口调用就极其清爽,比如请假列表只需要写const data = await getLeaveList(params),不用关心Token、状态码这些杂乱细节。
5.3 组件化思路:审批列表和表单的复用价值
前端最怕的情况是每个页面复制粘贴一大段重复代码。OA系统里最容易复用的是两块:一是带状态的列表页,二是审批表单的展示。
我的做法是,可以让审批模块页面配置化,也可以做一个PublicList公共组件,传入接口地址和列配置就可以渲染表格。请假、报销、公告列表都能用同一套组件,只是列配置不同。单文件组件的数据驱动方式非常灵活。
还有一个值得抽出来的组件是"状态标签"。请假状态只有三种:待审批用warning色,已通过用success色,已驳回用danger色。把这封装成StatusTag组件,传入status就自动渲染对应的标签,三行代码调用,全系统通用。这种小细节会让整个前端的代码质量提升一个档次。
6. 交付物整理:一份能直接用的完整源码该是什么样
回到这个项目的交付要求:完整项目源码加SQL脚本加接口文档。这三样缺一样,项目都算不上"完整"。
6.1 前后端源码目录结构怎么组织才专业
一份专业的源码目录,应该让接手的人看一眼就知道"哪里是前端,哪里是后端,怎么启动"。我最推荐的顶层结构是这样的:
oa-system/ ├── backend/ # SpringBoot后端工程 │ ├── src/main/java/com/xxx/oa/ │ │ ├── controller/ # 控制层 │ │ ├── service/ # 业务层 │ │ ├── mapper/ # 数据访问层 │ │ ├── entity/ # 实体类 │ │ ├── common/ # 通用类(Result、异常、常量) │ │ └── config/ # 配置类(拦截器、跨域) │ └── src/main/resources/ │ ├── application.yml │ └── mapper/ # XML文件 ├── frontend/ # Vue前端工程 │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── views/ # 页面 │ │ ├── router/ # 路由配置 │ │ ├── stores/ # Pinia状态管理 │ │ ├── components/ # 公共组件 │ │ └── utils/ # 工具类 │ └── vite.config.js ├── sql/ │ ├── 01_schema.sql # 建库建表脚本 │ ├── 02_data.sql # 初始数据脚本 │ └── 03_test_data.sql # 测试数据脚本 └── docs/ ├── 接口文档.md └── 部署说明.md两个工程分开放在backend和frontend目录下,这是最直观的方案。SQL脚本单独放一个目录,并且按"建表、初始数据、测试数据"拆分三个脚本,是专业度的直接体现。启动说明写在README里,不要写在代码注释里。
6.2 SQL脚本的三个文件怎么分工
我见过很多项目的SQL脚本只有一个,几百行混在一起,表结构和数据全都揉成一团。这样很不好维护。专业的做法是拆成三个文件:
01_schema.sql负责建库建表,里面只有CREATE DATABASE和CREATE TABLE语句。表脚本要按依赖顺序写:先建部门表,再建用户表,最后建关联表和业务表。如果顺序乱了,后执行建表语句会报"表不存在"的错误。
02_data.sql负责初始化必要数据,包括一个管理员账号、基础角色、系统菜单等。这里有一个关键点:所有数据插入的ID必须写死且保证唯一,而且菜单表的parent_id要和菜单一致。如果用的是自增ID,反复执行脚本可能导致ID对不上。
03_test_data.sql负责造测试数据,目的是让系统一跑起来就有数据可以展示。建议至少插入几个用户、部门、多条不同状态的请假记录。演示效果靠的就是这份测试数据,数据越真实,老师看着越舒服。
6.3 接口文档:在线Swagger和离线Markdown怎么选
接口文档是很多同学最容易忽视但实际非常重要的交付物。我推荐做两种形态。
形态一:集成Swagger自动生成在线文档。只需要在SpringBoot里引入springfox或springdoc依赖,加几个注解就能在浏览器里访问/swagger-ui/index.html,每一个接口都可以在线调试。这个方案的好处是零维护成本,代码即文档,每次改代码接口文档自动更新。
形态二:写一份简洁的Markdown接口文档。这是给没有运行环境的人看的,比如论文评审老师。每页包含接口名称、请求方式、请求路径、请求参数表格、返回参数示例、备注说明。不需要逐字写完所有接口,但核心的登录认证、员工管理、请假审批这三个模块必须写全。
我的建议是两者都做:Swagger用于开发期联调,Markdown用于答辩交付。哪怕都会用不到一半,但"我有文档意识"这件事就能在答辩老师那里留下好印象。
7. 部署上线的实操经验与踩坑记录
最后这部分聊聊部署和上线的坑。一个项目代码写得再好,部署不起来,就一切归零。
7.1 本地运行的正确顺序
跑通这个项目的正确顺序是:数据库优先创建和初始化,再启动后端,最后启动前端。很多人一上来先npm run dev,结果页面从白屏到报错,一直怀疑是前端的问题,其实后端根本没起来。
第一步,执行SQL脚本注意顺序。先执行01_schema.sql,再执行02_data.sql。如果已经执行过一遍,可以删库再重建,保持数据干净。
第二步,修改后端application.yml中的数据库账号密码,确保和本机MySQL一致。把端口固定为8080,上下文路径不要设置,保持默认最好。
第三步,启动后端类里面的main方法,看到"Started Application in xxx seconds"即成功。
第四步,启动前端前先确认node_modules已安装,然后npm run dev,打开浏览器访问localhost:5173即可。
7.2 跨域问题:前端代理和后端CORS不要同时开
前后端分离项目开发时,必然遇到跨域问题。前端跑在5173端口,后端跑在8080端口,两个端口不同,浏览器会拦截跨域请求。解决方案有两种:
一是前端配置Vite代理,将请求前缀/api代理到localhost:8080,利用开发服务器转发请求,绕开浏览器跨域限制。
二是后端配置CORS,在拦截器或配置类里允许跨域访问。
这里一定要记得:两种方案选一种,不要同时开。如果同时开了,会出现预检请求被拦截器拦截把CORS响应头去掉,结果明明做了跨域配置,请求还是报错。这是所有跨域坑里最隐蔽的一个。
7.3 时区、编码、文件上传三个隐形的雷
最后三个坑,任何一个都能让人排查一下午。第一,数据库连接串里加上serverTimezone=Asia/Shanghai,否则日期时间查询可能差8小时。第二,数据库建表时统一用utf8mb4字符集,否则前端传了表情符号直接报乱码。第三,如果你做了头像上传功能,文件保存路径一定要在配置里写一个绝对路径并在本地创建好,否则上传时会报"目录不存在"。
部署时后端打包用mvn package生成jar包,前端用npm run build生成dist目录。如果要做合并部署,把dist目录复制到后端src/main/resources/static目录下重新打包,就能实现一个jar包同时包含前端页面和后端接口,这种部署方式在毕设答辩演示时特别方便,不用开两个终端跑两个服务。
从我个人的体会来说,做完这个OA项目最大的收获不是跑通了某个功能,而是建立起了一套"需求拆解-数据库设计-接口约定-前端落地-交付整理"的完整链路思维。尤其是把SQL脚本和接口文档当作一等交付物来对待,这种习惯在之后的实际工作里非常受用。最后再分享一个实用的扩展方向:如果你做完这些之后还有精力,试着在公告模块里加一个Redis缓存,把热点公告放到缓存里,然后去文档里解释"为什么用Redis",这个点足够让答辩老师眼前一亮。