☰
Java Swing+MySQL教务管理系统开发实战:从数据库设计到界面实现
2026/10/9 11:20:19 网站建设 项目流程

简介:面向Java初、中级学习者及期末大作业、课程设计场景的学校教务管理系统项目包,基于Java Swing与MySQL实现,覆盖学生信息维护、课程安排、成绩录入与查询、教师管理等典型教务业务模块。项目源码已通过本地编译验证,评审得分98分,难度适中,并经助教老师审定,适合直接用于课程设计、期末大作业或作为毕业设计的参考底座。压缩包共39个文件,包含25个Java源码文件、6张界面运行截图、5个XML项目配置、SQL数据库脚本以及README说明文档,目录按Maven结构组织,导入IDE后可快速启动,便于结合截图核对界面效果。整个ZIP包仅240KB,轻量实用。资源已有220人学习使用,能帮助读者理解Swing事件驱动编程、MySQL表设计与增删改查操作,是一份完整的教学管理综合实训项目。

1. 学校教务管理系统,为什么用 Java+Swing+MySQL 做桌面端

一个普通学院每学期要处理几千条选课和成绩记录。如果教务老师手里只有一张共享 Excel,多人同时编辑时要么锁文件,要么互相覆盖,等到期末导出成绩,格式又对不上,数据就乱了。与其等一个完整 Web 平台,不如先用 Java+Swing+MySQL 做一个学校教务管理系统:桌面窗口、本地数据库、双击启动,一台低配电脑就能跑。它能覆盖学生信息维护、教师排课、课程选课和成绩登记这几条主流程,代码结构也能让 Java 初学者同时练到 JDBC、Swing 事件和 SQL 事务。这套方案特别适合三类人:正在做 Java 课程设计的学生、小规模内部管理系统的一线开发,以及想快速搭工具验证教务流程的负责人。下面的内容按先建库、再连库、后写界面的顺序,把这条路完整走一遍。

2. 先把数据库立住:教务系统的表结构设计与初始化 SQL

很多人在这类项目里习惯先拖 Swing 界面,把窗口画得差不多了再回头建表,结果往往是界面改起来容易,SQL 返工却要推倒重来。我的经验是反过来:第一步就把表结构定死,后面所有代码都围绕这几张表转。教务管理系统的核心数据逃不开学生、教师、课程、选课、成绩这五个实体,再额外加一张用户表做登录鉴权。表设计对了,后面的 JDBC 和 Swing 代码都是顺水推舟。

2.1 五张核心表如何设计:从实体关系到字段级约束

先理清实体关系。一个学生可以选多门课,一门课可以被多个学生选,这是典型的多对多,中间必须有一张选课表来承接;成绩是选课行为的结果,单独放一张成绩表更便于统计。教师与课程是一对多,一门课只有一个主讲教师。加上用户表,整个系统一共有六张表。

表名核心字段说明
studentid, stu_no, name, gender, dept, grade, phonestu_no 唯一,学号是业务主键
teacherid, teacher_no, name, gender, dept, titleteacher_no 唯一,title 存职称
courseid, course_no, name, credit, teacher_id, capacity, selected_countselected_count 用于选课容量控制
course_selectionid, student_id, course_id, select_time联合唯一索引防重复选课
scoreid, student_id, course_id, score, comment联合唯一索引保证一课一成绩
sys_userid, username, password, role, ref_idrole 区分管理员/教师/学生,ref_id 关联对应业务表

课程表里的 capacity 和 selected_count 是这套系统的关键。capacity 是课程容量,selected_count 是当前已选人数,每次选课成功就把 selected_count 加一。把这个字段冗余在课程表里,选课时判断容量只需要一次查询,不需要 count 选课表,性能更好,逻辑也更直观。score 表用联合唯一索引约束(student_id, course_id),确保同一个学生对同一门课只可能有一条成绩记录,从数据库层面杜绝重复录入。

外键是否要物理创建,是个值得纠结的点。课程设计阶段我会建议建物理外键,因为 MySQL 的 InnoDB 引擎会帮你维护引用完整性,删除选课记录时不会留下孤儿数据。但生产环境里很多团队反而故意不建物理外键,改用逻辑外键加应用层校验,因为物理外键在批量导入、分库分表时会成为瓶颈。这套桌面系统就老老实实建物理外键,毕竟数据量不大,正确性优先。唯一索引一定要建,重复选课这个问题如果只靠 Java 代码去查,并发场景下照样会穿,唯一索引是最后的底线。

