简介这是一份面向学校环境开发的校园请假管理信息系统源码资源适合管理信息系统学习者、开发者及教务管理者参考。系统覆盖学生请假申请、教师审批、多级审批流程、数据统计与报表等核心模块涉及用户界面、数据库、业务规则、安全隐私等典型信息系统设计环节能够帮助理解教育管理场景下的自动化流程。压缩包共136个文件以C#源代码39个cs、资源文件resources/resx、界面图片jpg/png、可执行程序exe/dll及配置文件config/xsd为主整体大小仅3.26MB目录结构清晰便于直接查看或二次开发。包内还包含解决方案文件sln、数据库结构脚本、帮助文档等可支撑需求分析、系统设计、编码实现到测试部署的完整学习链路虽然系统功能未必完全完善但作为实践案例仍具有参考价值。目前已有1971人学习下载适合希望快速上手管理信息系统项目开发并了解请假审批业务逻辑的读者。1. 校园请假管理信息系统整套线上审批流程到底怎么搭不少高校的请假流程还停留在纸质审批单学生先找辅导员签字再跑到系办盖章碰上辅导员出差或临时调课审批进度就成了黑匣子单子卡在哪一环没人说得清。校园请假管理信息系统要解决的就是把这条线整体搬到线上学生在线提交请假单系统按预设规则自动流转到对应节点辅导员处理待办系办可以批量导出备查每一步都留审批记录。做这类管理信息系统的开发者、拿课程设计练手的学生以及想改造旧流程的信息中心人员都能从这套实现里拿到可直接复用的领域模型和关键代码。2. 领域建模先行三种角色、四层状态机与核心表设计2.1 角色权限矩阵先把谁能干什么钉死我接手这类系统时习惯先画角色权限矩阵而不是直接写接口。因为请假流程表面上只有“提交”和“审批”两步实际牵扯到学生、辅导员、系办管理员三个身份每个身份的操作边界如果不提前定义清楚后面写接口时很容易把权限判断写散。角色核心操作数据可见范围学生提交请假、撤销未审批的单、查看审批进度、销假本人名下所有记录辅导员查看待办、审批通过或驳回、退回修改本班或本院学生记录系办管理员按条件查询、导出、统计、归档全院记录这个矩阵至少要带来两个约束。第一个约束是操作边界例如学生不能撤回已经被辅导员审批过的单据管理员不能代学生提交请假。第二个约束是数据隔离学生调接口查请假列表时SQL 里必须无条件带上 student_id 过滤条件否则会出现串数据的严重问题。有一个常见的翻车场景是有的开发者把辅导员和学生共用同一张用户表却没加 role 字段索引导致权限判断时全表扫描前端响应直接超时这个坑放到第 5 章细说。2.2 请假单状态机四个状态的流转规则状态机是整个系统最值得提前设计的地方。请假单不是简单的“提交→审批通过”线性流程它有草稿、待审批、已通过、已驳回、已销假、已归档等多个状态。这里我把核心状态收敛成四个待审批提交成功后进入待审批队列辅导员在待办列表里能看到已通过所有审批节点都通过学生可凭此销假已驳回审批人不通过单据关闭学生可以重新编辑后再次提交已销假学生到校后主动确认销假流程闭环状态流转的规则要特别注意“驳回后重新提交”。我一般会让驳回的单据保留原有的申请内容学生编辑后重新提交时直接生成一条新的待审批记录而不是在原记录上修改状态。因为原记录要留痕如果覆盖原来的审批意见事后追溯时就会看不清历史。还要注意状态机守卫审批接口里必须校验当前状态是否等于待审批否则一个已经关闭的单据被重复审批数据就会错乱。2.3 核心表结构与字段设计请假系统的表不需要设计得太花哨七张核心表足够用户表、请假单表、审批记录表、通知表、课程表、附件表、操作日志表。下面给出请假单表和审批记录表的建表 SQL这是整个系统的地基。-- 请假单表一条记录对应一次完整的请假申请 CREATE TABLE leave_request ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 请假单ID, student_id BIGINT NOT NULL COMMENT 学生ID关联用户表, type TINYINT NOT NULL DEFAULT 1 COMMENT 请假类型1-事假 2-病假 3-其他, start_time DATETIME NOT NULL COMMENT 请假开始时间, end_time DATETIME NOT NULL COMMENT 请假结束时间, days DECIMAL(5,1) NOT NULL COMMENT 请假天数按小时可拆半天, reason VARCHAR(500) NOT NULL COMMENT 请假原因, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-待审批 2-已通过 3-已驳回 4-已销假, current_level TINYINT NOT NULL DEFAULT 1 COMMENT 当前审批节点级别, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, created_at DATETIME NOT NULL COMMENT 创建时间, updated_at DATETIME NOT NULL COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT请假单表;-- 审批记录表每次审批操作写入一行不能覆盖更新 CREATE TABLE approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 审批记录ID, request_id BIGINT NOT NULL COMMENT 请假单ID, approver_id BIGINT NOT NULL COMMENT 审批人ID, level TINYINT NOT NULL COMMENT 当前是第几级审批, action TINYINT NOT NULL COMMENT 动作1-通过 2-驳回 3-退回修改, comment VARCHAR(500) DEFAULT NULL COMMENT 审批意见, acted_at DATETIME NOT NULL COMMENT 审批时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批记录表;这里有两个字段值得强调。第一个是 version 字段我强烈建议加上因为请假审批有并发场景两个审批人同时操作同一个单据时没有乐观锁就会出现后写覆盖先写的问题。第二个是 level 字段它支撑多级审批流程比如班级辅导员先审系办再确认每级审批人根据 level 找到自己的待办流程扩展时不需要改表结构只需要调整配置。审批记录表只做插入不做更新这是留痕设计的基本原则。有的开发者图省事把审批意见直接更新在请假单表上结果看历史记录时只能看到最后一条出了问题连回溯的依据都没有。这个设计直接决定了后面审计功能的可用性。3. 把流程跑起来Spring Boot Vue MySQL 的关键代码拆解3.1 后端三层结构怎么切后端建设我按 Controller、Service、Mapper 三层拆分这是一个在中小型管理系统里足够清晰、又不会过度设计的结构。Controller 只做参数接收和结果封装不写业务逻辑Service 层集中处理业务规则比如状态校验、天数计算、事务控制Mapper 层对应 SQL 操作一个实体对应一个 Mapper。有一个关键点事务控制必须放在 Service 层的方法上而不是Controller 层。比如提交请假单这个方法里包含了“校验时间重叠”“计算请假天数”“插入请假单”“写入通知”四步任何一步失败都不应该留下脏数据所以方法上标记 Transactional。如果把事务注解加到 Controller 方法上一旦有接口直接调用多个 Service 方法事务边界就不好控制。下面是请假提交接口的核心实现。3.2 请假提交与审批接口实现提交请假单的接口是整个系统使用频率最高的入口代码里我加了重叠校验、天数计算和通知写入。// LeaveController.java - 提交请假单 PostMapping(/api/leave/apply) Transactional public ResultLong apply(RequestBody LeaveApplyDTO dto) { // 1. 校验当前学生在该时间段内没有重叠的请假记录 int overlap leaveMapper.countOverlap(dto.getStudentId(), dto.getStartTime(), dto.getEndTime()); if (overlap 0) { return Result.fail(当前时间范围内已有请假记录); } // 2. 计算请假天数剔除周末和节假日的逻辑统一封装 int days dateUtil.calcWorkDays(dto.getStartTime(), dto.getEndTime()); if (days 0) { return Result.fail(请假天数必须大于0); } // 3. 创建请假单初始状态为待审批审批级别设为1 LeaveRequest req new LeaveRequest(); req.setStudentId(dto.getStudentId()); req.setStartTime(dto.getStartTime()); req.setEndTime(dto.getEndTime()); req.setDays(days); req.setReason(dto.getReason()); req.setStatus(LeaveStatus.PENDING_APPROVAL); req.setCurrentLevel(1); leaveMapper.insert(req); // 4. 写入站内通知提醒第一级审批人处理 notifyMapper.insert(buildNotice(req.getId(), req.getCurrentLevel())); return Result.success(req.getId()); }这段代码的核心逻辑在第 2 步。请假天数不是简单的 end 减 start我见过很多系统按自然日计算结果学生请周五到下周一显示 4 天实际只占了 1 个工作日统计报表完全失真。calcWorkDays 方法内部会读取一个节假日配置表把法定节假日和调休日期都考虑进去天数计算才能对上辅导员的直觉。参数 dto 里的 startTime 和 endTime 前端传入的是 yyyy-MM-dd HH:mm 格式字符串后端解析成 LocalDateTime避免用 java.util.Date 带来的时区和格式化问题。接下来是审批接口这一步最容易出错状态机守卫和行锁都要放在这里。// LeaveApprovalService.java - 审批请假单 PostMapping(/api/leave/approve) Transactional public ResultVoid approve(RequestBody ApproveDTO dto) { // 1. 使用悲观锁锁定单据防止同一单据被不同审批人同时处理 LeaveRequest req leaveMapper.selectByIdForUpdate(dto.getRequestId()); if (req null) { return Result.fail(请假单不存在); } // 2. 状态机守卫只有待审批状态才能继续流转 if (req.getStatus() ! LeaveStatus.PENDING_APPROVAL) { return Result.fail(当前状态不允许审批操作); } // 3. 写入审批记录不管结果如何都必须留痕 approvalMapper.insert(buildRecord(dto)); if (dto.getApproved()) { // 4. 判断是否还有下一级审批人 if (req.getCurrentLevel() MAX_APPROVAL_LEVEL) { req.setStatus(LeaveStatus.APPROVED); } else { // 流转到下一级并通知对应审批人 req.setCurrentLevel(req.getCurrentLevel() 1); notifyMapper.insert(buildNotice(req.getId(), req.getCurrentLevel())); } } else { // 5. 驳回后单据关闭学生可重新发起 req.setStatus(LeaveStatus.REJECTED); } leaveMapper.updateById(req); return Result.success(); }selectByIdForUpdate 这里用的是悲观锁对应 SQL 是 SELECT ... FOR UPDATE。为什么这里不用乐观锁因为审批是一个短事务操作锁的持有时间只有几十毫秒悲观锁不会产生明显的性能压力但能彻底避免两个审批人同时读取到待审批状态、各自更新导致状态错乱的情况。如果你担心数据库并发量很高其实第 5 章还有一套乐观锁的替代方案两套方案可以按业务量选。3.3 前端页面与组件拆分前端我用 Vue 组件化拆分页面级别组件是请假表单、待办列表、审批弹窗、记录查询通用组件是时间选择器和附件上传。这里把提交请假的业务逻辑放到表单组件里没有散落在页面里方便复用。// components/LeaveForm.vue - 提交请假单核心方法 async handleSubmit() { // 前端校验结束时间必须晚于开始时间 if (new Date(this.form.endTime) new Date(this.form.startTime)) { this.$message.error(结束时间必须晚于开始时间); return; } const payload { studentId: this.user.id, startTime: this.form.startTime, // 格式 yyyy-MM-dd HH:mm endTime: this.form.endTime, reason: this.form.reason.trim() }; const res await request.post(/api/leave/apply, payload); if (res.code 0) { this.$message.success(提交成功等待辅导员审批); this.$emit(refreshList); // 提交成功后刷新我的请假列表 } else { this.$message.error(res.msg); // 后端返回的错误信息直接展示给用户 } }前端校验和后端校验的关系要摆正。前端校验是为了提升用户体验比如结束时间早于开始时间这种低级错误没必要发给后端绕一圈。但真正可靠的校验必须放在后端像请假重叠检测、状态机守卫这类规则前端全部要做也做不彻底因为前端代码可以被跳过。所以提交接口的前端只做格式校验业务校验全部交给后端。待办列表组件需要注意轮询策略。有的开发者用 setInterval 每 2 秒请求一次待办接口这个做法在新消息到达时体验好但会给服务器带来不必要的压力。我习惯的做法是提交审批或接收通知后手动刷新一次列表再加一个 30 秒的轻量轮询兜底平时不会频繁打接口又能保证新单子不会被晾太久。4. 超出基础流程消息通知、销假闭环与 Excel 导出的做法4.1 站内信与模板消息推送的落地方式请假流程里最容易被忽略的是通知模块。学生想知道审批进度辅导员怕漏看新申请如果系统没有通知机制用户就得手动刷新列表体验和纸质流程没有什么区别。我见过不少管理系统把通知做成一个简单的站内信表但仔细一看只有读、未读两个状态没有已读时间、没有跳转参数用户根本不知道这条通知对应哪张请假单。站内信表的字段除了基本的通知内容和已读状态还要有一个 biz_type 和 biz_id。比如 biz_type 等于 LEAVE_APPROVEbiz_id 等于请假单 ID前端收到消息后可以通过这两个字段跳转到详情页。推送方式上分两层站内信直接查表渲染模板消息则通过公众号或办公软件的模板接口推送。模板消息推送建议做成异步的。审批通过后系统先更新请假单状态、写站内信然后发布一条消息到队列由消费者服务去调用模板消息接口。这样做的原因是外部推送服务的稳定性不可控接口超时、限流、网络抖动都会影响主流程。我曾见过一个项目把推送放到主事务里结果推送服务一超时整个审批事务回滚审批人以为没通过学生收到的提示也是矛盾的。正确的做法是推送失败只记录错误日志不影响核心流程并通过定时任务补偿重试。这部分在第 5 章会再展开。4.2 销假功能为什么必须做闭环管理销假是一个看起来简单、实际有点玄学的功能。很多系统做到“审批通过”就结束了没有销假环节结果学生请了五天假却十天没来上课辅导员根本发现不了。销假的意义是让学生到校后主动确认“我回来了”系统才能核对实际离校时间与请假时间是否一致超期未销假的记录要自动提醒辅导员跟进。实现上销假就是学生点击一个按钮系统把请假单状态从已通过改成已销假并记录销假时间。关键逻辑在判断逻辑上如果销假时间晚于 endTime 加一个宽限期系统应该标记为异常销假并推送提醒给辅导员。我在设计这个功能时还加了一层自动检查每天凌晨跑一次定时任务找出所有已通过但超过 endTime 一天仍未销假的记录生成异常列表推送给管理员这样就不会出现学生假期结束两周后才发现没回来上课的情况。4.3 Excel 导出基于 POI 的按条件导出与性能注意点导出功能是管理员的刚需因为系办往往需要按月或按学期给上级提交请假汇总表。导出实现用 Apache POI 比较成熟但要注意大数据量场景下的内存控制。POI 的 XSSFWorkbook 会把整个工作簿加载到内存导出三千条记录没问题超过几万条时内存占用就会让人头疼我推荐用 SXSSFWorkbook 流式写入。// ExportService.java - 按条件导出请假记录 public void exportLeaveRecords(Long deptId, LocalDate start, LocalDate end, OutputStream out) { ListLeaveExportVO list leaveMapper.selectExportList(deptId, start, end); // SXSSFWorkbook 的窗口参数 100 表示内存中只保留最近 100 行其余写入磁盘 try (SXSSFWorkbook wb new SXSSFWorkbook(100)) { Sheet sheet wb.createSheet(请假记录); String[] headers {学生姓名, 请假类型, 开始时间, 结束时间, 天数, 状态, 审批人}; // 表头行 Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行从第 1 行开始写入 for (int i 0; i list.size(); i) { Row row sheet.createRow(i 1); LeaveExportVO vo list.get(i); row.createCell(0).setCellValue(vo.getStudentName()); row.createCell(1).setCellValue(vo.getTypeName()); row.createCell(2).setCellValue(vo.getStartTime().toString()); row.createCell(3).setCellValue(vo.getEndTime().toString()); row.createCell(4).setCellValue(vo.getDays()); row.createCell(5).setCellValue(vo.getStatusName()); row.createCell(6).setCellValue(vo.getApproverName()); } // 列宽自适应放在写完数据之后避免大数据量下反复计算 for (int i 0; i headers.length; i) { sheet.setColumnWidth(i, 20 * 256); } wb.write(out); } }代码里有两个细节值得注意。第一SXSSFWorkbook(100) 的窗口参数不是缓冲区大小而是内存中保留的行数上限超过 100 行时旧行会被刷新到临时文件所以导出几万条记录内存也很稳定。第二setColumnWidth 在写完数据之后再统一设置不要在循环里逐行设置宽度否则每写一行都会触发一次宽度计算数据量稍大性能就明显下降。导出接口应该做成异步任务前端先提示“导出请求已提交”生成完成后提供文件下载链接避免用户盯着页面等响应。5. 避坑与常见问题排查七个容易翻车的点5.1 请假天数计算错误按自然日计算导致统计失真现象学生周五请假到下周一系统显示请假 4 天但辅导员核对实际只用请假 1 个工作日月度统计报表严重失真。原因天数计算直接用了 endTime - startTime 的自然日差值没有剔除周末和节假日。解决引入节假日配置表把法定节假日和调休补班日期都维护进去。计算逻辑是先算自然日天数循环每一天判断是否工作日累加工作日天数。我项目里用的 calcWorkDays 方法会先查配置表加载到本地缓存后做逐日判断性能损耗几乎可以忽略。5.2 并发审批导致状态覆盖两个审批人同时操作同一单据现象同一张请假单被两名辅导员同时打开一个点击通过一个点击驳回数据库里的最终状态取决于哪条 SQL 最后执行另一个人的审批意见丢失。原因审批接口没有加锁两个事务同时读取到待审批状态各自执行更新后执行的覆盖先执行的。解决优先用 SELECT ... FOR UPDATE 悲观锁锁定请假单行缺点是这个锁会阻塞其他所有对该单据的审批操作但对请假这种低频审批场景完全够用。如果业务量确实大也可以改用乐观锁版本号方案更新 SQL 带上 version 条件。5.3 推送服务超时拖垮主流程事务内调外部接口现象审批通过后模板消息推送服务响应超时学生那边没收到通知更严重的是整个审批事务被回滚审批结果也丢了。原因把外部接口调用写在了事务内部外部服务的网络抖动直接影响核心业务。解决主事务只做数据库操作写站内信和更新状态。外部推送改为异步消息或定时任务补偿。我当时的方案是审批成功后把一个推送任务写入消息表由后台线程消费推送失败就在表里打标记下次定时任务自动重试最多重试三次。5.4 日期格式序列化错误时间显示错乱八小时现象前端显示审批时间比实际时间多了 8 小时Excel 导出时间也整体偏移一天。原因后端返回 LocalDateTime 给前端框架序列化时没有指定格式也没有处理时区JavaScript 解析 ISO 时间字符串时按本地时区转换。解决在后端 JSON 序列化配置里统一指定日期格式为 yyyy-MM-dd HH:mm:ss并关闭时间戳自动转时区逻辑。数据库连接串里也要带 serverTimezoneAsia/Shanghai 参数否则 JDBC 驱动和 MySQL 的时区不一致查询出的时间全部偏移。还有一点是数据库字段类型尽量用 DATETIME不用 TIMESTAMPDATETIME 不参与时区转换省去一堆麻烦。5.5 附件上传路径泄露文件的绝对路径直接入库现象上传请假证明后附件表里存了本地磁盘绝对路径例如 /usr/local/upload/xxx.pdf一旦导出让其他系统读取直接暴露服务器目录结构。原因没有把附件访问抽象成虚拟路径数据库表里存了物理路径。解决数据库只存相对路径或文件标识例如 2025/06/xxx.pdf前端通过一个受控接口拼接完整 URL 访问。上传时还要校验文件扩展名白名单限制 pdf、jpg、png然后重命名成 UUID 文件名避免用户上传带中文名的文件导致编码问题。5.6 驳回后学生重新提交原记录审批意见被覆盖现象学生驳回后修改了请假原因再次提交后台一查原审批记录不见了历史审批意见消失。原因重新提交逻辑直接复用了原记录把状态从已驳回改回待审批同时覆盖了 reason 字段。解决重新提交时生成一条全新记录原记录保留已驳回状态不动。原请假单 ID 通过 parent_id 字段关联到新记录这样既能追溯完整历史又能避免审批记录被覆盖。我开发时一直坚持这个“只追加、不修改”的原则后来做全校数据审计时省了很多口舌。5.7 大数据量导出内存溢出表格还没生成服务先崩了现象管理员导出一个学期的全院请假记录后端直接报 OutOfMemoryError服务重启。原因用了 XSSFWorkbook 把全部数据一次性加载进内存一条记录占几 KB几万条记录就撑爆了堆内存。解决切到 SXSSFWorkbook设置合理窗口大小同时控制导出范围的粒度接口强制要求必须选择时间范围避免一次导出全部数据。给运维留一条活路可以在配置文件里限定单次导出的最大行数超过十万行就提示用户分月导出。6. 进阶验证技巧自检 SQL 与并发模拟脚本6.1 用 SQL 核对审批流程完整性系统上线前我会跑一组自检 SQL 来验证数据的完整性和流程的正确性这类检查其实比手动点页面更可靠。下面这条 SQL 用来找出所有已通过但没有审批记录的单据正常情况下结果应该为空如果查出数据就说明业务流程漏掉了写审批记录的环节。-- 找出状态为已通过但没有审批记录的请假单 SELECT r.id, r.student_id, r.status FROM leave_request r LEFT JOIN approval_record a ON a.request_id r.id WHERE r.status 2 AND a.id IS NULL;类似的检查还有找出状态是待审批但当前审批级别对应的审批人不在职的单据以及找出已销假但销假时间早于请假开始时间的异常记录。建立一套这样的检查 SQL每次版本迭代后跑一遍能省掉大量人工回归时间。我习惯把检查 SQL 写在一个 sql 文件里命名叫 audit_check.sql留档给团队成员随时使用。6.2 用脚本模拟并发审批并发问题是审批系统最容易翻车的点光靠代码 review 很难发现问题必须实际并发测试。下面这个 Python 脚本模拟两个审批人同时审批同一张请假单的场景可以验证悲观锁是否生效。import json import threading import urllib.request def send_approve(request_id, approved, token): data json.dumps({ requestId: request_id, approved: approved, token: token }).encode(utf-8) req urllib.request.Request( http://127.0.0.1:8080/api/leave/approve, datadata, headers{Content-Type: application/json} ) try: resp urllib.request.urlopen(req) print(f请求完成状态码: {resp.status}) except urllib.error.HTTPError as e: body e.read().decode(utf-8) print(f请求失败: {e.code} - {body}) # 两个线程并发提交一个通过、一个驳回 request_id 1001 # 替换成实际存在的请假单 ID t1 threading.Thread(targetsend_approve, args(request_id, True, token-a)) t2 threading.Thread(targetsend_approve, args(request_id, False, token-b)) t1.start() t2.start() t1.join() t2.join()跑完脚本后去数据库查 approval_record 表正常情况应该只有一条审批记录另一条请求被锁阻塞或返回了“当前状态不允许审批操作”的错误。如果查出两条审批记录说明锁没有生效必须回去检查事务和 SQL。这类模拟脚本不用写得太复杂也不需要引入压测工具简单粗暴就能验证核心并发逻辑。6.3 数据归档与查询优化系统运行两年后请假单表会积累几十万条历史数据查询性能会明显下降。我的做法是按年做分区或者把超过一年的已归档数据迁移到历史表线上业务表只保留活跃数据。查询接口上要紧盯两个索引leave_request 表的 status current_level 联合索引覆盖待办查询场景approval_record 表的 request_id 索引覆盖审批记录查询场景。待办列表如果还有性能问题就采用分页加时间过滤的策略限制默认只查最近三个月的单据。我从这套请假系统的开发中学到的最实在的一条经验是流程系统的核心不在页面样式多好看而在状态机设计和数据留痕。从那以后我每次接手这类管理系统都会先花时间把状态流转矩阵、权限边界、并发策略和审计检查 SQL 这四件事列清楚再碰代码。希望帮到你。本文还有配套的精品资源点击获取