接手这个课题的时候我脑子里先冒出来一句话不就是给几百号人安排个考场和座位吗结果真用SpringBoot去做这套高校考场座位安排系统的时候才发现排考这件事是典型的“看着简单、做起来全是细节”的Java Web项目。今天这篇就好好聊聊我完整开发一版智能排考管理系统的全过程包括技术选型、表结构设计、考场分配和座位蛇形编排的核心算法、实测踩坑和答辩演示的注意点给正在做毕业设计或者想系统了解排考方案的同学一个能直接参考的样本。项目名在不同的申报材料里可以有不同叫法本质上都是同一个东西——基于SpringBoot框架的高校智能排考管理系统在Java Web环境下实现考试座位的智能分配。它解决的痛点很具体一个学期末可能有几百门课程、上万名学生需要在一周内完成考试而考场容量、监考教师、学生时间都要互相不冲突靠人工排根本排不过来。1. 排考场景的真实约束不是随便把学生塞进教室1.1 高校教务排考的真实痛点先说说我调研过的高校排考场景。每个学期末教务科最头疼的事情就是考试周安排。表面上看排考就是把学生、课程、考场、时间四个要素组合起来但实际上每个要素之间都有约束条件。举例来说同一位学生不能在同一时间段出现在两场考试里这是硬约束同一个教室不能同时安排两场考试监考教师不能监考自己带的班级同一班级的学生在考场里要尽量打散防止交头接耳不同考场容量不一样有的教室能坐60人有的只能坐30人座位还是阶梯教室的固定桌椅必须按实际座位布局来编排。我接手需求后的第一反应是这东西用Excel也能做不就是筛选、排序、复制粘贴吗但试过的人都知道一旦课程数超过20门、考生人数超过1000Excel公式就会卡成PPT而且冲突检查要靠肉眼漏掉一场就得从头再来。更别提考务办还要给每个学生打印带座位号的准考证给监考教师打印考场记录单给巡考打印考场分布图这些衍生需求用表格处理简直就是灾难。1.2 功能模块怎么划分才不失控做毕业设计最容易犯的错就是需求膨胀。我在前期画原型的时候列了十几个功能点包括消息推送、题库管理、成绩查询、移动端小程序。后来冷静下来把跟“排考”无关的功能全部砍掉只保留一条核心链路。最终确定的模块是四个基础数据管理学生、班级、课程、教室、排考任务管理设置考试场次、选课名单导入、一键排考、座位与结果管理座位图查看、手动调座、准考证打印、考场导出、系统管理用户登录、角色权限、操作日志。这个范围对毕业设计来说已经足够撑起一个完整的SpringBoot项目又不会让自己在答辩前夜还在赶工。我的经验是凡是跟“排考”这个动词没有直接关系的模块都放到“后续扩展”列表里不要在初版里实现。1.3 角色与业务流程梳理系统涉及的三种角色对应三种完全不同的使用视角。教务管理员负责全局操作导入学生和教师Excel创建考试场次维护教室信息点击“一键排考”按钮最后导出各种报表。普通教师能看到的是自己监考的场次和自己所授课程的考生分布方便打印考生名单或者临时调换监考。学生端的核心需求是查询自己的考试时间、考场和座位号我在打印准考证页面里顺便做了一个二维码扫码查询功能用ZXing生成一个包含考生信息的二维码手机扫一下就能看到自己的座位信息。业务主流程是创建排考任务 - 导入选课名单 - 选择考试场次 - 执行自动排考 - 人工微调 - 发布考试安排。整个流程里有几个状态节点要把握好第一版我只做了“未发布”和“已发布”两态后来发现人工微调时需要有“排考中”这个中间态否则发布后改了座位学生手机里数据还是旧的会在实际考试中出现座位对不上的尴尬。2. 技术选型的取舍逻辑为什么最终定为SpringBoot全家桶2.1 后端框架选型SpringBoot 2.7.18还是3.x技术选型这块我在最开始纠结过一段时间。标题里明确写了SpringBoot和Java Web那后端主体框架基本就是SpringBoot没得跑了。真正纠结的是版本。我用的是SpringBoot 2.7.18 JDK 8的组合。理由很朴素一是JDK8在企业里存量最大遇到问题时随便一搜都是答案二是SpringBoot 2.7是2.x系列的最终维护版本稳定适配SpringCloud旧组件也没障碍三是很多国产化环境和老服务器上装JDK8比装JDK17省心太多。如果你非要用SpringBoot 3.x JDK17也完全没问题功能上没本质差别只是注意MyBatis-Plus要引入mybatis-plus-spring-boot3-starter这种专门的适配包踩坑的内容会多一些。热词里也有“springboot配置”和“idea创建springboot项目”这些高频搜索。IDEA新建SpringBoot项目时如果选择默认的start.spring.io网络不好可能卡在拉取脚手架那一步。我习惯把Server URL换成阿里云镜像https://start.aliyun.com生成的工程结构基本一致依赖下载也快。另外2020年后的SpringBoot项目很多用Gradle构建如果是自己从零写强烈建议Mavenpom文件的可读性和镜像配置对新手友好得多后续写实验报告也方便贴配置。2.2 持久层方案MyBatis-Plus比JPA更适合这类系统排考系统的数据操作特点是关联查询多、批量插入多、事务边界明显。对比JPA和MyBatis-Plus之后我选了后者。JPA对双向关联和级联的建模很舒服但在“多表动态查询”场景下只要查询条件稍微复杂一点要么写Query拼JPQL要么拆多个方法远不如MyBatis的XML里直接写联表SQL来得直观。排考系统里经常要做“查某场次下某学生座位是否已存在”“查某考场某列的剩余容量”这种带业务规则的查询SQL一眼能看懂是最高优先级。MyBatis-Plus的分页插件是我这次用得最顺的功能。配置方法不能忘先注入PaginationInnerInterceptor再调用分页查询时传入Page对象。热词里也有人搜“mybatis的分页插件的用法 springboot”我这里给出最简配置。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后查询时这么写PageSeatAllocationVO page new Page(pageNum, pageSize); LambdaQueryWrapperSeatAllocation wrapper new LambdaQueryWrapper(); wrapper.eq(SeatAllocation::getExamSessionId, sessionId); IPageSeatAllocationVO result seatAllocationMapper.selectPageWithCourse(page, wrapper);要注意这个分页插件不配置的话Page对象不生效查出来的还是全量数据。我在早期测试时发现列表页返回了所有记录第一反应以为是前端没传页码排查半天才发现是拦截器没注入。2.3 前端与部署前后端分离还是服务端渲染毕业设计项目里前后端分离已经是主流但我要提醒一句如果团队只有你一个人且你前端基础一般前后端分离意味着你要同时写Vue和Java工作量是翻倍的。我的实际情况是后端API全都走RESTful前端用Vue3 Element Plus搭建管理界面但这套组合在部署时要额外处理跨域和静态资源问题。有一个折中方案我觉得更省心——如果时间紧直接把前端用Vue构建后的dist目录放到SpringBoot的src/main/resources/static下由SpringBoot统一托管这样不需要单独启Nginx也不用配CORS对毕业设计演示非常友好。我在本地开发时用Vite代理转发/api到后端8080端口生产环境直接打成一个jar包docker部署也省事。数据库我用的MySQL 8.0连接串里务必带上时区参数serverTimezoneAsia/Shanghai不然时间字段会差8小时这个坑后面详细说。权限这块我没有用厚重的Spring Security而是选了个轻量级框架Sa-Token登录、鉴权、踢人下线的功能都有学习成本比Security低很多适合单人开发的毕业设计。3. 数据库设计与核心表结构用数据撑起排考逻辑3.1 基础数据表的设计思路排考系统的表设计核心就是要回答四个问题谁考试、考什么、在哪考、什么时候考。我设计了六张核心表student学生表、course课程表、teacher教师表、exam_room考场表、exam_session考试场次表、seat_allocation座位分配表外加一张arrange_task排考任务表用来记录每次自动排考的操作上下文。学生表和课程表不赘述就是最普通的单表结构。考场表要额外存两个字段row_num和col_num表示教室座位的行列数。这个字段非常重要因为自动排座时要根据行列数生成座位矩阵阶梯教室的行列数各不相同必须单独维护。我设计exam_room的关键字段如下字段名类型说明room_idbigint主键room_codevarchar(32)教室编号如A-101campus_namevarchar(64)校区/楼栋capacityint容量冗余字段等于row_num * col_numrow_numint座位行数col_numint座位列数is_availabletinyint是否启用exam_session考试场次表存的是时间信息比如“2025年1月6日上午9:00-11:00”就是一个场次。这里要提醒一个建模细节考试时间应该存到“场次”这个粒度而不是直接存到每条座位记录上。原因很简单同一场考试有几十个考场如果时间字段冗余到座位表改一次考试时间要update几十条数据很容易出现漏改。3.2 座位分配表整张表设计的重中之重seat_allocation是排考系统的核心表它把学生、课程、考场、场次、座位号、准考证号、监考教师全部关联起来。CREATE TABLE seat_allocation ( id bigint NOT NULL AUTO_INCREMENT, arrange_task_id bigint DEFAULT NULL, exam_session_id bigint NOT NULL, course_id bigint NOT NULL, student_id bigint NOT NULL, exam_room_id bigint NOT NULL, seat_no varchar(20) NOT NULL, seat_row int DEFAULT NULL, seat_col int DEFAULT NULL, exam_card_no varchar(32) DEFAULT NULL, invigilator_id bigint DEFAULT NULL, status tinyint DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_session_student (exam_session_id,student_id), UNIQUE KEY uk_room_seat (exam_room_id,seat_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个唯一索引非常关键。uk_session_student保证同一个学生在同一场次只有一个座位这是时间不冲突的兜底uk_room_seat保证同一个考场内同一个座位号只能分配一次。seat_no我设计成“考场号-行-列”的字符串格式比如A101-03-05既能给人看又方便系统排序。因为座位号要支持“同班邻座打散”的校验在存储时额外冗余了seat_row和seat_col两个整型字段查询自己和相邻座位的同学时用整型加减比解析字符串快得多。exam_card_no是准考证号生成规则我是在排考完成后统一刷新的校区码 场次码 5位流水号。用UPDATE语句在事务中统一更新避免自动排座时频繁判断是否重复。3.3 事务边界与冗余表的取舍排考系统的核心操作“一键排考”是一个典型的事务场景先清空该任务下所有旧座位分配再重新计算并批量插入新座位。这个操作涉及多张表的读写必须放在同一个事务里否则排到一半失败旧数据没了新数据又不全整个系统就废了。我在ArrangeService中对一键排考方法加了Transactional(rollbackFor Exception.class)。注意rollbackFor必须写因为Spring默认只回滚RuntimeException选课名单导入时抛出的CheckException如果不指定回滚事务不会回滚。另外数据量优化方面我实测过3500名考生、6个场次、120个考场的完整排考如果一条一条insert耗时在十几秒左右用户体验很差。后来优化成先计算好所有分配结果再用MyBatis-Plus的saveBatch批量插入单次批量500条整个排考流程压缩到3秒以内。提示批量插入时如果把全部结果一次性塞进一条SQL超过MySQL的max_allowed_packet会报错分批是必须的。4. 考场分配与蛇形座位算法核心逻辑拆解4.1 考场容量匹配的贪心策略自动排考的第一步是把某个场次下所有课程的考生分配到合适的考场中去。这里的约束是考场容量不能超员如果一个教室装不下一个课程的全部考生就要拆分到多个教室。我采用的是一种朴素但有效的贪心策略。先把所有参与排考的考试条目按考生人数降序排序人数多的先分配同时把所有空闲考场按容量降序排序容量大的优先分配。这样做的好处是让大考场尽量给大课程用避免出现“大教室空着小教室塞不下”的尴尬。伪代码如下public void assignRooms(ArrangeTask task, ListCourseArrange courseList, ListExamRoom rooms) { // 课程按考生人数降序 courseList.sort(Comparator.comparingInt(CourseArrange::getStudentCount).reversed()); // 考场按容量降序 rooms.sort(Comparator.comparingInt(ExamRoom::getCapacity).reversed()); for (CourseArrange course : courseList) { int remain course.getStudentCount(); for (ExamRoom room : rooms) { if (remain 0) break; int take Math.min(room.getCapacity(), remain); createRoomAllocation(course, room, take); remain - take; } if (remain 0) { // 记录无法安排的课程返回人工处理提示 } } }这里有个细节当某门课程的学生要拆到多个考场时我的算法还加了“最小拆分人数”控制比如容量120人的教室如果只塞进去10个人就拆分太浪费。我设置如果分配人数小于考场容量的30%就优先选择其他更小容量的考场实在没有才允许碎片化分配。4.2 考试时间冲突检测方法排考系统里最严重的错误就是同一个学生被安排到两场在同一时间段的考试。我在排考数据校验阶段用了基于场次分组的冲突检测。检测逻辑不复杂把某场考试的所有学生ID加载到一个HashSet中如果后续场次的学生ID出现在集合里说明该学生跨场次冲突直接拦截并提示。private void checkTimeConflict(ListArrangeTask tasks) { MapLong, SetLong sessionStudentMap new HashMap(); for (ArrangeTask task : tasks) { Long sessionId task.getExamSessionId(); SetLong students sessionStudentMap.computeIfAbsent(sessionId, k - new HashSet()); for (StudentCourse sc : task.getStudentCourses()) { if (!students.add(sc.getStudentId())) { throw new ArrangeException(学生ID: sc.getStudentId() 在同一场次存在重复考试); } } } }这个检测的好处是它不依赖数据库索引全内存执行几万条学生记录也就毫秒级别的耗时。而且我把冲突检测从“自动排考”里独立出来放在“导入选课名单”的步骤后就执行提前发现问题而不是等排考结束去人工筛查。4.3 S型蛇形座位生成核心算法的实现座位编排是整个系统最有技术含量的部分。我采用的是S型蛇形排列法这个方法有一个非常直观的好处学生在考场里是按“之”字型走位入座的同一班级的考生不会连成一排干扰邻座的可能性大幅降低。我先说一下基本原理。假设一个考场有5行6列普通顺序排列是这样的第1行1-6号第2行7-12号第3行13-18号这样第1行的6号和第一列第2行的7号恰好是前后紧挨着的相邻座位。蛇形排法让偶数行反向排列第1行1-6号第2行12-7号反向第3行13-18号这样第2行和第一行同一列的座位就不容易位置相邻学生即使坐在前后排也不是正对着。再加上“隔列入座”策略实际分配时我只用奇数列坐人留空偶数列为隔离列座位利用率降到一半以下但防作弊效果显著提升。核心代码实现如下public String buildSeatNo(ExamRoom room, int row, int col) { boolean reverse (row % 2 0); // 偶数行反向 int actualCol reverse ? (room.getColNum() - col 1) : col; return room.getRoomCode() - String.format(%02d, row) - String.format(%02d, actualCol); } public ListSeatCell generateSeatMatrix(ExamRoom room, ListStudent students) { ListSeatCell cells new ArrayList(); int idx 0; for (int row 1; row room.getRowNum(); row) { boolean reverse (row % 2 0); int start reverse ? room.getColNum() : 1; int end reverse ? 1 : room.getColNum(); int step reverse ? -1 : 1; for (int col start; col ! end step; col step) { if (idx students.size()) break; cells.add(new SeatCell(row, col, students.get(idx))); } } return cells; }这里有一个容易出错的地方当某场考试的考生人数不是考场容量的整倍数时蛇形排列会在末尾留下一个“尾巴”。比如6列5行的考场最后一行只坐了3人我用的是“先列后行”的顺序填充保证末尾缺的是后排而不是中间某行这样从讲台往下看学生的分布相对整齐。4.4 监考教师分配与回避规则监考教师分配很容易被做成一个鸡肋功能。我看了很多同类毕业设计有的直接把教师绑在考场记录上写死交互倒是简单了实际业务一跑就露怯。我的做法是先维护teacher表和course表的授课关系形成“授课教师-课程-班级”的映射。自动排考时监考教师从教师池中轮转选择但有一个硬性回避规则——如果某个教师讲授该课程则不能监考该课程的考试。这个是硬性约束必须写进校验逻辑。当系统发现某考场可用的监考教师数量不足2人时需要提示管理员手动分配同时把“已监考次数”统计到教师表中自动轮转时优先选择监考次数少的教师。这种“公平优先”的简单策略比随机分配看着专业得多在答辩时也是加分项。5. 从数据导入到结果导出一条完整的排考链路复盘5.1 工程搭建与基础配置从零搭建工程的步骤大致是这样IDEA新建SpringBoot项目选择Java 8依赖选Spring Web、MySQL Driver、Lombok然后在pom.xml中手动添加MyBatis-Plus、EasyExcel、Sa-Token、ZXing这几个库。核心的配置文件application.yml我直接贴出来这些都是经验确认过的稳定项server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_arrange?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 20MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml type-aliases-package: com.exam.arrange.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl sa-token: token-name: satoken timeout: 2592000 is-concurrent: false特别注意两点map-underscore-to-camel-case设为true后数据库的exam_session_id能自动映射到实体的examSessionId省一大堆XML里的映射配置日志的StdOutImpl方便开发阶段打印SQL但生产环境一定记得关掉。5.2 核心接口设计与前端交互后端接口我做了统一的ResultT返回体结构是code msg data。前端封装了axios请求库统一拦截401跳登录页。这里简要列出排考相关核心接口。接口路径方法用途/api/arrange/task/createPOST创建排考任务/api/arrange/task/importPOST导入选课名单Excel/api/arrange/task/conflict/checkGET冲突检测/api/arrange/auto/runPOST执行一键排考/api/arrange/adjust/seatPOST手动调座/api/arrange/publishPOST发布考试安排/api/arrange/export/seatGET导出座位表Excel/api/arrange/export/cardGET导出准考证PDF这里我最想强调的是“一键排考”接口的事务设计。它接收一个taskId后端先查该任务下所有课程、所有学生、所有考场然后在事务里执行三步清理旧分配、调用算法生成新分配、批量写库。接口尽量设计成幂等的前端按钮在请求期间要置灰否则用户手抖连续点了两次就会出现同一场次所有座位被清空又重新写入的问题虽然最终结果一致但中间状态让人心里没底。5.3 Excel导入导出与易用性打磨选课名单的导入我用的阿里开源的EasyExcel而不是原生POI。原因很简单5000条以上数据的Excel原生POI的XSSFWorkbook是先把整个文件读进内存再解析内存消耗很夸张而EasyExcel是流式读取模式内存占用稳定在10MB左右答辩演示时不容易卡死。导入Excel的模板里至少要包含三列学号、课程编码、考试场次名称。我在导入时做了三层校验格式校验Excel列数是否对、存在性校验学号是否在student表中存在、业务校验该学生是否已选该课程。校验失败的数据统一收集到一个List中在前端展示“失败原因”而不是导入到一半中断。导出座位表时我按考场分组生成一个Sheet对应一个考场每行包含学号、姓名、班级、座位号、准考证号。这个格式是直接拿给监考老师贴在讲台上用的所以列必须少而清晰。准考证导出用A4纸排版每页10张每张打上学生信息加二维码。6. 实测中踩过的坑五个让人头皮发麻的细节6.1 源发行版17警告与JDK编译版本错位热词里有人搜“java: 警告: 源发行版 17 需要目标发行版 17”这个问题我在换电脑后也遇到过一回。现象是代码里用了var和Switch表达式编译时IDEA提示需要17但项目pom里明明写的是1.8。原因通常是IDEA的Project Structure里的Project SDK和Module SDK设置了不同版本或者Maven默认的compiler参数没有跟着pom走。检查顺序是File - Project Structure - Project Settings - Modules把Language level改成8再把Settings - Build Tools - Maven - Runner里的JRE设为1.8。如果是Maven命令行构建报错就在pom里显式指定properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties这个问题虽然不复杂但它会浪费半天时间因为报错信息极具迷惑性。6.2 MySQL时区导致考试时间偏移8小时这是所有Java Web项目都会遇到的经典坑。某天我创建了一场比赛开始时间填的是9:00页面上回显变成了17:00。查了实体类上的JSON格式化注解、前端的时间选择器最后才定位到是JDBC连接串少了serverTimezoneAsia/Shanghai。MySQL 8.0驱动默认使用服务器时区如果服务器时区是UTC而本地是中国标准时间insert进去的时间就会偏移8小时。解决方案是连接串加上serverTimezoneAsia/Shanghai同时在MySQL里执行set global time_zone 08:00双保险。另外一个经验是所有时间字段在Java实体类上用LocalDateTime接收不要用java.util.Date配合JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解前后端时间格式就统一了。6.3 事务方法内部调用导致Transactional失效我在手动调座功能上踩过一个隐蔽的坑。adjustSeat方法本身加了Transactional但它内部调用了同一个类中的releaseSeat方法。测试的时候发现如果releaseSeat抛了异常事务并没有回滚。原因很经典Spring的声明式事务基于AOP代理代理对象只有在通过外部调用进入时才会触发事务逻辑。同类内部自调用走的是当前对象的直接调用绕过了代理事务注解自然无效。解决方案有两种一是把releaseSeat方法拆到另一个Service类中通过注入的代理对象调用二是使用AopContext.currentProxy()获取当前代理对象再调用。我建议用第一种结构更清晰。6.4 MyBatis-Plus分页插件没配置导致“假分页”这个前面提过这里补充一个细节。MyBatis-Plus的分页插件依赖PaginationInnerInterceptor而且有一个固定的行为分页SQL会先执行一条count查询再执行limit查询。如果自定义Mapper方法里写了复杂的联表查询count语句可能因为include条件写得不严谨而报错。我在导出考场明细时自定义了一个多表联查方法字段里包含重复的列名导致countSQL生成了错误的字段引用报Unknown column id in where clause。排查了半天最后只是给表名加了别名问题就解决了。经验是自定义分页方法里的SQL规范写所有列都要带表别名。6.5 Docker部署后的时区、文件上传与内存限制把系统部署到Docker容器是答辩演示的高频操作遇到的坑比裸机部署多一层容器隔离的复杂度。打包命令我习惯这样写mvn clean package -DskipTests docker build -t exam-arrange:1.0 . docker run -d -p 8080:8080 \ -e TZAsia/Shanghai \ -v /opt/exam/uploads:/app/uploads \ -v /etc/localtime:/etc/localtime:ro \ --name exam-arrange \ exam-arrange:1.0-e TZAsia/Shanghai解决容器内时区问题这个不设的话日志时间和数据库时间都会差8小时排查起来相当痛苦。文件上传目录必须挂载到宿主机否则容器重启后上传的Excel模板就没了。另外提一句JVM内存容器里如果没做限制SpringBoot默认会尝试使用宿主机四分之一内存作为堆内存。我在Dockerfile里加了ENV JAVA_OPTS-Xms256m -Xmx512m然后启动命令改成java $JAVA_OPTS -jar app.jar避免在内存小的服务器上把容器搞崩。7. 功能测试与答辩演示让评委快速看懂你的系统7.1 测试用例设计排考系统的测试思路重点围绕“约束是否被破坏”展开。我在开发过程中设计了一组核心测试用例每个用例都对应一个业务规则。用例编号场景描述输入数据预期结果实测结果T01正常排考300名考生、5门课、3个考场所有考生分配到座位无冲突通过T02教室容量不足1门课200人只有2个60人考场提示“容量不足请增加考场”通过T03学生时间冲突同一学生在两个场次均有选课记录自动排考前拦截提示冲突名单通过T04同班邻座检查同一班级10名学生任意两人座位不相邻通过T05拆场场景1门课150人分配到2个考场分配后每个考场人数不超过容量通过T06调座后重新排考发布后手动修改5个座位重新排考后新座位在原逻辑内有效通过T04是答辩时比较亮眼的用例。我在座位表查询接口里写了一个辅助校验方法遍历每个考场的座位列表比对相邻座位的学生班级是否相同相同则视为违规。这个方法一方面可以自检算法质量另一方面在演示时打开页面对评委说“看到没有同班学生没有邻座”说服力会强很多。7.2 性能和异常场景验证我在一个8G内存、4核CPU的机器上做了压力测试。模拟数据是120个考场、6个场次、3500名考生选课记录约8000条。完整的一键排考从按钮点击到全部座位写入数据库耗时稳定在3秒左右。批量的关键是把计算结果放在内存里先跑完再落库算法本身的时间复杂度不高耗时的重点在数据库写入。异常场景主要测的就是并发点击。我用JMeter模拟了20个线程同时点击“一键排考”按钮由于接口幂等且使用Transactional最终数据一致没有出现重复分配。这里要强调接口设计成“先删除该任务下旧数据再写入新数据”的策略并发时可能出现“last commit wins”的现象但对排考业务来说后发起的排考覆盖先前的排考逻辑上是可接受的。7.3 答辩演示的路径规划如果这是毕业设计答辩用的项目我强烈建议演示时走一条固定的“故事线”不要自由发挥点菜单。我的建议顺序是进入系统 - 展示基础数据学生和教室已经导入 - 新建排考任务 - 导入选课名单 - 演示冲突检测拦截一条错误数据 - 执行一键排考 - 打开座位图展示蛇形排列和同班打散效果 - 导出准考证和考场表 - 演示管理员把某位同学手动调整到另一个座位 - 发布。这条路径走完正好覆盖系统所有核心功能而且每一步都有画面感。中间评委最容易问的问题基本集中在这几个座位分配算法怎么保证公平、时间冲突怎么解决、切换场次会不会串数据、系统能支持多少人。这些问题在开发笔记里有意识地准备一下答案答辩时会从容很多。最后补一句我做完整个项目的体会排考系统的技术难点绝对不在CRUD而在算法约束和事务边界。如果你正在做类似课题建议把核心的排考算法单独抽成一个独立的Service类先写好单元测试确保它正确之后再往上搭Controller和页面。数据流理清楚了代码怎么写都不会乱的。这套系统做完之后你再回头看需求文档会发现“智能排考”这四个字是真的每天都在帮你写Excel。
