简介:一套面向校园场景的请假管理信息系统源码包,由开发者黄娅于2016年创建,适合管理信息系统方向的学生、开发者以及学校信息化管理者参考学习。压缩包共136个文件、约3.26MB,涵盖C#源代码(39个cs)、窗体与资源文件(resources/resx)、界面图片(jpg/png/gif)、可执行与动态库(exe/dll)、数据库架构定义(xsd)以及解决方案/项目配置文件(sln/csproj/config)等,完整呈现了该系统从UI界面到数据访问层的实现结构。系统围绕学生请假申请、教师审批、统计报表等核心功能展开,将复杂的业务规则如请假期限、审批流程和多级通知固化到程序逻辑中,同时通过数据库存储与权限控制保障数据安全。虽描述中提及功能尚未完全完善,但作为实践案例,可帮助读者梳理管理信息系统的需求分析、数据库设计、编码实现与测试部署全流程。已有1971人浏览学习,适合用于课程设计、毕业设计或二次开发时的代码参考与结构借鉴。
1. 校园请假管理信息系统到底在解决什么:一张假条背后的审批链路
校园请假管理信息系统,真正的难点不在“提交请假”那个表单,而在表单提交之后发生的一切。学生在手机端填完假条,只是一条数据的诞生;接下来这条数据要流经辅导员、学院、任课老师,走审批、抄送、销假闭环。这些角色各自能看到什么、能操作什么、操作之后数据状态如何变化,才是这个系统的灵魂。很多人第一次做这个项目,把注意力全放在列表和表单上,最后做出来的东西其实就是个信息登记簿,审批还是靠线下吼一嗓子。
这个系统适合两类人:一是学校信息部门或外包团队,需要把存量的请假流程数字化并接入教务数据;二是想做完整业务闭环练手的开发者,通过它理解状态机、权限、工作流和消息通知。我下面会按一套完整可落地的方案讲清楚:从请假状态机设计、数据库建模,到后端接口、前端对接,再到部署后最容易踩坑的几个问题,最后给两个从“能用”到“好用”的进阶功能。
2. 先把数据模型立住:请假状态机、审批链和三张核心表
2.1 从提交到销假:请假单的六个状态与转换规则
第一步,明确请假业务流程的关键路径。常见做法是:
学生提交请假的完整链路是:草稿 → 待审批 → 审核通过/驳回 → 销假关闭。学生填写假条后可以先保存为 DRAFT(草稿),正式确认后变更为 PENDING(待审批)。审批一般至少两级:辅导员先审核,需要时再转学院或教务部门备案。审批不通过则变为 REJECTED(已驳回),学生可以修改重新提交,也可以放弃。假条获批后,学生返校需要销假,销假完成则状态变为 CLOSED(已销假),整个流程闭环。假条在未审批前,学生可以主动撤销,状态变为 CANCELLED(已撤销)留档备查。
围绕这个流程,请假状态机包含六个状态:
| 状态值 | 含义 | 允许的下一步状态 |
|---|---|---|
| DRAFT | 草稿 / 未提交 | PENDING、CANCELLED |
| PENDING | 待审批(正在审批链上) | APPROVED、REJECTED、CANCELLED |
| APPROVED | 审批通过 | CLOSED |
| REJECTED | 审批驳回 | PENDING(修改后重新提交) |
| CLOSED | 已销假 | 无 |
| CANCELLED | 已撤销 | 无 |
这里有三个设计分歧值得说清楚。第一个是有没有必要保留 DRAFT?很多初次实现者不做草稿箱,学生点提交直接进入 PENDING,返回去改只能靠驳回。如果目标是让学生端体验好一点,DRAFT 是值得保留的。但要注意,保存草稿不产生审批记录,也不占用审批人的待办列表,否则草稿满天飞,审批人待办列表会被无关数据污染。
第二个分歧是销假是否必须作为独立操作。不少简化版系统审批通过后就结束了,学生返校不销假,考勤数据对不上。为了让闭环闭合,我一般会保留销假为独立操作,而且只有 APPROVED 状态才允许销假,销假必须记录实际返校时间。第三个分歧是多级审批是否做成可配置的工作流引擎。学生项目通常用 if-else 写死两级审批就能交差,但实际校园场景里,公假、调课、跨学院上课都会改变审批链。后面第 5 章会专门讲审批链配置化,这里先保留状态机的规则。
设计时要把状态转换逻辑放进代码,而不是写成文档。状态转换拆成三步:权限判断 → 状态比对 → 持久化更新。权限判断在前面拦截“谁能操作”,状态比对保证“当前状态允许这个操作”,持久化更新才真正改动数据。
2.2 三张核心表结构:请假单、审批记录、销假登记
数据库选型上,常见做法是直接用 MySQL 8(如果现有环境是 5.7 也完全可以),表结构以三张表为主线:请假申请表(leave_application)、审批记录表(leave_approval_record)、销假登记表(leave_cancel_record)。
以下是请假申请表的 DDL:
CREATE TABLE `leave_application` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `student_id` VARCHAR(32) NOT NULL COMMENT '学生学号,关联学生基本表', `student_name` VARCHAR(64) NOT NULL COMMENT '学生姓名,冗余字段减少联表', `class_id` VARCHAR(32) NOT NULL COMMENT '班级ID,用于按班级维度统计', `leave_type` TINYINT NOT NULL COMMENT '请假类型:1=病假 2=事假 3=公假 4=其他', `start_time` DATETIME NOT NULL COMMENT '请假开始时间', `end_time` DATETIME NOT NULL COMMENT '请假结束时间', `leave_reason` VARCHAR(500) DEFAULT NULL COMMENT '请假原因描述', `attach_url` VARCHAR(255) DEFAULT NULL COMMENT '附件路径(病假单/证明材料)', `status` VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PENDING/APPROVED/REJECTED/CLOSED/CANCELLED', `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,用于防并发覆盖', `create_by` VARCHAR(32) NOT NULL COMMENT '创建人学号', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_student_start_time` (`student_id`, `start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='请假申请表';说明几个关键选型。student_id 与 student_name 同时存在,是故意做冗余;因为请假单的查询高频场景是“某学生的历史请假记录”和“某班级当天的请假汇总”,冗余姓名可以避免每次 join 学生表。leave_type 用 TINYINT,并落到代码枚举上,不建议直接用字符串“病假”,否则后期扩展请假类型时,数据库和代码两边都要改。status 用 VARCHAR 存状态枚举值,便于直接查看数据时阅读;也有人用 TINYINT 映射枚举,但我更推荐 VARCHAR,因为运维排错时WHERE status='APPROVED'比查数字码表直观得多。
审批记录表的 DDL:
CREATE TABLE `leave_approval_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `leave_id` BIGINT NOT NULL COMMENT '请假单ID,关联 leave_application.id', `approver_id` VARCHAR(32) NOT NULL COMMENT '审批人工号', `approver_name` VARCHAR(64) NOT NULL COMMENT '审批人姓名', `approver_role` VARCHAR(32) NOT NULL COMMENT '审批人角色:COUNSELOR=辅导员,COLLEGE=学院教务,ADMIN=管理员', `action` VARCHAR(16) NOT NULL COMMENT '审批动作:SUBMIT=提交,APPROVE=通过,REJECT=驳回,CANCEL=撤销', `comment` VARCHAR(500) DEFAULT NULL COMMENT '审批意见', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '审批时间', PRIMARY KEY (`id`), KEY `idx_leave_id` (`leave_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='请假审批记录表';这张表本质上是工作流日志表。我见过有人只存最新审批状态、不存过程记录,结果出了问题无法追溯是谁批的、批了多久。审批历史完整落表,是审计排查的基础。action 字段的 SUBMIT 动作也有用处:提交时也写一条审批记录,方便完整还原请假单的生命周期。
销假登记表的 DDL:
CREATE TABLE `leave_cancel_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `leave_id` BIGINT NOT NULL COMMENT '请假单ID', `student_id` VARCHAR(32) NOT NULL COMMENT '学生学号', `cancel_time` DATETIME NOT NULL COMMENT '实际销假时间/返校时间', `cancel_type` TINYINT NOT NULL DEFAULT 1 COMMENT '销假类型:1=按时销假 2=提前销假 3=逾期销假', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注,例如逾期原因', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_leave_id` (`leave_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销假登记表';uk_leave_id 唯一约束,保证一张请假单只能销假登记一次;销假记录与请假单是一对一关系。统计“学生是否按时销假”,直接拿请假单的 end_time 与销假单的 cancel_time 对比即可。三张表完成闭环:请假单管流程,审批记录管链路,销假登记管闭环。有人还想把请假单和课表绑定,加一张课表快照表,这个等第 6 章讲课时冲突检测时再展开。
2.3 请假时长计算:按自然日算还是按课节算
请假时长是数据模型里最容易忽略的细节。学生请假一般表述为“周五下午到周一上午”,但课程是按节次排的,如果只存 start_time 和 end_time,后续判断“是否与上课时间冲突”时就缺了课程维度语义。
所以我会在请假单上额外留两个字段:period_start和period_end,分别表示请假起始课节与结束课节,用 TINYINT 存课节序号。它与 start_time / end_time 配合使用:start_time / end_time 用于日历展示和时长统计,period 字段用于课表冲突检测。这一设计在后面的课时冲突自动检测里会体现价值。
另一个实际约束是请假跨度需要被限定,比如“单次请假最多不超过 14 天”,超长请假必须走线下材料审批。在学校校验环节做这个限制,比在数据库中做限制好,因为跨天、跨月的计算逻辑放在代码里维护更方便。与此相关的法定节假日与调休问题,直接影响时长计算是否正确,我把它归到第 5 章的避坑清单里。
3. 用 Spring Boot 把请假后端跑起来:提交、审批、销假三个核心接口
3.1 项目结构与核心依赖选型
后端选型上,Spring Boot + MyBatis-Plus 是校园项目里最常见的组合。Spring Boot 生态成熟,MyBatis-Plus 在 CRUD、分页、乐观锁插件上能省不少样板代码。如果你是 Python 背景,用 FastAPI + SQLAlchemy 也能实现同样的逻辑,下面的代码重点是逻辑而不是框架语法。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>MyBatis-Plus 3.5.5 这个版本相对稳定,支持逻辑删除和乐观锁插件,后面审批接口会用到乐观锁。引入 Lombok 只是为了减少实体类 getter/setter 样板代码,不引入也完全没问题,看团队习惯。项目结构上,按 controller → service → mapper 分层,这里不赘述,重点是三个接口的写法。
3.2 请假提交接口:状态初始化与时长校验
Controller 层:
@PostMapping("/api/leave/submit") public Result<Long> submitLeave(@RequestBody @Valid LeaveSubmitDTO dto, @RequestAttribute("userId") String userId) { // userId 由登录过滤器注入,真正的身份以服务端为准,不能信任前端传值 LeaveApplication application = leaveService.createAndSubmit(dto, userId); return Result.success(application.getId()); }@RequestAttribute("userId") 是从登录过滤器里取得当前用户身份。不少实现里把 studentId 放在前端传参,这是巨大的越权漏洞,第 5 章会细说。@Valid 触发 DTO 参数校验,比如 start_time 不能为空、请假类型必须在枚举范围内等。
Service 实现:
@Transactional public LeaveApplication createAndSubmit(LeaveSubmitDTO dto, String userId) { LeaveApplication entity = new LeaveApplication(); BeanUtils.copyProperties(dto, entity); entity.setStudentId(userId); entity.setStatus(LeaveStatusEnum.PENDING.getCode()); entity.setVersion(0); // 校验请假开始时间不能晚于结束时间 if (entity.getStartTime().isAfter(entity.getEndTime())) { throw new BizException("请假开始时间不能晚于结束时间"); } // 校验单次请假最大天数不超过 14 天 long days = Duration.between(entity.getStartTime(), entity.getEndTime()).toDays(); if (days > 14) { throw new BizException("单次请假不能超过14天,超长请假请线下提交材料"); } leaveApplicationMapper.insert(entity); // 写入提交动作的审批记录,便于后续追溯完整生命周期 LeaveApprovalRecord record = new LeaveApprovalRecord(); record.setLeaveId(entity.getId()); record.setApproverId(userId); record.setApproverRole("STUDENT"); record.setAction("SUBMIT"); approvalRecordMapper.insert(record); return entity; }这段代码三个关键点。第一,@Transactional 保证主单数据与审批记录写入的一致性;第二,时间校验用 Duration,而不是直接把两个 Date 相减,避免跨月计算错误;第三,entity.setStatus 直接设为 PENDING,这里跳过了 DRAFT 状态,是为了简化“一步提交”流程;如果要做草稿箱,应拆成 saveDraft 和 submit 两个方法,草稿不写审批记录,只有 submit 才写。
3.3 审批接口:状态比对、权限校验、审批留痕缺一不可
审批接口是工作流系统的核心。很多入门项目只做一件事:更新 leave_application 的 status。形式上流程能走通,但权限和留痕都缺失。正确的做法是:
@Transactional public void approve(Long leaveId, String approverId, String comment) { LeaveApplication leave = leaveApplicationMapper.selectById(leaveId); if (leave == null) { throw new BizException("请假单不存在"); } // 1. 状态比对:只有待审批状态可以继续流转 if (!LeaveStatusEnum.PENDING.getCode().equals(leave.getStatus())) { throw new BizException("当前状态不允许审批操作"); } // 2. 权限校验:审批人必须在该请假单的审批链上 if (!approvalChainService.canApprove(leave, approverId)) { throw new BizException("当前用户不在该请假单的审批链上"); } // 3. 更新主单状态(乐观锁版本号更新见 3.4) leave.setStatus(LeaveStatusEnum.APPROVED.getCode()); leave.setUpdateTime(LocalDateTime.now()); leaveApplicationMapper.updateById(leave); // 4. 写审批记录 LeaveApprovalRecord record = new LeaveApprovalRecord(); record.setLeaveId(leaveId); record.setApproverId(approverId); record.setApproverRole("COUNSELOR"); record.setAction("APPROVE"); record.setComment(comment); approvalRecordMapper.insert(record); }把“审批人必须在审批链上”单独隔离到 approvalChainService,核心原因是审批链可能会变(辅导员调岗、学院秘书轮换),解耦后改动时不影响审批主流程。审批记录这一步不可省略,只更新状态不写记录,事后没法排查。这里有一个进阶问题:为什么不能只做“先查询再更新”?因为如果两个审批人同时审批同一张单子,两个线程都读到 status=PENDING,先后执行 update,状态虽然都是 APPROVED,但审批记录会写入两条。乐观锁的做法在 3.4 展开。
3.4 销假接口:返校时间登记与逾期判断
销假接口是闭环的最后一环,容易被做成“把状态改成 CLOSED”就完事,但返校时间和是否逾期都没记录。
@Transactional public void cancelLeave(Long leaveId, String userId) { LeaveApplication leave = leaveApplicationMapper.selectById(leaveId); if (leave == null || !leave.getStudentId().equals(userId)) { throw new BizException("请假单不存在或无权操作"); } if (!LeaveStatusEnum.APPROVED.getCode().equals(leave.getStatus())) { throw new BizException("只有审批通过的假单才能销假"); } LeaveCancelRecord cancel = new LeaveCancelRecord(); cancel.setLeaveId(leaveId); cancel.setStudentId(userId); cancel.setCancelTime(LocalDateTime.now()); // 判断是否逾期销假:实际销假时间晚于请假结束时间 if (cancel.getCancelTime().isAfter(leave.getEndTime())) { cancel.setCancelType(3); // 逾期销假 } else { cancel.setCancelType(1); } leaveCancelRecordMapper.insert(cancel); leave.setStatus(LeaveStatusEnum.CLOSED.getCode()); leaveApplicationMapper.updateById(leave); }销假操作先插入销假记录,再更新主单状态,这个顺序不是随意的。如果先更新主单再插销假记录,第二步失败时主单已经是 CLOSED 但销假记录缺失,后面统计“按时销假率”就会漏数据。虽然事务回滚能保证最终一致,但把关键数据的写入顺序放在前面,是运维上的好习惯。此外,销假操作还得写一条审批记录,action 为 CANCEL,让审批链路也能看到销假这个动作。
4. 前端与移动端对接:Vue 学生端、审批工作台与消息触达
4.1 学生端请假表单:字段校验与最大可请天数控制
前端选型用 Vue 3 + Element Plus 比较普遍,表单部分核心是时间范围选择器与校验规则。
<template> <el-form :model="leaveForm" :rules="rules" ref="formRef" label-width="100px"> <el-form-item label="请假类型" prop="leaveType"> <el-select v-model="leaveForm.leaveType"> <el-option label="病假" value="1" /> <el-option label="事假" value="2" /> <el-option label="公假" value="3" /> <el-option label="其他" value="4" /> </el-select> </el-form-item> <el-form-item label="请假时间" prop="timeRange"> <el-date-picker v-model="leaveForm.timeRange" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间" /> </el-form-item> <el-form-item label="请假事由" prop="reason"> <el-input type="textarea" v-model="leaveForm.reason" maxlength="500" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSubmit">提交审批</el-button> </el-form-item> </el-form> </template>提交逻辑:
const handleSubmit = async () => { const [start, end] = leaveForm.timeRange; const days = Math.ceil((end.getTime() - start.getTime()) / 86400000); if (days > 14) { ElMessage.error('单次请假不能超过14天'); return; } await api.submitLeave({ leaveType: leaveForm.leaveType, startTime: formatDateTime(start), endTime: formatDateTime(end), reason: leaveForm.reason, }); ElMessage.success('已提交,等待审批'); router.push('/leave/list'); };这里的经验是:前端时间格式化统一用 dayjs 或工具函数,不要手动拼字符串,否则不同浏览器时间格式不一致,后端解析时容易翻车。前端校验和后端校验必须一致而不是互相替代:前端管体验,后端管安全,两层都要有。请假天数在前端算一遍,后端还要再算一遍,因为接口可以被绕过去。
4.2 审批人工作台:待办列表与批量审批
审批人工作台按状态过滤是核心功能。后端提供待办查询接口,前端做三个 tab:待处理、已处理、全部。
<el-tabs v-model="activeTab"> <el-tab-pane label="待审批" name="pending"> <el-table :data="pendingList"> <el-table-column prop="studentName" label="学生" width="90" /> <el-table-column prop="leaveTypeName" label="类型" width="80" /> <el-table-column prop="startTime" label="开始时间" width="150" /> <el-table-column prop="endTime" label="结束时间" width="150" /> <el-table-column label="操作" width="160"> <template #default="{ row }"> <el-button type="success" @click="approve(row, true)">通过</el-button> <el-button type="danger" @click="approve(row, false)">驳回</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <el-tab-pane label="已处理" name="handled"> <!-- 已处理列表,仅展示,不允许操作 --> </el-tab-pane> <el-tab-pane label="全部" name="all"> <!-- 无条件分页列表,通常带时间范围过滤 --> </el-tab-pane> </el-tabs>批量审批的接口设计建议是 POST /api/leave/batch-approve,传入 leaveIds 数组和统一意见。批量场景在节假日前后出现高峰,学生集中申请,辅导员要逐个点会崩溃。批量接口内部仍是循环调用单条审批方法,但把异常处置改为“单条失败不阻断其他条目”。实现时要注意事务边界:不要在批量的外层再加一个大事务,否则一条数据异常会导致整个批次回滚,前端也没法知道哪几条成功了。通常的做法是单条审批方法内部带事务,批量方法在循环外捕获异常并汇总结果。
4.3 审批结果通知:模板消息与失败重试
审批通过或驳回后如何通知学生?常见做法有三种:站内信轮询、WebSocket 推送、微信服务号模板消息。校园场景里最可靠的是“微信模板消息 + 站内信兜底”。
微信模板消息涉及 openid 绑定流程,发送部分的伪代码:
public void sendTemplateMessage(String openid, String templateId, Map<String, String> data) { Map<String, Object> body = new HashMap<>(); body.put("touser", openid); body.put("template_id", templateId); body.put("data", buildData(data)); // 调用微信接口发送 String result = restTemplate.postForObject(apiUrl, body, String.class); if (result.contains("\"errcode\":0")) { // 发送成功 } else { // 发送失败,写入消息发送失败表,由定时任务重试 messageFailMapper.insert(failRecord); } }核心注意点:微信模板消息有频率限制,不能作为唯一通知渠道;模板接口偶尔超时,发送失败写失败表比直接抛异常更合适,定时任务每 10 分钟重试一次。站内信的实现思路更简单:审批状态变更时,往 message 表插入一条 UNREAD 记录,学生端登录后拉取未读消息,已读后更新状态。不要让学生端靠轮询请假单状态来判断,那样既浪费接口资源,又会在状态变更与消息展示之间出现时间差。
5. 校园请假系统的五个高频坑:越权、节假日、审批链断裂、销假超时、并发覆盖
5.1 越权操作:学生直接调接口把假单置为已通过
现象:某开发者在联调时用测试工具抓包,发现审批接口没有任何身份校验,把自己的假单编号直接调 approve 接口,状态立刻变成 APPROVED。学生不需要经过辅导员,自己就把假批了。
原因:前端把按钮隐藏了,但接口层没有做校验。审批接口里,approverId 直接从前端传参获取,没有从登录会话中获取。
解决:服务端必须重新鉴权。审批接口从上下文取当前用户身份,再匹配审批链关系;不要信任前端传的 userId / approverId / studentId。
// 错误写法示例:前端可以传任意 approverId @PostMapping("/api/leave/approve") public Result<Void> approve(@RequestBody ApproveDTO dto) { leaveService.approve(dto.getLeaveId(), dto.getApproverId(), dto.getComment()); return Result.success(); } // 正确写法示例:approverId 从会话上下文取 @PostMapping("/api/leave/approve") public Result<Void> approve(@RequestBody ApproveDTO dto, @RequestAttribute("userId") String approverId) { leaveService.approve(dto.getLeaveId(), approverId, dto.getComment()); return Result.success(); }这个坑常被忽略,因为前端体验一切正常;接口被绕过时,界面看不出来,只有数据异常排查时才发现。安全建议是:所有涉及“谁操作了哪条数据”的接口,身份一律从会话上下文来,而不是从请求体里取。
5.2 请假时长按自然日算错:法定节假日和调休怎么处理
现象:学生在国庆假期前请 9 月 30 日到 10 月 8 日的假,10 月 1 日到 7 日是法定节假日,学生在校时间实际只有 10 月 8 日,但系统请假时长显示 9 天,考勤数据完全对不上。
原因:直接用(结束时间 - 开始时间)除以一天计算,没有排除周末、法定节假日和调休上班日。
解决:引入工作日历表,而不是在代码里硬编码节假日。
CREATE TABLE `workday_calendar` ( `date` DATE NOT NULL COMMENT '日期', `day_type` TINYINT NOT NULL COMMENT '1=工作日 2=周末 3=法定节假日 4=调休上班日', `remark` VARCHAR(100) DEFAULT NULL COMMENT '备注,例如国庆调休', PRIMARY KEY (`date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作日历表';请假时长计算只统计 day_type 为 1 和 4 的天数,周末和法定节假日不计入请假时长;调休上班日必须按工作日处理。每年年初教务系统会公布全年日历,运维人员导入这张表。累计请假日数用一条 SQL:
SELECT COUNT(*) FROM workday_calendar WHERE date BETWEEN #{startDate} AND #{endDate} AND day_type IN (1, 4);这个方案比在代码里写死“排除周六周日”可靠,因为节假日调休每年不同,硬编码会积累成技术债。这里最容易被忽略的是“调休上班日”要算作工作日,很多实现只识别了周六周日,却把调休上班日也算作休息,导致请假时长又被算少。
5.3 辅导员调岗后审批链断裂,假单卡死在待审批
现象:某辅导员调走后,他名下学生的新请假单全部停在 PENDING,系统没有任何提醒,直到学生来问才发现,线下审批已经耽误了好几天。
原因:审批人配置写死在单条数据里,比如在 leave_application 表存了 approver_id,人走之后没有自动重定向机制。
解决:审批链配置化。单独建一张班级与审批人的映射表,把当前有效关系存起来。
CREATE TABLE `class_approver_config` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `class_id` VARCHAR(32) NOT NULL, `approver_id` VARCHAR(32) NOT NULL, `effective_time` DATETIME NOT NULL, `invalid_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级审批人配置表';查询请假单的当前审批人时,只取 invalid_time 为 NULL 且 class_id 匹配的配置。辅导员调岗时,把旧配置的 invalid_time 置为当前时间,插入新配置;已有请假单的审批记录不会变,新请假单会自动流到新审批人。再加一个定时任务:每小时扫描 PENDING 状态超过 24 小时的请假单,向系统管理员推送预警,防止再次出现“假单卡死无人管”的情况。
5.4 销假超时没有提醒,学生已返校但仍被记缺勤
现象:学生请假到期后没有手动销假,系统默认按未返校处理,考勤记录里出现“无故缺勤”。学生拿出已返校的凭证申诉,引发纠纷。
原因:销假被设计成纯人工操作,没有自动关单机制。很多学生返校后忘记线上销假。
解决:把“自动销假”作为兜底策略。到 end_time 后 24 小时,系统自动生成销假记录,cancel_type 标记为“自动销假”,同时发送站内信提醒“若未实际返校请联系辅导员”。这样学生忘记销假,数据也不会变成缺勤;如果实际返校时间晚于 end_time,再由人工改成逾期销假。
实现自动销假时注意,定时任务不要用“扫全表 + 更新”的粗暴方式,要按 last_update_time 增量扫描,或者维护一张待销假队列,否则大表场景下会把数据库性能拖死。扫描间隔建议是每小时一次,而不是每分钟一次,避免无谓的压力。
5.5 两个审批人同时点通过,后写的把先写的覆盖
现象:一张请假单进入审批链第二环,两个学院秘书同时打开审批页面,都点了通过。结果两条审批记录都写入了,主单状态虽然还是 APPROVED,但一条假单被两个人批了,流程上说不清楚。
原因:没有使用乐观锁,审批接口里的“查询 → 更新”是两步操作,两个事务同时读到旧的状态,先后执行 update,导致状态更新被后者覆盖,审批记录却各写各的。
解决:在 update 语句中把 version 作为条件,通过乐观锁控制并发。
LeaveApplication updateEntity = new LeaveApplication(); updateEntity.setId(leave.getId()); updateEntity.setStatus(LeaveStatusEnum.APPROVED.getCode()); updateEntity.setVersion(leave.getVersion() + 1); int affected = leaveApplicationMapper.update( updateEntity, new LambdaQueryWrapper<LeaveApplication>() .eq(LeaveApplication::getId, leave.getId()) .eq(LeaveApplication::getVersion, leave.getVersion()) ); if (affected == 0) { throw new BizException("操作冲突,该请假单已被其他人处理,请刷新后重试"); }受影响行数为 0,说明当前版本已被其他事务修改,事务回滚,提示用户刷新后重试。注意 version 字段必须在每次 update 时递增,且 update 条件必须包含 version 字段。如果 MyBatis-Plus 配置了乐观锁拦截器,可以用 @Version 注解自动完成,但手写这个 SQL 更直观,也方便排查问题。
6. 从“能用”变“好用”:课时冲突自动检测与请假统计报表
6.1 课时冲突自动检测:把课表接进请假流程
很多请假系统的审批人,真正头疼的不是“批不批”,而是“批了之后学生错过的课怎么补”。如果系统能在提交阶段就提示“该假条覆盖了周三第 3-4 节的高数课”,辅导员审批时心里就有数了。
实现思路是引入课表快照表,在请假提交时做一次课时冲突计算。核心逻辑是:把请假时间段与课表时间段做区间重叠判断,重叠超过一节课的时长就标记为冲突。
筛选冲突课程的 SQL 示例:
SELECT course_name, teacher_name, weekday, period_start, period_end FROM course_schedule WHERE class_id = #{classId} AND weekday BETWEEN DAYOFWEEK(#{startTime}) AND DAYOFWEEK(#{endTime}) AND period_start <= #{endPeriod} AND period_end >= #{startPeriod};这里用课节序号而不是时间点来判断,可以避免上午最后一节和下午第一节被误判为连续冲突。比如周五下午第 5-6 节请假,周一上午第 1-2 节上课,时间上连续,但课节序号没有重叠,就不会误报。
6.2 请假统计报表:给辅导员和管理员的三个维度
统计报表是请假系统最容易被忽略的价值点。给辅导员看班级维度汇总,给学院教务看年级维度趋势,给管理员看总体规则偏离预警。常用的三个报表:
- 按请假类型统计:每个月的病假、事假、公假数量与占比,用于分析学生请假的主要原因。
- 按时销假率:已销假数量 / 应销假数量,低于某阈值的班级需要重点提醒。
- 课时冲突统计:本月冲突课程排前 10 的课程与老师,帮助教务判断是否需要调整补课安排。
报表的查询不建议在业务库直接跑大聚合,可以从业务表定时汇总到一张统计中间表,每天凌晨生成一次,页面只查中间表,避免影响请假主流程的性能。
我做这类系统最后沉淀下来的习惯是:把状态流转和权限校验全部放在服务端,把体验优化放在前端,不要做成“接口裸奔 + 前端保护”的半成品;每次状态变更都留痕,宁可多写一条记录,也不要事后靠猜。这个思路在校园请假系统里适用,换到其他审批类系统也一样。希望帮到你。
本文还有配套的精品资源,点击获取