
简介这是一套面向高校计算机专业学生与Java开发学习者的考务管理系统完整源码基于SpringBoot框架构建旨在帮助读者理解教育考试流程信息化管理的实现方式。资源包共174个文件以139个Java源文件为核心业务代码搭配25个FreeMarker模板文件负责视图渲染另含yml配置、properties属性文件、sql建表脚本及少量css、js静态资源压缩包约284KB结构紧凑便于快速导入IDE运行调试。系统覆盖考生管理、考试安排、试题管理、在线考试、成绩统计与权限控制等模块并集成Spring Security、MyBatis等常用组件适合作为课程设计、毕业设计或SpringBoot实战练手的参考项目。目前已有197人学习下载可帮助读者掌握从数据库设计到前后端交互的完整开发链路积累企业级Web应用开发经验。1. 考务管理系统为什么总在排考这一步翻车每学期期末教务老师最怕的不是出卷是排考。一个年级几百号人、十几门课、七八个考场、监考老师还有时间冲突用 Excel 手动排改一个变量全盘重来。基于 SpringBoot 的考务管理系统核心要解决的就是这件事把课程、班级、考场、监考老师、时间段这几组约束塞进数据库用代码算出可行解再让教务在线微调。它适合两类人一是要做课程设计或毕设的学生需要一套结构完整、能跑起来、能讲清楚技术选型的项目二是中小学校或培训机构的 IT 负责人想用最低成本搭一套内部排考工具。SpringBoot 在这里的价值不是“流行”而是它把 Web 层、数据层、事务、定时任务、权限这些考务系统必备的模块用自动配置串起来省掉大量 XML。下面我按“建表 → 排考算法 → 接口 → 部署 → 避坑”的顺序把一套能落地的方案拆开讲。2. 考务管理系统的数据模型与 SpringBoot 工程骨架2.1 五张核心表撑起排考业务考务系统的数据模型不复杂但字段设计直接决定后面排考算法好不好写。我一般会先落五张表课程表、班级表、考场表、教师表、考试安排表。其中考试安排表是结果表也是排考算法的输出目标。表名关键字段说明courseid, name, class_id, student_count课程绑定班级学生人数决定考场容量需求classroomid, name, capacity, building考场容量必须大于等于课程人数teacherid, name, available_slots可用时间段用 JSON 或逗号分隔存exam_slotid, date, start_time, end_time考试时间段通常一天两到三场exam_arrangeid, course_id, classroom_id, teacher_id, slot_id唯一约束同一考场同一时段只能有一场建表时有一个容易忽略的点exam_arrange 上要加联合唯一索引(classroom_id, slot_id)和(teacher_id, slot_id)否则排考算法跑出来的解可能让同一个老师在同一时段出现在两个考场。这个约束在数据库层兜底比在 Java 代码里判断更可靠。2.2 用 Spring Initializr 生成可运行骨架工程骨架不需要手写 pom用 Spring Initializr 选好依赖直接生成。我常用的依赖组合是Spring Web、MyBatis Framework、MySQL Driver、Lombok、Spring Boot DevTools。JDK 选 17SpringBoot 选 3.x。如果你本地 MySQL 是 5.7注意 SpringBoot 3.x 默认的 MySQL 驱动版本对 5.7 兼容性一般建议升到 MySQL 8.0或者把驱动版本手动降下来。# 用 curl 调 Spring Initializr 生成项目骨架 curl https://start.spring.io/starter.zip \ -d typemaven-project \ -d languagejava \ -d bootVersion3.2.5 \ -d baseDirexam-system \ -d groupIdcom.example \ -d artifactIdexam-system \ -d nameexam-system \ -d packageNamecom.example.exam \ -d javaVersion17 \ -d dependenciesweb,mybatis,mysql,lombok,devtools \ -o exam-system.zip unzip exam-system.zip这段命令生成的是一个标准 Maven 工程目录结构是src/main/java/com/example/exam放启动类和业务代码src/main/resources放application.yml和 MyBatis 的 mapper XML。参数里bootVersion决定 SpringBoot 版本dependencies决定起步依赖。生成后先跑一次mvn spring-boot:run确认能起来再写业务避免后面分不清是环境问题还是代码问题。2.3 application.yml 里必须改的三个配置默认生成的application.yml几乎不能用至少要改三处数据源、MyBatis 映射路径、端口。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.exam.entity configuration: map-underscore-to-camel-case: trueserverTimezone不写会导致插入考试时间时差 8 小时排考结果全乱。map-underscore-to-camel-case打开后数据库的classroom_id能自动映射到 Java 的classroomId省掉大量 resultMap 配置。mapper-locations指向 XML 目录如果你用纯注解方式写 SQL这一行可以去掉但考务系统查询条件多XML 更好维护。3. 排考算法的落地从贪心到冲突检测3.1 先跑通贪心排考再谈优化排考本质是带约束的分配问题理论上可以用遗传算法或模拟退火但实际项目里我建议先用贪心跑通。贪心策略很简单按学生人数从多到少排序课程每门课找一个容量够、且该时段没被占用的考场再找一个该时段没监考任务的老师。这个策略不保证全局最优但能在秒级给出可行解教务老师微调的成本远低于等算法跑十分钟。// 贪心排考核心逻辑 public ListExamArrange greedyArrange(ListCourse courses, ListClassroom classrooms, ListTeacher teachers, ListExamSlot slots) { // 按学生人数降序大班优先占大考场 courses.sort((a, b) - b.getStudentCount() - a.getStudentCount()); ListExamArrange result new ArrayList(); SetString roomSlotUsed new HashSet(); SetString teacherSlotUsed new HashSet(); for (Course course : courses) { boolean placed false; for (ExamSlot slot : slots) { if (placed) break; for (Classroom room : classrooms) { if (room.getCapacity() course.getStudentCount()) continue; String roomKey room.getId() - slot.getId(); if (roomSlotUsed.contains(roomKey)) continue; for (Teacher teacher : teachers) { String teacherKey teacher.getId() - slot.getId(); if (teacherSlotUsed.contains(teacherKey)) continue; // 找到可行组合落一条安排 ExamArrange arrange new ExamArrange(); arrange.setCourseId(course.getId()); arrange.setClassroomId(room.getId()); arrange.setTeacherId(teacher.getId()); arrange.setSlotId(slot.getId()); result.add(arrange); roomSlotUsed.add(roomKey); teacherSlotUsed.add(teacherKey); placed true; break; } if (placed) break; } } if (!placed) { // 记录排不进去的课程返回给前端人工处理 log.warn(课程 {} 无法排入任何时段, course.getName()); } } return result; }这段代码里roomSlotUsed和teacherSlotUsed是两个内存集合分别记录“考场-时段”和“教师-时段”的占用情况。三层循环的顺序是时段 → 考场 → 教师实际调优时可以改成先选教师再选考场取决于哪个资源更紧张。排不进去的课程不要抛异常记日志并返回给前端让教务手动指定这比算法直接失败体验好得多。3.2 冲突检测用 SQL 兜底别只信内存贪心算法在内存里维护占用集合但并发提交或数据被手动改过之后内存状态和数据库可能不一致。所以落库前必须用 SQL 再查一次冲突。-- 检查某考场某时段是否已有安排 SELECT COUNT(*) FROM exam_arrange WHERE classroom_id #{classroomId} AND slot_id #{slotId}; -- 检查某教师某时段是否已有监考 SELECT COUNT(*) FROM exam_arrange WHERE teacher_id #{teacherId} AND slot_id #{slotId};这两条查询在插入前执行返回大于 0 就跳过当前组合。配合前面说的联合唯一索引即使并发插入也会有一条失败事务回滚后重试即可。注意exam_arrange表的数据量通常不大一个学期几千条这两条查询走索引后毫秒级返回不会成为瓶颈。3.3 排考结果的可视化接口怎么写排考结果最终要展示成一张时间表前端按“时段 × 考场”渲染网格。后端提供一个聚合接口把exam_arrange关联课程、考场、教师、时段后返回嵌套结构。GetMapping(/arrange/table) public ResultMapString, ListArrangeVO getArrangeTable() { ListArrangeVO list arrangeMapper.selectAllWithDetail(); // 按 slotId 分组前端直接遍历渲染 MapString, ListArrangeVO grouped list.stream() .collect(Collectors.groupingBy(vo - vo.getSlotDate() vo.getSlotName())); return Result.success(grouped); }selectAllWithDetail是一条多表 JOIN 查询把课程名、考场名、教师名一次性查出来避免前端拿到 ID 再逐个请求。返回的 Map key 是“日期 时段名”value 是该时段所有安排。前端拿到后按考场分组渲染成表格教务老师可以直接在表格上拖拽调整调整后调一个更新接口改classroom_id或teacher_id更新前同样走一遍冲突检测。4. 避坑考务系统上线前必须排查的五个问题4.1 排考结果每次刷新都不一样现象同一批数据点两次“自动排考”结果不同。原因贪心算法里课程排序用了List.sort如果两门课学生人数相同排序结果不稳定导致后续分配顺序变化。解决在排序比较器里加第二排序键比如课程 ID保证顺序确定。courses.sort(Comparator.comparingInt(Course::getStudentCount).reversed().thenComparing(Course::getId));4.2 考试时间存进去差 8 小时现象前端传2025-01-10 09:00:00数据库里变成2025-01-10 01:00:00。原因JDBC 连接串没指定serverTimezoneMySQL 驱动用了 UTC 时区解析。解决连接串加serverTimezoneAsia/Shanghai同时确认 MySQL 服务端time_zone参数是08:00。如果已经存了错数据用UPDATE exam_slot SET start_time DATE_ADD(start_time, INTERVAL 8 HOUR)批量修正。4.3 考场容量够但排不进去现象某考场容量 100课程 80 人但算法说排不下。原因考场容量字段在数据库里是varchar类型比较时按字符串比“100”小于“80”。解决建表时容量字段用int实体类用Integer。如果已经建错用ALTER TABLE classroom MODIFY capacity INT;改过来同时检查 MyBatis 映射有没有做类型转换。4.4 监考老师同一时段被排了两次现象张老师同一时段出现在两个考场。原因贪心算法里teacherSlotUsed集合在并发排考时被两个线程同时读写或者手动调整时没走冲突检测。解决排考接口加synchronized或分布式锁手动调整接口在更新前必须查一次teacher_id slot_id的冲突。数据库层的联合唯一索引是最后一道防线不能省。4.5 前端打包后刷新页面 404现象Vue 打包放进 SpringBoot 的static目录首页能打开刷新子路由报 404。原因SpringBoot 默认只处理静态资源不处理前端路由的 history 模式回退。解决加一个配置类把所有非 API 路径转发到index.html。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/**) .addResourceLocations(classpath:/static/); } // 配合前端 history 模式非 /api 开头路径回退到 index.html Bean public WebServerFactoryCustomizerConfigurableServletWebServerFactory customizer() { return factory - factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, /index.html)); } }这段配置把 404 错误页指向index.html前端路由就能接管。注意/api开头的接口路径不受影响因为 Controller 会优先匹配。5. 用定时任务和导出功能把考务系统用起来排考只是开始真正让教务老师离不开的是“排完能导出、临期能提醒”。SpringBoot 的Scheduled可以每天检查一次考试安排把第二天有考试的课程和监考老师生成提醒记录。导出用 EasyExcel 或 POI把exam_arrange的查询结果写成 Excel按考场分 Sheet每个 Sheet 里列课程、时间、监考老师。我一般会在导出接口上加一个Transactional(readOnly true)避免导出过程中数据被改。Scheduled(cron 0 0 18 * * ?) public void remindTomorrowExams() { LocalDate tomorrow LocalDate.now().plusDays(1); ListArrangeVO list arrangeMapper.selectByDate(tomorrow); for (ArrangeVO vo : list) { // 给监考老师发站内信或邮件具体通道按学校现有系统对接 noticeService.send(vo.getTeacherId(), 明天有监考任务 vo.getCourseName()); } }cron表达式0 0 18 * * ?表示每天 18 点执行。selectByDate按exam_slot.date过滤走索引后很快。发送通知的通道不要写死留一个接口学校有企业微信就接企业微信有邮件服务器就接邮件没有就存站内信表让老师登录后看。导出 Excel 时有一个血泪经验考场名里如果有/或\POI 创建 Sheet 时会报错。导出前用sheetName.replaceAll([\\\\/:*?\\[\\]], _)过滤掉非法字符。另外一个 Sheet 最多 65535 行考务数据一般不会超但如果要导出历史所有学期记得分 Sheet。这套系统我前后改过三版最大的教训是排考算法不要追求一步到位先把贪心跑通让教务用起来收集他们手动调整的数据再反过来优化算法权重。一开始就上遗传算法调参调到怀疑人生教务还嫌慢。希望帮到你。本文还有配套的精品资源点击获取