2.2 初始化 SQL:建库建表一次跑通,字符集与参数说明

下面这段 SQL 是整套系统的地基。我习惯把建库、建表、插入测试数据放在一个脚本里,MySQL 命令行或者 Navicat 里直接执行一遍就能得到可用的初始环境。数据库名用 school_ms,字符集统一 utf8mb4。utf8mb4 是 utf8 的超集,能存 emoji 和生僻字,现在 MySQL 5.7 和 8.0 都默认支持,没有理由不用。

CREATE DATABASE IF NOT EXISTS school_ms DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE school_ms; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT '男', dept VARCHAR(50), grade VARCHAR(20), phone VARCHAR(20) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT '男', dept VARCHAR(50), title VARCHAR(30) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, teacher_id INT, capacity INT DEFAULT 50, selected_count INT DEFAULT 0, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), comment VARCHAR(200), UNIQUE KEY uk_score_stu_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(200) NOT NULL, role TINYINT NOT NULL DEFAULT 2, -- 0管理员 1教师 2学生 ref_id INT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO teacher (teacher_no, name, dept, title) VALUES ('T001', '张建国', '计算机学院', '副教授'); INSERT INTO course (course_no, name, credit, teacher_id, capacity, selected_count) VALUES ('C001', 'Java程序设计', 3.0, 1, 50, 0); INSERT INTO student (stu_no, name, gender, dept, grade) VALUES ('20230001', '李小明', '男', '计算机学院', '2023级'); INSERT INTO sys_user (username, password, role, ref_id) VALUES ('admin', 'admin123', 0, NULL), ('20230001', '123456', 2, 1);

这段初始化脚本里要注意几个细节。外键约束的命名我习惯显式写出 fk_ 前缀,这样后面删除索引或约束时能一眼看出它是干什么的。course 表的 capacity 默认给 50,selected_count 默认 0,实际使用中要根据教室容量和教务处政策调整。sys_user 表的 password 字段这里直接用明文,正式落地必须存 MD5 或 BCrypt 哈希值,登录校验时比对哈希而不是比原文。

另一处关键设计是 sys_user 表里的 ref_id。管理员登录后,role 为 0,ref_id 为空;教师登录后,ref_id 指向 teacher 表的 id;学生登录后,ref_id 指向 student 表的 id。这样登录成功后,界面只需要拿着 role 和 ref_id 去查对应业务表,就能确定当前用户是谁、能看哪些数据。

3. JDBC 连接层:封装 DBUtil 与登录模块的落地代码

数据库建好了,下一步是写 Java 这边的数据库访问层。这个项目不需要引入 MyBatis 或 Hibernate,原生 JDBC 足够。整套 JDBC 代码的核心就两件事:第一,写一个 DBUtil 类管理连接的创建和释放;第二,业务代码里用 PreparedStatement 执行 SQL,绝不能用字符串拼接。很多人在这一步翻车,根源往往不是 SQL 写错,而是驱动 jar 没放对位置,或者连接参数少了某一个。

3.1 写一个 DBUtil 连接类:驱动、URL 三个必调参数

先把 MySQL 的 JDBC 驱动 jar 放进项目的 lib 目录。MySQL 5.7 对应 5.x 版本的 mysql-connector-java,MySQL 8.0 对应 8.x 版本。驱动类名也有区别:5.x 是 com.mysql.jdbc.Driver,8.x 是 com.mysql.cj.jdbc.Driver。如果你连接 8.0 数据库却写 5.x 的类名,Class.forName 会直接抛 ClassNotFoundException,这是最常见的第一道坎。

package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/school_ms" + "?useSSL=false" + "&useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(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(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) {} } } }

