简介:这是一套面向高校计算机专业学生与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 五张核心表撑起排考业务
考务系统的数据模型不复杂,但字段设计直接决定后面排考算法好不好写。我一般会先落五张表:课程表、班级表、考场表、教师表、考试安排表。其中考试安排表是结果表,也是排考算法的输出目标。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| course | id, name, class_id, student_count | 课程绑定班级,学生人数决定考场容量需求 |
| classroom | id, name, capacity, building | 考场容量必须大于等于课程人数 |
| teacher | id, name, available_slots | 可用时间段用 JSON 或逗号分隔存 |
| exam_slot | id, date, start_time, end_time | 考试时间段,通常一天两到三场 |
| exam_arrange | id, 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 选 17,SpringBoot 选 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 type=maven-project \ -d language=java \ -d bootVersion=3.2.5 \ -d baseDir=exam-system \ -d groupId=com.example \ -d artifactId=exam-system \ -d name=exam-system \ -d packageName=com.example.exam \ -d javaVersion=17 \ -d dependencies=web,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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 List<ExamArrange> greedyArrange(List<Course> courses, List<Classroom> classrooms, List<Teacher> teachers, List<ExamSlot> slots) { // 按学生人数降序,大班优先占大考场 courses.sort((a, b) -> b.getStudentCount() - a.getStudentCount()); List<ExamArrange> result = new ArrayList<>(); Set<String> roomSlotUsed = new HashSet<>(); Set<String> 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 Result<Map<String, List<ArrangeVO>>> getArrangeTable() { List<ArrangeVO> list = arrangeMapper.selectAllWithDetail(); // 按 slotId 分组,前端直接遍历渲染 Map<String, List<ArrangeVO>> 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 连接串没指定serverTimezone,MySQL 驱动用了 UTC 时区解析。解决:连接串加serverTimezone=Asia/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 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> 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); List<ArrangeVO> 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。
这套系统我前后改过三版,最大的教训是:排考算法不要追求一步到位,先把贪心跑通,让教务用起来,收集他们手动调整的数据,再反过来优化算法权重。一开始就上遗传算法,调参调到怀疑人生,教务还嫌慢。希望帮到你。
本文还有配套的精品资源,点击获取