news 2026/9/17 19:28:02

基于SpringBoot+Vue的学生请假管理系统:状态机驱动流程设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SpringBoot+Vue的学生请假管理系统:状态机驱动流程设计

简介:基于SpringBoot+Vue的学生请假管理系统毕业设计论文,面向高校计算机相关专业毕业生及正在学习Java Web开发的初学者,解决毕业设计选题、论文撰写与系统设计参考的需求。论文完整覆盖系统分析、可行性分析、功能设计和数据库设计,详细展示了管理员、教师、学生三类角色的核心模块,包括学生管理、请假表格管理、学生考勤管理、缺课记录管理等,有助于读者理解B/S架构下Spring Boot与Vue的实际应用。压缩包内仅包含1个doc格式文档,大小约1.65MB,便于直接阅读和修改,适合用作毕业论文框架参考或项目文档模板。目前已有130人学习,对于需要快速搭建论文结构、梳理系统功能模块的读者具有较高的借鉴价值。借助该文档可以了解从需求分析到系统实现、再到论文撰写的完整流程,节省从零开始整理资料的时间。

1. 学生请假管理系统为什么先定状态机再写 CRUD

很多学生请长假条系统最终能跑通,但一到真实使用就乱:请假条提交之后还能被自己撤回重改,辅导员批过的假条又被覆盖成待审,月底统计请假天数时发现同一张单子被算了两遍。这些问题的根不在增删改查,而在状态——系统没有把「待审核、已通过、已驳回、已销假」当成一条不可乱跳的流程来设计。

所以做这个基于 SpringBoot+Vue 的学生请假管理系统,先别急着搭页面,先把「谁能在什么状态下把假条推到哪个状态」这件事定死。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 建表 SQL:student_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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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: 12

map-underscore-to-camel-case: truestudent_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 接口。全部返回统一结构Result<T>,包含 code、message、data 三个字段,前端 axios 拦截器只需处理 code 为 0 的场景。

审批接口是整个系统里最需要写对状态约束的地方:

@PutMapping("/{id}/approve") public Result<Void> 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, "不能审批自己的假条"); } LambdaUpdateWrapper<LeaveApplication> 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 前端路由、请求封装和请假页面这样对接 SpringBoot

4.1 初始化一个 SpringBoot + Vue 的前后端分离项目

前端用 Vue 3 + Vite,创建命令是npm create vue@latest,装完依赖后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.vueLeaveApplyView.vueLeaveAuditView.vueLeaveListView.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 :data="pendingList" v-loading="loading"> <el-table-column prop="studentName" label="学生姓名" /> <el-table-column prop="leaveTypeText" label="请假类型" /> <el-table-column prop="totalDays" label="天数" /> <el-table-column prop="startTime" label="开始时间" /> <el-table-column prop="reason" label="原因" show-overflow-tooltip /> <el-table-column label="操作" width="200"> <template #default="{ row }"> <el-button type="success" @click="handleApprove(row, true)">通过</el-button> <el-button type="danger" @click="handleApprove(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 论文章节和系统产出怎么对应

「基于SpringBoot+Vue的学生请假管理系统」的论文一般五章到六章:绪论、需求分析、系统设计、系统实现、系统测试。写的时候最忌把代码整段粘进去,应该把章节落在图表和设计决策上:

论文章节对应产出建议表达方式
需求分析角色用例图、状态流转表用第 2 章的状态机表格直接改绘成 UML 状态图
系统设计E-R 图、接口设计表按第 3 章的接口表格扩展,标明参数和返回结构
系统实现核心代码片段 + 说明只放状态校验和 JWT 拦截器的代码,别贴 CRUD
系统测试测试用例表和截图用「学生提交-审批-销假」的完整路径做主流程用例

6.2 演示脚本:按一条完整假条走完闭环