URL 里的四个参数各有来头。useSSL=false 是关掉 SSL 加密,本地开发环境没配证书,开着反而报警告甚至连接失败。useUnicode=true 和 characterEncoding=utf8 是解决中文乱码的关键,告诉 MySQL 驱动用 UTF-8 编码传输字符串,少了这个参数,插入的中文在数据库里大概率变成问号。serverTimezone=Asia/Shanghai 是 MySQL 8.0 的硬性要求,8.0 驱动默认取 JVM 时区,如果 JVM 时区和数据库不一致,连接时会抛 serverTimezone 相关的异常。allowPublicKeyRetrieval=true 是配合 MySQL 8.0 缓存 SHA-2 认证插件的参数,不设置的话,首次连接可能报 Public Key Retrieval is not allowed。

连接释放这一块,我坚持在 finally 块里调用 close。Java 7 以后可以用 try-with-resources 自动关闭,但 DBUtil 的静态方法更适合手动管理,尤其是在 Swing 界面里,连接生命周期往往跨越多个方法调用。实际开发中还可以引入 HikariCP 连接池替换这里的 DriverManager,后面第 6 章再说。

3.2 登录模块实现:PreparedStatement 防注入与角色读取

有了 DBUtil,写登录功能就顺理成章。登录的 SQL 是查 sys_user 表,按用户名和密码匹配记录。注意这里必须用 PreparedStatement,不能用 Statement 拼字符串。用户名里如果传入 ' OR '1'='1,拼接 SQL 就会变成恒真条件,直接把系统脱裤,这是 Java 面试里高频考察的安全问题,也是实测必踩的坑。

