Java Swing + MySQL 学生选课成绩管理系统完整实现与避坑指南
2026/9/24 19:22:15 网站建设 项目流程

简介:这是一套基于Java Swing与MySQL实现的学生选课及成绩管理系统课程设计源码包,适合Java初学者、毕业设计及课设学生使用。系统覆盖登录注册、管理员/教师/学生三类角色权限,包含学生信息管理、课程管理、选课操作、成绩录入与查询等完整业务模块,数据库脚本和初始账号(管理员admin、教师tch、学生stu,密码均为123)已内置,下载后修改JdbcConfig即可运行。压缩包共53个文件,以16个Java源码文件为主,辅以MySQL建表SQL脚本、IDEA配置、外部Jar驱动以及20张界面运行截图和E-R图,整体大小8.63MB,结构清晰便于对照开发。目前已有219人学习下载,适合作为课程设计、大作业或初期项目立项的参考模板,能够帮助理解Swing界面编程与JDBC数据库操作的完整流程。

1. 基于 Java + Swing + MySQL 的学生选课及成绩管理系统,先别急着说它老

很多人一听“基于 Java + Swing + MySQL 的学生选课及成绩管理系统”就皱眉,觉得 Swing 是十几年前的老古董。但课程设计答辩现场,恰恰是这种组合最稳:界面用 Swing 组件拖出来,数据落在 MySQL 里,中间用 JDBC 串起来,逻辑清楚,评委提问也有抓手。这个系统要解决的是学生登录、浏览课程、选课退课、查成绩,以及老师录入成绩这些典型管理场景。它适合第一次做完整工程的人,也适合想在短时间内交付可演示结果的在职读者。下面这套落地步骤,是我带过三轮课程设计辅导以后沉淀下来的,照着走,新手能跑通,熟手能避坑。

2. 先把地基打牢:环境变量、建表设计和项目骨架

2.1 Java 环境变量配置与 MySQL 安装:装完先验证三条命令

课程设计翻车最多的地方往往不是代码,而是环境。JDK 装了一半,MySQL root 密码忘了,驱动 JAR 没放进 lib,这些问题在教室演示时一碰一个准。网上很多 java 环境变量配置和 mysql 安装教程都写得很详细,但我要提醒的是,装完以后一定要用命令行验证一遍,别等双击 Main 才报错。

java -version javac -version mysql --version

逻辑说明:第一条验证 JRE,第二条验证 JDK 编译器,第三条验证 MySQL 客户端是否在 PATH 中。前两条命令都来自 JDK,如果 javac 提示找不到命令,说明环境变量没配好,而不是 JDK 没装上。

参数说明:JAVA_HOME 必须指向 JDK 安装根目录,比如C:\Program Files\Java\jdk-17,不能只配到 bin 目录。Windows 的 Path 里追加%JAVA_HOME%\bin,Linux 或 macOS 追加$JAVA_HOME/bin。MySQL 8.0 安装时默认选 caching_sha2_password 插件,如果后面 JDBC 连接报错,再回来改配置或换旧版驱动。

提示:本地课程设计用 MySQL 5.7 或 8.0 都可以,但连接驱动 JAR 要和 MySQL 版本对应,别拿 5.1 的驱动连 8.0 的库,否则大概率报 “Unsupported major.minor version”。

数据库连接测试建议用 MySQL Workbench。mysql workbench 使用教程里经常有人卡在连不上 localhost,其实先检查服务有没有启动就行。Windows 下 Win + R 输入 services.msc,找到 MySQL80,确认状态是“正在运行”。Workbench 里连接 root 的时候,如果提示 Access denied,就是密码不对,没有快捷办法,只能重置密码。

2.2 建库建表:学生、课程、选课成绩三张表,字段就该这样定

表结构是系统的地基。我的建议是三张表:student、course、student_course。student_course 同时承担“选课关系”和“成绩表”两个职责,成绩字段直接挂在选课记录上,这是最省事也最容易被评委接受的做法。

CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE course_system; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', sname VARCHAR(50) NOT NULL COMMENT '姓名', password VARCHAR(64) NOT NULL DEFAULT '123456' COMMENT '登录密码', class_name VARCHAR(50) COMMENT '班级' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号', cname VARCHAR(50) NOT NULL COMMENT '课程名', credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT '学分', capacity INT NOT NULL DEFAULT 30 COMMENT '选课容量' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) DEFAULT NULL COMMENT '成绩,未录入为 NULL', UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:student 表的 id 是代理主键,sno 是业务主键,登录用 sno。course 表同样用 id 做主键,cno 做唯一编号。student_course 表的 score 设置为 NULL,表示老师还没录入成绩,不要用 0 或 -1 去代替。UNIQUE KEY 加在 student_id 和 course_id 上,数据库层面直接拦住重复选课。

参数说明:DECIMAL(3,1) 表示最多三位数字、一位小数,能存 0.0 到 99.9,课程学分和成绩都够用。外键约束在课程设计里建议保留,虽然很多人说生产环境不用外键,但答辩时“数据完整性”是一个重要加分点。所有表显式指定 utf8mb4,是为了和后面 JDBC 连接串里的 characterEncoding=UTF-8 对齐,避免中文乱码。

建完表后再补几条测试数据,后续界面调试会舒服很多:

INSERT INTO student (sno, sname, class_name) VALUES ('2023001', '张三', '计科2301'), ('2023002', '李四', '计科2301'); INSERT INTO course (cno, cname, credit, capacity) VALUES ('CS101', 'Java程序设计', 3.0, 30), ('CS102', '数据库原理', 2.5, 30); INSERT INTO student_course (student_id, course_id) VALUES (1, 1), (1, 2);

这段 SQL 的用意是给登录和查询页面留好基础数据。张三有两门选课且都没成绩,正好用来测试“未录入”的展示逻辑。

2.3 项目骨架:普通 Java 工程加 lib 目录,不折腾 Maven 也够用

很多 java 课程设计案例源码喜欢上 Maven,但对课程设计来说,Maven 第一次下载依赖就可能劝退一半人。我更推荐普通 Java 工程,把 MySQL 驱动 JAR 放进 lib 目录,五步就能跑起来:

course-system/ src/ com/example/ Main.java util/DBUtil.java dao/CourseDao.java dao/StudentDao.java model/Course.java model/ScoreVO.java view/LoginFrame.java view/MainFrame.java view/CoursePanel.java view/SelectedCoursePanel.java view/ScorePanel.java lib/ mysql-connector-java-8.0.x.jar

入口类不需要任何花哨写法:

public class Main { public static void main(String[] args) { new LoginFrame().setVisible(true); } }

逻辑说明:main 方法只做一件事,创建 LoginFrame 并显示。至于 LoginFrame 构造方法里要初始化组件、注册监听器,那是 view 包自己的职责。这种写法虽然简单,但答辩时可以顺手提一句“单层入口,组件自包含”。

参数说明:这里不能忽略 IDEA 的类库添加操作。写完代码后,右键 lib 目录并选择“Add as Library”,再运行。如果漏了这一步,控制台会报ClassNotFoundException: com.mysql.cj.jdbc.Driver。Eclipse 用户则要 Build Path -> Configure Build Path -> Add JARs。

在正式写功能前,建议先用一个测试类确认驱动能加载,排除环境问题再往下写:

public class TestDriver { public static void main(String[] args) throws Exception { Class.forName("com.mysql.cj.jdbc.Driver"); System.out.println("驱动加载成功"); } }

逻辑说明:MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,旧版本是com.mysql.jdbc.Driver。写死这个类名,跑通就说明 JAR 和环境变量都没问题。这里先走一遍,能帮后面省掉大量无关报错。

3. JDBC 连接层写好了,后面所有功能都是照着填 SQL

3.1 DBUtil 工具类:连接串、驱动加载和资源关闭

Swing 每个窗口都可能要查库,所以连接逻辑必须收敛到一个工具类里。一个合格的 DBUtil 至少要做到三件事:加载驱动、获取连接、释放资源。我的习惯是用静态块加载驱动,用可变参数关闭资源。

package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/course_system" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动未找到,请检查lib目录"); } } 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) {} } } } }

逻辑说明:静态块在类加载时执行一次,Class.forName的作用是触发驱动注册到 DriverManager。getConnection 每次调用都返回一个物理连接,课程设计阶段没必要做连接池。close 方法接收可变个数的 AutoCloseable,Connection、Statement、ResultSet 都实现了这个接口,finally 里一次调用就能全部关掉。

