1. 这题目为什么年年有人做却年年有人翻车每年到大四下学期实验室里最不缺的就是“SpringBootVueMySQL企业OA管理系统”这个题目的组合。我刚帮某学弟把整套项目从零搭到答辩通过前后改了七版最大的感受是这套题能做出来的人很多能讲清楚“为什么这么设计”的人很少而答辩挂掉的人往往不是代码崩了而是被问到数据库三范式、权限表结构、Token过期策略这些问题时答不上来。企业OA管理系统本质上是把公司里“人在走流程、事在等审批、信息在流转”这些日常搬到线上。用SpringBoot做后端接口Vue做前端页面MySQL存业务数据这个组合最大的优点是各管一摊、边界清楚特别适合课程设计和毕业设计这种周期紧、需要快速出成果的场景。但恰恰因为题目太常见如果你只是照着网上的脚手架把增删改查拼一遍最后大概率拿不到理想的成绩。这篇文章我会按我实际给学弟梳理项目的顺序来写先讲清楚这四个月里每个阶段真正该做什么再拆开讲数据库怎么设计才经得起追问后端接口的权限和工作流怎么写不露怯前端配合后端联调时容易踩哪些坑以及最后部署和答辩准备的关键点。文章里所有代码片段都是我在真实项目里调通过的核心逻辑你可以直接当模板改。2. 需求分析阶段别把OA做成“电子表格”2.1 先回答一个问题你的系统到底解决什么痛点很多同学拿到题目第一反应是列功能清单用户管理、部门管理、公告管理、会议管理、请假审批、报销审批……列完就开写写到后面发现每张表都像“字典表”每个页面都是“表格弹窗”整个系统没有灵魂。我建议你先想清楚一个场景一个小公司一个员工早上请了半天事假从提交到主管确认、到人事备案、到月底考勤核对这条线在没有系统时靠的是纸质单子和微信私聊有系统后靠的是什么——是审批流的状态流转。所以OA系统的核心不是“管理”而是“流程”。你所有功能设计都应该围绕“一件事从发起到结束每一步谁看到了什么、能做点什么”来展开。基于这个思路我最后给学弟定的核心模块只有六个登录与权限、组织架构用户部门角色、请假审批、报销审批、公告通知、审批记录查询。会议管理和日程管理我直接砍掉了一是答辩时间有限二是这两个功能跟审批流关系弱塞进来反而稀释主线。2.2 功能边界怎么画能砍就砍能合并就合并准备需求的阶段画功能图很重要但更重要的是学会“合并同类项”。拿审批来说请假的字段是开始时间、结束时间、事由、类型报销的字段是金额、事由、票据张数、类型。两者业务字段不同但审批动作完全一样同意、驳回、转交、撤回。所以我在设计时没有为请假和报销各写一套审批代码而是抽了一张“审批实例表”统一承载流程信息业务表只存自己的业务字段。到答辩时老师追问“你这里是不是重复造轮子了”你就可以理直气壮地说做了抽象。3. 数据库设计最能看出项目含金量的部分3.1 用户、角色、权限三张表怎么落到实处权限模型我直接用RBAC基于角色的访问控制不需要设计复杂的组织级权限。标准做法是五张表用户表、角色表、用户角色关联表、菜单表、角色菜单关联表。很多同学会问管理员和普通用户都直接加个type字段不就行了吗问题是答辩时老师会追问“如果新增一个‘部门经理’角色这个角色能看一部分审批但不能看薪酬你怎么办”如果只有type字段你就要在代码里写大量if-else判断而RBAC模型只需要往角色菜单表里插几条记录。具体建表时我建议菜单表的表结构这样设计比较好用CREATE TABLE sys_menu ( id bigint NOT NULL AUTO_INCREMENT, parent_id bigint DEFAULT 0 COMMENT 父菜单ID0表示根菜单, menu_name varchar(64) NOT NULL COMMENT 菜单名称, path varchar(128) DEFAULT NULL COMMENT 前端路由路径, component varchar(128) DEFAULT NULL COMMENT 前端组件路径, perms varchar(128) DEFAULT NULL COMMENT 按钮权限标识如 sys:user:add, menu_type char(1) DEFAULT M COMMENT M目录 C菜单 F按钮, visible char(1) DEFAULT 0 COMMENT 0显示 1隐藏, sort_order int DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT菜单权限表;这表里的perms字段是精华。前端的“新建请假”按钮要不要显示后端的“新增用户”接口能不能调都靠这个标识去判断。这样权限控制就不只是“整个页面能不能访问”而是细化到“页面里的某个按钮能不能点”。3.2 审批流的表设计业务表与流程表分离审批相关的表我设计了三张请假申请表、报销申请表、审批记录表。前两张存业务字段第三张专门存流程节点的每一步操作。审批记录表的结构我定了这些字段CREATE TABLE approval_record ( id bigint NOT NULL AUTO_INCREMENT, biz_type varchar(32) NOT NULL COMMENT 业务类型leave/expense, biz_id bigint NOT NULL COMMENT 业务表单ID, approval_user_id bigint NOT NULL COMMENT 当前处理人ID, approval_status tinyint NOT NULL COMMENT 1待审批 2通过 3驳回, comment varchar(255) DEFAULT NULL COMMENT 审批意见, created_time datetime NOT NULL, finished_time datetime DEFAULT NULL COMMENT 处理时间, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT审批记录表;这张表有个取巧但很实用的设计biz_type biz_id的组合让同一套审批记录可以服务请假和报销两种业务后续就算加一个采购审批也不需要改这张表的结构只需要在代码里约定一个新的biz_type值。这种设计在答辩时是加分项因为老师能看出你有“扩展意识”。3.3 索引、时间字段、删除策略这些细节别翻车建表还有几个容易翻车的细节第一所有表都要有create_time和update_time并且建议让数据库自动维护不要靠代码去set。第二逻辑删除字段deleted必须加用户删除了但审批记录要保留历史物理删除会出大问题。第三biz_id和approval_user_id这种高频查询字段一定要建普通索引否则数据量一上来全表扫描答辩演示时卡顿会非常尴尬。第四状态字段不要用字符串用 tinyint 加注释比如1待审批 2通过 3驳回写代码时用枚举类去映射千万不要在代码里直接写魔法数字。4. 后端开发权限鉴权和审批流是两个“硬骨头”4.1 登录鉴权选JWT还是Session建议直接用JWT前后端分离的项目Session方案要处理跨域携带Cookie的问题麻烦事不少。JWTJSON Web Token方案的好处是服务端不需要存登录状态后端只要验签就能知道请求是谁发的。我用的依赖是jjwt登录成功之后生成一个带过期时间的Token返回给前端前端每次请求把Token放在Header的Authorization字段里。核心逻辑可以这样写public String generateToken(Long userId, String username, String roleCode) { Date now new Date(); Date expireDate new Date(now.getTime() 7 * 24 * 3600 * 1000); // 7天过期 return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, roleCode) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); }注意两个细节一是密钥jwtSecret不要写死放在代码里放到application.yml的配置项中二是Token过期时间不要动不动设成30天否则答辩时老师一问“过期了怎么办”你还要费力解释。7天比较合理同时额外提供一个“刷新Token”的思路哪怕不实现都能体现你考虑过这个问题。4.2 拦截器写一个全局鉴权而不是每个接口复制粘贴如果我们写的每个Controller方法开头都加一段“从Token取用户、判定是否有权限”代码很快会烂。做法是实现一个拦截器在preHandle里统一做三件事解析Token、校验过期时间、把用户信息放到ThreadLocal里。HandlerInterceptor的具体实现如下public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { response.setStatus(401); return false; } try { Claims claims Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token.replace(Bearer , )).getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (ExpiredJwtException e) { response.setStatus(401); return false; } catch (JwtException e) { response.setStatus(401); return false; } } }这里有个容易踩的坑前端如果传的Token带了Bearer前缀你解析时没处理会直接抛异常。我在联调时被这个坑折磨了一个多小时前端看请求头写得没毛病后端看每次都是401最后才发现是前缀问题。所以要么拦截器里统一replace要么跟前端约定死不带前缀二选一即可。4.3 审批流的状态机用状态枚举管理流转逻辑审批流最容易写乱的地方是请假可能经过“员工提交→主管审批→人事备案”三个节点如果你在Controller里用if-else一个个判断下一步是谁以后加一级审批节点就要改动原来的代码。我的做法是定义一个ApprovalStatusEnum枚举把当前状态、操作动作、下一状态对应关系集中管理public enum ApprovalStatusEnum { PENDING(1, 待审批), APPROVED(2, 已通过), REJECTED(3, 已驳回), WITHDRAWN(4, 已撤回); private Integer code; private String desc; // 构造方法和getter省略 }流转逻辑放在Service层统一处理当前状态是待审批时如果操作是同意表单状态改成已通过生成一条审批记录如果操作是驳回表单状态改成已驳回。有一点必须提醒审批记录表每产生一条记录都要把当前审批人的ID写进去而不是只写业务表的状态。这样以后查“某个用户参与过哪些审批”一条SQL就能搞定。4.4 事务和异常处理审批到一半崩了怎么办审批操作至少要更新两张表业务表的状态、审批记录表的新记录。这两步必须放在同一个事务里否则会出现业务表显示已通过但审批记录里查不到任何审批痕迹的情况。在SpringBoot里加一个Transactional就能解决。但要注意事务只对RuntimeException回滚如果你在审批方法里捕获了异常又没重新抛出事务是不会回滚的。我在项目里统一用了全局异常处理器Service层只抛业务异常不吞异常。5. 前端开发页面能做少但这四个问题必须想明白5.1 动态菜单是怎么根据角色渲染出来的系统里管理员看到的菜单和普通员工看到的菜单不一样做法是登录成功后后端返回当前用户可访问的菜单列表前端根据菜单列表动态生成路由。实现上要用到Vue Router的addRoutes方法。大致思路是登录返回数据里带一个menus数组前端遍历这个数组把对应组件注册进路由实例。动态路由有个坑刷新页面时路由表会重置如果没做持久化一刷新就回到登录页。解决办法是把菜单列表存进localStoragemain.js里在应用初始化前判断本地有没有缓存数据有就直接重新注册路由。5.2 Axios拦截器统一处理Token和报错所有请求都要带Token、所有401响应都要跳转登录页、所有业务错误都要弹出提示这三个需求用Axios拦截器统一处理。我在项目里的封装逻辑是请求拦截器从localStorage读Token并加到Header里响应拦截器判断状态码401就清空本地存储并跳转登录非2xx状态码用Element Plus的Message组件弹出后端返回的错误信息。这样做之后每个业务页面里发请求只需要关心成功逻辑错误处理完全不用重复写。5.3 前后端联调的跨域问题先分清是CORS还是代理问题本地开发时前端跑在8080端口后端跑在9090端口跨域问题几乎必现。两个解决办法后端写一个CorsFilter放行指定来源或者前端Vite配置文件里配proxy代理。我建议开发环境用Vite代理因为这样前端请求路径可以写成/api代理转发到后端地址线上部署时再用Nginx统一配置前后端不用改任何请求路径。个人经验是联调阶段遇到的跨域导致的问题八成不是跨域本身而是写错了代理路径或后端拦截器把OPTIONS请求拦截了。OPTIONS预检请求绝对不能在鉴权拦截器里被401否则前端每次请求都失败。5.4 表格和表单别堆组件库让页面风格统一更重要OA系统的核心交互就是“表格弹窗表单”用Element Plus完全够用。但我建议页面骨架自己写不要全套复制后台管理模板不然页面里混着一堆用不上的统计卡片和皮肤切换按钮体积又大、答辩时还要解释来历。做一套统一风格的布局组件顶部用户信息退出按钮左侧菜单主内容区放路由视图这套东西自己写不超过200行代码但看起来像是你认真设计过的产品。6. 部署与答辩很多项目不是做砸的是“说不清”砸的6.1 部署方式选择一篇文章搞定Jar包Nginx毕业后如果项目要提交演示最好部署到云服务器上而不是只在本地跑。最稳的方案是前后端分离部署后端打成Jar包运行在9090端口前端npm run build生成静态文件放到Nginx目录然后Nginx里配置请求路径/api反向代理到后端9090端口。这个方案的好处是服务器只需要装JDK、MySQL、Nginx三样东西对初学者友好。注意后端连接MySQL时不要把密码硬编码用环境变量注入更规范。6.2 答辩前要演练的十个问题我把学弟被问到的、以及我预判会问的十类高频问题列了一下每一条都要能讲出“为什么”问题方向回答思路为什么用SpringBoot而不用SSM简化配置、内嵌Tomcat、快速启动Token过期后用户怎么操作前端跳转登录页重新登录获取新Token用户密码怎么保存BCrypt加密数据库不存明文逻辑删除怎么恢复数据更新deleted字段为0保留数据完整审批状态并发提交怎么办乐观锁或状态条件更新防止重复审批菜单权限怎么控制到按钮菜单表里perms字段自定义指令控制按钮显隐数据库为什么加索引提高查询效率举例说明高频查询字段如何扩展一个新的审批类型新增业务表复用审批记录表枚举加一个类型前后端联调遇到过什么问题跨域、请求头、时间格式讲真实排查过程系统安全性做了哪些措施密码加密、Token鉴权、参数校验、统一异常处理6.3 论文素材要边做边留别最后一个月补写论文时最痛苦的是补截图、补测试数据、补设计过程。我当时给学弟的建议是每完成一个模块就截一张操作效果图每条功能录一段屏幕录制。测试数据至少准备三套管理员一套、部门主管一套、普通员工一套演示时切换账号能直观看到菜单和按钮权限的差异。论文的重心放在需求分析、数据库设计、系统设计这三块不要用大篇幅贴代码老师看的是分析过程不是代码清单。做这套OA系统我的整体感受是真正拉开差距的并不是代码量而是你对自己系统的“理解深度”。一个只有增删改查的OA项目撑死是一个管理后台但当你把权限模型、审批流程、Token鉴权这些设计逻辑真正弄明白、能讲清楚它就是一篇有质量的毕业设计。项目从零到验收用了六周中间改数据库结构三次、重构审批逻辑一次每一版迭代都在让系统更接近“做给人用的产品”而不是“交作业的Demo”。如果让我给一个建议那就是别急着写代码把你系统里最核心的一条审批流程在纸上走一遍你会发现后面所有的设计和开发都顺畅了很多。
阅读完成 · 觉得有帮助?