public User login(String username, String password) { String sql = "SELECT id, username, role, ref_id FROM sys_user " + "WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRole(rs.getInt("role")); u.setRefId(rs.getInt("ref_id")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

这里的逻辑分三层。第一层是 SQL 里的占位符,PreparedStatement 先让数据库编译 SQL 模板,再设置参数,参数只被当作字面量处理,永远不会改变 SQL 结构。第二层是 try-with-resources,Connection、PreparedStatement、ResultSet 三个资源自动关闭,省去大量 finally 样板代码。第三层是 ResultSet 的读取,列名直接从数据库表字段来,不要依赖索引序号,因为一旦表结构调整,按序号取值容易踩坑。

login 方法返回的 User 对象是简单的 POJO,包含 id、username、role、refId 四个字段。登录成功后,Swing 界面拿着这个对象决定跳转到哪个主界面,同时把当前用户信息保存在内存里,后续所有业务操作都基于这个身份做权限判断。密码在真实系统里要加密存储,登录 SQL 里应该比对哈希值,例如传入的密码先 MD5 再查库,sys_user 初始化数据也不要再放明文。实际落地时,我一般会在 LoginFrame 里做三件事:空值校验、调用 login 方法、成功后 new 主窗口并 dispose 当前窗口。这样登录模块与业务界面解耦,后面替换加密方式只需要动 login 方法内部。

4. Swing 界面与业务联动:主界面切换、表格刷新与选课事务

连接层就绪后,进入工作量最大的 Swing 界面层。Swing 本身不复杂,用到的组件无非是 JFrame、JPanel、JLabel、JTextField、JButton、JTable、JComboBox,加一个 CardLayout 或 JSplitPane 做布局。真正的难点不在单个组件,而在两个容易被忽略的点:一是登录后如何从登录窗平滑切到主界面,二是 JTable 的数据在增删改查之后如何正确刷新。这两个点处理不好,界面给人的感觉就是卡顿、不跟手、数据对不上。

4.1 从登录窗到主界面:CardLayout 切换与权限分级

登录成功后的跳转,很多初学者把新窗口直接 new 出来,旧窗口 dispose 掉,这没问题。但更好的做法是主界面用 CardLayout 管理多个业务面板,登录窗口只负责验证并打开主界面。主界面内部再用角色字段决定哪些卡片可见,这样一个窗口搞定所有功能,不用来回弹窗。

public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle("学校教务管理系统 - 登录"); setSize(400, 260); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); initUI(); } private void initUI() { JPanel panel = new JPanel(new GridBagLayout()); usernameField = new JTextField(16); passwordField = new JPasswordField(16); JButton loginBtn = new JButton("登录"); // 布局代码省略,事件是核心 loginBtn.addActionListener(e -> doLogin()); add(panel); } private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } User user = new LoginDao().login(username, password); if (user == null) { JOptionPane.showMessageDialog(this, "用户名或密码错误"); return; } new MainFrame(user).setVisible(true); dispose(); } }

这段代码里有两个细节值得注意。第一个是 JPasswordField 的取值,必须用 getPassword() 拿到 char[],再转成 String。getText() 方法在 JPasswordField 上被标记为废弃,因为字符串会常驻内存,安全性差。第二个是登录按钮的 ActionListener,登录动作放在独立的 doLogin 方法里,后续要加回车键触发登录,直接调用同一个方法就行。MainFrame 构造器接收 User 对象,根据 role 值决定显示哪些菜单。角色 0 显示全部管理功能,角色 2 学生端只显示选课和成绩查询,这一步做完,系统的权限骨架就立住了。

4.2 JTable 展示与增删改查:刷新表格的两种正确姿势

JTable 是 Swing 里最实用的数据展示组件,但它的使用有一个经典误区:直接把 ResultSet 的数据塞进 JTable,然后发现数据改完后界面不更新。JTable 本身不存数据,数据在 TableModel 里,修改数据后必须通知 TableModel 刷新。

private DefaultTableModel buildStudentModel() { String[] columns = {"ID", "学号", "姓名", "性别", "学院", "年级"}; DefaultTableModel model = new DefaultTableModel(columns, 0); List<Student> list = new StudentDao().findAll(); for (Student s : list) { model.addRow(new Object[]{s.getId(), s.getStuNo(), s.getName(), s.getGender(), s.getDept(), s.getGrade()}); } return model; } // 增删改查之后调用 studentTable.setModel(buildStudentModel());

第一种刷新方式是重新 setModel,粗暴但有效。每次从数据库查出最新 List,重新构建 DefaultTableModel,整体替换掉 JTable 的模型。这种方式代码最少,数据量在几千条以内完全感觉不到性能差异,适合课设和学习阶段。第二种方式是自己继承 AbstractTableModel,持有数据集合,增删改后调用 fireTableDataChanged()。这种做法的价值在于,JTable 的排序、选中状态和列宽会被保留,不会因为重新 setModel 而重置。后续要扩展成按列排序,自定义模型也更方便。

实际项目里我一般先用第一种方式跑通功能,等界面稳定后再改成自定义 TableModel。开发效率优先,过早优化反而是负担。表格数据显示后,增删改查的常规套路是:新增弹 JDialog,删除先确认再执行 JDBC delete,修改选中行回填数据再 update。无论哪种操作,执行完 SQL 后一定要刷新表格,这一步漏了就会让用户以为操作没生效。

4.3 选课与成绩录入:一个事务把并发问题拦在数据库层

选课是整个系统里最需要谨慎的业务。容量判断、插入选课记录、更新已选人数,这三步必须放在同一个事务里。如果分开执行,两个学生同时选同一门只剩一个名额的课时,两个线程都会通过容量检查,最后超员。这种并发问题,光靠 Java 代码判断解决不了,必须在数据库层面加锁。

public void selectCourse(int studentId, int courseId) throws Exception { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 第一步:行锁锁定课程记录,阻止并发修改 String sql1 = "SELECT selected_count, capacity FROM course WHERE id = ? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(sql1)) { ps.setInt(1, courseId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { int selected = rs.getInt(1); int capacity = rs.getInt(2); if (selected >= capacity) { throw new RuntimeException("该课程容量已满"); } } else { throw new RuntimeException("课程不存在"); } } } // 第二步:插入选课记录,唯一索引会兜住重复选课 String sql2 = "INSERT INTO course_selection(student_id, course_id, select_time) " + "VALUES (?, ?, NOW())"; try (PreparedStatement ps = conn.prepareStatement(sql2)) { ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); } // 第三步:更新已选人数 String sql3 = "UPDATE course SET selected_count = selected_count + 1 WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql3)) { ps.setInt(1, courseId); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn, null, null); } }

这段代码的核心是 SELECT ... FOR UPDATE。它把课程那行记录锁住,直到事务提交或回滚。第二个事务执行同样的查询时,会阻塞等待第一个事务结束,然后读到更新后的 selected_count,自然就不会超选。第二步的 INSERT 如果违反 uk_stu_course 唯一索引,会抛 DuplicateKeyException,整个事务回滚,selected_count 不会误加。这三步缺一不可:不加行锁会超员,不设唯一索引会重复选课,不用事务的话前面两步执行到一半失败,数据就处于中间状态。

