ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot排考系统实战:从冲突检测到智能编排

SpringBoot排考系统实战:从冲突检测到智能编排 排考系统这四个字在我第一次拿到这个毕设题目的时候心里想的是不就是把考试安排做成增删改查吗数据库加几张表前端写几个页面用SpringBoot一套完事。真正动手设计和编码之后我才发现这个题目最有意思也最难的地方根本不在CRUD而在“智能”两个字上——如何让几百门课程、上万名考生、几十个教室、一批监考老师这四维信息互相不冲突并且能在几秒内跑出一份可用的考场编排结果。这个项目我从选题、数据建模到算法实现、前后端联调、写论文前前后后花了大概六周。这篇文章就是一次完整的复盘把排考系统的需求拆解、表结构设计、冲突检测算法、监考分配逻辑、报表导出以及我在开发中踩过的坑全部整理出来。如果你也选了SpringBoot方向的教务类毕设或者工作中要做一个考场调度/实验室预约类的系统这篇内容应该能帮你少走很多弯路。1. 排考系统的核心难题不是写接口而是解约束1.1 手动排考的痛点在哪里先还原一下教务处的真实场景。每个学期末考试周到来之前教务员要做的事情大概是这样收集所有课程的考试人数、汇总可用教室和监考老师名单、把每门课安排进一个时间片然后确保同一个学生在同一时间段不会出现在两场考试里同一个监考老师不会同时出现在两个考场里同一个教室不会被两门考试重复占用。听起来很简单实际做起来非常容易翻车。我曾经亲眼见过一位教务员用Excel排了三天结果下午的监考安排表打印出来之后发现一位老师被同时排在了A栋和C栋的考场里。因为数据量一旦上来光靠人脑做交集判断很容易漏掉某个组合。这个场景就是排考系统的根需求把人从大量的约束判断中解放出来让计算机去处理排列组合的复杂度。1.2 所谓“智能化”实现上一般分三个层次我在设计之前参考了不少已有的排考系统发现“智能”这个词在实现上大体有三个递进层次。第一个层次是自动排考系统根据预设的规则自动生成排考结果。这是每套排考系统的基本盘也是最容易拿分的部分。第二个层次是冲突检测与人工干预。自动排完并不代表绝对完美有时候课程有特殊要求比如某个老师只能周四上午监考或者某门课必须用机房这时候就要允许管理员手动调整并且每次调整系统都实时校验冲突。第三个层次是智能优化比如尽量让每个学生的考试间隔均匀尽量让监考老师的总场次均衡避免某位老师连续四场不休息。这个层次属于锦上添花毕设能做到前两个层次已经可以拿到很不错的评分第三个层次可以当作论文的加分点来写。1.3 我的系统边界划定需求再大也要控制边界不然一个毕设能无限膨胀下去。我最终把系统限定在以下功能范围内基础数据维护学生、教师、课程、教室、班级考试任务管理创建考试批次比如“2024-2025第一学期期末考试”绑定课程和参考学生自动排考输入时间片列表系统自动为每门考试分配时间和教室人工调改管理员手动调整排考结果系统实时检测并提示冲突监考分配自动为每个考场分配监考老师支持按工作量均衡报表导出导出考场名单、座次表、监考安排表、考试任务单至于在线考试、答题卡识别这类功能我没做也不建议做它们已经不属于“排考”这个题目的范畴了硬加进去反而会让答辩时被问得很难受。2. 技术选型为什么是SpringBoot MyBatis-Plus Vue2.1 后端框架选型的现实考量后端我选的是SpringBoot 2.7.x配JDK 8。为什么不用SpringBoot 3因为SpringBoot 3要求JDK 17起步而且不少第三方组件的兼容性在毕业设计阶段会让人非常头疼。SpringBoot 2.7.x是目前最稳妥的选择资料多、答疑多、碰到问题几乎都能搜到解决方案这对开发周期有限的毕设来说太重要了。ORM框架我用的MyBatis-Plus而不是纯MyBatis。排考系统里最频繁的操作是关联查询、批量插入和条件更新MyBatis-Plus的LambdaQueryWrapper写起来比XML里堆SQL要快得多而且内置了分页插件导出报表时的分页查询可以直接复用。在论文里我也可以很自然地写一句“基于MyBatis-Plus的ActiveRecord模式与Lambda表达式查询减少样板代码”这也是答辩时的一个谈资。2.2 前端方案与系统集成方式前端我用的是Vue 3 Element Plus构建工具是Vite。之所以选这套组合是因为Element Plus的表格、弹窗、表单组件非常契合后台管理系统的页面需求考场信息展示、排考结果表格这类界面不需要太多定制化的视觉设计直接用现成组件效率最高。前后端通信用Axios接口返回统一封装成Result对象结构是code message data。这里有个细节建议状态码不要只依赖HTTP状态码业务层面的状态尽量放到code字段里。比如排考接口检测到冲突时HTTP依然返回200code返回50001前端拿到之后弹窗提示冲突详情。这样做的好处是网络层和业务层的错误互相隔离排查问题的时候非常省事。部署方面Vue打包后的dist目录可以直接放进SpringBoot的src/main/resources/static下也可以单独部署到Nginx。开发调试阶段建议前后端分离跑一个跑8080一个跑5173最终答辩演示的时候如果你不想现场同时开两个服务可以把dist静态文件并入SpringBoot打成一个jar包一个命令启动全部服务。这个方案我在后面还会详细讲。2.3 项目初始化清单这张图是我初始化项目时的操作清单照着走基本不会卡壳步骤操作内容说明1在start.spring.io生成基础工程引入Web、MySQL Driver、Lombok依赖2手动加入MyBatis-Plus依赖SpringBoot 2.7对应MyBatis-Plus 3.5.x3配置application.yml数据源注意时区参数serverTimezoneAsia/Shanghai4初始化数据库并导入建表SQL建议用MySQL 8.05编写全局异常处理器统一兜底避免堆栈直接暴露给前端6引入Knife4j在线接口文档调试接口和写论文截图都方便提一个很多人忽略的细节application.yml里的spring.jackson.date-format只能格式化java.util.Date对LocalDateTime不生效。排考系统里大量使用考试开始时间、结束时间这类字段如果你用LocalDateTime前端展示会出现ISO格式的字符串比如2025-01-15T08:30:00而不是期望的2025-01-15 08:30:00。我是在联调时才发现这个问题的后面会单独讲解决办法。3. 数据模型设计撑得起排考的核心表结构3.1 从考试任务到考场安排的数据流排考系统的数据流可以用一句话概括从考试批次的课程列表出发根据选课关系确定每门课的参考学生集合再结合教室容量将学生集合拆成若干个考场分片最后为每个分片分配时间片和监考老师。这句话听起来抽象落到表结构上就非常清晰了。我设计了九张核心表其中最重要的六张是exam_task考试任务表、exam_course_ref考试课程关联表、exam_session考试场次表、exam_room考场表、exam_arrangement排考结果表、exam_invigilator监考分配表。下面把这六张表的结构讲透。3.2 核心表结构参考考试任务表用来区分不同批次的考试。比如上学期期末和下学期期末就是两个不同的exam_task这样排考数据不会相互污染。CREATE TABLE exam_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, task_name VARCHAR(100) NOT NULL COMMENT 考试批次名称如2024-2025第一学期期末考试, start_date DATE NOT NULL COMMENT 考试周起始日期, end_date DATE NOT NULL COMMENT 考试周结束日期, status TINYINT DEFAULT 0 COMMENT 0草稿 1已发布 2已归档, create_time DATETIME NOT NULL COMMENT 创建时间, update_time DATETIME NOT NULL COMMENT 更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试批次表;考试课程关联表决定“这一次考试要考哪些课”。课程和学生之间是选课关系所以需要一张单独的关联表来记录参考范围而不是直接查所有的课程表。CREATE TABLE exam_course_ref ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL COMMENT 考试批次ID, course_id BIGINT NOT NULL COMMENT 课程ID, student_count INT DEFAULT 0 COMMENT 参考人数冗余字段, duration_minutes INT DEFAULT 120 COMMENT 考试时长分钟 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试课程关联表;考试场次表是最关键的设计之一。它描述的是“某一天某个时间段”比如1月15日上午8:30到10:30作为一个场次1月15日下午14:00到16:00作为另一个场次。排考的本质就是给每门考试分配一个场次ID。CREATE TABLE exam_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, session_name VARCHAR(50) NOT NULL COMMENT 场次名称如1月15日上午, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, max_exam_count INT DEFAULT 0 COMMENT 该场次最大可排考试门数预留字段 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试场次表;考场表记录教室信息容量是排考时的重要约束。我额外加了campus和building字段方便按校区、楼栋过滤后期打印监考安排表时也能分区域展示。CREATE TABLE exam_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(50) NOT NULL COMMENT 例如A101, campus VARCHAR(50) COMMENT 校区, building VARCHAR(50) COMMENT 楼栋, capacity INT NOT NULL COMMENT 座位数, room_type VARCHAR(20) DEFAULT normal COMMENT normal普通教室 computer机房, status TINYINT DEFAULT 1 COMMENT 1可用 0禁用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考场表;排考结果表是整个系统的心脏。每条记录表示“某门考试在某个场次占用某间教室”。CREATE TABLE exam_arrangement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, course_id BIGINT NOT NULL COMMENT 课程ID, session_id BIGINT NOT NULL COMMENT 场次ID, room_id BIGINT NOT NULL COMMENT 教室ID, total_students INT NOT NULL COMMENT 该考场应到人数, status TINYINT DEFAULT 0 COMMENT 0自动排定 1人工调整, create_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT排考结果表;监考分配表单独拎出来因为监考分配和排考结果虽然有关联但它们是两个维度的安排。一张排考记录对应两个监考老师按常规考场规模所以用一张独立表记录。CREATE TABLE exam_invigilator ( id BIGINT PRIMARY KEY AUTO_INCREMENT, arrangement_id BIGINT NOT NULL COMMENT 排考结果ID, teacher_id BIGINT NOT NULL, CREATE_TIME DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT监考分配表;3.3 冗余字段的设计逻辑上面表结构里有几个冗余字段比如exam_course_ref.student_count。为什么明明可以通过选课表student_course实时统计参考人数还要冗余一个字段因为在排考算法里要对所有参考课程按人数从多到少排序如果每次排序都实时count在数据量稍大、场次较多时会有明显的性能开销。我在测试时导入了一万名学生、六百门课程的数据实时统计接口耗时从几十毫秒涨到了八百多毫秒。用冗余字段在创建考试批次时一次性统计好排考算法直接读字段这一轮的时间开销就完全消失了。另外exam_arrangement里没有直接放学生名单而是通过course_id去派生。这是因为考试学生和选课学生是同一批人单独存一份冗余的学生名单反而容易因为数据不一致造成漏排。真正需要学生明细时用SQL现查SELECT sc.student_id, s.student_name, s.class_name FROM exam_course_ref ecr JOIN student_course sc ON ecr.course_id sc.course_id JOIN student s ON sc.student_id s.id WHERE ecr.task_id #{taskId} AND ecr.course_id #{courseId} ORDER BY s.student_id这种冗余加适度的实时查询结合是本系统数据设计的关键思路。全冗余会变成数据泥潭全实时查询又扛不住算法的性能要求。4. 排考算法从零实现冲突检测是第一关4.1 排考问题到底在解什么把需求抽象成数学模型来看排考是一个典型的约束满足问题。我需要满足的核心约束有四条学生约束同一时间片内一个学生只能参加一场考试教师约束同一时间片内一位监考老师只能出现在一个考场教室约束同一时间片内一个教室只能被一门考试占用容量约束每个考场的参考人数不能超过座位数只要这四件事全部满足排考结果就是合法的。算法设计要做的就是在所有合法组合里找出一组分配方案。4.2 冲突检测的两种实现SQL方案与内存方案冲突检测是算法的基础设施。我写了两种方案一种用于算法内部高频调用另一种用于人工调整时实时校验。先看SQL方案。人工调整时当管理员强行把某门课程拖到某个场次系统需要立刻回答“这个场次里有没有学生同时考了两门课”。对应SQL可以写成SELECT sc.student_id, COUNT(DISTINCT ea.course_id) AS subject_cnt FROM exam_arrangement ea JOIN student_course sc ON ea.course_id sc.course_id WHERE ea.session_id #{sessionId} AND sc.course_id IN ( SELECT course_id FROM exam_course_ref WHERE task_id #{taskId} ) GROUP BY sc.student_id HAVING COUNT(DISTINCT ea.course_id) 1只要这条SQL返回了记录就说明存在学生冲突。算法内部我不建议频繁抛SQL因为一次排考要尝试几百次甚至上千次分配每次都查库的话性能太差。我改成内存检测核心结构是MapLong, SetLong sessionStudentMap键是场次ID值是该场次已排课程的参考学生并集。分配一门考试前只需要做一次集合交集判断private boolean hasStudentConflict(Long sessionId, SetLong courseStudentIds) { SetLong existing sessionStudentMap.getOrDefault(sessionId, new HashSet()); for (Long studentId : courseStudentIds) { if (existing.contains(studentId)) { return true; } } return false; }这个方法的复杂度是O(n)n是考试人数。实测一万名学生、六百门课、二十个场次完整排考一次大约在三百多毫秒完全够用。4.3 贪心加回溯的排考主流程整个排考主流程我分成三步。第一步把考试课程按参考人数从大到小排序。人数多的考试优先安排这样能降低后期“小考试插不进空档”的概率。第二步遍历每个场次先做学生冲突检测再做教室容量检测如果没有冲突且有空教室就分配。第三步如果某个课程在所有场次都冲突就让算法回退撤销前面对应场次中某门课程重新分配。第二步里还涉及教室分配代码核心是这样的逻辑public boolean assignExamToSession(ExamCourse examCourse, ListExamSession sessionList) { ListExamCourse examGroups splitByRoomCapacity(examCourse); ListExamRoom availableRooms getAvailableRooms(); for (ExamSession session : sessionList) { if (hasStudentConflict(session.getId(), examCourse.getStudentIds())) { continue; } ListExamRoom idleRooms getFreeRoomsInSession(session.getId(), availableRooms); if (idleRooms.size() examGroups.size()) { continue; } for (int i 0; i examGroups.size(); i) { ExamRoom room idleRooms.get(i); createArrangement(examGroups.get(i), session.getId(), room.getId()); markRoomOccupied(session.getId(), room.getId()); addStudentMapping(session.getId(), examGroups.get(i).getStudentIds()); } return true; } return false; }这里有个特别容易出错的地方我一开始就踩了一门考试如果人数超过最大教室容量必须拆成多个考场分片但同属一门考试的分片必须出现在同一个场次里。如果只按考试整体做学生冲突检测然后单独给分片分配不同场次就会出现同一门考试被拆到上午和下午两个时间段这个结果在业务上是有问题的。所以代码里assignExamToSession是整体分配一次循环里把全部分片都放进去。4.4 回溯策略的具体实现当贪心失败时我采用了一个轻量级的回溯策略叫做“最近冲突回退”。具体做法是当前课程如果无法放入任何场次就把上一个场次中最后分配的课程拿出来重新放入下一个可用场次腾出一个缺口再尝试当前课程。为了控制复杂度我只回退一层不去做深层的全局搜索。对于毕设场景这个策略已经能解决绝大多数情况而且代码简单、逻辑清晰论文里也容易讲明白。注意如果你想要更强的全局最优解可以改成遗传算法或者模拟退火但我觉得对于毕设来说回报率不高。贪心加一层回溯已经能处理我在测试中构造的极端数据包括某门课人数超过所有教室容量、某场次被占满等。先把简单方案做稳再谈优化。监考分配的独立规则可以体现在系统中教师不能监考自己担任授课老师的课程这是我在抽教师时候做的过滤条件。5. 监考分配与学生名单容易被忽略又必须做对的环节5.1 监考分配怎么做到公平且无冲突排考结果出来之后还要为每个考场分配监考老师。监考分配必须满足两个约束同一场次同一个老师只能在一个考场老师不能监考自己任教的课程避免出题人监考自己学生的尴尬情况。我的实现思路不复杂把监考任务看成一个个“坑”每门考试需要两个监考老师按排考结果逐条处理。候选老师池中排除掉本课程授课老师后按“历史监考次数”排序谁监考过的最少优先分配分配后该老师进入该场次的占用集合。public void assignInvigilators(ExamArrangement arrangement) { ListTeacher candidates teacherMapper.selectList( new LambdaQueryWrapperTeacher() .notIn(Teacher::getId, courseTeacherIds) ); candidates.sort(Comparator.comparingInt(Teacher::getInvigilateCount)); for (Teacher teacher : candidates) { if (!isTeacherOccupied(arrangement.getSessionId(), teacher.getId())) { createInvigilator(arrangement.getId(), teacher.getId()); teacher.setInvigilateCount(teacher.getInvigilateCount() 1); markTeacherOccupied(arrangement.getSessionId(), teacher.getId()); } if (alreadyHasTwoInvigilators(arrangement.getId())) { break; } } }这里isTeacherOccupied同样是内存集合判断MapLong, SetLong sessionTeacherMap结构上和前面学生冲突检测完全一致。同一个时间段老师不能出现在两个考场这个判断必须放在分配循环里面。5.2 考场学生名单的生成细节每个考场对应的学生名单并不是简单把课程的学生列表切一半就完事。实际场景里多个班级的学生往往会混合在一个考场里需要按学号或者是班级进行排序同时生成座次表。我生成名单的规则是先按班级分组组内按学号排序然后把学生依次分配到教室的座位编号上。这样做的效果是同一班的学生大概率坐在相邻位置方便监考老师快速核对学生证。这个模块用到的查询代码其实不复杂重点是输出格式。考场名单通常要打印成两种东西一种是贴在考场门口的名单用于学生查找自己在哪个教室另一种是监考老师手里的座位表按座位号排列方便开考时逐一核对。导出的内容一样但是排序方式和表头完全不同。这个细节我在第一次设计时就漏掉了后期补了两个导出模板才解决。5.3 定时任务自动执行排考排考算法虽然操作起来很快但我还是加了一个定时任务入口。用Spring自带的Scheduled注解每天凌晨自动为状态为“草稿”且已配置好基础数据的考试批次执行排考。这样做的好处是管理员只需要提前把数据导入系统会自动生成一份初步方案第二天上班时打开系统直接审核即可。Component public class AutoScheduleTask { Scheduled(cron 0 30 2 * * ?) public void autoArrange() { ListExamTask draftTasks examTaskMapper.selectList( new LambdaQueryWrapperExamTask().eq(ExamTask::getStatus, 0) ); for (ExamTask task : draftTasks) { arrangeService.executeAutoArrange(task.getId()); } } }不过定时任务有一个隐藏的坑默认的Scheduled是单线程执行的如果你配置了多个定时任务它们会排队执行。排考算法本身比较吃CPU如果和别的任务挤在一起调用链路上可能出现延迟。我当时的做法是给排考任务单独配置一个ThreadPoolTaskScheduler避免阻塞其他任务。5.4 消息通知排考完成之后告诉学生系统里还有一类用户是学生他们登录之后想看到的是“我这学期期末考试在哪个教室”。所以排考结果发布之后系统需要通知学生。我用的是WebSocket加站内信的方式排考结果发布时向在线学生的WebSocket连接推送一条消息离线学生则在下次登录时通过接口拉取未读站内信。在毕设场景下不建议引入独立的MQ消息中间件比如ActiveMQ、RabbitMQ因为对于这个体量的通知需求完全是杀鸡用牛刀而且部署和配置会占用大量时间。直接使用Spring自带WebSocket加一张通知表简单且完全够用。如果论文里非要写消息机制站内信这种轻量方案反而更容易讲清楚。6. 开发到联调阶段的踩坑实录6.1 LocalDateTime序列化引发的格式问题前面提到的LocalDateTime问题在我开发到一半的时候终于爆发了。前端页面拿到考试开始时间是2025-01-15T08:30:00而页面表格里期望显示的是2025-01-15 08:30:00。解决方案我用了两步。第一步在application.yml里配置全局Jackson序列化规则spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai第二步给LocalDateTime字段统一配置自定义序列化器。第一种方案对java.util.Date有效对LocalDateTime无效这是Jackson的历史遗留问题所以要额外写配置类或者直接使用JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解标注每个日期字段。我图省事写了一个全局的Jackson2ObjectMapperBuilderCustomizer一次性解决问题。这个坑的成本其实很低但如果不在开发前期处理后期几十张表改起来就很烦。6.2 MyBatis-Plus批量插入的性能问题排考结果表需要一次性插入几百条甚至上千条记录。一开始我用了MyBatis-Plus的saveBatch方法单测时发现数据到两万条之后插入耗时明显上升。后来排查发现saveBatch本质上是一条一条执行INSERT没有真正使用MySQL的批量插入特性。解决方法是用自定义的SQL方式通过INSERT INTO ... VALUES (...), (...), (...)一次性插入多条数据Insert(script INSERT INTO exam_arrangement(task_id, course_id, session_id, room_id, total_students) VALUES foreach collectionlist itemitem separator, (#{item.taskId}, #{item.courseId}, #{item.sessionId}, #{item.roomId}, #{item.totalStudents}) /foreach /script) int batchInsert(Param(list) ListExamArrangement list);这个改动让批量插入耗时从几秒降到了几百毫秒效果非常明显。在案头整理论文的时候这也成了我“性能优化”章节的一个实证数据。6.3 POI导出大名单时的内存溢出考试座位表动辄几百行数据用Apache POI导出Excel原本没什么问题。但我在测试最终生成全校考试周安排总表时一条导出接口一次性写了八千多行、十几列的数据直接用XSSFWorkbook把内存吃满了报出OutOfMemoryError。解决方法是把对象换成SXSSFWorkbook它是POI提供的流式写入版本数据不常驻内存。写法上只有一行区别// 原来 Workbook workbook new XSSFWorkbook(); // 改成 Workbook workbook new SXSSFWorkbook(100);构造参数100表示内存中最多保留100行其余写入临时文件。这个改动带来的风险是导出的临时文件路径需要手动清理不过对于应用服务来说交给操作系统临时目录管理即可。另外如果你的项目部署在容器里导出后建议用异步方式让用户下载而不是同步请求的时候生成Excel。我最终做成了导出任务后台生成并存到本地然后通过接口返回下载链接。6.4 Vue 打包后放进 SpringBoot 的部署细节毕业设计答辩现场比较推荐的方式是提前打包好一个可运行的jar包现场一条命令启动。Vue项目打出来的dist目录如何放进SpringBoot这里有一个常见的错误直接把dist目录整个扔进resources/static但前端用history路由模式时刷新页面会404。SpringBoot的静态资源默认映射了/static、/public等目录解决路由刷新404问题需要自定义一个WebMvcConfigurer把找不到的路径转发到index.htmlOverride public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:\\w}) .setViewName(forward:/index.html); registry.addViewController(/**/{spring:\\w}) .setViewName(forward:/index.html); }需要注意这种方式只适用于单页应用且API接口路径不能和前端路由冲突。我当时的接口统一前缀是/api所以和前端路由天然隔离没有出问题。6.5 SpringBoot版本过高带来的额外麻烦我最初用的是SpringBoot 3.2后来因为一个拦截器类找不到的问题排查了很久。新版本中SpringBoot 3把很多spring.factories配置改成了AutoConfiguration.imports而且底层换成了Jakarta命名空间不少第三方库还停留在旧版本兼容性问题很多。最后我果断降级回2.7.18相关问题全部消失。如果你不是对SpringBoot 3的新特性有刚需毕设阶段听我一句劝用2.7.x。7. 答辩演示与验收时值得注意的加分细节7.1 演示数据怎么准备演示效果好不好数据准备占一半。我的经验是准备两套数据套是小规模的演示数据大概20门课程、6个教室、30名教师、300名学生演示时重点展示排考过程的前台交互比如点击“自动排考”按钮后排考结果表格快速刷新的过程。另一套是压力数据一万学生、六百课程、二十个场次用于展示算法耗时和排考结果质量。答辩演示时先跑小数据把页面交互完整展示一遍再跑大数据当着评委的面点击自动排考然后用“本次排考耗时328毫秒成功安排598门课程冲突0条”这样的结论收尾说服力非常强。7.2 评委常问的问题怎么准备根据我身边做毕设的同学被问过的问题排考系统这个方向有几个点要提前准备。第一个问题是算法复杂度。需要能够说清楚贪心加回溯的时间复杂度和为什么这样设计。我的回答基调是排序阶段是O(k log k)冲突检测阶段平均情况接近O(n)最坏情况是O(k*n)其中k是课程数n是课程平均参考人数回溯只在贪心失败时触发回退层数固定。第二个问题是冲突检测的正确性如何保证。这时候前面讲的SQL检测方案就派上用场了可以现场演示一条SQL查出某个场次是否存在学生考多门课的结果。直接演示比说十句话都有用。第三个问题是为什么选择MyBatis-Plus而不是JPA。如实回答即可说清楚MP在关联查询、批量插入、分页插件上的优势以及自己已经踩过批量插入的坑并做了优化。7.3 系统还可以扩展的方向做完这个系统之后如果时间和精力允许我认为有三个扩展方向值得写进论文的展望部分。第一个是全局时间冲突的自动检测升级。目前的冲突检测只对已经排定的记录做即时判断如果再加入一个“最优间隔”评估比如自动避免学生两场考试间隔过短算法的智能程度会上一个台阶。第二个是文件服务集成比如MinIO把导入的课程名单Excel、导出的监考安排表统一存到对象存储上避免本地磁盘单点问题。第三个是增加基于统计分析的可视化看板比如考生分布热力图、教室利用率图表让管理者一眼看出考场资源是否紧张。这三个方向都不是我随口说的而是我在实际使用这个系统时真实感受到的痛点。有一次系统跑完排考我发现某栋教学楼下午的利用率只有百分之四十另外一栋却挤满了如果能有一个可视化的教室热度图调度效果会直观很多。整个项目做完我的感受是排考系统表面上是一个Web管理系统真正拉开差距的地方却在约束建模和算法设计。只要把冲突检测的逻辑想透表结构设计得干净再用SpringBoot把各个模块串联起来最终交付的东西就能达到“既能跑通流程、又有技术深度”的效果。这套思路不仅适用于排考实验室预约、面试时间安排、会议室调度本质上都是同一类问题。
返回列表