简介:基于SpringBoot的学生请假管理系统毕业设计项目,面向Java后端学习者与高校毕设学生,覆盖管理员、辅导员、学生三类角色的全流程请假审批场景,运行环境采用IDEA、MySQL5.7与JDK1.8,整体前后台结构清晰易读。资源共1423个文件,压缩包约131.05MB,包含482个js、258个html、126个css等前端静态资源,51个java与53个class构成后端业务逻辑,另有xml配置、sql数据库脚本、yml配置文件及png/jpg图片素材,目录结构完整,便于按模块阅读和部署调试。目前已有1369人学习下载,适合正在搭建Spring Boot管理系统的读者参考。从预览可见项目包含控制器、实体类与多种工具类,可学习请假单审核、列表分页、Excel导出、验证码等常见功能实现;配套数据库脚本与前端页面,可作为毕业设计或课程设计的完整起步模板,按需替换业务字段后二次开发,也能帮助梳理学生请假场景中的权限与状态流转。
1. 为什么毕设选 SpringBoot 学生请假管理系统,性价比高在哪
用 Spring Boot 做学生请假管理系统,是每年计算机类毕业设计里重复率最高的题目之一。它表面上是增删改查,但请假单从提交、审批到统计的完整链路,天然覆盖工作流、数据权限、并发控制和统计报表四个面试高频考点。相比电商、外卖这类需要大量页面和第三方依赖的选题,请假系统业务边界清晰,数据量小,适合在三个月内独立完成,也适合在答辩时把 Spring Boot 的核心机制讲清楚。这篇文章不打算复述某个现成教程,而是按我自己做这类管理系统时的顺序往下走:先定表结构,再搭 Spring Boot 工程,然后实现请假、审批、统计三个核心闭环,最后补充答辩前值得做的加固。
2. 需求与表结构:请假单才是整个管理系统的核心
2.1 最小可用功能清单:学生、辅导员、管理员三类角色
做毕业设计的第一件事不是写代码,是把功能边界画出来。学生请假管理系统最常见的角色划分是学生、辅导员(或班主任)、管理员三类;有的题目里还有“任课老师”,大部分校级场景可以并入辅导员处理,不必额外建表。这三类角色的功能对比如下:
| 角色 | 核心功能 | 对应接口 |
|---|---|---|
| 学生 | 提交请假申请、查看自己的申请列表、撤销/销假 | POST /api/leave/apply、GET /api/leave/my、POST /api/leave/cancel |
| 辅导员 | 查看待审批列表、通过/驳回、查看本班或多班级统计数据 | GET /api/leave/pending、POST /api/leave/audit、GET /api/leave/statistics |
| 管理员 | 用户管理、全量数据查看、状态修正 | GET /api/user/list、PUT /api/user/status、POST /api/leave/reset |
这个清单能砍掉很多想象出来的功能,比如“短信提醒”“消息推送”,这些在毕设里大概率做不完整,做出来也无法演示。把上面表格里的接口做扎实,答辩时每个接口都能对应到一个页面上,系统就已经完整了。功能清单定下来之后,再往下一步设计表结构时,就能知道哪些字段是真正会被用到的。
2.2 数据模型设计:请假单与审核记录分开存
请假系统最核心的表是请假单,但只有一张表不够。审核记录要单独存,否则“谁在什么时间批的、批了什么意见”会被覆盖,统计平均审批时长也写不出 SQL。这两张表的职责必须分开:请假单表保存当前状态和请假信息,审核记录表保存每一次状态变更的明细。
2.2.1 请假单表字段设计
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,雪花算法生成 |
| user_id | bigint | 学生 ID,关联用户表 |
| leave_type | tinyint | 1=事假,2=病假,3=其他 |
| start_time | datetime | 请假开始时间 |
| end_time | datetime | 请假结束时间 |
| reason | varchar(500) | 请假原因 |
| status | tinyint | 0=草稿,1=待审批,2=已通过,3=已驳回,4=已销假 |
| version | int | 乐观锁版本号 |
| create_time / update_time | datetime | 审计字段 |
这里有两个容易被忽略的点。第一,不要存“请假天数”字段,因为“半天”“跨周末”的计算口径很容易和前端的显示不一致,用 start_time 和 end_time 两个时间点,需要展示时再用 SQL 或 Java 计算。第二,version 字段不是必须的,但加上之后,审批接口能直接用乐观锁挡住重复操作,成本极低,后面会写对应的 update 语句。
2.2.2 审核记录表设计
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| leave_id | bigint | 请假单 ID |
| auditor_id | bigint | 审核人 ID |
| action | tinyint | 2=通过,3=驳回 |
| comment | varchar(300) | 审批意见 |
| create_time | datetime | 操作时间 |
action 字段直接存目标状态,比存“同意/拒绝”之类的业务描述更利于写 SQL。比如统计驳回率,一条GROUP BY action就能算出来。这张表只追加不更新,也不需要 version 字段,属于典型的流水表。
2.3 状态机:非法跳转要在 SQL 层拦住
状态机看起来是设计文档里的概念,但在请假系统里它会直接变成一条 WHERE 条件。常见的流转是这个样子:
| 当前状态 | 允许的操作 | 目标状态 |
|---|---|---|
| 0 草稿 | 提交 | 1 待审批 |
| 1 待审批 | 通过 / 驳回 | 2 已通过 / 3 已驳回 |
| 2 已通过 | 销假 | 4 已销假 |
| 3 已驳回 | 重新提交 | 1 待审批 |
后端写校验时,不能只判断“当前状态是不是待审批”,因为那只是 Java 内存里的旧值。并发情况下两个请求同时读到 status=1,两个审核都会通过。正确做法是把它写进 SQL 的 WHERE 里:UPDATE leave_request SET status = #{target} WHERE id = #{id} AND status = 1,影响行数为 1 才算操作成功。这样即使并发请求进来,数据库的行锁也只会放行一个。这个模式在后文审批接口里会直接出现。
2.4 建表 SQL 与初始化数据
CREATE TABLE `leave_request` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '学生ID', `leave_type` tinyint NOT NULL DEFAULT 1 COMMENT '1事假 2病假 3其他', `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `reason` varchar(500) DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 0 COMMENT '0草稿 1待审批 2通过 3驳回 4销假', `version` int NOT NULL DEFAULT 0, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_status` (`user_id`, `status`), KEY `idx_status_start` (`status`, `start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `leave_audit_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `leave_id` bigint NOT NULL, `auditor_id` bigint NOT NULL, `action` tinyint NOT NULL COMMENT '2通过 3驳回', `comment` varchar(300) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_leave_id` (`leave_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;两个索引分别服务两类高频查询:学生端“我的请假列表”走idx_user_status,审批端“某状态下按时间排序的待办列表”走idx_status_start。leave_audit_record只按 leave_id 查明细,所以一个普通索引就够。初始化数据方面,user 表里至少要准备学生、辅导员、管理员各一个账号,密码统一预置 BCrypt 密文,方便前端联调。
这个建表 SQL 里没有外键约束,是有意为之。请假单和审核记录、用户表之间的完整性依赖通过业务代码控制,而不是数据库外键。原因很简单:毕设系统会被反复改表,外键会让我们在删除测试数据、批量调整数据时处处受限。日常开发中,需要对账的严肃系统仍然建议保留外键,但请假管理系统这个规模,业务层控制是更常见的做法。
3. Spring Boot 工程搭建与配置:先把地基打牢
3.1 版本选型:JDK 17 + Spring Boot 3.x + MyBatis-Plus 的搭配
毕设选题里最常见的技术组合是 Spring Boot + MyBatis-Plus + MySQL,前端要么是 Vue 要么是 Thymeleaf。先说后端版本搭配,这个组合的容错率会直接影响后面排错的时间:
| 组件 | 推荐版本范围 | 说明 |
|---|---|---|
| JDK | 17 | Spring Boot 3.x 的最低要求是 JDK 17,面试也常问 |
| Spring Boot | 3.2.x | 功能稳定,三方库适配度高,不要追 3.5 之后的最新版本 |
| MyBatis-Plus | 3.5.x | 与 Spring Boot 3 兼容;旧版 3.4.x 会报类找不到 |
| MySQL | 8.0 | 8.0 是主流,字符集统一用 utf8mb4 |
标题里写的是 SpringBoot,很多同学会在选版本时纠结“最新版本”。我的建议是反过来,选已经被验证过的组合。Spring Boot 版本太高时,MyBatis-Plus 的 starter 没跟上,或者分页插件写法不兼容,这类问题在毕设阶段最容易卡住人。反过来,Spring Boot 2.7 也可以用,但面试时被问到“为什么不用 3.x”,回答成本会高一些。选择 3.2.x 是一个平衡点。
另一个隐蔽的坑是包名变化:Spring Boot 3.x 把javax.*迁移到了jakarta.*,如果网上的旧教程还在 import javax.servlet,编译直接失败。用 IDEA 创建项目时模板默认就是 jakarta,问题不大,但参考老代码时要留意。
3.2 工程目录与分层:controller 别写业务
学生请假管理系统的代码规模不大,但仍然要按分层的结构组织。我一般会这样建包:
com.example.leave ├── controller # 只做参数接收与结果封装 ├── service # 业务逻辑,事务边界 ├── mapper # MyBatis-Plus 的 Mapper 接口 ├── entity # 与表一一对应的实体 ├── dto # 接收参数对象(如 AuditDTO, ApplyDTO) ├── vo # 返回给前端的视图对象 ├── config # 拦截器、MyBatis-Plus 分页插件配置 ├── common # 统一返回体、全局异常、常量实体类用 MyBatis-Plus 的@TableName标注表名,DTO 和 entity 分开,这一点对请假管理系统尤其重要。比如审批接口接收的只有一个 leaveId、action、comment,而 entity 里还有 version、status 字段,直接拿 entity 接参数,等于允许前端伪造 status 和 version,这就是一个越权入口。DTO 里的字段只保留接口真正需要的,并在 Controller 里用@Valid做参数校验,能拦住一大部分低级问题。
3.3 application.yml 里 Spring Boot 配置的关键项
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/leave_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置里三个最容易出问题的点。第一,serverTimezone=Asia/Shanghai必须写,否则 MySQL 连接会因为时区偏差报错,或者时间字段在前后端差八个小时。第二,map-underscore-to-camel-case开启后,create_time才能自动映射到createTime,省掉大量@TableField注解。第三,logic-delete-field: deleted引入了逻辑删除,用户表、请假单表加一个 deleted 字段就行,删除用户时不会真的删行,数据不会神秘消失。
注意:本地演示为了启动方便,密码会直接写在 yml 里。真实项目或放到答辩服务器演示时,建议用 Jasypt 之类的工具把 yml 里的数据库口令做成密文,避免配置文件泄露后顺手把数据库也暴露了。
3.4 统一返回体与全局异常处理
前后端分离已经是这个题目的默认形态,接口返回格式统一,前端才能少写判断。我会定义一个 Result<T>,code=0 表示成功,非 0 表示业务失败:
@Data public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "success"; r.data = data; return r; } public static <T> Result<T> fail(int code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }配合@RestControllerAdvice做全局异常处理,把校验异常、业务异常、未知异常分别映射到不同的 code,Controller 里就不用到处 try-catch。这样一来每个接口的代码量都很短,答辩时顺着调用链路往下讲,结构也清晰。业务异常统一抛一个BizException,携带 code 和 message,全局处理器接住后返回对应的 Result。这个模式属于 Spring Boot 里的基础套路,但学生请假管理系统这种接口数量在 20 个以内的系统,它能把代码量压缩得非常可观。
4. 核心接口实现:请假申请、审批、统计的闭环
4.1 请假申请接口:事务、时间校验与冲突检测
请假申请的入参和保存看起来是简单的 insert,实际上有三个校验不能省:时间顺序、时长上限、是否与其他请假单重叠。前两个是显性校验,第三个是很多人忽略的。学生不小心提交两段重叠的请假,审批人看到两个单子都会困惑。
@Override @Transactional(rollbackFor = Exception.class) public Long apply(ApplyDTO dto) { LocalDateTime now = LocalDateTime.now(); if (dto.getStartTime().isBefore(now)) { throw new BizException(1001, "请假开始时间不能早于当前时间"); } if (!dto.getEndTime().isAfter(dto.getStartTime())) { throw new BizException(1002, "请假结束时间必须晚于开始时间"); } Integer conflict = leaveMapper.countConflict(dto.getUserId(), dto.getStartTime(), dto.getEndTime()); if (conflict != null && conflict > 0) { throw new BizException(1003, "与已有请假单时间冲突"); } LeaveRequest entity = new LeaveRequest(); entity.setUserId(dto.getUserId()); entity.setLeaveType(dto.getLeaveType()); entity.setStartTime(dto.getStartTime()); entity.setEndTime(dto.getEndTime()); entity.setReason(dto.getReason()); entity.setStatus(1); // 提交即进入待审批 leaveMapper.insert(entity); return entity.getId(); }countConflict对应的 SQL 大概是这样的:
SELECT COUNT(*) FROM leave_request WHERE user_id = #{userId} AND status IN (1, 2) AND start_time < #{endTime} AND end_time > #{startTime}这个重叠判断条件是区间查询的经典写法:两条记录在时间轴上有交集,等价于 A 的开始时间早于 B 的结束时间,并且 A 的结束时间晚于 B 的开始时间。SQL 里只用四个字段就能完成判断,不用在 Java 里把已请假的记录全查出来再两两比较。@Transactional保证插入、日志记录等操作要么全部成功,要么全部回滚,避免出现“单子存上了,日志没记上”的中间状态。
4.2 审批接口:用状态条件更新防止重复审批
审批是请假系统里并发语义最强的一个接口。两个审批人同时打开同一张待审批单,一个人点了通过,另一个人也点了通过,如果没有并发保护,就会出现两次 update 都成功的情况。解决方式在第二章已经埋了伏笔:在 UPDATE 的 WHERE 条件里带上当前状态和 version。
@Override @Transactional(rollbackFor = Exception.class) public void audit(AuditDTO dto) { LeaveRequest record = leaveMapper.selectById(dto.getLeaveId()); if (record == null) { throw new BizException(1101, "请假单不存在"); } if (record.getStatus() != 1) { throw new BizException(1102, "该请假单当前不可审批"); } int targetStatus = dto.getPass() ? 2 : 3; int rows = leaveMapper.auditWithStatus(dto.getLeaveId(), record.getStatus(), targetStatus, dto.getUserId(), dto.getComment()); if (rows == 0) { throw new BizException(1103, "审批失败:请假单状态已变化,请刷新"); } }对应的 Mapper 语句:
<update id="auditWithStatus"> UPDATE leave_request SET status = #{targetStatus}, update_time = NOW() WHERE id = #{leaveId} AND status = #{expectStatus} </update>这个写法的关键点在于:rows == 0不一定是记录不存在,而是记录还在,但 status 已经不是之前查到的值,说明别人抢先处理了。此时抛出“请刷新”的提示,比直接报“操作失败”准确得多。这也是面试时可以把“乐观锁”和“条件更新”串起来讲的素材。把期望状态放进 SQL 而不是只靠 Java 判断,是这套代码里最容易讲清楚并发控制的点。
审批完成后要记得插入审核记录表,action 直接存目标状态 2 或 3。这个 insert 与状态 update 在同一个事务里,保证状态变化一定有对应的审计流水。
4.3 数据权限控制:拦截器 + 用户维度 SQL
学生请假管理系统的“用户能看哪些数据”由角色决定,这个题目里它不是登录接口的安全问题,而是数据权限问题。学生只能查自己的单子,辅导员能查本学院的单子,管理员能查全部。拦截器层面的 Spring Boot 拦截器只解决“能不能进这个接口”,数据范围要靠 SQL 控制。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从登录态里解析用户信息,演示环境直接从 header 拿 userId String userId = request.getHeader("X-User-Id"); String role = request.getHeader("X-User-Role"); if (userId == null || role == null) { response.setStatus(401); return false; } request.setAttribute("userId", Long.valueOf(userId)); request.setAttribute("userRole", role); return true; } }在 WebMvcConfigurer 里注册拦截器时,把/api/leave/audit限制为只有teacher、admin角色可访问,/api/leave/apply限制为student可访问。但光有拦截器不够,比如学生直接调用/api/leave/my?userId=10002,把自己换到别人的 ID 上,如果 service 里还按参数里的 userId 查,就构成了越权。正确做法是 service 方法里用request.getAttribute("userId"),即登录态解析出来的值,而不是前端传的 userId。
数据范围在 SQL 上的体现是:学生查列表固定加AND user_id = #{currentUserId},辅导员默认加院系关联条件,管理员不加限制。这种“条件拼接”不需要搞复杂的框架,MyBatis-Plus 的 QueryWrapper 里加一个 eq 条件就够了。重点是让学生、辅导员、管理员共用一个查询方法,而不是写三个近乎复制的 Mapper 方法,否则后面任何一种状态调整都要同步改三处。
4.4 请假统计:日期函数与分组聚合
统计功能是答辩最常见的加分点,也是检验会不会写真实 SQL 的地方。三个常用的统计查询:按月统计请假单数量、按状态统计占比、计算平均审批时长。
-- 按月统计不同类型的请假申请数量 SELECT DATE_FORMAT(start_time, '%Y-%m') AS month, leave_type, COUNT(*) AS cnt FROM leave_request WHERE start_time >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(start_time, '%Y-%m'), leave_type ORDER BY month; -- 平均审批时长(小时) SELECT AVG(TIMESTAMPDIFF(HOUR, r.create_time, a.create_time)) AS avg_hours FROM leave_request r JOIN leave_audit_record a ON a.leave_id = r.id WHERE r.status IN (2, 3);第一个查询的输出适合直接画柱状图,date_format 把 datetime 截断到“年-月”,正好对应前端的横坐标。第二个查询把两张表的关联利用起来,审核时间减申请时间的差值就是审批耗时,单位用 HOUR,简单直接。如果在统计维度上还希望按班级或辅导员分组,把 GROUP BY 的列换成对应 ID 即可,SQL 不需要大改。
这里要提一个容易被忽略的细节:统计接口如果每次都实时跑全表,数据量大时会拖垮主库。对毕设系统来说,数据量通常只有几万条,全表没问题;但如果演示时希望页面更快,一个简单的做法是加 Redis 缓存,key 设为统计维度加日期,等有新的审批动作时再删掉缓存。在毕设答辩里能主动讲出“什么时候适合缓存、什么时候不适合”,比单纯背缓存概念要加分得多。
5. 答辩前值得多花时间的三个加固点
5.1 审核日志:AOP 切面比在业务代码里写日志更干净
表结构里已经有 leave_audit_record,那是一条业务流水。但答辩时,老师可能会问“系统里怎么记录谁在什么时候修改了什么”。更完整的做法是用注解加 AOP,给需要记录操作的方法打上@OperationLog,切面里统一记录操作人、操作类型、请求参数、接口耗时。日志表和业务表分开存放,设计上更清晰,代码逻辑也解耦了。
Spring Boot 集成的日志体系默认就很好用,但 log.info 打出来的日志是给开发看排错的,审计日志是给业务看的,两者不要混在一个文件里。用 logback-spring.xml 把审计日志单独输出到独立文件,归档策略保留 30 天,这个细节在运维场景里很实用。
5.2 安全细节:越权、敏感信息与接口幂等
学生请假管理系统最容易出现的安全问题不是 SQL 注入,而是越权和敏感信息泄露的两个层面。
越权方面,上一章提到要用登录态里的 userId 而不是前端传的 ID;更多一层实践是:查询详情接口也要校验资源归属。“辅导员有没有权限审批这个学生的假条”这种判断不能用角色掩过去,而要从 session 的角色信息之外去查数据关系,这在教学系统里就是“只能审批自己管辖学生的假条”。如果只判断角色是 teacher,两个人都是辅导员,就也能互相审批对方学生的假条,这属于典型的横向越权。
敏感信息方面,第一个要熟悉的是 Spring Boot Actuator 如果未做访问控制,会把 heapdump 接口暴露出来,任何人下载堆转储文件后,用分析工具就能解出正在运行的进程里存着的数据库口令、token。调试时放开这些端口没问题,答辩前要么关闭 endpoints 的 web 暴露,要么给监控端点加上独立的端口和访问认证。另外一个容易被点破的是密码:用户表里的密码必须 BCrypt 加密,注册时对密码做一次 encode,登录校验用 matches,绝不能在库里存明文或只做 MD5。
接口幂等方面,审批接口天然具备“重复提交”风险,前端连续点两次按钮,后端如果只做 status 判断,会重复跑两次业务。结合 4.2 节的状态条件更新,第二次提交会因为条件不匹配而返回失败提示。如果再想细一层,可以用请假单 ID 加审批人 ID 加上一个请求唯一键,在数据库层面做一次唯一约束,彻底保证重复请求不会产生第二条流水记录。对毕业设计来说,条件更新这层的保护已经足够演示和讲解。
5.3 打包与启动参数设置:让演示机器不卡顿
演示时被“服务起不来”“页面加载慢”拖垮是最亏的。打包时用mvn clean package -DskipTests,得到的 jar 包用下面这行启动参数直接跑,适合只有 2G 内存的演示机器:
java -Xms256m -Xmx512m -XX:+UseG1GC -jar leave-system.jar-Xms 和 -Xmx 设置的是 JVM 堆初始大小和最大值,把最大值限定在 512m,可以避免演示机器因为堆内存膨胀触发 swap,页面反倒更稳定。G1GC 是现代 JDK 的默认垃圾回收器,显式写出来是为了让答辩时能说出参数含义。如果你的管理系统里配了 Redis,记得在启动前先确认 Redis 服务活着、密码和 yml 一致,演示现场最常翻车的就是外部依赖连不上。
启动后不要只盯着“能点能跳”就够了。我一般在演示前会打开http://localhost:8080/actuator/health,确认状态为 UP,然后跑一遍“请假申请→审批通过→查看统计”的完整链路,顺手清空一次浏览器缓存。这个流程跑通,答辩演示环节基本上就不会出大问题。在此基础上,如果想在面试里多讲两句,可以把这一整套项目启动的顺序、外部依赖、以及每个核心接口的调用时序画成一张调用链备忘,到时候不用背稿也能把项目的边界讲清楚。
本文还有配套的精品资源,点击获取