
简介面向高校计算机相关专业学生的教务管理系统数据库课程设计资料包围绕 MySQL 与 Java 技术栈提供从需求分析、数据库设计到系统实现与测试的完整实践案例适合正在学习数据库原理、Java 应用开发或准备完成课程设计的人群参考与改造。压缩包共 28 个文件、约 4.45MB包含 9 个 Java 源码与对应 class 字节码、properties 配置、jar 依赖、SQL 建表脚本以及工程配置与说明文件基本覆盖数据库建模、表结构落地和 Java 端功能实现的完整链路便于对照理解教务管理场景中的数据关系与开发流程。已有 2649 人学习说明其在课程设计选题中具有一定参考价值读者可从中获取需求梳理思路、数据库表设计与 SQL 脚本、Java 实现代码及测试报告框架并在此基础上按自身题目调整模块与字段形成个性化方案。1. 从一张排课表说起教务管理系统要解决的问题1.1 需求从哪里来每到学期初教务处桌面上那张 Excel 排课表就是整个系统的需求源头学生要选课、教师要在期末录成绩、管理员要把教室和时间片排开还不能撞车。数据库课程设计里教务管理系统是命中率很高的选题MySQL 加 Java 的组合也最稳不用装容器、不用配中间件一个可执行 jar 加一个库就能跑通全流程。但真正拉开分数差距的不是界面有几个菜单而是能不能把选课超卖、成绩并发覆盖、排课冲突这三件事在表结构和事务里说清楚。下面按需求、模型、架构、模块、避坑、验收的顺序走一遍适合要交课程设计、也要在答辩时被追问 SQL 的人。1.2 这套方案适合谁三类人可以直接拿去改正在做数据库课程设计、需要一份能跑通且有设计说明的完整方案的学生要做课程作业答辩、担心被问「你的索引为什么这么建」的人以及刚接手校园信息化小项目、需要一个最小可用教务模型的开发者。规模定位要先说清楚这套设计面向单校区、万级学生、千级教学班的量级不追求分布式和高并发秒杀追求的是约束完整、事务正确、SQL 写得明白。把这个边界写在设计文档第一页答辩时能省掉一半的质疑。2. 需求到数据模型四类实体与建表脚本的落地2.1 角色梳理与业务用例边界教务系统的角色只有三个但每个角色背后的动作差别很大。学生关心的是「我这学期选了什么、还剩多少学分、成绩出来没有」教师关心的是「我教哪几个教学班、名单里有多少人、成绩录进去能不能改」管理员关心的是「哪些课程这学期开、谁来讲、哪个教室哪个时间段」。把这三条线摊开用例其实就七条课程维护、教学班开设、选课与退课、名单查询、成绩录入与修改、成绩查询与统计、排课与冲突校验。写需求文档时不要贪多把奖学金、学籍异动、培养方案这些扩展项列进「非本期范围」比硬塞进来做不完要好得多。这里有个容易被忽略的点课程course和教学班section必须拆成两张表。课程是「数据结构 3 学分」这种稳定信息教学班是「2024 秋季 数据结构 周一第 3-4 节 王老师 A301」这种每学期都变的信息。很多课设把两者合成一张表结果一门课开两个班就得复制两行后续统计学分和选课人数全是坑。这个拆分是整个模型的地基答辩时被问到一定要讲出来。实体清单最终确定为学生、教师、课程、教学班、选课记录、成绩、教室、时间片、系统账号。其中成绩单独成表而不是并入选课记录理由后面在事务那章细说。2.2 表结构设计与字段约束字段设计的第一原则是约束往前放。能用 NOT NULL、UNIQUE、CHECK、外键拦住的错误不要留给 Java 层去判因为 Java 层总有漏判的分支而数据库约束是最后一道闸。-- 学生表学号做主键班级和年级用于统计维度 CREATE TABLE student ( student_no VARCHAR(12) NOT NULL COMMENT 学号业务主键, name VARCHAR(32) NOT NULL, gender CHAR(1) NOT NULL DEFAULT U COMMENT M/F/U, grade_year SMALLINT NOT NULL COMMENT 入学年份, class_name VARCHAR(32) NOT NULL, total_credit DECIMAL(5,1) NOT NULL DEFAULT 0 COMMENT 已选学分合计, PRIMARY KEY (student_no), KEY idx_class (class_name, grade_year) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci; -- 教学班一门课在某学期的一次开课 CREATE TABLE section ( section_id BIGINT NOT NULL AUTO_INCREMENT, course_id VARCHAR(16) NOT NULL, teacher_no VARCHAR(12) NOT NULL, semester VARCHAR(16) NOT NULL COMMENT 如 2024-2025-1, capacity INT NOT NULL DEFAULT 60, selected INT NOT NULL DEFAULT 0 COMMENT 已选人数做容量扣减, day_of_week TINYINT NOT NULL COMMENT 1-7, start_slot TINYINT NOT NULL COMMENT 第几节开始, slot_count TINYINT NOT NULL DEFAULT 2, room_id VARCHAR(16) NOT NULL, PRIMARY KEY (section_id), KEY idx_semester (semester, course_id), KEY idx_room_time (room_id, day_of_week, start_slot), CONSTRAINT ck_capacity CHECK (capacity 0 AND selected 0 AND selected capacity) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录唯一索引是防重复选课的最后一道闸 CREATE TABLE enrollment ( id BIGINT NOT NULL AUTO_INCREMENT, student_no VARCHAR(12) NOT NULL, section_id BIGINT NOT NULL, enroll_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在选 0已退, PRIMARY KEY (id), UNIQUE KEY uk_stu_section (student_no, section_id), KEY idx_section (section_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表独立于选课支持重修和多次录入留痕 CREATE TABLE score ( id BIGINT NOT NULL AUTO_INCREMENT, student_no VARCHAR(12) NOT NULL, section_id BIGINT NOT NULL, usual_score DECIMAL(4,1) NULL COMMENT 平时分, exam_score DECIMAL(4,1) NULL COMMENT 考试分, final_score DECIMAL(4,1) NULL COMMENT 总评, grade_point DECIMAL(4,2) NULL COMMENT 绩点, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stu_sec (student_no, section_id), CONSTRAINT ck_score_range CHECK (final_score IS NULL OR (final_score 0 AND final_score 100)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几处设计意图要说清楚。section.selected是冗余字段正常范式里它应该由enrollment表 count 出来但选课时如果每次实时 count会有「查完再插」的竞态而冗余一个计数配合后面的条件更新UPDATE ... WHERE selected capacity就能把并发控制压到一条 SQL 里这是典型的用少量冗余换一致性。enrollment上的uk_stu_section唯一索引同理它挡住的是「同一学生同一教学班两条记录」这是应用层判断最容易在并发下失效的地方。score单独成表的理由是重修和留痕。选课记录一退成绩如果挂在选课表上就一起丢了而重修场景下一个学生在一门课上可能有多条历史成绩独立表配uk_stu_sec唯一约束既能保证当期唯一又能在扩展时改成带学期的联合唯一。DECIMAL(4,1)而不是FLOAT存分数是因为浮点数在求和算绩点时会出现 89.99999 这种玄学结果被老师当场质疑就很难解释。day_of_week start_slot slot_count用三段整数表示时间比存TIME字符串好比较也好建索引。教室用room_id关联教室表冲突检测时直接在这两列上做区间判断。2.3 索引取舍与建表顺序索引不是越多越好。这套表结构里我一般只保四类索引主键、唯一约束、外键方向上的查询列、以及按学期和班级的统计列。像student.name这种模糊搜索列早期不上索引因为LIKE %张%用不上 B 树加了也是白占空间。建表顺序要按依赖走course → teacher → student → room → section → enrollment → score。外键在本方案里建议只在enrollment和score上建且用ON DELETE RESTRICT而不是CASCADE。原因很实在课程设计答辩时如果被问「删掉一个学生他的成绩怎么办」RESTRICT能让你回答「不允许物理删除走状态位软删」而CASCADE会让你在演示时一不小心把数据删空那才是真的翻车。另外提醒一个字符集问题库、表、连接三处的字符集必须一致全部utf8mb4。曾经见过库是 utf8mb4、JDBC 连接串没写characterEncoding结果学生姓名里的生僻字存进去变成问号查出来对不上排查了半天才发现是连接层的事。3. 技术选型与总体架构JDBC 三层结构与连接管理3.1 为什么不用重型框架课程设计的评分点在设计能力不在框架熟练度。用 MyBatis 或 Spring Boot 当然更快但答辩时被问「你这个事务是怎么开的」「SQL 在哪拼的」答不上来就尴尬。直接用 JDBC 加手写 DAO虽然代码量多一些但每条 SQL、每次提交、每个连接关闭都在明面上讲得清楚。具体选型JDK 8 或以上、MySQL 8.0、JDBC 驱动mysql-connector-j、连接池用 HikariCP也可以手写一个简易池但 Hikari 配置简单、文档全课程设计够用。界面层用 Swing 或 JavaFX 都行这一层不是重点能演示流程即可。构建用 Maven把驱动和连接池版本固定下来避免换机器跑不起来。要注意 MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver老教程里的com.mysql.jdbc.Driver会打一堆警告甚至报错。连接串必须带serverTimezoneAsia/Shanghai否则DATETIME读写会出现 8 小时偏移这种问题在自己机器上看不出来一换环境就暴露。3.2 三层包结构与请求链路包结构按职责切edu-system ├── ui 界面层只负责收集输入和展示结果 ├── service 业务层事务边界都在这一层开启和提交 ├── dao 数据访问层只放 SQL 和结果集映射 ├── entity 实体类与表字段一一对应 ├── util 连接池、日期工具、异常工具 └── config 数据库配置读取一次「学生选课」的调用链路是这样的界面层拿到学号和教学班号做非空校验后交给EnrollService.enroll()service 从连接池取连接、关掉自动提交先执行容量扣减的UPDATE判断影响行数再插入enrollment两步都成功才commit任何一步异常就rollbackdao 层只负责PreparedStatement的赋值和执行。事务边界放在 service 而不是 dao是因为 dao 无法知道一次业务动作包含几条 SQL。这个分层的价值在答辩时很直观把 service 层的方法签名列出来就是一份业务清单把 dao 层的 SQL 列出来就是一份完整的数据访问说明。评审老师顺着看下来逻辑自洽。3.3 连接池与配置外置配置文件不要硬编码在代码里单独放db.properties打包时排除或者放外部目录# db.properties jdbc.urljdbc:mysql://127.0.0.1:3306/edu_system?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameedu_app jdbc.password****** jdbc.maxPoolSize10 jdbc.minIdle2 jdbc.connectionTimeout3000连接池大小的经验值课程设计级别的并发10 个连接绰绰有余。设太大反而会掩盖「连接没关」的 bug——池子够大泄漏的连接短时间看不出问题演示到一半池子耗尽才开始报错。所以我在调试阶段会把maxPoolSize故意调到 4逼自己在压测时把没关的连接全暴露出来。public final class DbUtil { private static final HikariDataSource DS; static { Properties p new Properties(); try (InputStream in DbUtil.class.getClassLoader().getResourceAsStream(db.properties)) { p.load(in); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置加载失败: e.getMessage()); } HikariConfig cfg new HikariConfig(); cfg.setJdbcUrl(p.getProperty(jdbc.url)); cfg.setUsername(p.getProperty(jdbc.username)); cfg.setPassword(p.getProperty(jdbc.password)); cfg.setMaximumPoolSize(Integer.parseInt(p.getProperty(jdbc.maxPoolSize))); cfg.setMinimumIdle(Integer.parseInt(p.getProperty(jdbc.minIdle))); cfg.setConnectionTimeout(Long.parseLong(p.getProperty(jdbc.connectionTimeout))); cfg.setPoolName(edu-pool); DS new HikariDataSource(cfg); } public static Connection getConnection() throws SQLException { return DS.getConnection(); } public static void shutdown() { DS.close(); } }DbUtil用静态块初始化全局只建一个池getConnection()每次从池里借用完必须close()——注意这里的close()是归还连接而不是真的关闭物理连接这是连接池最容易误解的一点很多人以为close掉池子就废了。配置里的allowPublicKeyRetrievaltrue是 MySQL 8 默认认证插件下的常见提示配合useSSLfalse在本地开发时能避免握手失败生产环境另说课设场景够用。4. 核心模块实现选课、成绩、排课的 SQL 与事务4.1 选课并发与容量扣减选课是整个系统里唯一有真实并发压力的动作。开学第一天几十个学生同时点「选课」如果写成「先 count 再 insert」两个请求都读到 59 人、都判断没满、都插入结果 61 人挤进 60 人的班这就是超卖。正确写法是把判断和扣减压进一条UPDATE靠数据库的行锁保证原子性public boolean enroll(String studentNo, long sectionId) throws SQLException { Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 第一步条件更新扣减容量WHERE 里带容量判断影响行数为 0 说明已满 String sqlUpdate UPDATE section SET selected selected 1 WHERE section_id ? AND selected capacity; int affected; try (PreparedStatement ps conn.prepareStatement(sqlUpdate)) { ps.setLong(1, sectionId); affected ps.executeUpdate(); } if (affected 0) { conn.rollback(); return false; // 容量已满直接返回由界面提示 } // 第二步插入选课记录唯一索引兜住重复选课并发的冲突 String sqlInsert INSERT INTO enrollment(student_no, section_id, status) VALUES (?, ?, 1); try (PreparedStatement ps conn.prepareStatement(sqlInsert)) { ps.setString(1, studentNo); ps.setLong(2, sectionId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLIntegrityConstraintViolationException dup) { // 唯一键冲突重复选课回滚后按业务失败处理 if (conn ! null) conn.rollback(); return false; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); // 归还连接池 } } }这段代码里三个关键点。第一UPDATE ... WHERE selected capacity在 InnoDB 下会对命中的行加排他锁两个并发请求会串行执行后一个读到的selected已经是加过的值所以不会超卖。这比SELECT ... FOR UPDATE再更新少一次往返也更好讲。第二enrollment的唯一索引是兜底。极端情况下比如同一学生快速点两次第一步的容量扣减会成功两次第二次插入时唯一键冲突抛SQLIntegrityConstraintViolationException捕获后回滚容量扣减也一起撤销数据依然一致。这个异常必须单独 catch不能混在通用SQLException里否则你没法区分「重复选课」和「数据库挂了」。第三事务隔离级别用READ_COMMITTED而不是默认的REPEATABLE_READ。选课不需要可重复读用低一级的隔离级别能减少间隙锁带来的死锁概率这是实践中踩过才知道的细节——两个学生同时选两个不同的班在REPEATABLE_READ下可能因为间隙锁互相等待日志里报Deadlock found when trying to get lock。退课逻辑是对称的先把enrollment.status改成 0软删保留痕迹再UPDATE section SET selected selected - 1 WHERE section_id ? AND selected 0同样一个事务包住。4.2 成绩录入的批量提交与回滚一个教学班 60 人老师一次性录完提交如果循环 60 次单条 insert网络往返就是 60 次而且中途失败会留下半截数据。正确做法是addBatch加一个事务public int saveScores(long sectionId, ListScoreRow rows) throws SQLException { String sql INSERT INTO score(student_no, section_id, usual_score, exam_score, final_score, grade_point) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE usual_scoreVALUES(usual_score), exam_scoreVALUES(exam_score), final_scoreVALUES(final_score), grade_pointVALUES(grade_point); Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (ScoreRow r : rows) { ps.setString(1, r.getStudentNo()); ps.setLong(2, sectionId); ps.setBigDecimal(3, r.getUsualScore()); ps.setBigDecimal(4, r.getExamScore()); ps.setBigDecimal(5, r.getFinalScore()); // 总评在 service 层已算好 ps.setBigDecimal(6, r.getGradePoint()); ps.addBatch(); } int[] result ps.executeBatch(); conn.commit(); return result.length; } } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }ON DUPLICATE KEY UPDATE让这段代码同时兼顾「首次录入」和「修改成绩」两种场景配合uk_stu_sec唯一键一条 SQL 解决不用先查再判。总评和绩点必须在进入批处理前算好不要在 SQL 里做加权计算因为绩点换算规则比如 90 分以上 4.0、60 分以下 0将来要改放在 Java 常量里改起来方便也便于在文档里单独列一张换算表。整批失败整批回滚是这里的设计原则老师录 60 个人如果第 37 个人学号打错导致失败不应该留下前 36 条。回滚后界面提示「第 37 行学号不存在」老师改完重提比数据半对半错好收拾得多。4.3 排课冲突检测的一条 SQL排课冲突有两个维度同一教室同一时间不能有两个班同一教师同一时间不能有两个班。判断逻辑是时间区间重叠用整数节次表示后重叠条件就是A.start B.end AND B.start A.end。-- 检查某教室在某天某时段是否已被占用排除自己 SELECT s.section_id, c.course_name, s.start_slot, s.slot_count FROM section s JOIN course c ON c.course_id s.course_id WHERE s.semester ? AND s.day_of_week ? AND s.room_id ? AND s.section_id ? AND s.start_slot ? ? -- 已有课程开始 新课程结束 AND ? s.start_slot s.slot_count -- 新课程开始 已有课程结束 LIMIT 5;参数顺序是学期、星期、教室、排除的教学班 id、新开始节次、新持续节数、新开始节次。把教师维度再查一遍两段结果都为空才允许保存。用LIMIT 5是刻意的冲突一般不会超过几个返回全部没意义还能避免大结果集。idx_room_time (room_id, day_of_week, start_slot)这个复合索引就是为这条查询建的走索引后扫描范围能压到很小。注意索引列顺序必须按等值条件在前、范围条件在后写成(day_of_week, room_id, start_slot)也能用但选择性不如前者。排课还有个隐含约束教学班的教室容量要大于等于选课容量这个在保存教学班时用一条SELECT capacity FROM room WHERE room_id ?校验即可不值得放进 SQL 约束里。4.4 分页查询与动态条件拼接管理员查名单、学生查成绩都需要「按条件筛 分页」。动态条件用StringBuilder拼WHERE但值一律走占位符public ListEnrollView pageQuery(String semester, String keyword, int page, int size) throws SQLException { StringBuilder sql new StringBuilder( SELECT e.student_no, st.name, c.course_name, t.name AS teacher, s.semester, e.status FROM enrollment e JOIN student st ON st.student_no e.student_no JOIN section s ON s.section_id e.section_id JOIN course c ON c.course_id s.course_id JOIN teacher t ON t.teacher_no s.teacher_no WHERE 11 ); ListObject params new ArrayList(); if (semester ! null !semester.isEmpty()) { sql.append(AND s.semester ? ); params.add(semester); } if (keyword ! null !keyword.isEmpty()) { sql.append(AND (e.student_no LIKE ? OR st.name LIKE ?) ); params.add(% keyword %); params.add(% keyword %); } sql.append(ORDER BY e.student_no LIMIT ? OFFSET ?); params.add(size); params.add((page - 1) * size); // 后续按 params 顺序 setObject再映射结果集 ... }WHERE 11是拼接写法里最省事的技巧省掉「第一个条件不加 AND」的判断分支。LIMIT ? OFFSET ?也用占位符别觉得分页参数是数字就直接拼字符串习惯一旦养成别处的注入漏洞就是迟早的事。深分页OFFSET 很大在这个数据量下不是问题万级数据 OFFSET 几千仍然很快。如果将来要优化改成「记住上一页最后一个学号用WHERE student_no ?」的游标分页这个可以写进设计文档的「扩展方向」答辩时是加分项。5. 避坑与排查教务系统最容易翻车的五个点5.1 选课超卖现象、原因与解决现象压测时发现某个 60 人教学班选进去 63 个人界面显示「已满」但名单超员。原因代码写成「先SELECT COUNT(*)判断再INSERT」两条语句之间没有锁并发下多个请求读到相同的未满状态。这是最典型的「检查后使用」竞态。解决改成 4.1 节的条件更新加唯一索引兜底把判断和写入合并到数据库的一次原子操作里。同时把section.selected的CHECK约束补上即使代码再出错数据库也会拒掉越界的更新。5.2 成绩并发覆盖现象、原因与解决现象两位老师同时打开同一个教学班录成绩后保存的把先保存的整批覆盖先保存的老师坚持说自己录过。原因整批ON DUPLICATE KEY UPDATE是覆盖式写入没有版本或时间戳判断谁最后执行谁赢。解决给score表加version INT NOT NULL DEFAULT 1更新时带WHERE version ?影响行数为 0 就提示「数据已被他人修改请刷新后重试」。这套乐观锁在课设里只需要几行代码但讲出来能让评审看到你考虑过并发写。5.3 时间与字符集现象、原因与解决现象成绩录入时间显示比实际早 8 小时学生姓名里的生僻字存进去变成问号查出来是乱码。原因时间问题是连接串缺serverTimezoneMySQL 服务端和 JVM 默认时区不一致乱码问题通常是库/表/连接三处字符集没统一或者连接串少了characterEncodingutf8mb4。解决连接串固定写全参数建库语句显式指定DEFAULT CHARSETutf8mb4建表语句再写一遍。排查时用SHOW VARIABLES LIKE character_set%看服务端用SELECT HEX(name) FROM student WHERE ...看实际存储的字节能立刻分清是存坏了还是读坏了。5.4 连接泄漏现象、原因与解决现象系统能正常用十几分钟然后所有操作卡住日志里出现「Connection is not available, request timed out」。原因某个 dao 方法里Connection没在finally里关闭或者抛出异常时跳过了close。连接池的 10 个连接被慢慢耗尽。解决全部改成 try-with-resourcesConnection、PreparedStatement、ResultSet都在括号里声明调试阶段把池大小调到 4 做压力测试泄漏会很快暴露。另外setAutoCommit(false)的地方finally里记得恢复成true再归还否则这个连接回到池里还带着未提交的事务状态下一个借到它的人会莫名其妙地读不到数据。5.5 外键级联与演示事故现象、原因与解决现象演示「删除课程」功能时一点确认这个课程下所有教学班的选课记录和成绩全没了。原因建表时图省事用了ON DELETE CASCADE删除沿外键链一路传导。解决教务数据里的删除一律改成软删加status字段DELETE语句从 dao 层删掉只保留UPDATE ... SET status 0。外键改成ON DELETE RESTRICT让数据库在物理层面拦住误删。演示脚本里也要避开真实删除的入口血泪经验是演练时手滑一次答辩当场就没数据可讲了。6. 验收与答辩用二十分钟证明这套设计站得住验收不是把功能点挨个点一遍而是按「数据一致性 → 业务正确性 → 边界处理」三层去证。我的习惯是准备一份固定的验收清单答辩前自己走一遍每一行都能当场演示或当场执行 SQL 验证。验收项验证方式通过标准表结构与约束执行建表脚本SHOW CREATE TABLE逐表检查主键、唯一键、CHECK、外键齐全无遗漏选课并发用两个线程各发起 100 次选同一 60 人班成功次数恰好 60selected与enrollment计数一致重复选课同一学生连续提交两次第二次返回失败提示enrollment只有一条成绩批量录入提交 60 行含 1 行错误学号整批回滚表中零新增界面提示行号排课冲突故意排同教室同时段两个班保存被拒绝提示冲突的教学班分页与筛选按学期 姓名关键字查询翻到最后一页总数正确末页条数符合取模结果连接回收连续操作 500 次后查池活跃连接数活跃连接回落到 0无超时演示顺序建议反着来先讲数据模型和约束再跑选课并发最后跑成绩批量。原因是并发和批量是最能体现设计功底的两个场景放在中段注意力最集中的时候。如果时间被压缩砍掉界面截图保留这两个。答辩提问高频集中在三处提前准备答案一是「为什么selected要冗余」答并发控制和一条 SQL 完成扣减二是「唯一索引冲突怎么处理」答捕获SQLIntegrityConstraintViolationException并回滚整个事务三是「为什么成绩不放在选课表里」答重修留痕和退课不丢成绩。这三个问题几乎每次都会被问到答得干脆后面的追问就会往你熟悉的方向走。还有一个实用技巧把关键 SQL 单独整理成一个demo.sql里面按顺序放建库、建表、造测试数据、几条验证查询。答辩现场如果界面卡了或者环境不对直接开命令行跑这个脚本结果一样能说明问题。这个文件我每次都会准备因为它救过我一次——现场投影连不上开发机靠它撑完了整场。从那以后我每次交课程设计都会强制走一遍「清空数据库 → 跑建表脚本 → 跑测试数据 → 走一遍验收清单」的完整流程任何一步报错就说明这套东西还不可交付。这套流程花不了半小时但能挡掉绝大多数现场翻车。希望帮到你。本文还有配套的精品资源点击获取