news 2026/9/17 5:59:17

Java Swing+MySQL选课系统开发详解:从数据库设计到并发事务控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing+MySQL选课系统开发详解:从数据库设计到并发事务控制

简介:基于Java Swing和MySQL实现的学生选课管理系统,是面向计算机相关专业毕业设计学生与Java实战学习者的完整项目资源。系统设置管理员和普通用户两类角色,覆盖登录验证、学生选课、课程添加与修改、课程查看、按课程名称或任课教师搜索、学生资料与个人信息管理等常用功能,界面采用Swing组件构建,层次清晰,操作直观,既能满足课程设计需要,也可作为毕业设计的基础框架。资源包为zip压缩格式,共158个文件,其中Java源码负责系统功能逻辑,Swing窗体文件对应程序操作界面,SQL脚本用于创建和初始化数据库,JAR包是运行依赖库,PNG与JPG图片提供界面素材和运行截图,AVI视频为部署运行指导,整体大小58.59MB。项目已经过严格调试,可导入Eclipse或IDEA配合Navicat直接启动,配套指导视频与数据库脚本能帮助使用者快速完成环境配置、跑通项目,并理解Swing与MySQL结合下的面向对象开发思路。当前已有451人学习下载,适合需要快速获取可运行课设源码并进行二次扩展的读者。

1. 一个 Swing+MySQL 的选课系统,最值得拆的不是界面

拿到这套基于 Java Swing 和 MySQL 的学生选课管理系统,第一反应往往是看窗口布局、按钮点击、表格刷新。真正跑起来之后会发现,纠结的点全在数据一致性上:同一门课两个学生同时选,会不会超员;同一个学生重复提交,会不会生成两条选课记录;管理员改了课程时间,已选学生要不要收到通知。这些才是管理系统的灵魂,Swing 只是皮囊。这个项目比较适合三类人:正在做课程设计、需要快速出完整 Demo 的大三学生;刚学完 Java SE 和 JDBC、想看看面向对象怎么落在具体业务上的自学者;以及接手别人课设代码、需要改成自己毕设课题的二次开发者。下文按照「表结构设计 → 连接与登录 → 选课事务 → 查询导出」的顺序拆,每步都给可直接抄的代码和参数说明。

2. 设计表之前,先把选课业务的边界捋清楚

2.1 角色权限决定了功能树,也决定了表和字段的粒度

系统分管理员和普通学生两个角色,这个区分不是 UI 层隐藏按钮那么简单,而是要从数据模型上体现。管理员关心的是课程维护:添加、修改、查看、按名称或老师搜索;学生关心的是选课、退课、个人信息查看。如果只做一张 user 表、用一列 role 区分身份,后续每次查询都要带上where role='student'这种逻辑散落在 DAO 里的写法,维护成本很高。常见做法是拆成学生表和管理员表,虽然某些字段冗余,但胜在语义清晰。

我这里建议拆四张表:管理员表、学生表、课程表、选课关系表。管理员表只需要 id 和账号密码;学生表存学号、姓名、专业、年级、密码;课程表存课程编号、名称、学时、学分、上课时间、任课老师、容量、已选人数;选课关系表记录学生和课程的关联、选课时间、状态。这个拆分的好处是:课程信息变更不影响学生数据,选课记录可以独立做统计,后续加「开课学期」字段也不用到三张表上去改。

2.2 建表脚本和注意点:字符集、引擎和默认值

需要关注的东西很多,但最致命的是字符集和存储引擎。字符集不对,插入中文姓名或者课程名称直接乱码甚至报错;存储引擎用错,事务回滚就不生效。下面的脚本是跑通整个系统的基线。

CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE course_selection; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, password VARCHAR(64) NOT NULL, name VARCHAR(20) NOT NULL, major VARCHAR(50), grade VARCHAR(10) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, credit DECIMAL(3,1), teacher VARCHAR(20), schedule VARCHAR(50), capacity INT DEFAULT 20, selected INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sc ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20), course_id VARCHAR(20), select_time DATETIME, status TINYINT DEFAULT 1, UNIQUE KEY uk_sc (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

脚本中有三个关键点。student_idcourse_id都用VARCHAR,不用自增主键,一方面是因为学号和课程号本来就是业务编码,另一方面防止并发下自增值错乱。sc表上加了UNIQUE KEY uk_sc,这条唯一索引是从数据库层面杜绝同一学生重复选同一门课,比在 Java 代码里先查再插可靠得多。外键约束保留着,虽然会让删除课程时多点检查开销,但对于毕设系统来说,保证数据完整性更值。status字段用TINYINT,1 表示已选,0 表示退选,保留历史记录而不是直接删除,方便出缺勤分析。

2.3 为什么必须用事务:selected 字段和 sc 记录就像跷跷板

课程表里的selected是冗余字段,用来快速查询余量。正因为它冗余,才最容易出错。选课成功时要执行两步:插入 sc 记录、更新 course 表的selected = selected + 1。这两步如果没有事务包裹,第一步成功、第二步失败,学生看到的是「选课失败」但实际上 sc 里已经有记录;反过来更糟,selected增加了但 sc 没有记录,名额被白白占用。所以中间层必须要用setAutoCommit(false)+commit+rollback的经典组合,后面第三章的代码会完整演示。另外,selected字段不是摆设,查询课程列表时的余量计算直接读它,不要用COUNT(*)去临时统计 sc 表,那样课程多的时候会很吃力。

3. 从数据库到界面:JDBC 封装与登录态管理

3.1 连接配置与驱动加载:用类封装还是直接 JDBC?

资源包里给的运行指导视频通常默认你已经装好了 eclipse 和 MySQL,但实际问题往往出现在最前面。驱动加载Class.forName("com.mysql.cj.jdbc.Driver")是老写法,MySQL 8.0 以上必须带cj,而 5.7 及以下用com.mysql.jdbc.Driver。这里建议统一用 MySQL 8.x 的驱动,因为 5.x 的驱动连新版数据库会报 SSL 和时区错误。

连接参数里最值得说的是serverTimezoneuseSSL。不做时区设置,select_time会产生 8 小时偏差;不关 SSL 或没配证书,控制台会刷一堆警告。下面的类作为整个系统的 DBHelper,在 main 方法里初始化一次即可。

import java.sql.*; public class DBHelper { private static final String URL = "jdbc:mysql://localhost:3306/course_selection" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

characterEncoding=utf8要和建库时的utf8mb4配套。如果不写,驱动默认读配置文件,可能出现服务器是 utf8mb4 但连接串是 latin1 的情况,届时中文在 JTable 里显示成问号。password如果为空就写空字符串,注意 MySQL 安装时如果选了安全性插件,root 密码可能是空或带复杂规则,运行前先用 Navicat 验证一次连接,排除数据库端的问题。

3.2 登录校验不要写死账号,要过 PreparedStatement

Swing 登录窗口通常是一个JFrame加两个 JTextField 和一个 JPasswordField,重点在后端校验方式。最忌讳的是用字符串拼接 SQL:"SELECT * FROM student WHERE id='" + input + "'",这是 SQL 注入的标准写法。虽然课设系统没有安全测试,但面试时一旦被问到「你的登录怎么防止注入」,拼 SQL 的回答分数会低很多。正确做法是用PreparedStatement,参数通过占位符传入。

public int login(String role, String id, String pwd) { String sql = "SELECT COUNT(*) FROM " + role + " WHERE " + (role.equals("student") ? "student_id" : "admin_id") + " = ? AND password = ?"; try (Connection conn = DBHelper.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, id); pstmt.setString(2, pwd); try (ResultSet rs = pstmt.executeQuery()) { rs.next(); return rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); return 0; } }

注意这里把表名和列名做了一次显式判定,role 只能从界面传入的固定值里选,不能直接拼进 SQL。密码字段建议统一存储为 MD5 或 SHA-256 加盐后的字符串,数据库中不要留明文。代码里rs.next()之后直接读第 1 列,因为COUNT(*)永远只返回一行,如果返回 1 说明账号密码匹配,否则登录失败。有一个坑:如果用executeQuery()后不关闭 PreparedStatement,在长连接里多次登录会导致资源占用越来越多,try-with-resources会自动关闭,这也是推荐它的原因。

3.3 用 JTable 展示课程列表:DefaultTableModel 是现成的最佳实践

课程查看界面是整个系统最核心的展示窗口。JTable 的常用套路是把数据填进DefaultTableModel,再把TableModel传给JTable。很多人会直接在TableModelListener里改数据,结果发现表格刷新时死循环,因为没有区分「用户编辑」和「程序更新」。

下面是课程列表的查询代码,支持按名称、上课时间、任课老师三种模糊过滤。

public DefaultTableModel getCourseTable(String keyword) { String sql = "SELECT course_id, name, credit, teacher, schedule, " + "capacity, selected, (capacity - selected) AS remaining " + "FROM course WHERE name LIKE ? OR teacher LIKE ? OR schedule LIKE ? " + "ORDER BY course_id"; DefaultTableModel model = new DefaultTableModel( new Object[]{"课程号","课程名","学分","老师","时间","容量","已选","余量"}, 0); try (Connection conn = DBHelper.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; pstmt.setString(1, like); pstmt.setString(2, like); pstmt.setString(3, like); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { model.addRow(new Object[]{ rs.getString("course_id"), rs.getString("name"), rs.getDouble("credit"), rs.getString("teacher"), rs.getString("schedule"), rs.getInt("capacity"), rs.getInt("selected"), rs.getInt("remaining") }); } } } catch (SQLException e) { e.printStackTrace(); } return model; }

