
作为一名常年混迹在毕业生项目群和课程设计评审现场的老开发我可以很负责任地告诉你高校在线请假与审批系统是计算机专业毕设和课设里最经典的题目之一。它看起来就是一个学生提交假条、老师点一下同意的简单CRUD但真拿去答辩被导师问得哑口无言的案例我见得太多了。原因很简单——这题涉及的不只是增删改查而是角色权限模型、流程状态机、时间冲突检测、消息通知机制这些实战系统才有的核心问题。你把这四件事理清楚系统就是优秀毕设级别的理不清楚就是库存管理系统换皮。这篇文章我就以这套系统的完整开发路径为主线把从需求拆解、技术选型、数据库设计到核心流程实现的所有关键细节一次讲透包括那些你在课本和视频教程里学不到的坑。1. 为什么这套系统年年被选为毕设需求背后有三层逻辑很多人上来就打开IDE写代码这是大忌。先搞清楚一件事导师选这个题目想考察你什么作为过来人我可以直接告诉你答案——三层递进的能力。第一层是基础CRUD能力。学生能注册登录、能提交请假申请、能查看自己的请假记录辅导员能看到待审批列表、能通过或驳回管理员能管理用户和课程。这是底线任何一个毕设系统都跑不掉。第二层是业务规则建模能力。请假不是填个表提交就完了它有一套隐含规则。比如请假时间不能和已有课程冲突请假超过三天需要院系领导二级审批学生只能撤回待审批状态下的申请审批中的不能撤回辅导员不能审批自己的请假。这些规则看着小每一处都是导师喜欢追问的业务复杂度来源。第三层是工程化思维。你有没有做登录拦截有没有做角色权限隔离有没有处理并发场景同一个学生同时提交两个重叠的请假单数据库有没有设计合理的主外键和索引这些才是区分课设和毕设的关键分水岭。所以你在网上看到那些卖成套源码的商家页面写得花团锦簇但实际代码打开一看——所有业务逻辑全堆在Servlet里、状态字段用字符串硬拼、连个事务注解都没有这种代码拿过去答辩就是送人头。这套系统的真正价值不在能不能跑而在能不能讲清设计理由。2. 技术栈选型别追新框架稳才是毕设的第一原则我见过不少学生上来就想用Spring Cloud Alibaba微服务、Redis缓存、消息队列说是要体现技术含量。我只能说这属于自己给自己挖坑。毕设答辩的核心是自圆其说不是技术炫技。以目前的主流环境和大多数高校导师的认可度来说我推荐的组合非常明确后端Spring Boot 2.7.x MyBatis-Plus前端Vue 2 Element UI或Vue 3 Element Plus数据库MySQL 8.0权限框架Spring Security JWT或Shiro二选一即可接口文档SwaggerSpringDoc为什么这么选我给你拆开讲。Spring Boot是目前Java后端的绝对主流导师自己就在用看到你用它写毕设会天然觉得靠谱。MyBatis-Plus是MyBatis的增强版单表CRUD连SQL都不用写直接继承BaseMapper就行能省下大量时间去做业务逻辑而不是浪费在写重复SQL上。前端用Vue是因为现在高校的Web课程基本都讲Vue你答辩时被问到为什么用虚拟DOM组件之间怎么通信你都能答上来换成React反而容易被问住。数据库用MySQL没什么好说的免费、主流、资料多。关于权限框架Spring Security确实功能强但学习曲线陡峭Shiro轻量简单API直观学生自己折腾个把星期就能掌握。我的建议是如果你对Spring Security比较熟就用它否则用Shiro两个都算标准答案。这里我要特别强调一个选型思路一切为答辩可解释性服务。导师问你为什么选这个技术你要能说出因为Spring Boot的自动装配让我能快速集成多个组件因为MyBatis-Plus的LambdaQueryWrapper可以避免SQL注入风险这种有深度的理由而不是大家都用这个所以我也用。这三个理由在答辩现场分量完全不同。3. 数据库设计才是成败关键状态、索引与冗余字段的取舍请假审批系统听上去功能不多但要把业务跑顺至少需要设计六张核心表。这套表结构我建议你直接在项目里用它经过了我多次评审验证结构清晰、扩展性也好。用户表sys_user和角色表sys_role就不细说了标准的RBAC模型。学生、辅导员通常是班主任或辅导员本人、院系管理员这三类角色必不可少。注意用户表里要有学号/工号字段这是登录账号也是唯一性约束的天然载体。核心是请假申请表leave_request我列出关键字段设计字段名类型说明idbigint主键user_idbigint学生ID外键关联sys_userleave_typetinyint请假类型1病假、2事假、3公假、4其他start_timedatetime请假开始时间end_timedatetime请假结束时间reasonvarchar(500)请假事由statustinyint审批状态0待辅导员审批、1辅导员已通过待院系审批、2已通过、3已驳回、4已撤回current_approver_idbigint当前待审批人ID冗余字段reject_reasonvarchar(255)驳回原因驳回时必填created_atdatetime提交时间updated_atdatetime最后更新时间这个表的精髓在status字段和current_approver_id字段。很多初学者习惯把状态存成字符串待审批已通过但我在评审时最反感这种设计因为你没法对字符串写范围判断也没法快速分组统计。用tinyint存整数配合代码里的枚举类查询高效逻辑也干净。current_approver_id这个字段属于典型的冗余设计。按理说当前审批人可以通过审批记录表去查但那样每次查询都要子查询而直接冗余在请假单上一个索引就搞定了。代价是插入和更新时要额外维护这个字段但在这套场景里完全值得。审批记录表approval_record同样重要它记录了每一次审批动作。字段包括record_id、request_id外键、approver_id、approval_action1通过、2驳回、approval_comment、approval_time。为什么要单独一张表因为谁能审批是一个动态过程你需要在答辩时说清楚这条请假单经历了哪些人、按什么顺序审批的这就是审计日志的价值。另外两张表是课程表course和请假课程关联表leave_course_relation用来支撑时间冲突检测。课程表字段包括课程名、任课老师、上课时间星期几、第几节、上课周次、上课地点。关联表则记录某条请假单占用了哪些课程。这个设计是为了回答这个学生请假期间有没有课这个问题不建关联表冲突检测就得写一堆复杂的JSON逻辑。最后索引设计给出我的标准答案在leave_request.status上建普通索引因为列表页最频繁的查询就是按状态筛选待审批列表在leave_request.user_id上建普通索引学生的我的申请列表靠它在leave_request(start_time, end_time)上建联合索引——别这么干。这条查询的量级根本到不了需要联合索引优化的程度联合索引反而会增加写入开销属于过度优化。给学生列表页加上ORDER BY created_at DESC的分页查询就行。每当快答辩的时候我都要念叨一遍数据表设计千万不要过度设计但是状态机和审计这两个东西无论如何都不能省。前者体现你的业务建模能力后者体现你的工程素养。4. 审批流转的核心实现状态机、时间冲突检测与权限控制表结构定好之后最难啃的骨头就是审批流程的状态流转。我看了太多学生的代码最典型的问题是——在Controller里写一堆if else每个方法里都对status做判断结果状态越改越乱最后自己也分不清驳回后能不能重新提交。正确做法是用一个Java枚举类把状态机和可允许的迁移封装在一起所有状态变更都必须走这个枚举的校验方法。我在项目里是这样实现的public enum LeaveStatus { PENDING_FALCUTY(0, 待辅导员审批) { Override public boolean canTransitTo(int target) { return target PENDING_DEAN || target REJECTED || target CANCELLED; } }, PENDING_DEAN(1, 辅导员已通过待院系审批) { Override public boolean canTransitTo(int target) { return target APPROVED || target REJECTED; } }, APPROVED(2, 已通过) { Override public boolean canTransitTo(int target) { return false; } }, REJECTED(3, 已驳回) { Override public boolean canTransitTo(int target) { return target PENDING_FALCUTY; } }, CANCELLED(4, 已撤回) { Override public boolean canTransitTo(int target) { return false; } }; public abstract boolean canTransitTo(int target); }核心逻辑在Service层public void approve(ApprovalDTO dto) { LeaveRequest request getById(dto.getRequestId()); // 关键用状态机判断而不是自己拿着状态值到处对比 if (!request.getStatus().canTransitTo(dto.getAction().getTargetStatus())) { throw new BusinessException(当前状态不允许该操作: request.getStatus().getDesc()); } // 检查审批人是否为当前待审批人 if (!request.getCurrentApproverId().equals(dto.getApproverId())) { throw new BusinessException(您不是当前待审批人); } // 更新状态 LeaveStatus next LeaveStatus.of(dto.getAction().getTargetStatus()); request.setStatus(next); // 二级审批如果辅导员通过更新当前审批人为院系管理员按学院匹配 if (next PENDING_DEAN) { request.setCurrentApproverId(deptManagerMapper.selectByDept(request.getDeptId())); } else if (next APPROVED) { request.setCurrentApproverId(null); // 流程结束 } // 写入审批记录审计日志 approvalRecordMapper.insert(...); updateById(request); }这样设计的直接好处是哪天需求变化——比如超过三天的请假需要院长审批三天内辅导员直接通过——你只需要在枚举里加一个新的状态常量再去canTransitTo里定义迁移规则不用动任何Controller和Service的原逻辑。答辩现场你把这个设计讲出来评委基本不会在这个环节为难你。接下来是时间冲突检测。学生提交请假单时系统要检查请假的起止时间段内该学生是否有课。很多学生觉得这是小事写一个SQL查课程表就行。但实际上这里有个坑——你不可能用一条SQL就能精准匹配所有情况必须分两步第一步查出该学生在请假时间范围内的所有课程第二步在Java里做精确的时间段重叠判断。SQL部分用MyBatis-Plus的LambdaQueryWrapper写ListCourse conflictCourses courseMapper.selectList( new LambdaQueryWrapperCourse() .eq(Course::getStudentId, userId) .le(Course::getStartTime, leaveReq.getEndTime()) // 课程开始时间 请假结束时间 .ge(Course::getEndTime, leaveReq.getStartTime()) // 课程结束时间 请假开始时间 ); if (!conflictCourses.isEmpty()) { throw new BusinessException(请假时间与课程冲突请调整时间或说明原因); }这两条条件一组合正好覆盖了时间段重叠的四种情况请假包含课程、课程包含请假、请假开始时间落入课程期间、请假结束时间落入课程期间。你不需要做四种判断两条SQL条件全部搞定。再补充一个细节请假申请的前端表单里时间选择器应该按课程粒度做限制。比如最简单的方式是让用户先选请假哪几节课再自动生成时间区间这样从源头就避免了大部分冲突。冲突检测是做兜底不是做主要手段。再说权限控制。我用的是最直接的方案——拦截器加自定义注解。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }在Controller方法上标注PostMapping(/approve) RequireRole({FALCULTY, DEAN}) public Result approve(RequestBody ApprovalDTO dto) { ... } PostMapping(/submit) RequireRole({STUDENT}) public Result submit(RequestBody LeaveDTO dto) { ... }拦截器里的逻辑就是解析JWT、获取用户角色、比对注解要求、不通过直接抛出401。这样一个角色方法级权限模型就搭起来了代码也很干净。如果你用Shiro也类似核心思路一样。关键是你要能在答辩时解释清楚RBAC模型是怎样的以及为什么用注解加拦截器而不是在Service层手写判断——因为Service层判断会散落在每个方法里重复代码多而且拦截器统一处理更符合Spring MVC的设计哲学。5. 答辩前必须自检的五个深水坑这套系统你按上面的路子做完能跑、能演示但离从容答辩还有一段距离。我根据自己的评审经验列出五个导师最爱问的深水坑你提前把答案准备好比什么都管用。第一个坑为什么状态字段用int不用字符串这个问题几乎必问。你的回答要抓住三点一是整数占用空间小、查询快用tinyint(1)就够了二是方便扩展加个新的审批层级状态只是加个数字不用改表结构三是配合枚举类在代码层面可读性并不比字符串差反而更安全——字符串随手写一个待審批错别字排查半天都查不出来整数类型在编译期就能帮你拦截错误。第二个坑如何防止两个审批人同时对同一张单子审批这就是并发问题。如果你没有做任何处理两个人同时点通过最后更新的状态可能互相覆盖。解决方案是用乐观锁在leave_request表加一个version字段更新时带上where条件AND version 旧版本号更新成功则版本号加一失败说明有人抢先改了直接提示该单据已被处理请刷新页面。这样不需要加数据库悲观锁性能好也能答清楚原理。第三个坑请假时长跨天怎么算如果学生请的是3月1日晚上到3月3日早上你直接用end_time - start_time算出来是两个夜晚加一个白天明显不对。正确做法是按自然日拆分明细——算出总天数再算出请假期间的课程节次。更稳妥的方案是在提交时间时限制粒度要么按半天选要么按节次选。你只要在任何时候知道请假的粒度是半天还是节次跨天计算就只是简单的日期循环。第四个坑如何处理超时未审批很多学生根本没考虑这种情况导师一问就愣住。这里有个很简单的小功能可以加定时任务每天扫描一次把超过48小时未处理的待审批申请自动提醒一次站内信或者邮件。用Spring Boot自带的Scheduled注解就能实现给审批人增加压力业务上完全合理。这个功能加完之后你的系统就比90%的同题竞品多了一个运维思维答辩加分项。第五个坑学生自己能不能审批自己的请假单不能逻辑上必须拦截。在审批方法里除了校验当前待审批人还要校验审批人不能是申请人自己。虽然按角色模型来说学生没有审批权限但现实中如果一个人既是学生又是学生助理两种角色就存在自审的可能。提前在业务层做一道校验顺手写一句注释在代码里导师翻代码的时候会留下这个学生考虑问题很全面的印象。我帮你把这五条整理成一个自检表照着过一遍心里就有底了检查项合格标准不合格的典型表现状态字段设计tinyint 枚举类封装状态机字符串状态Controller里到处if并发控制有乐观锁或悲观锁机制直接updateById不校验版本角色权限注解 拦截器统一鉴权Service里手写角色判断代码重复时间冲突检测SQL重叠查询 前端粒度限制只在提交时弹个提示不真正校验审批超时处理有定时任务或提醒机制完全没有6. 我踩过的坑和给你的最后建议最后聊点掏心窝的话。这套系统我从帮人改代码到做评审前前后后接触过不下二十个版本最让我觉得遗憾的不是技术做得烂而是技术明明做得不错却因为一些细节问题被压分。第一个细节是数据初始化。你交给老师的项目里至少要预置两个学生账号、一个辅导员账号、一个管理员账号并且每种角色下都有一条处于不同状态的请假单。这样老师打开系统第一眼就能看到列表数据、待办任务和流程痕迹而不是面对一个空数据库自己注册账号。演示流畅度直接影响第一印象。第二个细节是事务处理。审批这个动作同时要更新请假单状态、插入审批记录、可能要更新课程关联表这三个操作必须在同一个事务里任何一个失败都要整体回滚。在Service层方法上加上Transactional(rollbackFor Exception.class)不是难事但很多人就是漏了。漏了的结果是——数据库里出现状态改了但记录没写的脏数据这在答辩现场极其尴尬。第三个细节是前端展示的细节。审批列表页的每一条记录最好把请假类型、起止时间、当前状态、当前审批人这四列做成醒目的标签或徽章样式。评委不关心你的CSS写得多么花哨但一定要一眼能看出系统的业务维度。我见过一个学生做的系统功能全都有但列表信息太密集导师找了十秒钟都没找到待审批在哪一行体验分直接崩了。最后如果你要把这套东西打包成毕业设计资源论文PPT源码论文的结构一定要和代码结构对应第三章数据库设计要画出ER图并解释每张表的用途第四章系统实现要按学生端-审批端-管理端的模块线展开关键业务逻辑用代码片段配合流程图说明。PPT则控制在15页以内一页讲一个问题千万不要复制大段文字上去。这套系统做完之后你会发现自己实际上已经把从0到1开发一个Web系统的完整链路走了一遍——需求分析、数据库建模、后端API设计、前端页面交互、权限控制、流程设计、测试调优。这比单纯背知识点强太多了。如果真的卡在某个环节比如权限那块绕不明白或者状态机理解不了可以回头再把我这篇里写的代码思路多看几遍照着敲一版再改成你自己的。不要在一开始就追求完美代码先让流程跑通再逐步优化这才是做毕设该有的节奏。