基于SpringBoot+Vue的学生请假管理系统:状态机驱动流程设计 📅 发布时间:2026/9/17 19:28:10 👁 浏览次数: 简介基于SpringBootVue的学生请假管理系统毕业设计论文面向高校计算机相关专业毕业生及正在学习Java Web开发的初学者解决毕业设计选题、论文撰写与系统设计参考的需求。论文完整覆盖系统分析、可行性分析、功能设计和数据库设计详细展示了管理员、教师、学生三类角色的核心模块包括学生管理、请假表格管理、学生考勤管理、缺课记录管理等有助于读者理解B/S架构下Spring Boot与Vue的实际应用。压缩包内仅包含1个doc格式文档大小约1.65MB便于直接阅读和修改适合用作毕业论文框架参考或项目文档模板。目前已有130人学习对于需要快速搭建论文结构、梳理系统功能模块的读者具有较高的借鉴价值。借助该文档可以了解从需求分析到系统实现、再到论文撰写的完整流程节省从零开始整理资料的时间。1. 学生请假管理系统为什么先定状态机再写 CRUD很多学生请长假条系统最终能跑通但一到真实使用就乱请假条提交之后还能被自己撤回重改辅导员批过的假条又被覆盖成待审月底统计请假天数时发现同一张单子被算了两遍。这些问题的根不在增删改查而在状态——系统没有把「待审核、已通过、已驳回、已销假」当成一条不可乱跳的流程来设计。所以做这个基于 SpringBootVue 的学生请假管理系统先别急着搭页面先把「谁能在什么状态下把假条推到哪个状态」这件事定死。SpringBoot 管接口和数据Vue 管操作和回显但真正决定系统能不能用的是那张状态流转表。这篇文章就按业务建模、后端接口、前端对接、一致性收口的顺序把一套可以直接照着写的方案讲清楚适合做毕设、接手旧代码改造、以及第一次做前后端分离管理系统的同学。2. 学生请假管理系统的数据模型状态字段先于接口2.1 角色与用例学生、辅导员、管理员各管一段一个常规的学生请假管理系统最常见的是三类角色学生负责提交和撤销辅导员负责审批和销假确认院系管理员只做查询和统计不参与流程。把角色分开的价值在于后端每一个接口都能明确一个「操作者边界」而不是靠前端按钮去限制。我在设计用例时一般会先画一张角色-操作表它同时也是论文需求分析章节能直接用的素材角色核心操作禁止操作学生提交请假、撤销待审假条、查看审批结果不能审批、不能修改已通过的假条辅导员查看待审列表、通过/驳回、确认销假不能审批自己的假条如果辅导员也是学生管理员查看全校请假记录、统计请假天数不参与单条审批流转这里有个容易忽略的点有些学校辅导员是教师编制但也可能是在读研究生此时一张假条的 student_id 和 approver_id 不能落在同一人身上。后端在审批时要显式判断这个条件不能只依赖前端把审批按钮藏掉。2.2 状态机请假单的六个状态与合法跳转请假单的状态设计我见过很多种有的用字符串pending、approved有的用数字 0/1/2只要枚举清晰都可以。重点是状态之间的跳转必须受约束不能出现「已通过的假条还能被学生撤销」这种逻辑漏洞。一个比较稳妥的状态机是五态加一个结束态状态码含义谁触发可跳转到0草稿学生待审核、撤销1待审核学生提交已通过、已驳回、撤销2已通过辅导员已销假3已驳回辅导员待审核学生修改后重新提交4已撤销学生无5已销假辅导员无这套状态的顺序也直接对应论文里的流程图学生提交 - 辅导员审批 - 学生返校 - 辅导员销假确认。销假这个动作经常被初学者漏掉但它恰恰是系统比 Excel 登记表更有说服力的地方——假条有没有闭环状态一眼能看出来。2.3 建表 SQLstudent_id、approver_id、status、version 一个都不能少数据库表结构建议拆成两张核心表用户表和请假申请表。用户表存角色和密码哈希请假申请表存流程数据。下面是请假申请表的建表 SQL字段注释就是字段说明书CREATE TABLE leave_application ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id BIGINT NOT NULL COMMENT 学生ID, approver_id BIGINT NOT NULL COMMENT 当前审批人ID通常为辅导员, leave_type TINYINT NOT NULL COMMENT 1事假 2病假 3丧假 4其他, start_time DATETIME NOT NULL COMMENT 请假开始时间, end_time DATETIME NOT NULL COMMENT 请假结束时间, total_days DECIMAL(4,1) NOT NULL COMMENT 后端计算的请假天数0.5为半天, reason VARCHAR(500) NOT NULL COMMENT 请假原因, status TINYINT NOT NULL DEFAULT 1 COMMENT 0草稿 1待审核 2已通过 3已驳回 4已撤销 5已销假, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, audit_comment VARCHAR(255) DEFAULT NULL COMMENT 审批意见, audit_time DATETIME DEFAULT NULL COMMENT 审批时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_student_status (student_id, status), KEY idx_approver_status (approver_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生请假申请表;两个联合索引分别对着两个最频繁的查询场景学生端查「我的假条」辅导员端查「待我审批」。如果不建idx_approver_status辅导员待审列表在数据量上来后会走全表扫描毕设答辩时被问到数据库优化也能答得上来。2.4 请假天数必须后端算半天折算规则要落进代码前端经常会把total_days当成数字框直接传给后端这是隐患。请假的起止时间可能跨中午、跨周末天数怎么折算应该由后端统一算否则不同人填出来的数据口径不一致。我一般用「8 小时为一天不足 4 小时算半天超过 4 小时算一天」的规则public BigDecimal calcLeaveDays(LocalDateTime start, LocalDateTime end) { if (!end.isAfter(start)) { throw new BusinessException(结束时间必须晚于开始时间); } long minutes Duration.between(start, end).toMinutes(); if (minutes % (8 * 60) 0) { return BigDecimal.valueOf(minutes / (8 * 60)); } long halfDays (minutes 4 * 60 - 1) / (4 * 60); // 向上取整到半天粒度 return BigDecimal.valueOf(halfDays).divide(BigDecimal.valueOf(2)); }这其实是借了「不足半小时按半小时计」的思路先把时间粒度对齐到半天再转成天。这段逻辑放在 Service 层在 insert 之前调用前端提交上来的total_days一律以后端计算结果覆盖。参数上注意两个边界一是跨天时长超过 24 小时的单子按 8 小时/天折算是符合学校考勤习惯的二是结束时间等于开始时间必须直接报错不允许 0 天假条存在。3. SpringBoot 后端接口与状态校验这样写才稳3.1 工程结构与 SpringBoot 自动装配的取舍后端工程我用 IDEA 的 Spring Initializr 创建包名按业务拆controller/service/mapper/entity/config/common五层不引入过多的微服务概念。选 SpringBoot 版本时注意别追最新Spring Boot 3.x 默认使用jakarta.*命名空间和老教程里的javax.*不兼容JDK 版本也要对应3.x 需要 Java 17。如果只是做单机管理系统Spring Boot 2.7 JDK 8 或 Java 17 Spring Boot 3.x 都可以关键是一套代码里别混两套命名空间。SpringBoot 的自动装配帮我们省掉了数据源、Web 容器的手工配置但「自动」不代表「隐形」。比如你引了spring-boot-starter-data-redis后Redis 连接失败会导致部分端点启动报错这就是自动装配的副作用。对这个系统而言核心依赖只需要 Web、MyBatis-Plus、MySQL 驱动和 JWT 工具其他按需再加。3.2 application.yml 里的关键配置数据源、MyBatis-Plus 和 JWT配置文件的重点是让 MyBatis-Plus 的驼峰映射和数据源对齐JWT 密钥单独拎到配置里方便答辩时演示不同环境切换server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/student_leave?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 jwt: secret: your-256-bit-secret-key-change-in-production expire-hours: 12map-underscore-to-camel-case: true让student_id自动映射到studentId避免每张表写一堆TableField。JWT 的expire-hours设 12 小时学生和辅导员一天内的连续操作不会频繁掉线又不至于太长。密钥在仓库里必须用环境变量覆盖硬编码进 yml 只适合本机演示。3.3 登录与 JWT 拦截器放行登录、拦截其余、解析角色登录接口返回一个 JWT后续每个请求都在请求头带Authorization: Bearer token。我不用 Spring Security因为这套系统角色少、接口少一个 HandlerInterceptor 就能完成 Token 校验和角色识别代码更直白答辩时也更好讲清原理Component 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 auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } String token auth.substring(7); // 用 Hutool 的 JWTUtil 做校验和解析token 里只放 userId、role JWTValidator.of(token).validateDate(); JWT jwt JWTUtil.parseToken(token); Long userId jwt.getPayload(userId, Long.class); String role jwt.getPayload(role, String.class); request.setAttribute(currentUserId, userId); request.setAttribute(currentRole, role); return true; } }这段代码要配合 WebMvcConfig 注册并设置拦截路径/api/auth/login和/api/auth/register放行其他/api/**全部走拦截器。Controller 里用RequestAttribute(currentUserId)拿当前用户不要信任前端传来的 userId。3.4 请假接口设计与审批状态校验接口按资源划分路径和应用层语义对齐学生提交请假是 POST/api/leave辅导员审批是 PUT/api/leave/{id}/approve查询分为「我的假条」和「待我审批」两个 GET 接口。全部返回统一结构ResultT包含 code、message、data 三个字段前端 axios 拦截器只需处理 code 为 0 的场景。审批接口是整个系统里最需要写对状态约束的地方PutMapping(/{id}/approve) public ResultVoid approve(PathVariable Long id, RequestAttribute(currentUserId) Long approverId, RequestAttribute(currentRole) String role, RequestBody ApproveRequest req) { if (!teacher.equals(role)) { throw new BusinessException(403, 仅辅导员可审批); } LeaveApplication leave leaveMapper.selectById(id); if (leave null) { throw new BusinessException(404, 假条不存在); } if (leave.getStudentId().equals(approverId)) { throw new BusinessException(403, 不能审批自己的假条); } LambdaUpdateWrapperLeaveApplication wrapper new LambdaUpdateWrapper(); wrapper.eq(LeaveApplication::getId, id) .eq(LeaveApplication::getStatus, LeaveStatus.PENDING) .eq(LeaveApplication::getApproverId, approverId); LeaveApplication update new LeaveApplication(); update.setStatus(req.getPass() ? LeaveStatus.APPROVED : LeaveStatus.REJECTED); update.setAuditComment(req.getComment()); update.setAuditTime(LocalDateTime.now()); int rows leaveMapper.update(update, wrapper); if (rows 0) { throw new BusinessException(409, 审批失败假条状态已变化或你不是当前审批人); } return Result.ok(); }LambdaUpdateWrapper里的三个eq条件就是并发安全和权限判断的关键id定位单子status保证只能从待审核状态流转approverId防止非当前审批人越权操作。MySQL 的行锁会在这条 UPDATE 语句上生效两个辅导员同时点审批时只有一个能成功另一个 rows 为 0 拿到明确报错。这里没有用select-then-update的写法因为那两步之间存在时间窗口条件 UPDATE 把原子性交给数据库才最省心。4. Vue 前端路由、请求封装和请假页面这样对接 SpringBoot4.1 初始化一个 SpringBoot Vue 的前后端分离项目前端用 Vue 3 Vite创建命令是npm create vuelatest装完依赖后npm run dev起本地开发服务器。开发环境配一个 Vite 代理把/api转发到http://localhost:8080这样前端代码里不需要写死后端地址部署时也只要改代理或 nginx 配置。// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })Vue 项目里我习惯把页面按视图拆分LoginView.vue、LeaveApplyView.vue、LeaveAuditView.vue、LeaveListView.vue再加一个components/StatusTag.vue做状态展示。组件和页面分离后后面切换 UI 库或者调整布局不用动业务逻辑。4.2 axios 封装请求头带 Token、401 统一跳转axios 实例化时统一注入 BaseURL 和超时时间请求拦截器从 localStorage 拿 token 放到 Authorization 头响应拦截器统一处理后端返回结构。这样每个页面组件里都只需要写request.get(/api/leave/my)不用重复处理错误码。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( res { const body res.data if (body.code ! 0) { ElMessage.error(body.message || 请求失败) return Promise.reject(new Error(body.message)) } return body.data }, err { if (err.response?.status 401) { localStorage.removeItem(token) localStorage.removeItem(userInfo) location.href /login } ElMessage.error(err.response?.data?.message || 网络异常) return Promise.reject(err) } ) export default request后端Result的 code 为 0 表示成功非 0 直接提示并抛出。401 单独处理是因为后端拦截器抛出的未登录异常要走「清空本地登录态 - 回登录页」的逻辑不能只弹个错误提示。4.3 路由守卫和角色控制没有 token 一律去登录页前端路由用createRouter创建配置history: createWebHistory()。路由守卫里只做登录态检查是不够的还要配合在后端做权限控制。前端路由守卫的实际作用是提升体验而不是安全边界router.beforeEach((to) { const token localStorage.getItem(token) if (!token to.path ! /login) { return { path: /login, query: { redirect: to.fullPath } } } if (token to.path /login) { return { path: / } } })在这个系统里学生登录后不应该在左侧菜单看到「审批中心」。我一般在登录拿到用户信息后把role字段存到 localStorage然后菜单和路由用v-if或动态路由控制。注意「隐藏菜单」和「不可访问」是两件事学生手动输入/audit路径时前端路由守卫里还要判断角色否则页面直接展示空白。4.4 请假申请页与审批页状态变化驱动界面刷新请假申请页的表单字段包括请假类型、开始时间、结束时间、请假原因。提交成功后清空表单并跳到待办列表。这里有个细节请假天数的展示直接用后端返回的totalDays前端不要自己算。审批页的核心是列表和操作按钮的联动el-table :datapendingList v-loadingloading el-table-column propstudentName label学生姓名 / el-table-column propleaveTypeText label请假类型 / el-table-column proptotalDays label天数 / el-table-column propstartTime label开始时间 / el-table-column propreason label原因 show-overflow-tooltip / el-table-column label操作 width200 template #default{ row } el-button typesuccess clickhandleApprove(row, true)通过/el-button el-button typedanger clickhandleApprove(row, false)驳回/el-button /template /el-table-column /el-table审批通过或驳回后调用PUT /api/leave/{id}/approve成功后在回调里重新拉一次待审列表。不要用「前端删除本地行」的方式更新列表因为多端操作时本地状态和数据库不一定一致。5. 让学生请假管理系统的状态一致并发审批与越权兜底5.1 哪些校验必须放在后端而不是前端前端做按钮级权限控制是体验后端做接口级权限校验才是安全。必须放在后端的校验有三类角色权限、资源归属、状态流转。角色权限指只有 teacher 角色的 token 能调审批接口学生角色即使手动构造请求也会被拦截资源归属指学生只能查自己的假条查询接口里必须强制加studentId 当前登录用户ID的条件而不是把前端传来的 studentId 拼进 SQL状态流转就是第 3 章那段条件 UPDATE 里的.eq(LeaveApplication::getStatus, LeaveStatus.PENDING)保证任何状态下都不能跳过待审直接变成已通过。5.2 并发重复审批用条件 UPDATE 代替 select-then-update两个辅导员同时打开同一张假条都点了通过。如果代码是先selectById查状态再updateById改状态两次 select 都读到待审核两次 update 都成功假条就被批了两次。第 3 章的写法通过update ... where status 1 and approver_id ?把这个竞态消掉了。如果代码里用的是 MyBatis-Plus 的乐观锁插件Version原理也一样只是把 version 字段校验交给了框架。两种方案实现不同但思路都是同一个「让数据库来决定这次更新是否有效」。UPDATE leave_application SET status 2, version version 1, audit_time NOW() WHERE id #{id} AND status 1 AND version #{expectVersion}影响行数为 0 时说明假条已经被其他人处理过这时候不要重试直接把冲突信息返回给前端提示即可。5.3 撤销权限与角色扩展总会有「第二审批人」需求部分学校流程是「班长初审 - 辅导员终审」这其实只要在申请表加一个current_level字段审批时按层级判断当前操作者是第几级审批人。数据模型不用大改状态机里增加一个PENDING_SECOND即可。要警惕的是撤销逻辑。学生只能撤销「待审核」的假条一旦辅导员已经审批通过学生无权再改。撤销和审批是同一个并发竞争点撤销接口也要用where status 待审核的条件更新。如果你拿到的需求里还包含「销假申请」那它就是另一个子状态流不要和请假审批的状态混在一个字段里否则状态图会变成一团乱麻。6. 论文结构、演示数据与答辩准备6.1 论文章节和系统产出怎么对应「基于SpringBootVue的学生请假管理系统」的论文一般五章到六章绪论、需求分析、系统设计、系统实现、系统测试。写的时候最忌把代码整段粘进去应该把章节落在图表和设计决策上论文章节对应产出建议表达方式需求分析角色用例图、状态流转表用第 2 章的状态机表格直接改绘成 UML 状态图系统设计E-R 图、接口设计表按第 3 章的接口表格扩展标明参数和返回结构系统实现核心代码片段 说明只放状态校验和 JWT 拦截器的代码别贴 CRUD系统测试测试用例表和截图用「学生提交-审批-销假」的完整路径做主流程用例6.2 演示脚本按一条完整假条走完闭环答辩演示不要打开系统随便点而是准备一份带数据的剧本演示前造好一名学生和一名辅导员的账号请假数据覆盖待审核、已通过、已驳回三种状态。演示时按「学生提交 - 辅导员驳回 - 学生改期再提交 - 辅导员通过 - 销假确认」的路径操作最后展示统计列表里该学生的请假天数变化。这条路径走完状态机、权限控制、前后端联调三个重点就全被覆盖到了。刻意留一个错误操作演示也很好比如用学生账号手动访问审批接口展示后端返回 403比只展示正常流程更有说服力。6.3 答辩高频问题要提前准备的口径几个大概率被问到的点为什么用 JWT 不用 Session回答口径是「后端不做 session 存储服务无状态后续扩展小程序或移动端时认证逻辑不用改」为什么请假天数在后端算回答口径是「前端时钟可改、口径不统一后端计算才能保证统计一致」MyBatis-Plus 和 MyBatis 的区别要说出「条件构造器减少 SQL 量、分页插件开箱即用但复杂报表还是手写 SQL」。最后一个建议把leave_application表的 status 字段注释打印出来贴在设计文档里。答辩时一眼能看出你对状态闭环的理解这一项比很多花哨的图表都管用。本文还有配套的精品资源点击获取