成绩录入比选课简单,但同样建议放在事务里,因为成绩表更新往往伴随评语、修改时间和操作日志等多张表的写入。这里给一个实战建议:事务里只放必要的 SQL,不要把 JOptionPane 弹窗或网络请求放进事务中间,否则用户不点确定,事务一直挂着,数据库连接很快就耗尽。

5. 避坑排查:教务管理系统最常见的 6 个翻车现场

这一章是给已经动手写代码的人看的。前四章把系统搭起来后,接下来遇到的问题几乎都是环境类、并发类、打包类的问题,跟业务逻辑关系不大。我把这些年在这个项目上见过的翻车现场按出现频率排了个序,每个都按现象、原因、解决三段式写清楚,你照着排查就能省下大量调试时间。

5.1 数据库连不上:驱动类名、时区参数与 jar 包位置

现象一:运行 DBUtil 的静态代码块直接抛 ClassNotFoundException,控制台提示找不到驱动类。原因一般有两个:mysql-connector-java 的 jar 没有放进项目的 lib 目录,或者驱动类名写错。MySQL 5.x 驱动类是 com.mysql.jdbc.Driver,8.x 驱动类是 com.mysql.cj.jdbc.Driver,很多人在升级数据库后忘了同步改类名。解决方式是确认数据库版本后,去 Maven 仓库下载对应版本的 jar,放进 IDE 项目的 lib 目录并加入构建路径。

现象二:Class.forName 过了,但 getConnection 报 CommunicationsException,提示连接 localhost:3306 失败。原因多数是 MySQL 服务没启动,Windows 下执行 net start mysql 启动服务;也可能是端口被改了,my.ini 里配了 3307,URL 里却还写 3306。还有少部分是防火墙拦了 3306 端口。解决方式是按顺序检查服务状态、端口占用、防火墙规则。检查端口占用用 netstat -ano | findstr 3306,一眼就能看出监听地址对不对。

现象三:连接报 Access denied for user 'root'@'localhost'。原因几乎是用户名密码错误,或者 root 账号只允许 localhost 登录,你用了远程地址连。这里有一个血泪经验:如果刚装完 MySQL,root 的密码插件是 caching_sha2_password,而项目用的 JDBC 驱动版本太老,会报 Unable to load authentication plugin。解决方式要么升级驱动到 8.0 以上,要么在 URL 里加 allowPublicKeyRetrieval=true。这个参数前文已提到,这里再次强调它是 8.0 连接成功的必要条件。

5.2 中文乱码与表不刷新:字符集和 EDT 线程

中文乱码是这个项目里出镜率最高的玄学问题。现象是界面输入中文后存进数据库变成问号,或者数据库里正常但界面上显示乱码。原因分为三层:连接层 URL 没加 characterEncoding=utf8,数据库和表默认字符集不是 utf8mb4,以及 MySQL 服务端 character_set_server 参数不对。解决方式是一套组合拳:建库时指定 DEFAULT CHARACTER SET utf8mb4,URL 中加 useUnicode=true&characterEncoding=utf8,再检查 my.ini 里 character-set-server=utf8mb4。三个位置统一后,乱码基本上绝迹。

JTable 不刷新则是另一个高频问题。现象是增删改查都提示成功,但界面表格还是旧数据。原因是 TableModel 没有重新设置,或者数据修改发生在非事件分发线程。Swing 是单线程模型,所有 UI 更新必须在事件分发线程(EDT)上执行。如果你在后台线程里查完数据后直接操作 JTable,轻则不刷新,重则抛 InterruptedException 或组件状态异常。解决方式是把数据加载和刷新都放在 SwingUtilities.invokeLater 里,或者用 SwingWorker 做后台查询。

SwingUtilities.invokeLater(() -> { studentTable.setModel(buildStudentModel()); });

这段代码不是魔法,它只是告诉 Swing 把更新操作排队到事件分发线程上执行。后台线程查数据库,查到结果后切回 EDT 刷新表格,界面就不会卡死,数据也一定能显示出来。

5.3 外键约束、重复选课与打包闪退:三桩血泪经验

