
简介本资源为基于Java的学生选课管理系统答辩PPT面向计算机相关专业毕业生、课程设计或毕业设计答辩人以及需要梳理SSMVue技术方案的开发者。整套答辩材料围绕需求分析、可行性论证、功能模块与数据库设计展开可直接用于答辩汇报也能作为撰写论文与整理技术路线的参考。压缩包内仅含1个pptx文件约20.09MB内容涵盖研究背景与开发意义、经济与技术及操作三方面可行性分析、登录与课程信息、选课、成绩查询、作业等界面展示截图以及项目总结与致谢页篇幅完整、条理清晰。系统技术栈为SSM后端配合Vue.js、Vue-Router、Vuex与Element UI前端数据库采用Mysql通过Ajax完成前后端通信。目前已有129人学习适合需要快速成型答辩演示、对照界面截图理解选课业务逻辑与前后端协作方式的读者参考。1. 从一份答辩PPT倒推Java学生选课管理系统真正难在哪答辩现场最容易被追问的不是你用了什么框架而是一门课 200 个名额500 个人在同一秒点选课你的系统会不会放 201 个人进来。基于 Java 的学生选课管理系统课程、学生、教师三张表的增删改查一个下午就能搭完真正拉开水平的是三件事名额扣减在并发下的一致性、上课时间区间冲突的判定、以及退课后名额怎么安全还回去。它适合正在做课程设计或毕业设计的在校生也适合拿它当第一个 Java 后端作品、准备 Java 面试题里并发扣库存那类问题的求职者。下面按选型、建表、接口实现、并发排查、现场演示的顺序把这个系统完整走一遍。2. Java学生选课管理系统的技术选型与MySQL表设计2.1 三层架构与 Spring Boot MyBatis 的落地组合老教材里的实现方式是 JSP Servlet JDBC好处是依赖少坏处是页面和逻辑糊在一起答辩时想单独演示一个接口都做不到。现在的常见做法是 Spring Boot 提供 REST 接口MyBatis 负责 SQL 映射前端可以是 Vue 也可以是 Thymeleaf 服务端渲染把 Controller、Service、Mapper 三层切开。这样做的直接收益是接口能被 Postman 和压测脚本直接调用验证并发问题时不用绕开浏览器。层选型答辩时的说服点控制层Spring MVC接口可被 curl/JMeter 直接压测业务层手写 Service事务边界清晰能讲清回滚点持久层MyBatisSQL 可见方便解释行锁与索引数据库MySQL 8 InnoDB支持行级锁、事务、唯一索引前端Vue 3 / Thymeleaf演示选课结果刷新直观pom.xml里最少要引的几项!-- Web 层提供内嵌 Tomcat 这个 Java 容器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 起步依赖2.x 版本适配 Spring Boot 3 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency版本号不必照抄对齐自己 JDK 的版本即可Spring Boot 3 要求 JDK 17 以上。依赖装完之后如果启动报lombok will not work那是编译器和 Lombok 版本没对齐属于 Java 环境变量配置和 IDE 编译插件这类基础问题答辩机上提前跑一次 clean install 就能暴露。2.2 选课系统必须有的四张核心表表设计决定后面所有逻辑好不好写。学生、课程、教学班、选课记录四张表就够支撑一个可演示的系统教学班是课程在某学期的具体开设容量和已选人数放在教学班而不是课程上因为同一门课不同老师、不同学期的名额是独立的。表名作用关键字段student学生id、学号、姓名、专业course课程id、课程编号、课程名、学分teaching_class教学班容量、已选人数、周几、起止节次course_selection选课记录学生、教学班、状态、选课时间CREATE TABLE teaching_class ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, course_id BIGINT UNSIGNED NOT NULL COMMENT 关联的课程, teacher_id BIGINT UNSIGNED NOT NULL, semester VARCHAR(16) NOT NULL COMMENT 如 2025-2026-1, capacity INT NOT NULL DEFAULT 0 COMMENT 总名额, selected INT NOT NULL DEFAULT 0 COMMENT 已选人数, week_day TINYINT NOT NULL COMMENT 1周一 ... 7周日, section_start TINYINT NOT NULL COMMENT 开始节次, section_end TINYINT NOT NULL COMMENT 结束节次, classroom VARCHAR(32) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可选 0停开, PRIMARY KEY (id), KEY idx_course_semester (course_id, semester) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教学班开课班表; CREATE TABLE course_selection ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, student_id BIGINT UNSIGNED NOT NULL, teaching_class_id BIGINT UNSIGNED NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已选 2已退, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_class (student_id, teaching_class_id), KEY idx_class_status (teaching_class_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表;唯一键uk_student_class是防重复提交的最后一道闸前端按钮连点两次也不会产生两条记录。注意退课如果只把 status 改成 2学生就再也选不回这门课了因为唯一键还在所以退课记录要么物理删除、要么把唯一键加上 status 字段两种做法都要在答辩时能说清取舍。2.3 Mapper 接口没有实现类为什么能注入第一次接触 MyBatis 的人都会卡在这里CourseSelectionMapper只是一个接口没有实现类Spring 却能把它注进 Service。原因是 MyBatis 在启动时用 JDK 动态代理给每个 Mapper 接口生成了代理对象方法调用被代理拦截后去找同名的 SQL 语句执行并把结果映射成对象。这也解释了为什么 Mapper 接口不能重载方法、方法名和 XML 里的 id 必须一一对应。public interface TeachingClassMapper { // 条件扣减名额没满才会更新返回影响行数 int increaseSelected(Param(classId) Long classId); // 退课时归还名额selected 不允许减成负数 int decreaseSelected(Param(classId) Long classId); TeachingClass selectById(Param(classId) Long classId); }update idincreaseSelected UPDATE teaching_class SET selected selected 1 WHERE id #{classId} AND status 1 AND selected lt; capacity /update update iddecreaseSelected UPDATE teaching_class SET selected selected - 1 WHERE id #{classId} AND selected gt; 0 /update#{classId}是预编译占位符不要写成${classId}后者是字符串拼接会留下注入风险。selected lt; capacity这个条件写在 UPDATE 的 WHERE 里意味着判断和扣减在同一条语句里完成靠的是 InnoDB 对该行的排他锁这正是后面解决超卖的关键。3. 选课接口实现容量扣减、时间冲突与退课候补3.1 选课主流程的 Controller 与 Service 分层接口只做参数接收和结果包装业务判断全部放 Service这样压测脚本可以绕过页面直接打接口。Controller 里不要出现任何 SQL 或事务注解答辩时被问到事务从哪开始要能一句话指出位置。RestController RequestMapping(/api/selection) public class SelectionController { private final SelectionService selectionService; // 构造器注入避免字段注入在单元测试里难以替换 public SelectionController(SelectionService selectionService) { this.selectionService selectionService; } PostMapping(/select) public ResultVoid select(RequestParam Long studentId, RequestParam Long classId) { selectionService.selectCourse(studentId, classId); return Result.ok(); } }Service public class SelectionService { private final TeachingClassMapper classMapper; private final CourseSelectionMapper selectionMapper; private final ListSelectStrategy strategies; public SelectionService(TeachingClassMapper classMapper, CourseSelectionMapper selectionMapper, ListSelectStrategy strategies) { this.classMapper classMapper; this.selectionMapper selectionMapper; this.strategies strategies; } Transactional(rollbackFor Exception.class) public void selectCourse(Long studentId, Long classId) { // 1. 冲突校验放在扣减之前避免无效请求长时间持有行锁 checkTimeConflict(studentId, classId); // 2. 原子扣减影响行数为 0 说明名额已满或教学班已停开 if (classMapper.increaseSelected(classId) 0) { throw new BizException(名额已满请选择其他教学班); } // 3. 写入选课记录唯一索引兜底重复提交 selectionMapper.insert(studentId, classId); } }Transactional的边界是整个方法任何一步抛异常都要整体回滚。rollbackFor Exception.class不能省默认只对运行时异常回滚业务里如果抛的是受检异常扣减就白做了。第 3 步插入如果撞上唯一键会抛DuplicateKeyExceptionSpring 会把它归为运行时异常从而触发回滚所以扣了名额但记录写失败的情况不会留下脏数据。3.2 用条件 UPDATE 把名额卡在 capacity 之内超卖的根源是先 SELECT 查余量再 UPDATE 扣减这种两步写法两个线程可能同时查到还有 1 个名额。改成单条条件更新后数据库行锁保证了同一行的判断和修改是原子的。代价是拿不到精确的失败原因只能知道没扣成功需要更细的提示时再补一次查询。两种写法的差别可以直接列出来对比写法是否存在超卖说明先查后改存在两个事务可能都读到 selected199条件 UPDATE不存在WHERE 在行锁内求值SELECT ... FOR UPDATE不存在悲观锁事务变长容易堆积SELECT ... FOR UPDATE也能防超卖但它把锁的持有时间拉长到整个事务结束冲突多时表现为请求排队、响应变慢。条件 UPDATE 只在语句执行期间持锁更适合抢课这种瞬时高冲突场景。如果确实需要版本号控制可以在教学班表加version字段把AND version #{version}拼进 WHERE更新后版本自增效果一致但要多一次读取。3.3 上课时间冲突的 SQL 判定条件时间冲突的本质是两个闭区间是否相交。设已选课程区间为 [a1, a2]目标课程区间为 [b1, b2]相交的条件是a1 b2 AND b1 a2。这个条件直接翻译成 SQL交给数据库过滤比把学生所有已选课程拉回 Java 里循环比较要省得多。SELECT COUNT(1) FROM course_selection cs JOIN teaching_class tc ON tc.id cs.teaching_class_id WHERE cs.student_id #{studentId} AND cs.status 1 AND tc.semester #{semester} AND tc.week_day #{weekDay} AND tc.section_start lt; #{sectionEnd} AND tc.section_end gt; #{sectionStart}semester必须参与过滤否则上学期选过的课会把这学期的选课全判成冲突。week_day不同直接跳过剩下的才做节次区间比较。返回的 COUNT 大于 0 就抛业务异常。实际排课里还有单双周、前八周这种更细的维度简单做法是在教学班表加week_pattern字段先判断周次是否重叠再判节次。3.4 退课、候补与选课规则的策略组合退课的实现顺序是先把选课记录状态置为已退或直接删除再调用decreaseSelected归还名额两步在同一个事务里。顺序不能颠倒否则中途失败会出现名额还了但记录还在的情况。归还语句里的selected 0是防负数兜底重复退课请求打进来也不会把计数减穿。不同课程的选课规则差异很大必修课直选直中、公共选修课超额抽签、专业限选课按绩点排队。把规则写成 if-else 会越来越长常见做法是策略模式每种规则一个实现类由 Spring 注入成 List。public interface SelectStrategy { // 该教学班是否适用本策略 boolean support(TeachingClass tc); // 执行选课具体规则各自实现 void doSelect(Long studentId, Long classId); } Component public class DirectSelectStrategy implements SelectStrategy { Override public boolean support(TeachingClass tc) { return tc.getCourseType() CourseType.MANDATORY; } Override public void doSelect(Long studentId, Long classId) { // 必修课名额够就直接占位 } }注入ListSelectStrategy时 Spring 会按Order或类名顺序放入业务代码遍历找到第一个support为 true 的策略执行。答辩被问到扩展性时这套结构能直接回答新增一种抽签规则只需要加一个类不改主流程。4. 选课高并发下的压测、报错排查与参数调优4.1 用脚本模拟 200 人抢同一门课先造一门 200 名额的课用 shell 并发打接口是最快的验证方式把请求丢到后台wait等全部结束。这个脚本不统计成功数适合先看服务会不会被打挂。#!/bin/bash # 200 个请求同时抢 classId1001 的教学班 CLASS_ID1001 for i in $(seq 1 200); do curl -s -X POST http://127.0.0.1:8080/api/selection/select \ -d studentId$iclassId$CLASS_ID done wait echo 全部请求已发出想拿到精确的成功计数用 Java 写并发测试更可靠CountDownLatch在这里有两个用途一个当起跑线让所有线程在同一时刻释放一个当终点线等所有线程都执行完再统计结果。int threads 300; ExecutorService pool Executors.newFixedThreadPool(threads); CountDownLatch startGate new CountDownLatch(1); // 起跑线 CountDownLatch endGate new CountDownLatch(threads); // 终点线 AtomicInteger success new AtomicInteger(); for (long i 1; i threads; i) { final long studentId i; pool.execute(() - { try { startGate.await(); // 全部线程在此阻塞直到统一放行 if (select(studentId, 1001L)) { success.incrementAndGet(); } } catch (Exception e) { // 名额已满属于预期失败不计入成功数 } finally { endGate.countDown(); // 每个线程结束都要报数 } }); } startGate.countDown(); // 放行 endGate.await(); // 等待线程全部完成 pool.shutdown(); System.out.println(成功选课人数 success.get());名额设为 200 时success.get()必须正好等于 200教学班表的selected也必须等于 200。如果跑出 201说明扣减逻辑还是先查后改或者事务隔离级别和预期不一致。多跑几轮取最差结果比跑一次通过就收工更有说服力。4.2 三类典型报错与对应根因压测过程里最容易撞上的问题就那么几类提前把日志特征和处置方式整理成表排错时不用现查。现象日志特征根因处置名额超卖selected capacity先查后改判断与扣减分离改条件 UPDATE事务死锁Deadlock found when trying to get lock多行加锁顺序不一致固定按 id 升序更新连接拿不到HikariPool-1 - Connection is not available池太小或在事务里做远程调用调大池并缩短事务重复选课Duplicate entry for key uk_student_class前端连点或重试捕获后返回已选过死锁那条值得多说一句如果一个事务先更新教学班再写选课记录另一个事务顺序相反交叉持锁就会死锁。统一成先更新教学班、后写记录能消掉大部分这类问题。连接池告警则通常是事务里夹了 HTTP 调用或文件操作把耗时动作挪到事务外即可。4.3 索引与连接池参数调整清单选课记录的查询有两个方向按学生查已选课表、按教学班统计人数索引要两边都覆盖。只建student_id一个索引统计某班人数时就会全表扫。-- 查课表、判冲突走这个索引 ALTER TABLE course_selection ADD INDEX idx_student_status (student_id, status); -- 统计教学班人数、批量退课走这个索引 ALTER TABLE course_selection ADD INDEX idx_class_status (teaching_class_id, status);参数默认值建议值作用server.tomcat.threads.max200400请求处理线程上限hikari.maximum-pool-size1050连接池上限别超数据库 max_connectionshikari.connection-timeout300003000拿不到连接快速失败避免线程堆积innodb_lock_wait_timeout5010行锁等待秒数抢课场景缩短更快暴露问题调整顺序是先看SHOW ENGINE INNODB STATUS里的锁等待再看连接池活跃数最后才动线程数。把参数一次全调大会掩盖问题答辩时也说不清每个值的依据。5. 答辩现场把选课链路演示成一个能自证的系统5.1 演示数据的准备脚本演示最怕现场造数据来不及提前把脚本写好讲之前执行一次就有干净的初始状态。造一门 200 名额、周三 3 到 4 节的课方便现场把名额抢空。-- 回滚到初始状态保证每轮演示结果一致 UPDATE teaching_class SET selected 0 WHERE id 1001; DELETE FROM course_selection WHERE teaching_class_id 1001; -- 造一门容量 200 的教学班用于现场压测 INSERT INTO teaching_class (course_id, teacher_id, semester, capacity, selected, week_day, section_start, section_end, classroom, status) VALUES (1, 1, 2025-2026-1, 200, 0, 3, 3, 4, 教三-201, 1);selected和course_selection必须一起重置只清计数不清记录下一轮演示就会撞唯一键看起来像系统有 bug。5.2 答辩追问的应答要点评审的问题通常围绕为什么这么做而不是会不会写提前准备几组对比式回答比背代码有效。追问应答要点为什么不用 synchronized单机锁在多实例部署下失效还会把无关课程串行化条件 UPDATE 每门课互不影响时间冲突为什么交给 SQL课表数据在库里一次区间比较比拉几十条记录回内存再循环更省 IO退课归还名额会不会减成负数归还语句带 selected 0 条件且与记录删除在同一事务内名额扣减失败怎么给用户提示影响行数为 0 时回查一次教学班区分已满和已停开5.3 把压测脚本放进演示目录演示机上提前把服务起好把 4.1 的并发测试类打成一个可执行 jar评审问到并发时就地跑一遍把成功选课人数 200和数据库里selected 200两张截图并排放在同一页。截图的时间戳、班级 id、名额数三者能对上比口头解释超卖是怎么避免的更有分量。演示目录里同时留一份git log把条件 UPDATE 那次提交单独标出来评审追问实现演进时可以直接翻到改动前后两版 SQL 的差异。本文还有配套的精品资源点击获取