这里将剩余容量直接在 SQL 层算出remaining,而不是拿到 Java 里再减,好处是排序和后续分页可以直接复用 SQL。LIKE配合前后通配符会放弃索引,但在课程表体量小的情况下没什么影响,不用盲目优化。界面刷新只需要table.setModel(getCourseTable("")),不要试图逐行更新,DefaultTableModel重建后事件的复杂度撑得住。

4. 选课、退课与冲突检测:事务和锁的实战

4.1 选课的核心 SQL:先查再插还是直接插?

从业务角度,选课要处理三类冲突:重复选课、课程不存在、课程已满。数据库层面已经用唯一索引挡住了第一类错误,所以代码里的核心逻辑就简化成了下面这段事务操作。注意,INSERT语句本身没有做「已满」判断,所以必须依赖selected < capacity这个条件在更新语句里生效,而不是在 Java 里先查remaining再判断。

public boolean selectCourse(String studentId, String courseId) { String updateSql = "UPDATE course SET selected = selected + 1 " + "WHERE course_id = ? AND selected < capacity"; String insertSql = "INSERT INTO sc(student_id, course_id, select_time, status) " + "VALUES (?, ?, NOW(), 1)"; Connection conn = null; try { conn = DBHelper.getConnection(); conn.setAutoCommit(false); int rows = 0; try (PreparedStatement updateStmt = conn.prepareStatement(updateSql)) { updateStmt.setString(1, courseId); rows = updateStmt.executeUpdate(); } if (rows == 0) { conn.rollback(); return false; // 课程不存在或已满 } try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) { insertStmt.setString(1, studentId); insertStmt.setString(2, courseId); insertStmt.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } if (e.getErrorCode() == 1062) { System.err.println("重复选课"); } return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

这段代码的精髓在于把「更新余量」当作锁来用。UPDATE course SET selected = selected + 1 WHERE selected < capacity这行语句在 InnoDB 下会对命中行加上排他锁,如果两个请求同时选同一门课,第二个请求会等待第一个提交后才执行,发现selected > capacity时影响行数为 0,直接回滚。这比「SELECT 判断容量 → INSERT」的两步方案安全得多,后者在并发下必然超卖。错误码 1062 单独捕获,用来判断是唯一索引触发的重复选课,可以弹窗提示而不是泛泛地报「选课失败」。

4.2 退课和事务边界:删记录还是标记状态?

退课的逻辑是删除 sc 中的记录、把课程余量减一。但保留历史选课记录是有意义的,比如教务想要导出「退选名单」,或者学生想查看之前选过又退掉的课程。所以 sc 表的status字段在这里发挥作用,退课改成更新状态而不是DELETE

public boolean cancelCourse(String studentId, String courseId) { String updateSc = "UPDATE sc SET status = 0 WHERE student_id = ? AND course_id = ? AND status = 1"; String updateCourse = "UPDATE course SET selected = selected - 1 WHERE course_id = ? AND selected > 0"; Connection conn = null; try { conn = DBHelper.getConnection(); conn.setAutoCommit(false); int scRows; try (PreparedStatement ps1 = conn.prepareStatement(updateSc)) { ps1.setString(1, studentId); ps1.setString(2, courseId); scRows = ps1.executeUpdate(); } if (scRows == 0) { conn.rollback(); return false; // 没有有效选课记录 } try (PreparedStatement ps2 = conn.prepareStatement(updateCourse)) { ps2.setString(1, courseId); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }

UPDATE sc ... WHERE status = 1这个条件很重要,它避免了学生重复点击退课按钮时,把同一门课的状态连续改成两次 0。考虑到可能有「先选后退再选」的操作,状态为 0 的历史记录不要被再次更新,scRows这里的返回 0 就说明要么没选过、要么已经退过。退课事务中selected - 1AND selected > 0防止余量减成负数,虽然正常流程不会走到,但写上有助于防御脏数据。

4.3 常见排错:中文乱码、驱动投错、ClassNotFoundException

课上跑项目最容易遇到的三类错误都有固定解法。中文乱码出现在 JTable 或 JOptionPane 中时,先检查数据库和连接串的字符集,再把编译环境的文件编码改为 UTF-8(eclipse 在 Window→Preferences→Workspace 里改,IDEA 在 Settings→Editor→File Encodings 里改)。如果乱码只出现在 mysql 命令行而程序正常,那不是程序的锅,是命令行客户端用了 GBK,加一句SET NAMES utf8mb4;即可。驱动投错表现为No suitable driver found for jdbc:mysql...,这个时候看 MySQL 版本下载对应驱动的 jar 包,并确认 eclipse 里Project Properties → Java Build Path → LibrariesaddExternalJars,同时在Deployment Assembly里也加入这个 jar,否则运行时 Tomcat 或本地 main 都找不到。ClassNotFoundException 则优先检查是否多模块项目,jar 包要放到运行模块的 lib 目录,而不是只放在另一个模块下。

5. 把课设往上提一档:搜索、分页和导出选课名单

5.1 模糊搜索的进阶:多字段权重和下拉联动

基本搜索已经做到按名称、时间、老师三个字段同时命中。可改进的点是增加一个「搜索范围」下拉框,让用户指定只按名称搜还是全局搜。全局搜的 SQL 是WHERE name LIKE ? OR teacher LIKE ? OR schedule LIKE ?,如果用户只想搜课程名,其他两个字段的 LIKE 纯属浪费。可以按下面的动态 SQL 处理。

String sql = "SELECT ... FROM course WHERE 1=1"; if ("name".equals(field)) { sql += " AND name LIKE ?"; } else if ("teacher".equals(field)) { sql += " AND teacher LIKE ?"; } else { sql += " AND (name LIKE ? OR teacher LIKE ? OR schedule LIKE ?)"; }

WHERE 1=1是拼接动态条件的惯用写法,既不影响执行计划,也不用去判断「第一个条件前要不要加 AND」。真正要防范的是字段值被注入,这里 field 是固定枚举值,keyword 仍然用占位符,就安全了。搜索结果的余量小于等于 5 时,可以在DefaultTableModel渲染时把该行字体设成粗体,视觉上提醒管理员和学生的容量紧张。

5.2 给课程列表加分页:LIMIT ? OFFSET ? 的正确参数

毕设答辩时老师常问「课程多了怎么办」。加个分页其实不复杂,关键是分页接口要返回两个信息:当前页的数据列表和总记录数。总记录数和当前页数据要分别用两条 SQL 查。

public PageResult<Course> queryPage(int page, int pageSize, String keyword) { String countSql = "SELECT COUNT(*) FROM course WHERE name LIKE ? OR teacher LIKE ?"; String dataSql = "SELECT * FROM course WHERE name LIKE ? OR teacher LIKE ? " + "ORDER BY course_id LIMIT ? OFFSET ?"; // 执行两条查询,参数顺序不能乱 }

LIMIT ? OFFSET ?中第 1 个参数是每页条数,第 2 个参数是偏移量,偏移量 =(当前页码 - 1) * 每页条数。注意 PreparedStatement 设置参数时,因为前面 already 有两个%keyword%,所以第 3 个才是 pageSize,第 4 个是 offset,顺序错一位就查错页。Swing 端用JComboBox显示页码,点击下一页时重新调用queryPage,不要持有整个数据集然后内存里翻页,那样数据量大了不优雅。页码总数用total / pageSize向上取整,注意床整时total=0分母不能为 0,要单独判断。

5.3 导出选课名单:Java 写 CSV,别用 Excel 库

需求文档里的「课程查看、学生资料查看」看似只停留在界面,实际教务最想要的是把选课名单导出来。用 Apache POI 写 xlsx 要引入额外依赖,打包体积大;课设场景下 CSV 足够,而且 MySQL 和 Excel 都能直接打开。导出核心是统计每门课的选课学生,SQL 用JOIN一次成型。

SELECT c.name AS course_name, s.student_id, s.name AS student_name, s.major, sc.select_time FROM sc s JOIN course c ON sc.course_id = c.course_id JOIN student s ON sc.student_id = s.student_id WHERE sc.status = 1 AND c.course_id = ?

写成 CSV 时注意中文编码,Excel 直接打开 UTF-8 的 CSV 会乱码,需要在写文件时加 BOM 头,即先输出\uFEFF三个字节。下面的代码片段展示了带 BOM 的写法。

BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); writer.write('\uFEFF'); // BOM writer.write("课程名,学号,姓名,专业,选课时间\n"); while (rs.next()) { writer.write(rs.getString(1) + "," + rs.getString(2) + "," + rs.getString(3) + "," + rs.getString(4) + "," + rs.getString(5) + "\n"); } writer.flush();

导出动作要放在 Swing 的按钮事件里,使用JFileChooser选择保存路径,导出期间禁用选中按钮,防止用户重复点击触发多个写入线程。生成的 CSV 可以直接用记事本或 Excel 打开,字段间不需要特殊处理,只要数据本身不含逗号和换行。这个技巧虽然只有几行代码,但效果上比在界面里干巴巴展示表格更像一个「管理系统」。剩下的部分就是把这些类整合到各个 JFrame 的事件监听里,运行前用 Navicat 执行一遍course_selection.sql,再用管理员账号登录初始化课程数据。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 5:55:25

C#客户端CPU利用率监控:采样原理、模块设计与踩坑实践

做C#客户端开发做久了&#xff0c;尤其是做桌面工具、上位机这类跑在用户机器上的程序&#xff0c;一定会碰到一个绕不开的问题&#xff1a;用户说“你的程序把CPU吃满了”“风扇狂转”“点一下要卡三秒”。这类问题的第一现场信息&#xff0c;往往不是通过调试器抓出来的&…

作者头像 李华
网站建设 2026/9/17 5:54:40

DDR5 SPD读写实战:从SPD Hub原理到编程器安全修改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 5:52:30

供水管网压力监测平台是什么?5 大核心功能与应用价值详解

供水管网是城市运行最基础的民生设施之一&#xff0c;其压力稳定直接关系到千家万户的用水安全。长期以来&#xff0c;管网老化、漏损居高不下、爆管事故难以预警等问题&#xff0c;困扰着供水行业的管理者。2026年6月1日起施行的《供水条例》&#xff0c;首次以行政法规形式明…

作者头像 李华
网站建设 2026/9/17 5:51:16

从教辅 PDF 到可执行题库:知识点抽取、建模与出题校验

简介&#xff1a;这份人教版五年级上册数学知识点汇总以PDF形式呈现&#xff0c;面向五年级学生、家长及数学教师&#xff0c;用于课前预习、单元复习与期末查漏补缺。文件共1个&#xff0c;为113KB的PDF文档&#xff0c;篇幅精简、目录清晰&#xff0c;按教材顺序梳理七个单元…

作者头像 李华
网站建设 2026/9/17 5:50:25

解决Windows 10下Android Studio HAXM安装失败问题

1. 问题现象与初步排查最近在Windows 10环境下配置Android Studio开发环境时&#xff0c;遇到了一个典型问题——安装HAXM&#xff08;硬件加速执行管理器&#xff09;驱动时频繁报错。错误提示为"Intel HAXM installation failed"&#xff0c;控制台显示"Faile…

作者头像 李华