删除学生报外键约束失败,这是新手必踩的第三大坑。现象是删除学生时抛出 Cannot delete or update a parent row,原因是 course_selection 和 score 表里还有该学生的记录,外键拦着不让删。解决方式有两种:一是先删关联表再删主表,代码里按顺序执行三条 delete;二是把学生表的删除改成逻辑删除,加一个 is_active 字段,界面只查 is_active=1 的记录。我强烈推荐第二种,因为成绩是历史数据,不能因为学生退学就物理删掉,逻辑删除相当于给数据留了一颗后悔药。如果课设阶段必须物理删除,至少要在删除前提示用户“该学生的选课和成绩记录将一并删除”,避免用户手滑。

重复选课的问题是这样的:界面提示选课成功,但 course_selection 表里出现了同一条学生和课程的重复记录。原因要么是缺少唯一索引,要么是快速双击提交按钮触发了两次 insert。解决方式是表和代码两头堵:表上加 UNIQUE KEY uk_stu_course,代码里在 doLogin 或选课按钮的 ActionListener 执行后立刻禁用按钮,防止连点。唯一索引这条,即使事务代码写得再完备也别省,它是最便宜的数据兜底。

打包闪退是最容易劝退新人的一关。现象是 IDE 里跑得好好的,导出 Runnable JAR 后双击无反应,或者闪一下消失。原因是 jar 包里没有 MySQL 驱动,运行时 ClassNotFoundException,但桌面上没有控制台,看不到异常信息。解决方式是把驱动 jar 放在项目根目录的 lib 文件夹,并在导出时选择 Library folder 方式,让最终产物是一个包含 lib 的目录结构而非单 jar 包。再不行,就写一个 .bat 启动脚本,用 java -jar school.jar 方式运行,这样至少能看到堆栈信息,不至于对着黑匣子猜。我自己的习惯是发布前一定删掉本地仓库,在新电脑上跑一遍启动脚本,确认依赖完整再交付。

6. 怎么验证这套系统真正可用:数据校验 SQL 与扩展方向

系统写完不等于能用,我建议在交接之前跑一遍数据完整性校验。光看界面能不能弹窗是不够的,数据库里的脏数据才是埋雷点。下面三条 SQL 是我每次上线前必跑的固定动作,它们能快速暴露重复选课、超容量和成绩缺失三类问题。

-- 1. 查同一学生同一课程出现多条选课记录 SELECT student_id, course_id, COUNT(*) AS cnt FROM course_selection GROUP BY student_id, course_id HAVING cnt > 1; -- 2. 查已选人数超过课程容量的异常数据 SELECT c.id, c.name, c.capacity, c.selected_count FROM course c WHERE c.selected_count > c.capacity; -- 3. 查有选课记录但无成绩的学生课程组合 SELECT cs.student_id, cs.course_id FROM course_selection cs LEFT JOIN score sc ON cs.student_id = sc.student_id AND cs.course_id = sc.course_id WHERE sc.id IS NULL;

三条 SQL 的查询结果如果都为空,说明核心数据链路是干净的。第一条为空代表唯一索引和事务生效;第二条为空代表容量控制没有穿透;第三条为空代表所有选了课的学生都有成绩,教务老师在期末不会陷入对账地狱。这三条 SQL 建议存成一个 verify.sql 文件,每次改完代码就重跑一遍。

验证通过后,这套系统的下一步改进方向我已经想得很清楚。第一优先级是给登录密码换 BCrypt 哈希存储,sys_user 表里现在还是明文,这在任何真实环境里都是不可接受的。第二优先级是把数据库连接换成 HikariCP 连接池,代码改动很小,只要把 DBUtil 里获取连接的方式从 DriverManager 换成 DataSource,但并发能力会明显提升。第三优先级是添加成绩单导出功能,用一个简单的 Java 程序生成 CSV 文件,教务处老师用 Excel 打开就能打印,这个功能比花哨的图表更实用。如果哪一天用户量超过单机 MySQL 的承载力,就该考虑把 Swing 前端换成的 B/S 架构,但那是另一条路了。

最后说一个我自己的习惯:每次完成一个模块,我会在提交前先跑一遍对应的数据校验 SQL,再手动走一遍增删改查流程,最后才交付。这个习惯救过我无数次,因为界面上的“成功”提示和数据库里的真实状态,有时候是两回事。希望这套 Java+Swing+MySQL 的教务管理系统方案帮你少走弯路。

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

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

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

立即咨询