简介:一份基于MySQL+Java的数据库课程设计学生选课信息管理系统完整资源,覆盖学生、教师、管理员三类角色,适用于高校数据库课程设计或毕业设计参考。系统采用C/S架构,功能涵盖学生选课退课、成绩查询、教师成绩录入、管理员课程与选课管理等典型业务模块。资源为rar压缩包,约1.99MB,主要包含Java源代码工程与配套设计报告,设计报告中对数据库建表结构、表间关系进行了合理规范的设计说明,方便对照学习从需求分析到表结构落地的完整流程。由于包体较小,适合快速下载查阅;目前已有6064人学习下载。除了可直接运行功能外,配套设计报告还能帮助读者理清学生选课场景下的权限划分、数据表设计原则以及Java与MySQL的交互方式,对完成同类课程设计具有较强的参考价值。
1. 学生选课管理系统值不值得做:先想清楚“选课”为什么是数据库课设的首选
很多同学拿到“学生选课信息管理系统”这个题目,第一反应是去搜源代码,搜到一套 Java + MySQL 的东西就开始跑。但实际交上去之后,代码能跑、页面能开,答辩却一问就卡壳——老师随口问一句“选课表为什么用联合主键”“两个人同时选同一门课会不会重复”,你答不上来,分数就下去了。
这个系统的核心是 MySQL 里的表关系设计和 Java 侧的事务控制,不是那几十个页面。学生选课本质上就是一个“先查容量、再插入记录、最后更新余量”的过程,三个动作必须在一个事务里完成。数据库设计对了,Java 代码写起来会很顺;数据库设计错了,后面怎么改都是补丁。这也是这门课设最有价值的点:用最小规模的系统,把现实系统里最常见的并发、约束、事务问题全暴露出来。本文从建表到 Java 实现、再到设计报告,给你一条能直接复现的路径。
2. 数据库模型设计:MySQL 里这几张表的结构决定你后面好不好写
代码写得糙一点,后面还能修;表建得不合理,改了表结构就等于重写。选课系统涉及的角色就三类:学生、教师、管理员,但真正复杂的不是角色,而是角色之间的选课关系。先想清楚实体和关系,再落 DDL。
2.1 从需求反推实体:学生、教师、课程、选课记录一张都不能省
常见的错误做法是把所有信息塞到一张大表里:字段包含学号、姓名、密码、系别、课程名、老师、学分、成绩……这种设计在课设验收时一眼就会被看穿,因为第三范式不过关。拆分之后,最少也需要四张表:
- student:学生信息,学号做主键
- teacher:教师信息,教师编号做主键
- course:课程信息,课程号做主键,教师编号做外键
- sc:选课记录,学号 + 课程号做联合主键,成绩字段允许为空
课程表里存 teacher_no 而不是直接写死老师姓名,这是典型的规范化思路。如果老师调动,只需要改 teacher 表;如果直接在 course 表里写“张三”,那张三改名或者换人,所有课程记录都要跟着改。课设里老师喜欢问这类“为什么这么拆”的问题,答案就一句话:减少数据冗余,消除修改异常。
选课记录表 sc 是这套系统的核心,它的字段设计决定了后面所有业务好不好写。最关键的三个字段是 sno、cno、score,sno 和 cno 联合作为主键。为什么不用自增 id?因为业务上“一个学生选一门课只能有一条记录”是硬约束,用自增 id 无法阻止同一对学号课程号重复出现,而联合主键天然防重复。
2.2 为什么选课表必须加联合唯一约束
很多同学的做法是:插入之前先 SELECT 查一下有没有选过,没选过就 INSERT。这在单用户测试时完全正常,但答辩老师只要问一句“两个浏览器同时点选课怎么办”,这套逻辑就露馅了。查和插是两条独立语句,中间任何一个瞬间都可能被另一个请求插进来,造成重复选课。
正确做法是让 MySQL 在数据库层面兜底:sc 表用 (sno, cno) 做主键。这样无论 Java 代码怎么写,数据库都不允许同一学生同一课程出现两条记录。业务层可以先查,但那是为了给出友好提示;数据库约束才是最后一道防线。
另外一个容易被忽略的点:联合主键是 (sno, cno),那按 cno 单独查索引时是走不了联合索引的。如果系统里有“按课程查选了哪些学生”的功能,要给 cno 建一个普通索引。联合主键的最左前缀原则决定了 sno 开头的查询可以用索引,但 cno 开头的查询是全表扫描。这个细节写进设计报告里,答辩时价值很高。
2.3 建库建表 SQL:引擎、字符集与关键参数一次配好
存储引擎选 InnoDB 而不是默认的 MyISAM,理由有两条:支持事务、支持外键。选课场景里“插入选课记录”和“更新已选人数”必须同时成功或同时失败,MyISAM 不具备这个能力。字符集统一用 utf8mb4,不要为了省空间用 latin1,否则中文极容易变成乱码。
CREATE DATABASE course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_db; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student ( sno VARCHAR(20) PRIMARY KEY, name VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL, dept VARCHAR(50), grade VARCHAR(10) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE teacher ( tno VARCHAR(10) PRIMARY KEY, name VARCHAR(30) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( cno VARCHAR(10) PRIMARY KEY, cname VARCHAR(50) NOT NULL, credit DECIMAL(3,1), teacher_no VARCHAR(10), capacity INT NOT NULL DEFAULT 40, chosen INT NOT NULL DEFAULT 0, start_time VARCHAR(100), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_no) REFERENCES teacher(tno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sc ( sno VARCHAR(20) NOT NULL, cno VARCHAR(10) NOT NULL, score DECIMAL(5,1) DEFAULT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (sno, cno), KEY idx_cno (cno), CONSTRAINT fk_sc_student FOREIGN KEY (sno) REFERENCES student(sno) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course(cno) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几个参数说明:course 表里 capacity 表示课程容量,chosen 表示当前已选人数,这两个字段是后面选课事务的核心。chosen 其实属于冗余字段,因为通过 COUNT(*) 也能算出来,但课设里保留它能让事务逻辑更直观。sc 表的外键策略分成两种:学生删除时级联删除选课记录,因为学生没了选课记录没意义;课程删除时用 RESTRICT,因为如果还有人选着这门课,强制删除会破坏选课记录,必须先处理选课记录再删课程。
表建好后,需要塞入一些模拟数据供 Java 侧联调。手写几十条 INSERT 太慢,可以用 MySQL 的存储过程批量生成。下面这段生成 500 个学生的脚本在课设里很实用,一方面能真实测出分页效果,另一方面也向老师证明你做过数据量层面的验证。
DROP PROCEDURE IF EXISTS generate_students; DELIMITER $$ CREATE PROCEDURE generate_students(IN stu_count INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i <= stu_count DO INSERT INTO student (sno, name, password, dept, grade) VALUES (CONCAT('2024', LPAD(i, 4, '0')), CONCAT('学生', i), MD5('123456'), '计算机学院', '2024'); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL generate_students(500); INSERT INTO admin (username, password) VALUES ('admin', MD5('admin123'));学号用 CONCAT('2024', LPAD(i, 4, '0')) 生成,结果就是 20240001 到 20240500,格式统一且排序自然。LPAD 的作用是左侧补零,保证 1 变成 0001,而不是出现 20241 这种四不像的学号。密码统一用 MD5('123456') 存储,下面第三部分会讲为什么密码不能存明文。
3. 用 JDBC 把 Java 和 MySQL 连起来:连接参数、DAO 分层与事务边界
数据库部分定下来之后,Java 侧最核心的就是 JDBC 连接管理与 SQL 访问方式。课设里用纯 JDBC + Servlet 就能完成,不需要上 MyBatis 或 Hibernate。把数据库访问单独封装一层,业务层只面向 DAO 接口编程,这是代码结构上最实用的习惯。
3.1 驱动选择与 JDBC 连接参数在 Java 代码里怎么写
MySQL 5.x 和 8.x 的 JDBC 驱动类名不一样。5.x 时代用的是 com.mysql.jdbc.Driver,8.x 之后变成了 com.mysql.cj.jdbc.Driver。很多同学搜到老教程,把自己的 MySQL 8.0 配上了旧驱动类名,启动时报 ClassNotFoundException,这是课设里出现频率最高的报错之一。如果你是在 Windows 上自己装的 MySQL 8.0,驱动 jar 用 mysql-connector-java 8.x,类名必须写带 cj 的那个。
连接参数是另一处容易翻车的地方。同样一张表,同样的 JDBC URL,在 MySQL 8.0 上如果不带时区参数,连接时会直接报错。下面这段是标准的连接写法:
private static final String URL = "jdbc:mysql://localhost:3306/course_db" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8mb4" + "&connectTimeout=5000" + "&rewriteBatchedStatements=true"; private static final String USER = "root"; private static final String PASSWORD = "your_password";每个参数都对应一个真实的问题:useSSL=false 是因为本地开发没必要做 SSL 握手,加上去反而慢;serverTimezone=Asia/Shanghai 解决 MySQL 8.x 时差报错;characterEncoding=utf8mb4 保证中文写入不乱码;connectTimeout=5000 让连接失败时快速报错,而不是让页面卡在半分钟;rewriteBatchedStatements=true 是给批量导入学生数据用的,让 addBatch 真正走批量模式而不是逐条执行。
这些参数最好不要写死在类里,而是放在 src 下的 jdbc.properties 文件里,用 java.util.Properties 读取。答辩时老师翻代码,看到配置项集中管理而不是散落在各个类里,印象分会差不少。
3.2 DBUtil 与连接释放:每个方法都必须关干净
课设里最常见的 JDBC 工具类写法是提供一个静态方法返回连接,同时提供一个 close 方法接收 Statement、ResultSet、Connection。关键点是 close 必须放在 finally 或 try-with-resources 里,否则数据库连接会被耗尽,运行一段时间后所有查询全部卡住。
public class DBUtil { private static String URL; private static String USER; private static String PASSWORD; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("jdbc.properties")) { Properties props = new Properties(); props.load(in); URL = props.getProperty("url"); USER = props.getProperty("username"); PASSWORD = props.getProperty("password"); Class.forName("com.mysql.cj.jdbc.Driver"); } catch (Exception e) { throw new ExceptionInInitializerError("数据库初始化失败,检查配置文件"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) { } } } } }这里每调用一次 getConnection 就新建一个连接,用完立即关闭,适合课设这种低并发场景。不要在工具类里定义一个静态 Connection 字段然后所有方法共用,那样一旦一个请求忘记关闭,整个应用就瘫痪了。DAO 层代码用 try-with-resources 或者 finally 里调用 DBUtil.close,二选一,但必须做到位。
3.3 PreparedStatement:为什么拼字符串是最容易翻车的写法
很多初学者习惯用字符串拼接 SQL:"SELECT * FROM student WHERE sno = '" + sno + "'"。这种写法在值里出现单引号时就会破坏 SQL 语法,更严重的是存在注入风险。课程设计的登录功能如果被老师用' OR '1'='1试一下,直接以管理员身份登录进去,这个答辩基本就挂了一半。
PreparedStatement 的好处有三个:SQL 结构预先编译,参数通过 setString 传入,特殊字符会被转义;可读性比拼接字符串强,参数一目了然;同一个 SQL 多次执行时能利用数据库服务端的预编译能力。唯一要注意的是参数索引从 1 开始,这个和数组下标习惯不一样。
String sql = "SELECT sno, name, dept, grade FROM student WHERE sno = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, md5Password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Student stu = new Student(); stu.setSno(rs.getString("sno")); stu.setName(rs.getString("name")); return stu; } } }setString 永远处理的是字符串类型,如果查询条件是 int 就用 setInt,不要依赖 MySQL 的隐式类型转换。还有一种隐蔽的问题是查出来的列名大小写:MySQL 在 Windows 上列名不区分大小写,但代码里最好按建表时的字段名原样书写,少给自己留排查负担。另外这一段的 try-with-resources 写法会自动关闭 Connection、PreparedStatement、ResultSet,比手写 finally 再层层判空简洁得多,推荐直接使用。
4. 核心流程的 Java 实现:登录、选课、退课、成绩录入与课表查询
表结构和 DAO 层就位后,剩下就是把业务逻辑串起来。这套系统里有几条核心链路必须走事务:选课要更新两张表,退课同样。登录和查询不需要事务,但要注意密码处理和 session 权限控制。
4.1 登录与角色分发:密码不能存明文,session 里的身份标记怎么用
登录功能涉及两个表:admin 表管理管理员,student 表管理学生。实际做法是写一个统一认证的 DAO,先后查 admin 和 student,命中哪个角色就跳转到对应的首页。但有一个底线是密码不能明文存储,即使课设里不要求加密,也要用 MD5 摘要或至少加盐哈希。前面建表时我们存储的就是 MD5('123456'),登录时把用户输入的密码也做 MD5 再比较,这样数据库被翻出来也不会直接暴露密码。
public User login(String username, String password) { String md5Pwd = MD5Util.md5(password); User user = findAdminByUsernameAndPwd(username, md5Pwd); if (user != null) { return user; } return findStudentBySnoAndPwd(username, md5Pwd); }登录成功后一定要把用户身份放进 session,例如session.setAttribute("role", "admin")和session.setAttribute("loginName", username)。重点是受限页面必须校验 session 里的 role。Java 里最简单的方式是写一个过滤器,对所有 /admin/ 开头的请求检查 role 是否 admin,不是就重定向到登录页。如果不做这一步,任何知道页面 URL 的人都能绕过登录直接访问管理员功能。
4.2 选课的完整事务:先锁行、再查容量、后插入、最后提交
选课是整个系统含金量最高的一个功能,也是答辩时最容易提问的点。业务要求是:学生选课时不能超过课程容量,同一学生不能重复选同一课程。先查容量再插入,中间要求原子性,因此必须用事务。这里有一个非常关键的细节:事务边界要放在 Service 层,而不是 DAO 层。如果 DAO 里每个方法分别获取连接和提交事务,那“查容量”和“插入选课记录”就分属两个事务,中途断电或抛异常就会产生错乱数据。
public boolean selectCourse(String sno, String cno) { Connection conn = null; PreparedStatement lockStmt = null; PreparedStatement insertStmt = null; PreparedStatement updateStmt = null; PreparedStatement checkStmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); checkStmt = conn.prepareStatement( "SELECT COUNT(*) FROM sc WHERE sno = ? AND cno = ?"); checkStmt.setString(1, sno); checkStmt.setString(2, cno); try (ResultSet rsCheck = checkStmt.executeQuery()) { rsCheck.next(); if (rsCheck.getInt(1) > 0) { conn.rollback(); return false; } } lockStmt = conn.prepareStatement( "SELECT capacity, chosen FROM course WHERE cno = ? FOR UPDATE"); lockStmt.setString(1, cno); rs = lockStmt.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int capacity = rs.getInt("capacity"); int chosen = rs.getInt("chosen"); if (chosen >= capacity) { conn.rollback(); return false; } insertStmt = conn.prepareStatement( "INSERT INTO sc (sno, cno) VALUES (?, ?)"); insertStmt.setString(1, sno); insertStmt.setString(2, cno); insertStmt.executeUpdate(); updateStmt = conn.prepareStatement( "UPDATE course SET chosen = chosen + 1 WHERE cno = ?"); updateStmt.setString(1, cno); updateStmt.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { rollbackQuietly(conn); e.printStackTrace(); return false; } finally { DBUtil.close(rs, lockStmt, insertStmt, updateStmt, checkStmt); closeQuietly(conn); } }代码逻辑分四步:先查是否重复选课,再用 FOR UPDATE 锁住课程行,然后插入选课记录,最后把已选人数加一。FOR UPDATE 锁行的目的是让并发的选课请求排队:两个学生同时选同一门还有 1 个余量的课时,第一个事务锁住 course 行,第二个事务必须等第一个提交或回滚后才能读到数据,从而避免把最后 1 个名额超卖。这个点写进设计报告或答辩陈述,是很大的加分项。
有两处细节要说明。第一,UPDATE 语句写的是chosen = chosen + 1,而不是先在 Java 里把 chosen 读出来加一再写回去。写在 SQL 里的自增是原子的,不会覆盖别人的修改。第二,检查重复选课用的是 COUNT 子查询,但真正的硬约束仍然是 sc 表的联合主键。一旦两个事务同时通过 COUNT 检查,插入时也会有一个事务因主键冲突而抛出 DuplicateKeyException,从而回滚。数据库约束兜底比代码判断更可靠。
4.3 退课与成绩录入:外键在这两条路径上扮演的角色
退课的逻辑是选课的逆过程:删除 sc 记录,同时把 course.chosen 减一。这两步同样要包在事务里,否则会出现选课记录删了但人数没减。退课 SQL 简单,但要注意删除顺序:先删 sc,再更新 course。反过来操作,就会短暂出现课程人数多算一人的中间状态,如果第二步失败,不一致就固化了。
成绩录入是管理员功能:选中学号、课程号,把成绩写进 sc 表。这个操作不涉及容量变更,但有一个常见的错误是把成绩更新到了 course 表。成绩是属于某个学生选某门课这个关系的属性,必须存 sc 表。录入前要查当前选了这门课的学生列表,这个查询是三表关联中最经典的一类:
SELECT s.sno, s.name, s.dept, sc.score FROM sc JOIN student s ON sc.sno = s.sno WHERE sc.cno = ? ORDER BY s.sno;注意这里 JOIN 的顺序:先 sc 和 student 关联,用 cno 过滤。sc 表是事实表,student 表是维度表。很多同学写这个查询时会先从 sc 查 cno,再帮每个 sno 查一次 student,造成 N+1 次查询。课设代码里看不出性能差异,但老师追问时你的回答会暴露是否真正理解 JOIN 的作用。
4.4 课表与成绩单查询:JOIN 和 ORDER BY 怎么配合
学生在首页要看到自己的课表,这个查询是按 sno 查 sc 再 JOIN course。一个更经典的查询是“可选课程列表”:从 course 表里过滤掉这个学生已经选过的课程。
SELECT c.cno, c.cname, c.credit, t.name AS teacher_name, c.capacity, c.chosen FROM course c JOIN teacher t ON c.teacher_no = t.tno WHERE c.cno NOT IN ( SELECT cno FROM sc WHERE sno = ? ) ORDER BY c.cno;NOT IN 子查询在课设数据量下没有任何问题,直接使用即可。如果以后数据量到几万门课,可以改成 LEFT JOIN 加 IS NULL 的写法,但那是后话。这里重点说 ORDER BY:数据库排序发生在查询阶段,而不是 Java 拿到结果后再排序。如果在 Java 侧用 Collections.sort 排序,说明 SQL 本身就写歪了。课设里课程列表要有默认排序,按 cno 排即可,保证每次刷新顺序一致。
分页是另一个躲不开的功能,MySQL 的分页基本形态是 LIMIT 加 OFFSET:
SELECT c.cno, c.cname, c.credit, c.capacity, c.chosen FROM course c ORDER BY c.cno LIMIT ? OFFSET ?;第一页是 LIMIT 10 OFFSET 0,第二页是 LIMIT 10 OFFSET 10。注意 OFFSET 过大时性能会下降,因为 MySQL 必须跳过越多的行才能拿到目标位置,这是 LIMIT 分页的固有限制,课设里不需要优化,但答辩时被问到要知道问题在哪。
5. 从“能跑”到“不翻车”:MySQL+Java 课设最常见的 5 个坑
这段内容全是血泪经验。每个坑我都按“现象 → 原因 → 解决”的顺序写,你迟早会遇到至少其中两个。
5.1 中文乱码:页面显示“???”或方框
现象:学生姓名、课程名存入 MySQL 后变成问号,页面上读出来也是乱码。原因通常有三种:数据库或表本身是 latin1 字符集;JDBC URL 里没带 characterEncoding=utf8mb4;连接时客户端字符集不匹配。解决:第一步确认建库语句是不是带 DEFAULT CHARACTER SET utf8mb4,已经建好的库用 ALTER DATABASE course_db CHARACTER SET utf8mb4 转一下;第二步检查 JDBC URL,必须带编码参数;第三步在连接后执行SET NAMES utf8mb4或者给连接初始化 SQL。这三步做完,乱码基本消失。最隐蔽的一种是 MySQL 5.7 的配置文件里 character_set_server 还是 latin1,这种情况要去 my.ini 里改配置重启服务。
5.2 重复选课:同一学生同一课程出现两条记录
现象:学生选课时快速点了两次提交,数据库里出现两行完全相同的选课记录,课程已选人数也翻倍了。原因:业务层先 SELECT 再 INSERT 的流程存在时间窗口,两个请求同时通过检查。解决:sc 表必须用 (sno, cno) 联合主键,这是数据库层面的硬约束。就算代码漏了查重,第二条 INSERT 也会因为主键冲突直接抛异常,事务回滚后数据不会坏。另外,如果项目里用了 MyBatis 之类的框架,记住框架的缓存不会帮你处理数据库约束,约束必须建在表上。
5.3 外键导致删不掉、改不了课程
现象:管理员想删除一门课,控制台报 Error 1451:Cannot delete or update a parent row。原因:sc 表里还有学生选着这门课,外键约束默认的 ON DELETE RESTRICT 不允许直接删除父表记录。解决:按业务逻辑先删除选课记录,再删除课程,两步包在事务里。或者建表时把 sc 表的课程外键设成 ON DELETE CASCADE,这样删课程时自动清理选课记录。从数据安全角度我推荐前者,因为 CASCADE 会在你只是想删一门课时悄悄把所有学生的选课记录抹掉,出问题后没有后悔药。
5.4 更换 MySQL 版本后 JDBC 驱动加载失败
现象:MySQL 从 5.7 升到 8.0,项目启动时报 ClassNotFoundException: com.mysql.jdbc.Driver,或者报 Communications link failure。原因:MySQL 8.x 把驱动类名改成了 com.mysql.cj.jdbc.Driver,旧版驱动类路径已经不存在;同时 8.x 要求 URL 中必须有时区参数。解决:确认 mysql-connector-java 的 jar 版本与 MySQL 服务端对应,8.x 的 MySQL 必须用 8.x 的驱动,并在 URL 末尾补上serverTimezone=Asia/Shanghai。这里要注意,不要把高版本驱动 jar 用在 5.7 服务端上,通常也会报协议不兼容的错误。
5.5 选课成功但“已选人数”不变
现象:sc 表里选课记录插入成功,但 course.chosen 字段始终是初始值。原因有两个方向:第一个是事务里 UPDATE 语句执行失败但异常被吞掉,外层 commit 提交了一个不完整的变更;第二个是 UPDATE 写成了chosen = ?,而问号绑定的值是从旧数据查出来的,两个学生同时选课时互相覆盖。解决:UPDATE 语句必须写chosen = chosen + 1,让 MySQL 自己原子计算;同时把事务改成先插入 sc,再更新 course,最后 commit,任何一个步骤失败都 rollback。测试时开两个浏览器无痕窗口,同时选同一门课,看是否出现人数多算或漏算。
6. 设计报告怎么写才能让答辩老师挑不出毛病
整套系统的代码只是完成了一半课设,另一半是设计报告。报告既不是把代码截图贴满几十页,也不是写一堆百度来的概念。评审老师通常会在五分钟内翻完报告,直接看你是否理解了表结构设计、事务边界和功能测试结果。
6.1 设计报告里的六个必写板块与它们对应的细节
按照课程设计的常见要求,报告从需求分析到总结通常有六块:需求分析、概念结构设计、逻辑结构设计、功能设计、系统实现、测试与分析。我建议在概念结构设计里画一张简单的 ER 图,实体就四个:学生、教师、课程、选课记录,关系就是“学生选课程”“教师教课程”,把主键和联系属性标清楚即可,不需要画得非常专业。
逻辑结构设计部分要放一张表结构清单,列名、类型、是否主键、是否外键、约束说明都要有。测试分析部分不要只写“测试通过”,要给出一张测试用例表,包含测试项目、输入数据、预期结果、实际结果、是否通过。我一般会在测试表里专门加两条:重复选课是否被拒绝、课程容量满员后是否还能选成功。这两条对应的是数据库唯一约束和事务逻辑,是答辩老师最喜欢追问的点。
6.2 三个低成本加分改造:分页、唯一约束、连接池
如果想让成绩从“良”变“优”,下面这三个点选一个做进去就够。
分页已经在上一章写了 SQL 形态,把它做进课程列表即可。唯一约束不用改代码,把第 2 章 DDL 里的联合主键展示在报告里,并写明它的作用。连接池是第三个方向,课设里不引入也没问题,但可以在报告中写一句:正式生产环境会用 HikariCP 或 Druid 管理连接,代替 DriverManager 每次新建连接的模式。这样表达出你知道 JDBC 直连的局限性就够了,不一定真的改代码。
我自己的习惯是:代码提交前用 Navicat 或命令行的 source 重新执行一遍全部 DDL,确认数据库可以从零重建;然后把设计报告里的表结构和实际数据库导入出来对比。这一步能避免答辩时老师现场要求重置数据库,结果你发现脚本根本跑不通。最后记住一个关键原则:所有功能列表里写的功能,代码里必须有对应路径;报告里写的表结构,数据库里必须一模一样。对不上,比做得少更扣分。
希望这些内容对你有帮助。做课设的时候先别急着找现成代码,把第 2 章的建表语句手敲一遍,再动手写 Java,很多问题就会在设计阶段消失。
本文还有配套的精品资源,点击获取