答辩演示不要打开系统随便点,而是准备一份带数据的剧本:演示前造好一名学生和一名辅导员的账号,请假数据覆盖待审核、已通过、已驳回三种状态。演示时按「学生提交 -> 辅导员驳回 -> 学生改期再提交 -> 辅导员通过 -> 销假确认」的路径操作,最后展示统计列表里该学生的请假天数变化。

这条路径走完,状态机、权限控制、前后端联调三个重点就全被覆盖到了。刻意留一个错误操作演示也很好,比如用学生账号手动访问审批接口,展示后端返回 403,比只展示正常流程更有说服力。

6.3 答辩高频问题要提前准备的口径

几个大概率被问到的点:为什么用 JWT 不用 Session,回答口径是「后端不做 session 存储,服务无状态,后续扩展小程序或移动端时认证逻辑不用改」;为什么请假天数在后端算,回答口径是「前端时钟可改、口径不统一,后端计算才能保证统计一致」;MyBatis-Plus 和 MyBatis 的区别,要说出「条件构造器减少 SQL 量、分页插件开箱即用,但复杂报表还是手写 SQL」。

最后一个建议:把leave_application表的 status 字段注释打印出来贴在设计文档里。答辩时一眼能看出你对状态闭环的理解,这一项比很多花哨的图表都管用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 19:27:45

遭遇交通事故导致多处伤残,索赔金额的计算往往是当事人最关心的问题。根据《道路交通事故受伤人员伤残评定》(GB 18667-2002)及现行司法实践,多处伤残的索赔并非简单叠加,而是采用“伤残索赔附加指

一、多处伤残索赔金的阶梯式计算模型依据《最高人民法院关于审理人身损害索赔案件适用法律若干问题的解释》第十二条&#xff0c;残疾索赔金根据伤残等级&#xff0c;按受诉法院所在地上一年度城镇居民人均可支配收入&#xff0c;自定残之日起按二十年计算。但此处针对的是单一…

作者头像 李华
网站建设 2026/9/17 19:21:22

Step-Audio2 在线服务部署指南:vLLM-Omni 双阶段 TTS/ASR/S2ST 实践

Step-Audio2 在线服务部署指南&#xff1a;vLLM-Omni 双阶段 TTS/ASR/S2ST 实践 【免费下载链接】vllm-omni A framework for efficient model inference with omni-modality models 项目地址: https://gitcode.com/GitHub_Trending/vl/vllm-omni 导读 本指南以 vLLM-O…

作者头像 李华
网站建设 2026/9/17 19:17:43

扫描版PDF的OCR识别实战:以ISDA合同为例

简介&#xff1a;这份《ISDA 2002年主协议&#xff08;中英文版&#xff09;》是国际掉期及衍生工具协会标准合同的清洁扫描版&#xff0c;面向金融机构法务、衍生品交易员、风控人员及金融专业学生。资源为1个高清PDF文件&#xff0c;压缩包仅26KB&#xff0c;便于下载后随时查…

作者头像 李华
网站建设 2026/9/17 19:16:19

Flutter与OpenHarmony开发微动漫App实践

1. 项目背景与核心价值去年接触OpenHarmony时&#xff0c;我就被它的分布式能力深深吸引。作为一个长期从事跨平台开发的工程师&#xff0c;我一直在寻找能够同时覆盖移动端、IoT设备和智能硬件的解决方案。而这次将Flutter框架与OpenHarmony结合开发微动漫App的尝试&#xff0…

作者头像 李华
网站建设 2026/9/17 19:16:09

机房管理系统数据库设计:ER模型到SQL实现与并发优化实践

简介&#xff1a;这是一份软件学院机房管理系统的数据库课程设计完整说明书&#xff0c;面向高校软件工程、企业信息化方向学生及需要完成数据库课程设计的读者。系统采用面向对象语言结合SQL Server开发&#xff0c;实现无人值守上机、自动关电源、计费调整、信息查询等核心功…

作者头像 李华