首页 / 资讯中心 / 文章详情

Java Swing+MySQL教务管理系统开发实战:从数据库设计到界面实现

Java Swing+MySQL教务管理系统开发实战:从数据库设计到界面实现 ★ FEATURED ARTICLE
简介面向Java初、中级学习者及期末大作业、课程设计场景的学校教务管理系统项目包基于Java Swing与MySQL实现覆盖学生信息维护、课程安排、成绩录入与查询、教师管理等典型教务业务模块。项目源码已通过本地编译验证评审得分98分难度适中并经助教老师审定适合直接用于课程设计、期末大作业或作为毕业设计的参考底座。压缩包共39个文件包含25个Java源码文件、6张界面运行截图、5个XML项目配置、SQL数据库脚本以及README说明文档目录按Maven结构组织导入IDE后可快速启动便于结合截图核对界面效果。整个ZIP包仅240KB轻量实用。资源已有220人学习使用能帮助读者理解Swing事件驱动编程、MySQL表设计与增删改查操作是一份完整的教学管理综合实训项目。1. 学校教务管理系统为什么用 JavaSwingMySQL 做桌面端一个普通学院每学期要处理几千条选课和成绩记录。如果教务老师手里只有一张共享 Excel多人同时编辑时要么锁文件要么互相覆盖等到期末导出成绩格式又对不上数据就乱了。与其等一个完整 Web 平台不如先用 JavaSwingMySQL 做一个学校教务管理系统桌面窗口、本地数据库、双击启动一台低配电脑就能跑。它能覆盖学生信息维护、教师排课、课程选课和成绩登记这几条主流程代码结构也能让 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 默认给 50selected_count 默认 0实际使用中要根据教室容量和教务处政策调整。sys_user 表的 password 字段这里直接用明文正式落地必须存 MD5 或 BCrypt 哈希值登录校验时比对哈希而不是比原文。另一处关键设计是 sys_user 表里的 ref_id。管理员登录后role 为 0ref_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-javaMySQL 8.0 对应 8.x 版本。驱动类名也有区别5.x 是 com.mysql.jdbc.Driver8.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 ?useSSLfalse useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; 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 里的四个参数各有来头。useSSLfalse 是关掉 SSL 加密本地开发环境没配证书开着反而报警告甚至连接失败。useUnicodetrue 和 characterEncodingutf8 是解决中文乱码的关键告诉 MySQL 驱动用 UTF-8 编码传输字符串少了这个参数插入的中文在数据库里大概率变成问号。serverTimezoneAsia/Shanghai 是 MySQL 8.0 的硬性要求8.0 驱动默认取 JVM 时区如果 JVM 时区和数据库不一致连接时会抛 serverTimezone 相关的异常。allowPublicKeyRetrievaltrue 是配合 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 11拼接 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-resourcesConnection、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); ListStudent 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.Driver8.x 驱动类是 com.mysql.cj.jdbc.Driver很多人在升级数据库后忘了同步改类名。解决方式是确认数据库版本后去 Maven 仓库下载对应版本的 jar放进 IDE 项目的 lib 目录并加入构建路径。现象二Class.forName 过了但 getConnection 报 CommunicationsException提示连接 localhost:3306 失败。原因多数是 MySQL 服务没启动Windows 下执行 net start mysql 启动服务也可能是端口被改了my.ini 里配了 3307URL 里却还写 3306。还有少部分是防火墙拦了 3306 端口。解决方式是按顺序检查服务状态、端口占用、防火墙规则。检查端口占用用 netstat -ano | findstr 3306一眼就能看出监听地址对不对。现象三连接报 Access denied for user rootlocalhost。原因几乎是用户名密码错误或者 root 账号只允许 localhost 登录你用了远程地址连。这里有一个血泪经验如果刚装完 MySQLroot 的密码插件是 caching_sha2_password而项目用的 JDBC 驱动版本太老会报 Unable to load authentication plugin。解决方式要么升级驱动到 8.0 以上要么在 URL 里加 allowPublicKeyRetrievaltrue。这个参数前文已提到这里再次强调它是 8.0 连接成功的必要条件。5.2 中文乱码与表不刷新字符集和 EDT 线程中文乱码是这个项目里出镜率最高的玄学问题。现象是界面输入中文后存进数据库变成问号或者数据库里正常但界面上显示乱码。原因分为三层连接层 URL 没加 characterEncodingutf8数据库和表默认字符集不是 utf8mb4以及 MySQL 服务端 character_set_server 参数不对。解决方式是一套组合拳建库时指定 DEFAULT CHARACTER SET utf8mb4URL 中加 useUnicodetruecharacterEncodingutf8再检查 my.ini 里 character-set-serverutf8mb4。三个位置统一后乱码基本上绝迹。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_active1 的记录。我强烈推荐第二种因为成绩是历史数据不能因为学生退学就物理删掉逻辑删除相当于给数据留了一颗后悔药。如果课设阶段必须物理删除至少要在删除前提示用户“该学生的选课和成绩记录将一并删除”避免用户手滑。重复选课的问题是这样的界面提示选课成功但 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再手动走一遍增删改查流程最后才交付。这个习惯救过我无数次因为界面上的“成功”提示和数据库里的真实状态有时候是两回事。希望这套 JavaSwingMySQL 的教务管理系统方案帮你少走弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站