ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot学生请假管理系统开发:从表结构设计到审批闭环实现

Spring Boot学生请假管理系统开发:从表结构设计到审批闭环实现 简介基于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 请假单表字段设计字段类型说明idbigint主键雪花算法生成user_idbigint学生 ID关联用户表leave_typetinyint1事假2病假3其他start_timedatetime请假开始时间end_timedatetime请假结束时间reasonvarchar(500)请假原因statustinyint0草稿1待审批2已通过3已驳回4已销假versionint乐观锁版本号create_time / update_timedatetime审计字段这里有两个容易被忽略的点。第一不要存“请假天数”字段因为“半天”“跨周末”的计算口径很容易和前端的显示不一致用 start_time 和 end_time 两个时间点需要展示时再用 SQL 或 Java 计算。第二version 字段不是必须的但加上之后审批接口能直接用乐观锁挡住重复操作成本极低后面会写对应的 update 语句。2.2.2 审核记录表设计字段类型说明idbigint主键leave_idbigint请假单 IDauditor_idbigint审核人 IDactiontinyint2通过3驳回commentvarchar(300)审批意见create_timedatetime操作时间action 字段直接存目标状态比存“同意/拒绝”之类的业务描述更利于写 SQL。比如统计驳回率一条GROUP BY action就能算出来。这张表只追加不更新也不需要 version 字段属于典型的流水表。2.3 状态机非法跳转要在 SQL 层拦住状态机看起来是设计文档里的概念但在请假系统里它会直接变成一条 WHERE 条件。常见的流转是这个样子当前状态允许的操作目标状态0 草稿提交1 待审批1 待审批通过 / 驳回2 已通过 / 3 已驳回2 已通过销假4 已销假3 已驳回重新提交1 待审批后端写校验时不能只判断“当前状态是不是待审批”因为那只是 Java 内存里的旧值。并发情况下两个请求同时读到 status1两个审核都会通过。正确做法是把它写进 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个索引分别服务两类高频查询学生端“我的请假列表”走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。先说后端版本搭配这个组合的容错率会直接影响后面排错的时间组件推荐版本范围说明JDK17Spring Boot 3.x 的最低要求是 JDK 17面试也常问Spring Boot3.2.x功能稳定三方库适配度高不要追 3.5 之后的最新版本MyBatis-Plus3.5.x与 Spring Boot 3 兼容旧版 3.4.x 会报类找不到MySQL8.08.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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 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配置里三个最容易出问题的点。第一serverTimezoneAsia/Shanghai必须写否则 MySQL 连接会因为时区偏差报错或者时间字段在前后端差八个小时。第二map-underscore-to-camel-case开启后create_time才能自动映射到createTime省掉大量TableField注解。第三logic-delete-field: deleted引入了逻辑删除用户表、请假单表加一个 deleted 字段就行删除用户时不会真的删行数据不会神秘消失。注意本地演示为了启动方便密码会直接写在 yml 里。真实项目或放到答辩服务器演示时建议用 Jasypt 之类的工具把 yml 里的数据库口令做成密文避免配置文件泄露后顺手把数据库也暴露了。3.4 统一返回体与全局异常处理前后端分离已经是这个题目的默认形态接口返回格式统一前端才能少写判断。我会定义一个 ResultTcode0 表示成功非 0 表示业务失败Data public class ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 0; r.message success; r.data data; return r; } public static T ResultT fail(int code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }配合RestControllerAdvice做全局异常处理把校验异常、业务异常、未知异常分别映射到不同的 codeController 里就不用到处 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 idauditWithStatus 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?userId10002把自己换到别人的 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然后跑一遍“请假申请→审批通过→查看统计”的完整链路顺手清空一次浏览器缓存。这个流程跑通答辩演示环节基本上就不会出大问题。在此基础上如果想在面试里多讲两句可以把这一整套项目启动的顺序、外部依赖、以及每个核心接口的调用时序画成一张调用链备忘到时候不用背稿也能把项目的边界讲清楚。本文还有配套的精品资源点击获取
返回列表