参数说明:URL 里有三个高频坑,别删。useSSL=false 关掉 SSL 握手,本地开发能少一个证书报错;serverTimezone=Asia/Shanghai 解决 MySQL 8.0 的时区校验;characterEncoding=UTF-8 和建库的 utf8mb4 配套,防止中文乱码。USER 和 PASSWORD 要改成你自己安装 MySQL 时的账号密码。

接着写一个查询方法看看 DBUtil 怎么用:

public List<Course> findAllCourses() { List<Course> list = new ArrayList<>(); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement("SELECT id, cno, cname, credit, capacity FROM course"); rs = ps.executeQuery(); while (rs.next()) { Course c = new Course(); c.setId(rs.getInt("id")); c.setCno(rs.getString("cno")); c.setCname(rs.getString("cname")); c.setCredit(rs.getDouble("credit")); c.setCapacity(rs.getInt("capacity")); list.add(c); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; }

逻辑说明:先拿连接,再预处理 SQL,然后执行查询。rs.next()每调用一次就向下移动一行,返回 false 表示没有更多数据。最后 finally 里关闭三个资源,顺序从 ResultSet 到 Statement 再到 Connection,正好是反向创建顺序。

参数说明:rs.getInt("id")是按列名取数据,比rs.getInt(1)更不容易出错,SQL 里一旦改字段顺序,按列名取就依然安全。

3.2 PreparedStatement 登录校验:为什么不用 Statement 拼接

登录功能是评委最爱问的地方。网上很多旧教程还在用字符串拼接 SQL,看着简单,其实一个大坑。只要学生在学号框里输入' OR '1'='1,拼接出的 SQL 就变成了恒真条件,密码直接绕过。最典型的反例长这样:

// 反例,别这么写 String sql = "SELECT * FROM student WHERE sno='" + sno + "' AND password='" + password + "'";

所以我从第一版开始就只用 PreparedStatement:

public boolean login(String sno, String password) { String sql = "SELECT id FROM student WHERE sno = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { return rs.next(); } } catch (SQLException e) { e.printStackTrace(); return false; } }

逻辑说明:占位符?只代表参数位置,驱动会把 setString 传入的值转义后再放进 SQL 中,因此即使用户输入引号或关键字,也不会改变 SQL 结构。只要rs.next()能返回 true,就说明找到了匹配的学号和密码。

参数说明:这里没有做密码加密,因为课程设计一般不需要,但答辩时最好加一句“生产环境应该存加盐后的哈希值”。sno 和 password 都通过 setString 传入,数据库字段长度要留够,sno 用 20,password 用 64,足够装下常见的哈希串。

3.3 DAO 接口与实现类:把 SQL 从界面里赶出去

Swing 界面里不能直接写 JDBC 代码,否则按钮监听器会变得又长又乱。常见做法是拆出一个数据访问层,接口定义行为,实现类写 SQL。接口是“合同”,实现类才是“施工”。

public interface CourseDao { List<Course> findAllCourses(); boolean selectCourse(int studentId, int courseId); boolean dropCourse(int studentId, int courseId); List<ScoreVO> findScoresBySno(String sno); }

逻辑说明:CourseDao 把课程相关操作全部抽象出来。界面层只依赖接口,不知道底层是 MySQL 还是别的数据库。这样做的好处是,以后想换 ORM 框架,Swing 界面一行都不用动。

实现类里选课方法逻辑是插入一条选课记录:

public class CourseDaoImpl implements CourseDao { @Override public boolean selectCourse(int studentId, int courseId) { String sql = "INSERT INTO student_course (student_id, course_id) VALUES (?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, studentId); ps.setInt(2, courseId); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }

逻辑说明:executeUpdate 返回受影响行数。插入成功则返回 1,返回 true;失败则返回 false。如果学生已经选过这门课,数据库的 UNIQUE KEY 会直接抛 SQLException,界面层可以把这个异常翻译成“你已经选过这门课”的提示框。

参数说明:这里还要从登录 Session 拿到 studentId,但登录时只有学号,所以常见办法是在登录成功后执行一条SELECT id FROM student WHERE sno=?,把真实主键存起来。权限暂时不做,管理员登录也走同一张 student 表,只是角色字段没加,如果评委问起来可以说“增加一个 role 字段即可”。

3.4 数据模型类:Course、Student、ScoreVO,别用 Map 到处传

查询结果变成对象后,界面用起来才顺手。List<Map<String, Object>> 虽然也能用,但取值要转来转去,代码可读性差。课程设计用三个类就够:Course 对应课程表,Student 对应学生表,ScoreVO 对应成绩查询视图。

public class Course { private Integer id; private String cno; private String cname; private Double credit; private Integer capacity; // 构造方法、getter/setter 省略 }

逻辑说明:Course 类的字段类型和数据库要保持一致,但要小心数据库 DECIMAL 映射为 Double 或 BigDecimal 的选择。学分 DECIMAL(3,1) 用 Double 完全够,但如果涉及金额计算,更推荐 BigDecimal。课程设计里选 Double 代码更简单,能在答辩时解释清楚“简单场景不追求高精度”就够了。

4. Swing 界面从登录到主窗体:布局、事件和表格刷新

4.1 登录窗体的最小实现:JFrame + JTextField + JPasswordField

Swing 不需要任何第三方库,JDK 自带就是最大的优势。登录窗体用最简单的 GridLayout,三行两列,足够应付演示。

public class LoginFrame extends JFrame { private JTextField tfSno = new JTextField(); private JPasswordField pfPassword = new JPasswordField(); public LoginFrame() { setTitle("学生选课及成绩管理系统 - 登录"); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setLayout(new GridLayout(3, 2)); add(new JLabel("学号")); add(tfSno); add(new JLabel("密码")); add(pfPassword); JButton btnLogin = new JButton("登录"); add(btnLogin); btnLogin.addActionListener(e -> doLogin()); } private void doLogin() { String sno = tfSno.getText().trim(); String password = new String(pfPassword.getPassword()); if (new StudentDaoImpl().login(sno, password)) { new MainFrame(sno).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "学号或密码错误"); } } }

逻辑说明:GridLayout 三行两列依次摆放学号、输入框、密码、密码框、空组件、登录按钮,布局虽然朴素但不会错位。密码框用 JPasswordField 而不是 JTextField,用户输入的内容在界面上显示为圆点。doLogin 里先 trim 学号,去掉前后空格,再调用登录方法。

参数说明:setLocationRelativeTo(null)让窗体居中显示,这个细节放在 360 乘 220 的窗口上刚刚好。dispose()会在登录成功后释放当前窗体,不释放的话主窗体出现后登录窗口还留在任务栏里,进程行为会很奇怪。

注意:JPasswordField 的 getPassword 返回的是 char[],用 new String 转成字符串后用完最好及时置空。课程设计不用太较真,但代码里不要顺手 System.out.println 打印密码。

4.2 主窗体用 JTabbedPane 拆页面:课程列表、我的选课、成绩查询

课程设计阶段的系统功能不会特别多,主窗体用一个 JTabbedPane 就足够了。三个标签页分别对应三个 JPanel 子类,每个面板只负责自己的表格和按钮。

public class MainFrame extends JFrame { public MainFrame(String sno) { setTitle("欢迎:" + sno); setSize(800, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabs = new JTabbedPane(); tabs.addTab("课程列表", new CoursePanel()); tabs.addTab("我的选课", new SelectedCoursePanel(sno)); tabs.addTab("成绩查询", new ScorePanel(sno)); add(tabs, BorderLayout.CENTER); } }

逻辑说明:登录时把学号 sno 传进来,主窗体再把它分发给两个需要学生信息的页面。CoursePanel 不知道当前用户是谁,因为它只负责展示所有课程和“选课”按钮,选课时再根据学号回查学生 ID。这样划分,每个类的职责比较清晰。

参数说明:setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)是主窗体特有的,登录窗体如果也用 EXIT_ON_CLOSE,那么 dispose 时整个进程都退出。登录成功后的主窗体采用 EXIT 是合理的,关闭主界面等于退出程序。

4.3 成绩单表格:DefaultTableModel 怎么做到刷新不重复

表格是 Swing 里最容易写错的部分,尤其是刷新逻辑。常见错误是点一次查询,表格里的成绩多一行,越点越多。原因很直接:每次都 addRow,从不清理旧数据。正确的刷新方式是先清空再填充:

public void refreshTable(List<ScoreVO> scores) { DefaultTableModel model = (DefaultTableModel) table.getModel(); model.setRowCount(0); for (ScoreVO vo : scores) { model.addRow(new Object[]{ vo.getCno(), vo.getCname(), vo.getScore() == null ? "未录入" : vo.getScore() }); } }

逻辑说明:setRowCount(0) 把 TableModel 的所有现有行清空,相当于一张白纸。然后再 addRow 循环添加新数据,这样表格永远只显示最新查询结果。这里将 score 为 null 的情况显示为“未录入”,比直接显示 null 要专业得多。

参数说明:如果使用的是自己继承 AbstractTableModel 的自定义模型,改完数据后还要调用 fireTableDataChanged(),否则界面不会重新绘制。JTable 默认列宽会自动均分,课程列表课程名较长时容易被挤压,建议加一行:

table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); JScrollPane sp = new JScrollPane(table);

逻辑说明:把表放进 JScrollPane,并把自动调整列宽关掉,这样列宽由表格自身决定,内容太长时会出现横向滚动条,而不是把课程名压缩成省略号。

4.4 选课按钮的事件处理:先查后插,再给用户一个反馈

选课按钮是事件处理里最有代表性的。点击后不能只做一次 insert,否则用户连点两下就出重复数据。除了数据库的唯一约束之外,界面层也要给出友好反馈:

btnSelect.addActionListener(e -> { btnSelect.setEnabled(false); int selectedRow = courseTable.getSelectedRow(); if (selectedRow < 0) { JOptionPane.showMessageDialog(this, "请先选择一门课程"); btnSelect.setEnabled(true); return; } int courseId = (int) courseTable.getValueAt(selectedRow, 0); boolean ok = courseDao.selectCourse(studentId, courseId); if (ok) { JOptionPane.showMessageDialog(this, "选课成功"); refreshTable(); } else { JOptionPane.showMessageDialog(this, "选课失败,可能重复选课或课程不存在"); } btnSelect.setEnabled(true); });

逻辑说明:按钮置灰是为了防止快速连点导致重复提交,数据库唯一约束仍然是最底线的保护。getSelectedRow 返回 -1 表示没选中行,此时直接提示不执行 SQL。成功后立即刷新课程列表的剩余容量和“我的选课”页签,界面数据保持一致。

参数说明:getValueAt 返回的是 Object,这里选课表的第 0 列是课程 ID,所以强转成 int。如果你的表格列顺序调整过,一定要同步修改这里,这是表格代码里最常见的隐性 Bug。

5. 避坑指南:Swing + MySQL 项目最容易翻车的 5 个细节

5.1 中文乱码:三个地方少一个就白搭

现象:界面上课程名显示成“???”,或者 MySQL Workbench 里中文正常,Java 程序里读出乱码。

原因:建库字符集、JDBC 连接串字符集、Java 源文件编码三者不一致。最常见的是建库用了默认 latin1,而连接串里指定了 UTF-8,两边对不上。

解决:先确认建库和建表都用了DEFAULT CHARSET=utf8mb4,然后在 JDBC URL 里加characterEncoding=UTF-8。如果已经建好的库是 latin1,用下面两条 SQL 补救:

ALTER DATABASE course_system CHARACTER SET utf8mb4; ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4;

如果是 IDEA 项目编译乱码,则要检查 Settings -> File Encodings,把所有位置都设为 UTF-8。这三个地方对不上任何一个,乱码就跑不了。

5.2 MySQL 8.0 连不上:Public Key Retrieval is not allowed

现象:点击登录按钮后,控制台抛出java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed

原因:MySQL 8.0 默认用 caching_sha2_password 认证,客户端第一次连接时要从服务器获取公钥来加密密码,但 Connector/J 出于安全考虑,把 allowPublicKeyRetrieval 默认设为 false,所以连接被拒绝。

解决:在 JDBC URL 中显式加上allowPublicKeyRetrieval=trueuseSSL=false。本地开发没问题,但不要让这套配置出现在生产环境里。

String url = "jdbc:mysql://localhost:3306/course_system?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8";

逻辑说明:这两个参数通常成对出现,useSSL=false 避免证书校验,allowPublicKeyRetrieval=true 允许本地请求公钥。报了别的错误的话,优先看完整异常堆栈,而不是只搜前半段。

5.3 JTable 数据不刷新或越插越多

现象:第一次点“查询成绩”显示三行,第二次点变成六行,第三次变成九行,数据越查越多。

原因:点击事件里只调用 addRow,没有先清理旧数据,TableModel 是在老数据基础上不断追加。

解决:在每次查询填充数据前,调用model.setRowCount(0)。如果是扩展 AbstractTableModel 的自定义模型,则在数据更新后调用fireTableDataChanged()通知 JTable 重新绘制。还有一个小问题:不要把查询逻辑写在 JTable 的鼠标事件里,而是在一个独立的方法中完成查询加刷新,按钮和别的事件都能复用它。

5.4 选课重复提交:唯一约束兜底,按钮置灰防手滑

现象:快速双击“选课”按钮,同一个学生同一门课程出现两条记录。

原因:第一次点击的插入还没执行完,第二次点击已经发出了同样的 INSERT。界面层没有防抖,数据库也没有约束时,重复数据就进了表。

解决:数据库层面已经用 UNIQUE KEY 拦住重复,第一次插入成功,第二次会抛 SQLException。程序里可以捕获这个异常并提示“你已经选过这门课程”。更稳妥的做法是在点击后立刻 disable 按钮,等数据库返回结果后再恢复。这个场景在 mysql 面试题里也很常问,你答“唯一约束优先”比答“先查再插”更高级。

5.5 界面卡死:耗时查询不放在事件线程里

现象:点击“查询成绩”后,整个窗口像死机一样,拖不动也不能最小化。

原因:Swing 的事件处理代码运行在事件分发线程 EDT 上。数据库查询、网络请求这类阻塞操作如果直接写在按钮监听器里,EDT 会被阻塞,所有界面绘制都停住。

解决:最简单的处理是查询前禁用按钮并显示“查询中”,查询完再恢复。想加分的话,用 SwingWorker 把查询放到后台线程:

SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() { @Override protected Void doInBackground() { result = courseDao.findScoresBySno(sno); return null; } @Override protected void done() { refreshTable(result); } }; worker.execute();

逻辑说明:doInBackground 运行在后台线程,不会卡住界面;done 方法在主线程执行,可以安全更新表格和按钮状态。这就是 Swing 应用处理耗时任务的标准姿势,也是课程设计答辩中一个非常值的提的改进点。

6. 答辩前做这 3 件事,功能不变,分数明显不一样

如果你做完基本功能还有一两天时间,把以下三件事补上,系统会从“能跑”变成“能讲”。

第一件事是把选课方法改成事务版。数据库事务不用整太复杂,只要把原来的“一条 INSERT”改成“检查容量、插入选课记录、更新已选人数”的完整过程。手动调用conn.setAutoCommit(false),中间任何一步抛异常就conn.rollback(),全部成功再conn.commit()。这会让评委觉得你不是只会拼 SQL,而是考虑过数据一致性。

第二件事是在成绩页面加一个统计栏。SQL 用一条聚合查询就能实现:

SELECT COUNT(*) AS total, AVG(score) AS avg_score, MAX(score) AS max_score FROM student_course WHERE student_id = ?;

把结果显示在成绩表格下方,比如“共 3 门课,平均分 86.3,最高分 95”。这一小段能引出的问题包括聚合函数、NULL 值处理、分组查询,都是 java 八股文和数据库面试的高频点,你答起来也有底气。

第三件事是把成绩导出成 CSV 文件。这个功能不依赖任何第三方库,纯 JDK 就能写:

StringBuilder sb = new StringBuilder("课程编号,课程名称,成绩\n"); for (ScoreVO vo : list) { sb.append(vo.getCno()).append(',') .append(vo.getCname()).append(',') .append(vo.getScore() == null ? "" : vo.getScore()).append('\n'); } Files.write(Paths.get("scores.csv"), sb.toString().getBytes(StandardCharsets.UTF_8));

逻辑说明:用逗号分隔每列,用换行分隔每行。成绩为空时写空字符串,而不是写 null。Files.write 会覆盖已有文件,目录里有老文件时会直接替换。如果成绩里有特殊字符或姓名包含逗号,记得再加一层引号包裹,但课程设计场景一般用不到。

我当年做完这套系统后,觉得能跑通就算完事,结果评委问“选课并发时怎么保证容量”,我答不上来。后来把事务和统计加上,同样的题目,讲的内容一下子多了起来。你按照这个顺序往下做,最后输出的一定是一个能演示、能讲清、能抗追问的完整工程。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询