Spring Boot学生请假管理系统:权限控制与审批流程实战指南
简介这是一份基于SpringBoot的学生请假管理系统毕业设计论文以docx格式提供面向计算机专业学生、毕业设计作者及需要搭建请假管理系统的开发者旨在解决传统纸质请假流程繁琐、低效、易错的问题。论文系统梳理了开发背景、JAVA与SpringBoot后端的整合、Vue前端交互、MySQL数据库设计以及个人中心、班级管理、基础数据管理、公告管理、留言管理等模块的实现思路并包含测试和优化结论。资源压缩包共1个docx文档约3.05MB内含中英文摘要、目录、章节正文结构完整可直接用作撰写毕业设计论文的模板或项目设计参考。已有182人浏览/学习适合需要快速理解SpringBoot管理系统整体架构与模块分工的读者。1. 基于 Spring Boot 的学生请假管理系统课设和毕设里最稳的业务练手方向学生请假管理系统这个名字乍看只是“学生提交假条、老师审批”的小工具但在 Spring Boot 技术栈里它几乎覆盖了一个真实业务后端的所有核心环节多角色权限、审批流状态机、数据校验、定时任务、异常处理、日志记录。你在这套系统里写明白的东西可以直接迁移到工单系统、审核系统、报销系统本质都是“表单 状态流转 角色审批”。所以很多高校的课设、毕设选题都落在它身上不是没有道理的。这篇文章不给你粘贴一份 demo 代码了事而是按我实际做这类系统的顺序来讲表结构怎么设计、审批状态怎么流转、三种角色怎么防越权、哪些坑我踩过、以及如果你要配套写论文文档架构图和时序图应该怎么组织。你跟着走能从一张空白数据库表做到一个能跑通“学生提交 → 辅导员审批 → 院系备案”的完整后端服务如果你是熟手直接跳去第 5 章看避坑清单那里才是真正花时间换来的经验。2. 先把数据模型立住请假系统的四张核心表与状态字段设计2.1 用户-班级-课程RBAC 权限模型怎么落到表里请假系统里的用户天然分三类学生、辅导员或班主任、系统管理员。绝大多数毕设会把用户角色字段直接写进用户表用role一个字段区分常见取值是1学生、2辅导员、3管理员。这种设计在数据量不大时完全够用也是成本最低的做法没必要一开始就上五张表的完整 RBAC。但要注意一个细节学生的“班级归属”和辅导员的“管理班级”必须建立关联。否则辅导员登录后没法知道自己该审批哪些学生只能看到全部假条这在答辩时会被追问到。我的做法是加一张简单的class_info表学生表里放class_id辅导员表里放manage_class_id用班级 ID 关联而不是用字符串存班级名称。字符串对比在数据量小的时候看不出问题一旦涉及“查本班所有学生”“统计某班请假次数”这类聚合查询数值外键比字符串拼接靠谱得多。课程表在这个系统里属于可选模块。如果你的请假单需要精确到“第几节课请假”那就要有course表和course_student选课关系表如果只需要按“开始日期到结束日期”请假课程表可以省掉。做之前先想清楚自己的请假粒度这决定了后续所有接口的设计方向。2.2 请假单主表与审批记录表状态机数据是怎么流转的请假单是整个系统的核心业务表字段设计直接影响后面写代码的心情。我一般会这样设计字段名类型说明idbigint主键自增order_novarchar(32)请假单号业务编号展示用student_idbigint发起人关联用户表class_idbigint发起人所在班级冗余存储便于辅导员按班查询start_timedatetime请假开始时间end_timedatetime请假结束时间reasonvarchar(500)请假事由statustinyint状态0 待审批1 已通过2 已驳回3 已撤销create_timedatetime提交时间可由框架自动填充update_timedatetime最后更新时间审计用status字段是状态机的核心。我在2.1里提到“先想清楚请假粒度”这里还要再想清楚“审批层级”。最简版本是学生提交后由辅导员直接审批一步到位复杂一点的会加“院系备案”环节也就是辅导员通过后管理员或院系负责人再做一次确认。我建议做两级审批学生提交 → 辅导员审批通过/驳回 → 管理员可以查看和导出统计但不参与审批。这样状态流转简单接口数量可控论文里也好画时序图不会被“三级审批”的边界条件拖死。审批记录单独建一张approval_record表记录每次审批动作CREATE TABLE approval_record ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL COMMENT 请假单ID, approver_id bigint NOT NULL COMMENT 审批人用户ID, action tinyint NOT NULL COMMENT 动作1 通过2 驳回, comment varchar(500) DEFAULT NULL COMMENT 审批意见, create_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表的价值在答辩时最能体现老师问“审批有没有留痕”直接把记录表展示出来比口头说“有日志”有力得多。而且后续做“某人最近审批了多少单”“某学生请假通过率”这类统计也全靠这张表。2.3 建表 SQL 与常见字段陷阱时间字段、状态字段、冗余字段的取舍直接给一份能跑通核心流程的建表脚本字段我按实际项目经验做了精简和合并CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT 登录密码MD5或BCrypt加密, real_name varchar(32) NOT NULL COMMENT 姓名, role tinyint NOT NULL COMMENT 1 学生2 辅导员3 管理员, class_id bigint DEFAULT NULL COMMENT 学生所属班级辅导员可空, manage_class_id bigint DEFAULT NULL COMMENT 辅导员管理班级学生可空, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE class_info ( id bigint NOT NULL AUTO_INCREMENT, class_name varchar(64) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE leave_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, student_id bigint NOT NULL, class_id bigint NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, reason varchar(500) DEFAULT NULL, status tinyint NOT NULL DEFAULT 0, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_student_status (student_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;start_time和end_time用datetime不要用varchar存字符串。虽然前端传过来的时候都是字符串但数据库层用时间类型能直接排序、比较、计算时长省掉一堆转换代码。order_no要有唯一索引一方面避免重复单号另一方面做幂等时可以作为查重条件。class_id在请假单表里属于冗余字段因为通过student_id也能查到班级。这里冗余的目的是让“辅导员查本班请假单”直接走where class_id ?不用先查学生表再关联索引效率高很多。在数据量小的系统里这个优化不明显但这是一个好的习惯。提示password字段长度至少 64因为可以直接存 BCrypt 加密后的结果。用 MD5 的话遇到彩虹表查询没有安全性可言不建议。3. 用 Spring Boot 把请假审批流程跑通从提交到两级审批3.1 项目骨架与技术选型三层结构里各层的职责边界项目骨架用 Spring Boot 3.x 或 2.7.x 都可以看你的 JDK 版本。配套的持久层框架选 MyBatis Plus理由是单表 CRUD 不用写 XML条件构造器LambdaQueryWrapper对动态查询支持很好适合这种以表单数据为主的业务系统。数据库用 MySQL 5.7 或 8.0本地开发用 H2 也行但涉及datetime和索引行为时和 MySQL 有细微差别建议直接用 MySQL。包结构按经典三层划分controller只做参数接收和结果包装service写业务判断和状态流转mapper只做数据库操作。容易翻车的点是把状态判断写在 Controller 里乍看能跑后面前端多调一个接口就发现逻辑重复了。实体类上加上 MyBatis Plus 注解让代码和表字段对应清晰Data TableName(leave_order) public class LeaveOrder { TableId(type IdType.AUTO) private Long id; private String orderNo; private Long studentId; private Long classId; private LocalDateTime startTime; private LocalDateTime endTime; private String reason; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }TableName指定表名TableId指定主键生成策略字段驼峰命名自动映射下划线列。这台默认配置已经能覆盖绝大多数场景不用额外写 XML。3.2 请假提交接口DTO 接收参数与状态初始化写法提交请假单是流程的起点。请求参数用一个单独的 DTO 接收不要直接把实体类暴露给前端原因很简单前端多传一个status字段就把状态覆盖了这种安全问题在前后端联调时经常被忽略。PostMapping(/leave) public ResultString submit(RequestBody Valid LeaveApplyDTO dto) { Long studentId UserContext.getCurrentUser().getId(); LeaveOrder order new LeaveOrder(); order.setOrderNo(generateOrderNo()); order.setStudentId(studentId); order.setClassId(UserContext.getCurrentUser().getClassId()); order.setStartTime(dto.getStartTime()); order.setEndTime(dto.getEndTime()); order.setReason(dto.getReason()); order.setStatus(0); // 待审批 leaveOrderMapper.insert(order); return Result.ok(order.getOrderNo()); }generateOrderNo()我一般用时间戳加随机数格式类似20250521153000123保证业务可读性。Status必须是代码里写死的初始值不能从前端取。startTime和endTime要做校验结束时间必须晚于开始时间这个校验放在 DTO 上的自定义注解里或者写在 Service 里都行但必须做否则数据库里会出现负时长假条。当前登录用户信息怎么拿我建议用一个UserContext静态工具类登录成功后把用户对象放进去请求结束再清理。这个方案比每个接口都从 Session 里取要省事也比直接传userId参数安全得多因为后者意味着任何登录用户都可以冒充别人提交申请。3.3 审批接口与状态机校验驳回、通过、撤销的边界条件审批接口是这个系统的重头戏。核心不是“更新 status 字段”而是“在什么条件下允许把 status 从 A 变成 B”。一个已经驳回的假条不能被再次审批一个已经通过的假条不能由学生自己撤销这些规则写不好数据就乱了。PostMapping(/approve) Transactional(rollbackFor Exception.class) public ResultString approve(RequestBody Valid ApproveDTO dto) { LeaveOrder order leaveOrderMapper.selectById(dto.getOrderId()); if (order null) { throw new BizException(请假单不存在); } // 状态机校验只有待审批状态才能被审批 if (order.getStatus() ! 0) { throw new BizException(当前状态不允许审批); } // 权限校验辅导员仅能审批本班学生的假条 User approver UserContext.getCurrentUser(); if (!approver.getManageClassId().equals(order.getClassId())) { throw new BizException(只能审批本班学生的请假单); } // 审批通过或驳回 order.setStatus(dto.getApproved() ? 1 : 2); leaveOrderMapper.updateById(order); // 写审批记录留痕 ApprovalRecord record new ApprovalRecord(); record.setOrderId(order.getId()); record.setApproverId(approver.getId()); record.setAction(dto.getApproved() ? 1 : 2); record.setComment(dto.getComment()); approvalRecordMapper.insert(record); return Result.ok(); }Transactional保证“更新请假单状态”和“写入审批记录”两个操作要么都成功要么都失败。这里最容易踩的坑是只更新了主表状态忘记写记录表或者记录写进去了但主表更新失败这些都会让数据对不上。两件事必须在一个事务里。状态机校验我直接用了if (order.getStatus() ! 0)因为这套系统只有两级审批、四个状态。如果你后续要扩展“已通过但未销假”“辅导员通过后需院系确认”建议把状态流转规则抽成一个枚举类用集合存“当前状态允许迁移到哪些状态”代码会清爽很多。撤销接口的逻辑类似学生发起撤销时必须先由代码判断status 0待审批才允许撤销到status 3。如果学生已经请假成功了还想取消假期那是另一个业务销假不能复用撤销接口。3.4 超时未审批的定时任务Scheduled 注解与事务的配合这个功能不是必做的但做了之后系统会有明显的完整感。场景是学生提交请假单后辅导员两三天没登录系统假条一直悬在“待审批”学生也不知道该催谁。一个简单的定时任务每天上午 8 点扫一遍超过 24 小时仍未审批的假条批量的站内通知提醒辅导员。Component public class LeaveRemindTask { Scheduled(cron 0 0 8 * * ?) Transactional(rollbackFor Exception.class) public void remindPendingLeave() { LocalDateTime deadline LocalDateTime.now().minusHours(24); ListLeaveOrder pendingList leaveOrderMapper.selectList( new LambdaQueryWrapperLeaveOrder() .eq(LeaveOrder::getStatus, 0) .lt(LeaveOrder::getCreateTime, deadline) ); for (LeaveOrder order : pendingList) { // 发送站内通知提醒对应辅导员处理 messageService.sendRemind(order.getClassId(), order.getOrderNo()); } } }Scheduled(cron 0 0 8 * * ?)表示每天 8 点执行cron 表达式里的?专门用在“日”和“周”字段上表示不指定值。这个任务里要注意的是如果发送通知调用了外部接口而这个外部接口偶尔超时事务会把整批通知全部回滚导致一个都没发出去。稳妥做法是把提醒动作放到事务提交后执行或者记录提醒日志表下次重试时跳过已提醒的单子。4. 三种角色的权限控制登录态识别与越权防护的常见做法4.1 用拦截器做登录校验与角色区分权限控制是这类管理系统答辩时必问的点同时也是翻车重灾区。最简单的做法是每个接口里手动判断角色缺点是接口一多就开始漏。我习惯用一个拦截器按 URL 前缀统一拦截Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { User user (User) request.getSession().getAttribute(currentUser); if (user null) { throw new LoginException(未登录); } String uri request.getRequestURI(); if (uri.startsWith(/student/) user.getRole() ! 1) { throw new ForbiddenException(学生角色才能访问); } if (uri.startsWith(/counselor/) user.getRole() ! 2) { throw new ForbiddenException(辅导员角色才能访问); } UserContext.set(user); return true; } }拦截器里做角色判断好处是 Controller 里不用重复写角色校验代码只关注业务逻辑。但要注意这个方案管的是“粗粒度权限”也就是“什么角色能访问什么接口模块”管不了“这个辅导员只能批自己班级的假条”这种数据级权限。后者必须在 Service 里配合数据校验实现也就是第 3 章审批接口里检查manage_class_id的那段代码。注册拦截器时注意排除登录接口和静态资源Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /logout, /static/**); } }excludePathPatterns里漏掉登录接口是新手最常见的配置错误表现是登录页面都进不去因为登录请求本身也被拦了。另外注册拦截器有两个容易踩的点一是拦截器里抛出的异常要能被全局异常处理器接住二是HandlerInterceptor实现类要交给 Spring 管理否则拦截器里注入不了Mapper等依赖。4.2 数据级越权场景学生查别人的假条、辅导员批别班的单粗粒度权限只能挡住“还没登录的人”和“角色不对的人”挡不住“登录了但是操作了不属于自己的数据”。典型场景有三个我逐个说清楚第一个是学生查询自己的请假单列表。不能直接select * from leave_order必须带student_id 当前登录用户ID条件。有的同学把学生 ID 做成查询参数前端想查谁就传谁这就等于把所有人的假条都暴露了。第二个是学生查看审批详情。/leave/detail/{orderId}这个接口光校验“请假单存在”是不够的还要校验“这张单的 student_id 是当前用户”。不然学生只要遍历订单 ID 就能看到全校的请假记录和审批意见。第三个是辅导员审批时只能批本班。审批接口里我已经写了manage_class_id和class_id的比对这里不再重复但可以多提一句如果系统里一个辅导员管理多个班级可以用IN (?)查询不要写死单个班级 ID。4.3 前端按钮级控制与后端数据校验的双层配合很多同学只做后端权限前端登录后把所有按钮都渲染出来点一下才报“无权限”体验很烂。正确做法是登录接口返回用户角色信息前端按角色控制菜单和按钮显示。比如学生端只显示“提交请假”和“我的请假单”辅导员端显示“请假审批”和“班级请假统计”。但就算前端隐藏了按钮后端接口依然要做好权限校验。原因不是防内部人员而是防两类情况一是别人拿到接口地址绕过前端直接调用二是前端按钮逻辑出了 bug不该显示的功能显示了。后端校验是兜底不是多此一举。学生端按钮隐藏逻辑用 Vue 或 React 都很简单if (userInfo.role 1) { this.showLeaveSubmit true this.showApproveList false } else if (userInfo.role 2) { this.showLeaveSubmit false this.showApproveList true }后端响应要统一返回结构比如{ code: 200, data: ..., message: success }前端才能统一拦截 401 跳登录、403 提示无权限。有的项目后端接口直接返回ResponseEntity配 HTTP 状态码也能用但前后端对异常处理的分工要提前约定好否则排查问题时两边互相甩锅。5. 请假系统避坑指南状态覆盖、并发重复提交、时区混用的 5 个翻车现场5.1 连续审批导致状态覆盖现象辅导员先审批通过了某张假条管理员后台误操作又把同一张假条驳回前端看到的状态和审批记录对不上。原因审批接口只校验了“请假单存在”没有校验当前状态两条审批请求并发到达时后执行的覆盖了先执行的。解决审批前必须做状态机校验status不等于0直接拒绝操作更新时在 where 条件里带上原始状态例如UPDATE leave_order SET status 1 WHERE id ? AND status 0影响行数为 0 说明状态已变化需要重新加载数据。MyBatis Plus 里可以写LambdaUpdateWrapper带eq(LeaveOrder::getStatus, 0)。5.2 学生撤销了已通过的假条现象学生请假三天已经审批通过又想撤销结果系统允许直接删掉了假条辅导员端的审批记录还在但列表里查不到这张单了。原因撤销接口没有区分“待审批可撤销”和“已通过要销假”两种状态统一做了删除或置为撤销。解决撤销仅允许status 0时执行且只能置为status 3不能物理删除。已通过的假条如果要提前结束走销假流程新增status 4已销假保留完整的历史轨迹。这类“状态只能往前走不能回退”的思路在答辩时一定能给你加分。5.3 并发重复提交生成两张一模一样的假条现象学生网络卡顿连点两次“提交”系统生成了两条相同时间段的请假单辅导员审批时看到两条几乎一样的记录。原因提交接口没有做幂等控制两次请求都正常执行了 insert。解决方案一是在leave_order表加order_no唯一索引前端提交时生成一个clientRequestId后端先查这个编号是否已存在存在则直接返回已有结果方案二是提交时用synchronized或分布式锁对student_id start_time end_time的组合加锁。数据量不大时方案一最简单可靠。5.4 LocalDateTime 与前端字符串的时区混用现象前端传2025-05-21 08:00:00后端存到数据库变成了2025-05-21 00:00:00或者查询出来的时间比实际少了 8 小时。原因前后端时区不一致或者 JDBC 连接串没指定serverTimezone。解决MySQL 连接串里明确加上serverTimezoneAsia/Shanghai和useLegacyDatetimeCodefalse后端时间类型统一用LocalDateTime不要混用Date和Timestamp前端传参格式固定为yyyy-MM-dd HH:mm:ss用JsonFormat注解统一序列化格式。这个坑非常隐蔽有时候本地测得好好的部署到服务器上就出问题多半是服务器的默认时区是 UTC。5.5 逻辑删除导致唯一索引失效现象假条表做了逻辑删除用deleted字段标记是否删除但学生撤销后再次提交相同订单号数据库报唯一索引冲突。原因逻辑删除后数据还在表里order_no的唯一索引不会因为deleted 1就放过重复值。解决如果你的业务里order_no是全局唯一的撤销或删除时不要复用旧单号每次生成新单号如果必须复用就把唯一索引改成联合唯一例如(order_no, deleted)或(order_no, student_id)。MyBatis Plus 逻辑删除配置TableLogic时要注意这个注解只影响框架生成的 SQL不影响数据库层面的唯一约束。注意第 5.5 条里(order_no, deleted)联合索引只适合删除标记只有 0 和 1 的场景一旦支持多版本历史这个方案就不成立了。做之前先确认业务是否允许复用单号。6. 论文文档怎么写才能和代码对得上架构图、ER 图、时序图的三步自查6.1 技术选型章节的三段式写法如果你要交论文文档技术选型部分可以按“选什么、为什么选、对比对象”三段来写。不要只写“本项目采用 Spring Boot 框架”一句话要写清楚 Spring Boot 在简化配置、内嵌服务器、生态整合上的优势同时提一句为什么不选 SSM 或者为什么不用纯 JSP。答辩老师最喜欢问的一个问题就是“你为什么要用这个技术”三段式能帮你答得完整。6.2 ER 图与建表 SQL 的一致性检查画 ER 图时最容易出问题的是字段和表关系和实际代码不一致。比如 ER 图画了五张表代码里只建了三张或者 ER 图里leave_order和approval_record画了一对多但代码里审批记录只存了最新一条。交文档前对照建表 SQL 逐表核对字段名、类型、关系比对着代码看靠谱。我在某次答辩见过 ER 图画得挺漂亮但老师当场打开数据库看表结构发现少了两列场面很尴尬。6.3 时序图与代码逻辑的对齐答辩前自查清单时序图是最容易被追问的部分因为老师会拿着时序图对照你的代码实现。自查清单列三个最容易出错的点第一时序图里的“判断分支”要和代码里的if条件一致比如状态机校验画在“提交”还是“审批”环节不能画错第二异步操作比如定时任务不要在时序图里画成同步请求单独画活动图或说明文字第三异常分支要在时序图里有体现至少画出“审批不通过”的返回路径。我自己的习惯是写完代码后按“用户 → Controller → Service → Mapper → 数据库”的顺序画一遍时序图每画一个箭头就回源码里确认一次方法名。这个方法笨但有效至少能保证答辩时老师顺着代码提问你不会答不上来。如果你做这套系统是为了交课设或毕设记住一件事代码能跑只是底线“数据怎么流转、权限怎么控制、异常怎么处理”才是能聊深度的点也是论文文档里真正值得花篇幅展开的地方。希望这篇笔记帮你在做请假管理系统的路上少踩几个坑多留出一点时间睡觉。本文还有配套的精品资源点击获取