简介:这是一个基于 Java + Swing + MySQL 开发的学生选课与成绩管理系统课程设计项目,适合初阶与进阶学习者作为毕业设计、课程作业或工程实训参考。系统覆盖登录注册、管理员/教师/学生三类角色,包含学生信息管理、课程管理、选课与成绩查询等核心模块,并配有数据库 E-R 图与系统结构图,便于理解整体设计。压缩包共 53 个文件,核心为 16 个 Java 源码文件、1 个 SQL 数据库脚本及 lib 下 MySQL 连接驱动 jar;Java 源码按模块组织,SQL 脚本可直接导入 MySQL 8.0,jar 驱动免去额外下载依赖。另有 20 张界面截图与 2 张 PNG 结构图辅助对照,包体 8.63MB,整体轻量且目录清晰。项目在 JdbcConfig 中预留了数据库账号配置位置,压缩包内还含 README 与初始账号(admin/tch/stu),导入 SQL 后即可运行体验。目前已有 219 人学习,对希望快速上手桌面数据库应用开发、完成课程设计或梳理教务系统流程的读者有较强参考价值。
1. 基于Java + Swing + MySQL的学生选课及成绩管理系统:课程设计怎么做才能既保底又拿高分
期末前两周,选题页上“基于Java + Swing + MySQL的学生选课及成绩管理系统”永远是最抢手的那一个。原因很现实:它不涉及框架、不依赖Tomcat,一套JDK加一个MySQL就能跑起来,难度曲线恰好卡在“认真学过Java基础的人都能啃下来”的位置。但这道题也是最容易做成“学长旧代码换皮”的题目——登录窗口改个标题,课程表换几张图片,答辩时连数据库连接串都说不清。这篇文章想给你一条能从头敲到尾的完整路径:从成绩单反推数据库表结构,用Swing把界面分层,再用事务把选课这个核心动作做扎实。适合准备交课程设计、又不想光靠拷贝应付的读者。你最终交出去的不是一个能跑的demo,而是一套能讲清楚每一步为什么这样做的作品。
2. 从成绩单反推数据库设计:四张表把选课和成绩的边界画清楚
课程设计的第一个决策不是“登录窗口长什么样”,而是“成绩到底存在哪张表里”。很多第一次做这个题的人会把成绩作为一个字段放在课程表里,理由是“一门课有成绩”。这个直觉是错的:成绩是某个学生对某门课的结果,它既不只属于学生,也不只属于课程,而是属于“学生选了这门课”这个关系本身。想通这一点,表结构就清楚了一半。
选了这门课,系统就落在一个经典的多对多场景上:一个学生能选多门课,一门课能被多个学生选。多对多关系在关系型数据库里的标准解法是引入中间表,这个中间表就是选课表。学生表和课程表各管各的基础信息,选课表负责记录“谁在什么时间选了哪门课”,而成绩的三个分数——平时成绩、期末成绩、总评成绩——作为关系的属性挂在选课表的行上。这也是为什么这个系统的表结构永远长着一张“老面孔”:很常规,但挑不出毛病。
2.1 四张表的结构与主外键:学生、课程、选课、成绩
建表语句我一般会直接给出完整版本,因为课程设计的评审老师几乎必看数据库设计文档。下面这段SQL包含了学生表、课程表、选课表,以及必要的索引和外键约束,可以直接在MySQL里执行:
CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号,同时作为登录名', password VARCHAR(64) NOT NULL COMMENT '登录密码', name VARCHAR(50) NOT NULL COMMENT '姓名', class_name VARCHAR(50) DEFAULT '' COMMENT '班级', role VARCHAR(10) NOT NULL DEFAULT 'student' COMMENT 'student或admin' ) ENGINE=InnoDB; CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程号', name VARCHAR(50) NOT NULL COMMENT '课程名', teacher VARCHAR(50) DEFAULT '' COMMENT '任课教师', credit DECIMAL(3,1) DEFAULT 0 COMMENT '学分', capacity INT NOT NULL DEFAULT 60 COMMENT '课程容量', selected INT NOT NULL DEFAULT 0 COMMENT '已选人数', semester VARCHAR(20) NOT NULL COMMENT '开课学期,如2024-2025-1' ) ENGINE=InnoDB; CREATE TABLE student_course ( student_id INT NOT NULL, course_id INT NOT NULL, regular_score DECIMAL(5,1) DEFAULT NULL COMMENT '平时成绩', exam_score DECIMAL(5,1) DEFAULT NULL COMMENT '期末成绩', final_score DECIMAL(5,1) DEFAULT NULL COMMENT '总评成绩', PRIMARY KEY (student_id, course_id), KEY idx_course (course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE ) ENGINE=InnoDB;选课表用了联合主键(student_id, course_id),这是整个设计里最关键的一个约束——数据库层面直接禁止同一个学生插入同一门课的第二条记录。没有这个联合主键,防重复选课就只能依赖Java代码里的“先查再插”,一旦两个窗口同时操作,重复数据就进去了。外键都指向表的主键id而不是学号、课程号,原因是id是稳定且唯一的代理主键,学号这类业务字段理论上可能被修改,用id做关联在后续维护时更安全。查询时再通过JOIN把学号、课程名拼出来,代价很小。
2.2 主键用自增还是业务字段:选课表联合主键的分量
主键设计是另一个答辩必问的点。学生表的学号字段已经加了UNIQUE约束,为什么还要单独放一个自增id?课程表同样有唯一的课程号,为什么不用它做主键?我见过不少同学直接用student_no、course_no做跨表关联,表面上看省了一次JOIN,但有两个隐患。第一,课程号的格式如果因为教务规则调整发生变化,所有引用它的外键都要跟着改;第二,字符串主键在InnoDB的聚簇索引里比整型占用更大,数据多时索引效率和写入性能都不如自增整型。课程设计的规模体现不出性能差异,但“用代理主键而不是业务主键”这个设计习惯是加分项。
选课表的联合主键则恰好相反——它不引入额外的自增id,因为“同一个学生不能重复选同一门课”本身就是一条业务规则,而联合主键正好就是这条规则的数据库实现。插入时如果违反主键约束,JDBC会抛SQLIntegrityConstraintViolationException,程序捕获后可以立刻提示“你已经选过这门课了”。注意这里说的是“捕获异常做提示”,而不是一开始就完全依赖Java预检查——两道防线都留着,容错才完整。至于成绩表,这个系统里不需要单独建表,选课表上的三个可为空的分数字段已经足够承载一学期一门的成绩。
2.3 MySQL连接串与JDBC参数:DBUtil里三个必调的参数
表建好了,接下来是Java和MySQL的通道。课程设计里最常见的数据库连接方式是JDBC,工具类DBUtil是连接逻辑的唯一入口,所有DAO都从这里拿Connection。下面是完整代码:
import java.sql.*; public class DBUtil { // MySQL 8.0 的驱动类名; 如果你是 MySQL 5.7, 改成 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/course_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }这里有两个参数值得多说一句。第一个是serverTimezone=Asia/Shanghai,MySQL 8的驱动会在连接时校验时区,不配的话经常直接报异常;第二个是useSSL=false,本地开发不需要加密连接,加了它反而会在控制台刷一段SSL握手警告。characterEncoding=utf8要和数据库的utf8mb4字符集配合,少了这一项,中文写入读取都可能乱码。静态块里的Class.forName在类加载时执行一次,驱动注册只需要发生一次,这个写法也比每次连接都查驱动更规范。密码硬编码在常量里对课程设计可以接受,但你要在答辩时主动提一句“生产环境应该放到配置文件并加密”,老师对你的印象会好很多。
3. Swing界面分层:登录、主窗体、选课面板怎么配合事件模型
数据库准备就绪后,界面的工程量开始浮出水面。Swing这套GUI框架看起来很老,但它自带的组件和布局管理器足够完成一个管理系统,而且JDK直接包含javax.swing包,不需要额外引依赖,这对课程设计的部署非常友好。有的同学会纠结“为什么不用JavaFX”,答案很现实:你所在的环境大概率装的是JDK 8,学校机房和老笔记本上跑JavaFX要额外配JavaFX SDK,而Swing是开箱即用的。换成JavaFX确实界面更现代,但单是“让评审老师机器上能跑起来”这一条,Swing就赢了大半。
Swing编程的核心是事件模型:用户在组件上动作,系统派发事件给已注册的监听器,监听器里的代码做出反应。写这个系统不需要把所有事件都背下来,抓住ActionListener、ListSelectionListener两个就够。界面的结构也相对固定:登录窗口验证身份,主窗体承载所有业务功能,通过左侧导航切换右侧面板。下面按这三层拆开讲。
3.1 登录窗口到主窗口的跳转:new JFrame 还是 JDialog
登录窗口的实现思路是:窗体上放一个用户名输入框、一个密码框和一个登录按钮,点击按钮后调用数据库查询验证。验证成功,把当前登录的学生id或管理员的role字段取出来,传给主窗口;验证失败,用JOptionPane.showMessageDialog提示“用户名或密码错误”。这里有个设计选择:登录窗口用JFrame还是JDialog都行,关键是跳转方式——不要在一个监听器里连续new两个窗口然后让它们并列弹出来。
我常用的跳转写法是:
JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(e -> { String no = usernameField.getText().trim(); String pwd = new String(passwordField.getPassword()); // 调用 StudentDAO.login(no, pwd), 返回 Student 对象或 null Student student = dao.login(no, pwd); if (student != null) { dispose(); // 关闭登录窗口 new MainFrame(student).setVisible(true); // 打开主界面 } else { JOptionPane.showMessageDialog(LoginFrame.this, "用户名或密码错误"); } });注意dispose而不是setVisible(false),前者会释放窗口资源,后者只是把窗口藏起来。如果只藏不关,程序退出时如果只关了主窗口,登录窗口还挂在后台,JVM不会正常结束。这个问题在课程设计答辩中经常被当场演示出来:关掉主界面后,Java进程还在任务管理器里躺着。密码框用getPassword()返回char数组而不是getText(),虽然在本项目里意义不大,但这是一个Java开发者应该养成的习惯,答辩时提一句也是加分项。
3.2 主窗体布局:JSplitPane加JTable搭出管理系统的标准形态
主窗体是整个系统的容器。课程设计的主流做法是左边一个功能导航列表,右边一个数据表,中间用JSplitPane分割。左侧的JList放“可选课程”“我的选课”“成绩查询”三个入口,右侧的JTable负责展示数据。下面是骨架代码,你可以直接抄进MainFrame里再改字段:
public class MainFrame extends JFrame { private final Student student; private final DefaultTableModel model; private final JTable table; public MainFrame(Student student) { this.student = student; setTitle("学生选课及成绩管理 - " + student.getName()); setSize(900, 550); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); String[] columns = {"课程号", "课程名", "教师", "学分", "已选/容量"}; model = new DefaultTableModel(columns, 0); table = new JTable(model); JList<String> nav = new JList<>(new String[]{"可选课程", "我的选课", "成绩查询"}); nav.addListSelectionListener(e -> { if (e.getValueIsAdjusting()) return; // 避免事件触发两次 String selected = nav.getSelectedValue(); switch (selected) { case "可选课程" -> loadAvailableCourses(); case "我的选课" -> loadMyCourses(); case "成绩查询" -> loadScores(); } }); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new JScrollPane(nav), new JScrollPane(table)); split.setDividerLocation(180); add(split); } private void loadAvailableCourses() { model.setRowCount(0); String sql = "SELECT course_no, name, teacher, credit, " + "CONCAT(selected, '/', capacity) FROM course"; // 执行查询后逐行 model.addRow(new Object[]{...}) } }DefaultTableModel的构造方法第一个参数是列名数组,第二个参数是初始行数0,这样表格创建出来就只有表头没有数据。JList的监听器有一个容易踩的细节:选中项变化时事件会派发两次,第一次值还在调整中,第二次才是最终值。上面代码里用getValueIsAdjusting()做了过滤,不清这层的话,导航栏每点一次,查询方法会被调用两次。SwingWorker在课程设计里不是必须的,但用不用都行,核心是把事件处理器和数据库读写分开,别让界面刷新逻辑和SQL混在同一个方法里揉成一团。
3.3 JTable刷新为什么显示旧数据:TableModel和视图的坑
JTable的显示机制是Model-View-Controller的简化版:JTable是视图,TableModel是数据源,视图不会主动去感知数据源的变化,必须由你通知它“数据变了”。不少同学第一次做时踩的坑是——执行完SQL后直接调用table.repaint(),以为重画就能刷新,结果界面上还是旧数据。repaint只负责重绘,它不重新读取TableModel的内容。正确的做法是让TableModel自我更新,然后触发一个表格结构或内容变更事件。
用DefaultTableModel时,最简单的刷新方式是先model.setRowCount(0)清空所有行,再逐行addRow()填新数据。setRowCount(0)这个操作本身会触发表数据变更,JTable自动重绘,不需要额外调repaint。如果你的数据需要在原行上修改,比如成绩录入后某个单元格变了,记得调用model.fireTableRowsUpdated(row, row)来通知视图。数据库连接和查询在EDT上直接执行对于这个数据量完全够用,不用过度设计。但要警惕一件事:如果查询方法里忘了关闭ResultSet,连接和数据会一直占着,界面也会因为连接池耗尽慢慢变卡,所以每段查询代码都走DBUtil.close()收尾。
4. 选课与成绩的核心业务:事务、防重复、防超员
这个系统里真正有技术含量的是写操作。查询只是把数据库里的行搬到JTable上,难度不大;选课和成绩录入涉及多步写操作,稍微设计不当就会产生脏数据。以选课为例,一次成功选课最少要干三件事:读课程容量做判断、往选课表插一条记录、把课程表的已选人数加一。这三步里任何一步失败而其他步成功,数据就处于不一致状态——最常见的表现形式是“选课表多了一条记录,但已选人数没涨”,或者反过来“人数涨了,但选课表里查不到记录”。
4.1 选课三步为什么要放在同一个事务里:回滚是后悔药
上面的三步必须被放进同一个数据库事务:要么全部成功一起提交,要么全部失败一起回滚。java.sql.Connection默认是自动提交模式,每条SQL执行完就写盘,必须手动关闭自动提交才能控制事务边界。下面是选课方法的完整实现,也是这个系统里我最希望你读懂的一段代码:
public boolean selectCourse(int studentId, int courseId) { String preCheckSql = "SELECT selected, capacity FROM course WHERE id = ?"; String insertSql = "INSERT INTO student_course(student_id, course_id) VALUES (?, ?)"; String updateSql = "UPDATE course SET selected = selected + 1 " + "WHERE id = ? AND selected < capacity"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 开启事务 // 第1步: 预检查, 纯粹是为了给用户友好提示 try (PreparedStatement ps = conn.prepareStatement(preCheckSql)) { ps.setInt(1, courseId); ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt("selected") >= rs.getInt("capacity")) { conn.rollback(); return false; // 课程已满 } } // 第2步: 插入选课记录 try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); } catch (SQLIntegrityConstraintViolationException e) { conn.rollback(); return false; // 联合主键挡住了重复选课 } // 第3步: 原子更新已选人数 try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setInt(1, courseId); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 更新行数为0, 说明此刻课程刚好满了 } } conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); return false; } }这段代码有三个边界值得说透。第一,第1步的SELECT结果只能用于提示,不能作为最终判断,因为在SELECT执行完到UPDATE执行完之间,可能有其他请求插入把名额抢走;真正起防超员作用的是第3步UPDATE语句里那个WHERE selected < capacity条件——数据库是在当前行上加锁执行更新的,它才是原子判断。第二,第2步插入如果触发联合主键冲突,会抛SQLIntegrityConstraintViolationException,这里先rollback再返回false,脏写不会有落地机会。第三,第3步的executeUpdate返回值是影响行数,如果为0说明条件不满足,这一个分支就把“并发抢最后一个名额”的情况兜住了。项目答辩时,能把这段逻辑讲清楚,比界面多一个按钮有用得多。
4.2 防止重复选课:联合主键兜底和Java预检查各司其职
防重复选课的思路和防超员类似:数据库约束负责最终正确性,Java预检查负责友好的用户提示。如果完全依赖联合主键,用户重复选课时会看到一个白底红字的异常堆栈,体验很差;如果完全依赖Java预检查,即先SELECT再INSERT,就会留下并发窗口。所以两层都保留:事务里先查一次是不是已选,查到就直接返回“你已经选过这门课”,查不到再走插入流程,插入时如果主键冲突,再兜底返回。这样既不会让异常暴露给用户,又不会因为预检查的竞态而放行重复数据。
4.3 成绩录入与统计:一条UPDATE加一条聚合查询
成绩管理的核心是更新选课表上的三个分数。录入页面拿到学生id、课程id和三个分数后,执行下面的UPDATE:
UPDATE student_course SET regular_score = ?, exam_score = ?, final_score = ? WHERE student_id = ? AND course_id = ?;总评成绩怎么算,我建议在Java代码里算好再写入final_score,而不是在SQL里写表达式。原因很朴素:课程设计的评分规则可能随时改,老师如果说“平时四期末六”,你只需改一行Java代码;如果把公式写死在SQL里,后期改动要动数据库层,维护成本更高。成绩查询的SQL是一段带JOIN的聚合查询,这也是课程设计里少有的能展示你SQL基本功的地方:
SELECT c.course_no, c.name, sc.regular_score, sc.exam_score, sc.final_score FROM student_course sc JOIN course c ON sc.course_id = c.id WHERE sc.student_id = ?;按课程统计平均分、最高分、最低分时,再加GROUP BY c.id。这里注意一个细节:查询成绩时要确保只显示当前登录学生的数据,WHERE条件里的student_id从MainFrame持有的Student对象获取,不能从界面上的下拉框选,否则只要改一下下拉框就能查到别人的成绩,这是权限设计上的硬伤。
4.4 为什么全程用PreparedStatement:安全性和可读性都要
整个系统的SQL执行我都推荐PreparedStatement,而不是Statement加字符串拼接。直接拼接SQL存在注入风险,比如把用户输入的用户名直接拼到查询语句里,输入一个"abc' OR '1'='1"就能绕过登录。PreparedStatement用占位符先把SQL结构固定下来,参数只作为值传进去,不存在拼接机会。它还有一层编译缓存的好处:同一条SQL重复执行时,MySQL可以复用预编译的执行计划,比每次重新解析快一些。课程设计的数据量测不出性能差异,但代码规范性和安全性是评审老师愿意看到的亮点。
5. 避坑:本机跑通到答辩演示之间,最容易翻车的五个细节
代码写完能跑,和答辩现场能顺利跑完,是两个完全不同的阶段。我在带课程设计这类的项目时见过太多学生前一刻在宿舍演示正常,后一刻换到教室投影仪上就翻车。下面五条是我认为出现频率最高的疑难杂症,按现象、原因、解决三步写清楚,希望能帮你少走一段弯路。
5.1 MySQL 8驱动加载失败:ClassNotFoundException到底缺在哪
现象是程序一启动就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因基本出在两处:一是mysql-connector-java的jar包没放进classpath,很多情况下你在IDEA里引入了依赖,但忘了把它打到最终的Artifact里,换一台机器跑就找不到驱动;二是你用的MySQL 5.7的驱动字符串,即com.mysql.jdbc.Driver,而驱动版本是8.x,新版驱动已经移除了这个老类名。解决办法很简单:下载与MySQL版本对应的mysql-connector-java,放在项目根目录的lib文件夹下,IDEA里右键Add as Library。如果是构建Artifact,还要在Project Structure里把lib目录下的jar勾选进输出。这件破事我自己当年就栽过,代码逻辑全对,问题纯粹出在打包遗漏上。
5.2 查询结果中文全是问号:字符集三处必须对齐
现象是JTable里课程名显示成“????”。原因基本都出在字符集不对齐:数据库建库时不是utf8mb4,连接串里没有characterEncoding=utf8,或者表结构用的latin1默认字符集。解决办法是把三处统一:建库SQL里写DEFAULT CHARACTER SET utf8mb4,连接串带上useUnicode=true&characterEncoding=utf8,检查现有表的字符集可以用SHOW CREATE TABLE student看看是不是utf8mb4。如果表已经建了但字符集不对,ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4可以抢救。注意MySQL 5.7默认字符集是latin1,MySQL 8默认才是utf8mb4,如果你照着网上老教程建库,这一步很容易漏。
5.3 Swing界面假死:别把重活放在事件分发线程里
现象是点击“查询成绩”按钮后,整个窗口拖不动,标题栏出现“(未响应)”,过几秒又恢复正常。原因是Swing的所有事件处理都运行在事件分发线程EDT上,你的数据查询如果耗时较长,EDT就被占住,它没空处理重绘和鼠标事件,界面看起来就像死了一样。课程设计的数据量小,最直接的解决方法是先自查SQL有没有问题——很多“卡顿”其实是没写WHERE条件导致全表扫描,或者查询里的JOIN把笛卡尔积放大了。真遇到必须长时间执行的任务,可以选用javax.swing.Timer或者把任务拆小分片执行,SwingWorker在你的场景下不是必须。至少别在主线程里加Thread.sleep模拟网络延迟,那是给自己找麻烦。
5.4 选课超员:Java层的容量判断不是最终防线
现象是界面上明明显示“还剩5个名额”,两个同学同时选课,最后选上的人超过5个。原因在于第4章提到过的竞态:先SELECT再UPDATE之间存在时间窗口,两个请求都能读到“剩1个名额”,然后相继插入成功。解决的核心是让数据库在一步里完成原子判断:UPDATE course SET selected = selected + 1 WHERE id = ? AND selected < capacity,由WHERE条件决定这行数据是否可以被更新。更新影响行数是0,说明最后一个名额恰好被抢走,程序回滚即可。Java层的预检查保留,但它只负责显示“当前剩余名额”这种提示性信息,不能当作安全边界。这是整个系统里我认为最“值钱”的一个细节,答辩被问“高并发下怎么保证不超员”时,这段能直接答到点上。
5.5 答辩现场MySQL连不上:服务和端口两手查
现象是java.net.ConnectException: Connection refused: connect。原因优先排查两件事:MySQL服务没启动,或者端口被占用。管理员的CLI里执行net start mysql可以启动服务,如果提示服务名无效,说明安装时服务名不叫mysql,要去服务列表里查实际名字。端口排查用netstat -ano | findstr 3306,看LISTENING状态对应的进程。如果你用Docker安装MySQL,还要额外确认容器端口映射有没有写-p 3306:3306,以及容器是否在运行。这类环境问题最怕在答辩现场临时处理,我的习惯是答辩前一天把数据库用mysqldump导出成.sql文件,在另一台干净机器上从零导入一遍:创建表、插入测试数据、跑通登录和选课。能成功走完这套流程,答辩环境基本就稳了。
6. 进阶:把课程设计从“能跑”推到“值得讲”
课程设计答辩看的不只是功能对不对,还有你对自己作品的熟悉程度和设计思考。这里给你一份可以直接用的测试用例表,对照着逐条跑完,既能暴露潜在bug,也算提前准备答辩话术。
| 用例编号 | 测试操作 | 预期结果 | 是否通过 |
|---|---|---|---|
| TC-01 | 学生登录成功后进入主界面 | 主窗体标题显示学生姓名,左侧导航可用 | |
| TC-02 | 选择一门名额充足的课程 | 选课成功,课程列表的已选人数加1 | |
| TC-03 | 重复选择同一门课 | 弹出提示“已经选过该课程”,事务回滚 | |
| TC-04 | 选最后剩余1个名额的课 | 选课成功,再选时提示已满 | |
| TC-05 | 录入成绩后立即查询 | 成绩更新,平均分统计同步变化 | |
| TC-06 | 关闭主窗口 | 整个Java进程退出,无残留窗口 |
除了测试,还有两个低成本高收益的进阶改动。第一,student表已有role字段,你可以让管理员角色除了查看之外还能进入“成绩录入”面板,学生角色则只能查看成绩,按角色隐藏按钮这个改动半小时内能完成,但能让系统从“单机工具”变成“有个权限概念的系统”。第二,把数据库密码从常量提出来,放到根目录的config.properties里读取,这只是几行代码的事,但传递的工程意识很好。这两个改动不建议一起做大,选一个做到位,比堆十个半成品功能更划算。
我自己做过一次非常尴尬的答辩演示:选课成功后切到“我的选课”看新记录,表格没刷新,我下意识点了好几次“查询”,界面还是旧数据,最后重启程序才解决。后来我养成了一个习惯——所有表格写入完成后,都要有明确的刷新动作,而不是依赖数据库“自动”告诉界面数据变了。另一条教训是把选课的更新人数单独写在另一个类的另一个连接里,出问题后排错花了两个小时,从此我再没把同一事务的SQL拆到不同连接里执行。这些细节看起来小,但恰恰是课程设计答辩时老师最愿意追问的地方。希望帮到你。
本文还有配套的精品资源,点击获取