news 2026/9/13 2:46:30

Java+Swing+MySQL学生选课成绩管理系统:从表设计到并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java+Swing+MySQL学生选课成绩管理系统:从表设计到并发控制

简介:基于Java语言、Swing界面工具包以及MySQL关系型数据库构建的学生选课及成绩管理系统,是一份面向Java课程设计的完整项目源码包,主要服务正在学习图形界面编程和数据库设计的课设学生,能够有效覆盖选课、成绩管理等常见需求。功能上涵盖课程信息管理、学生信息管理、选课操作与成绩录入查询等模块,并演示了Swing组件和SQL结构化查询语句在系统中的整合方式。压缩包共53个文件、约8.61MB,包含16个Java源代码文件、8个XML配置文件、1个SQL脚本以及MySQL连接驱动,另有20张JPG和3张PNG界面截图与数据库E-R图辅助理解项目结构。包内文档完整、目录层次清晰,代码和脚本可直接导入开发环境运行,可作为课程设计报告、答辩演示与二次开发的参考资料。目前已有76人浏览/学习,对正在规划同类系统开发的读者具有较强的实际借鉴价值。

1. 基于Java+Swing+MySQL的学生选课及成绩管理系统,先想清楚它在解决什么问题

刚学完 Java 基础和 MySQL 的增删改查,Swing 勉强能拖出几个带按钮的窗体,就接到"基于Java+Swing+MySQL的学生选课及成绩管理系统"这个 Java 课程设计题目。此时最容易踩的坑是先把界面画出来,把库里排好的数据塞进去能跑就交差。真正拉开分数差距的是业务约束有没有落进代码:同一学生不能重复选同一门课、课程容量不能超、成绩区间必须被校验、选课失败时数据库不能留下半截数据。

这类系统的核心数据其实只有两类:选课关系和成绩字段。围绕这两个点展开的完整链路,包括登录鉴权、DAO 分层、事务提交与回滚,恰好是 Java 面试八股文里反复追问的内容。这套方案不依赖任何现成的课程设计源码,按一线工程师做小系统最常用的方式从头走一遍,从建表到并发验证一次讲清。适合正在赶课程设计、准备期末答辩,或者想在简历上放一个能讲明白的练手项目的读者。

2. 学生选课及成绩管理系统的表结构:用约束把规则锁进 MySQL

2.1 三张核心表:学生、课程、选课记录

学生和课程是多对多关系,所以必须有一张中间表,也就是选课记录表。很多初学者会把多个课程编号拼在一个字段里,这种"一列多值"的写法在查重、统计人数、按学期筛选时都会失控。比较稳妥的设计是拆成studentscoursesenrollments三张表,enrollments只负责表达"谁选了哪门课",同时把成绩字段也放进去,scoreNULL表示已选课未出成绩。

CREATE DATABASE course_db DEFAULT CHARACTER SET utf8mb4; USE course_db; CREATE TABLE students ( student_id VARCHAR(20) PRIMARY KEY COMMENT '学号,兼作登录账号', name VARCHAR(50) NOT NULL, password_hash CHAR(64) NOT NULL COMMENT 'SHA-256 十六进制串', role ENUM('student','admin') NOT NULL DEFAULT 'student', class_name VARCHAR(50) ); CREATE TABLE courses ( course_id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(2,1) NOT NULL DEFAULT 2.0, capacity INT NOT NULL DEFAULT 30, teacher VARCHAR(20), semester VARCHAR(20) NOT NULL COMMENT '格式如 2025-2026-1', is_open TINYINT NOT NULL DEFAULT 1 COMMENT '1 开放选课,0 关闭' ); CREATE TABLE enrollments ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id INT NOT NULL, score DECIMAL(4,1) NULL, enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id), KEY idx_course (course_id), CONSTRAINT fk_stu FOREIGN KEY (student_id) REFERENCES students(student_id), CONSTRAINT fk_course FOREIGN KEY (course_id) REFERENCES courses(course_id), CONSTRAINT chk_score CHECK (score IS NULL OR (score >= 0 AND score <= 100)) );

这段建表 SQL 可以直接在 MySQL Workbench 或 Navicat 里执行,用命令行则是mysql -u root -p < schema.sql。需要注意CHAR(64)存的是 SHA-256 后的十六进制字符串,长度固定 64 位,不要用VARCHAR(32)否则摘要会被截断。uk_stu_course唯一索引让重复选课直接触发Duplicate entry报错,程序里只需捕获这个异常就能给出"你已经选过这门课"的提示。enrollments里没有冗余semester字段,学期信息在courses表上,统计时按课程 join 即可,避免同一门课出现两个学期版本时数据不一致。

2.2 成绩放哪:可空 score 与双重校验

score放在enrollments表里是比单独建成绩表更常见的做法,因为"一次选课"和"一条成绩"天然一一对应。score允许为空恰好表达业务中间态:学生选上了课,但老师还没录入成绩。录入成绩用UPDATE ... SET score = ? WHERE id = ?,录完后这一行才变成一条完整记录。

这里有个 MySQL 版本相关的坑:CHECK (score IS NULL OR (score >= 0 AND score <= 100))在 MySQL 8.0.16 之前会被解析但不会真正强制,也就是说 5.7 环境里插入 120 分并不会报错。所以应用层必须做第二道校验,通常是在 DAO 里用if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0)提前拦截。两层校验的目的不是重复,而是防止"数据库约束失效 + 界面绕过"的组合情况。

2.3 索引用途与初始化数据

索引/约束作用说明
uk_stu_course防止同一学生重复选同一门课唯一索引同时也覆盖"查询我的课程"场景
idx_course加速从课程反向查选课名单MySQL 会为外键自动建索引,这里显式声明让意图更清楚
chk_score限制成绩范围8.0.16 前只解析不强制,应用层仍需校验

初始化数据用 mysql 或 Workbench 执行,注意密码字段要用SHA2函数而不是明文:

INSERT INTO students (student_id, name, password_hash, role, class_name) VALUES ('20250001', '张三', SHA2('123456', 256), 'student', '网工2401'), ('admin', '管理员', SHA2('admin123', 256), 'admin', NULL); INSERT INTO courses (course_name, credit, capacity, teacher, semester, is_open) VALUES ('Java程序设计', 3.0, 30, '王老师', '2025-2026-1', 1);

SHA2('123456', 256)与后面 JDBC 代码里MessageDigest.getInstance("SHA-256")算出的摘要一致,这样无论是手工排查数据库还是程序登录,密文都能对得上。初始化时先建管理员账户而不是直接在代码里写死if (username.equals("admin")),是为了让权限判断走同一条数据库路径。

3. 界面层与数据层解耦:Swing + DAO 的最小工程结构

3.1 一个课程设计最常见的问题:业务逻辑全写进按钮事件

不少 Swing 课设把 SQL 和校验全部塞进JButton.addActionListener的匿名内部类里,一个窗口四五百行,报错时不知道是界面问题还是 SQL 问题,答辩被追问"如果换成网页前端怎么办"也答不上来。划分包是最直接的解法:entity放实体类,dao放所有 SQL 访问,view只放 JFrame 和 JPanel。

包名职责典型成员
entity与表字段对应的 Java 对象StudentCourseEnrollment
dao封装全部 JDBC 操作,返回实体或集合StudentDaoCourseDao
view负责组件布局和事件转发LoginFrameStudentMainFrame

3.2 实体类与 DAO 接口

实体类不需要任何框架注解,纯粹是字段和 getter/setter。DECIMAL类型在 Java 里要对应BigDecimal,用double会在成绩累加时出现精度误差:

public class Course { private int courseId; private String courseName; private BigDecimal credit; private int capacity; private String semester; private boolean open; // getter / setter 省略 }

DAO 接口的意义在于,Swing 层只依赖接口方法名,不关心内部是 JDBC 还是 MyBatis。这个习惯在课程设计里属于"加分但不绕远"的部分,面试聊到 JDBC 和分层时也能顺带讲清楚:

public interface CourseDao { List<Course> findOpenCourses(String semester); int countEnrolled(int courseId); boolean enroll(String studentId, int courseId) throws SQLException; List<Enrollment> listByStudent(String studentId); }

3.3 JDBC 连接与登录查询实现

MySQL 8 的驱动名是com.mysql.cj.jdbc.Driver,连接串需要带时区和字符集参数,否则插入中文会乱码或报服务器时区错误:

public class JdbcUtil { private static final String URL = "jdbc:mysql://localhost:3306/course_db" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

连接串参数里characterEncoding=utf8解决中文乱码,serverTimezone=Asia/Shanghai解决 MySQL 8 的时区告警。如果用的是免安装版 MySQL,启动服务前还要确认mysql命令行能连上3306端口,否则 JDBC 报Communications link failure时排查方向会跑偏。

登录方法用PreparedStatement?占位符,避免字符串拼接 SQL 导致的注入问题:

public Student login(String studentId, String rawPassword) throws SQLException { String sql = "SELECT student_id, name, role, class_name FROM students " + "WHERE student_id = ? AND password_hash = SHA2(?, 256)"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, studentId); ps.setString(2, rawPassword); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Student s = new Student(); s.setStudentId(rs.getString("student_id")); s.setName(rs.getString("name")); s.setRole(rs.getString("role")); s.setClassName(rs.getString("class_name")); return s; } } } return null; }

这段代码把密码摘要放在 SQL 里用SHA2()计算,好处是登录和初始化数据走同一套加密规则,数据库中不需要再存原始密码。try-with-resources语法保证ConnectionPreparedStatementResultSet在方法结束后自动关闭,避免连接没释放导致 MySQL 连接数耗尽。

4. 登录、选课、成绩录入三大核心流程的实现

4.1 登录流程与密码处理

登录按钮的事件里拿到JPasswordField的值时,用getPassword()返回的char[]而不是getText()返回的StringString是不可变对象,会留在堆内存里直到 GC,而char[]用完后可以手动覆盖。课程设计阶段不强制,但这个习惯属于 Java 基础里的高频考点。

String id = idField.getText().trim(); String password = new String(pwdField.getPassword()); Student student = dao.login(id, password); if (student == null) { JOptionPane.showMessageDialog(loginFrame, "学号或密码错误"); } else if ("admin".equals(student.getRole())) { loginFrame.dispose(); new AdminMainFrame(student).setVisible(true); } else { loginFrame.dispose(); new StudentMainFrame(student).setVisible(true); }

切换窗体时先dispose()再创建新JFrame,而不是setVisible(false)藏着。藏着的窗体还占着内存,并且重新打开时状态容易混乱。trim()去掉首尾空格是为了防止用户在学号框里不小心打了空格导致查不到数据。

4.2 选课:容量校验必须放在同一个事务里

选课最核心的问题是并发:两个学生同时选只剩最后一个名额的课程,如果先查人数再插入,两个请求都会读到enrolled < capacity,最终超员。解决办法是在同一个事务里对课程行加排他锁,锁住之后其他人必须等当前事务提交才能继续。

public boolean enroll(String studentId, int courseId) throws SQLException { String lockSql = "SELECT capacity FROM courses WHERE course_id = ? AND is_open = 1 FOR UPDATE"; String countSql = "SELECT COUNT(*) FROM enrollments WHERE course_id = ?"; String insertSql = "INSERT INTO enrollments (student_id, course_id) VALUES (?, ?)"; Connection conn = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); int capacity; try (PreparedStatement ps = conn.prepareStatement(lockSql)) { ps.setInt(1, courseId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { throw new SQLException("课程不存在或未开放"); } capacity = rs.getInt(1); } } int enrolled; try (PreparedStatement ps = conn.prepareStatement(countSql)) { ps.setInt(1, courseId); try (ResultSet rs = ps.executeQuery()) { rs.next(); enrolled = rs.getInt(1); } } if (enrolled >= capacity) { return false; } try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setString(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.close(); } } }

关键点在SELECT ... FOR UPDATE:这一行的排他锁会一直保持到commitrollback。第二个学生执行同一句查询时会被阻塞,等第一个事务结束才能读,所以COUNT(*)拿到的总是最新人数。setAutoCommit(false)之后如果漏写commit(),关闭连接时未提交事务会被回滚,数据不会写进去,但代码里显式rollback()能让异常路径的行为更可控。事务内部不要弹出JOptionPane对话框等用户输入,锁持有时间越长,其他学生等待越久。

4.3 成绩录入:UPDATE 要多加一个防覆盖条件

成绩录入对应管理员的权限,SQL 本身不复杂,复杂的是防止重复录入覆盖已有成绩。UPDATE ... WHERE id = ? AND score IS NULL是关键,已录过成绩的记录不会再次命中:

String sql = "UPDATE enrollments SET score = ? WHERE id = ? AND score IS NULL"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setBigDecimal(1, score); ps.setLong(2, enrollmentId); int rows = ps.executeUpdate(); if (rows == 0) { JOptionPane.showMessageDialog(frame, "记录不存在或成绩已录入,无法覆盖"); } }

rows == 0有两种情况:选课记录不存在,或者成绩已存在。界面提示用"记录不存在或成绩已录入"统一处理,避免向用户暴露详细的数据库状态。score传入的是BigDecimal,对应数据库中DECIMAL(4,1),界面层的JSpinner虽已限定 0 到 100,但 DAO 里仍然建议再做一次范围判断,这是因为 2.2 节提到的 MySQL 版本差异。

三个流程的 SQL 与事务特征可以这样对照:

流程关键 SQL 写法事务要求
登录WHERE student_id = ? AND password_hash = SHA2(?, 256)单条查询,不需要事务
选课SELECT ... FOR UPDATE+COUNT(*)+INSERT必须同一事务,锁课程行
成绩录入UPDATE ... SET score = ? WHERE id = ? AND score IS NULL单条更新,靠 WHERE 防覆盖

4.4 刷新表格用 SwingWorker 避免界面卡死

选课列表和成绩清单在启动时加载,如果直接在构造方法里执行查询,大数据量下窗口会白屏。原因是查询跑在事件分发线程(EDT)上,界面无法重绘。SwingWorker是 JDK 自带的方案:

new SwingWorker<List<Enrollment>, Void>() { @Override protected List<Enrollment> doInBackground() throws Exception { return dao.listByStudent(studentId); } @Override protected void done() { try { List<Enrollment> list = get(); DefaultTableModel model = (DefaultTableModel) table.getModel(); model.setRowCount(0); for (Enrollment e : list) { model.addRow(new Object[]{e.getCourseName(), e.getScore()}); } } catch (Exception e) { JOptionPane.showMessageDialog(frame, "加载选课记录失败"); } } }.execute();

doInBackground()在后台线程执行数据库查询,done()回到 EDT 更新表格。get()如果抛异常,说明后台查询出错,此时界面给出统一提示,而不是让后台线程的异常直接打到控制台。这个模式在课程设计里用到一次,答辩时解释 EDT 概念,比单纯演示功能更有说服力。

5. 事务边界与并发验证:选课系统最容易丢分的两个细节

5.1 检查一:异常路径有没有漏掉回滚

setAutoCommit(false)之后只考虑正常提交,忽略异常分支的rollback(),是选课系统最常见的隐患。表现为某些选课操作在出异常后,后续连接拿到一个残留的开着事务的连接,行为变得不可预测。处理方法在 4.2 的catch块里已经体现:捕获异常后先回滚再抛出,finally里保证关闭。

验证方法很简单:打开mysql命令行执行SHOW PROCESSLIST;,如果看到大量Time很大且StateSleep的连接,多半是事务没有结束或连接没有归还。配合SHOW VARIABLES LIKE 'autocommit';确认会话默认值,能快速判断问题出在 JDBC 代码还是数据库配置。

5.2 检查二:并发选课真的会超员吗

写完代码可以开两个 mysql 终端手工模拟并发。终端 A 先锁行,终端 B 再锁,B 会进入等待状态:

-- 终端 A BEGIN; SELECT capacity FROM courses WHERE course_id = 1 FOR UPDATE; -- 终端 B BEGIN; SELECT capacity FROM courses WHERE course_id = 1 FOR UPDATE;

此时 A 执行COMMIT;,B 的查询才返回。在 B 终端执行ROLLBACK;释放事务后,再重复整个流程,就能观察锁的阻塞效果:

时刻终端 A终端 B
初始返回capacity=30执行锁查询,阻塞等待
A 提交COMMIT后事务结束查询返回,读取到已提交数据
后续插入根据最新人数决定是否允许

这个实验能直观验证FOR UPDATE确实让"检查人数 + 插入记录"变成原子操作。如果代码里去掉FOR UPDATE,两个终端分别BEGIN后同时执行COUNT(*),都能读到旧人数,最终结果会超员——这正是事务与普通 SELECT 的差别。

5.3 加分项:锁等待超时与最小化锁持有时间

MySQL 默认的innodb_lock_wait_timeout是 50 秒,手工验证时如果忘记提交,终端 B 会一直挂在那里。可以临时调低观察效果:

SET SESSION innodb_lock_wait_timeout = 10;

改成 10 秒后,终端 B 会在大约 10 秒时返回Lock wait timeout exceeded; try restarting transaction。这个报错信息在调优并发程序时是一个明确的信号:锁被别的事务持有的时间过长。把锁等待超时调成 10s 能让问题提前暴露,排查并发选课问题时比盯着界面日志更快定位到锁冲突的位置。

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

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

Wi-Fi Mesh排障不再靠猜:R-Mesh Gravitation拓扑与信号可视化实战

小区里一户别墅客户装了三套 Mesh&#xff0c;调试了整整一个周末&#xff0c;最后发现只是子节点摆放位置的墙体里有新风管道。这种经历多了之后&#xff0c;我越来越确信一件事&#xff1a;Wi-Fi Mesh 的安装调试&#xff0c;真正难的从来不是硬件本身&#xff0c;而是排障。…

作者头像 李华
网站建设 2026/9/13 2:42:55

Python脚本化数据库备份、导出与迁移的完整实践

做系统运维和数据开发的朋友&#xff0c;应该都遇到过这种尴尬&#xff1a;半夜收到磁盘告警&#xff0c;登上去一看&#xff0c;备份文件把空间塞满了&#xff1b;或者业务方要一份上个月的订单明细&#xff0c;你下意识写了一条select * from orders扔给 pandas&#xff0c;结…

作者头像 李华
网站建设 2026/9/13 2:42:46

Linux课程设计实战:从zip解压到源码阅读与实验报告对齐

简介&#xff1a;一套Linux课程设计资料包&#xff0c;面向计算机专业学生及需要完成Shell脚本数据库备份作业的开发者&#xff0c;重点展示如何用Shell与mysqldump实现MySQL数据库的即时备份、cron定时备份、增量备份及旧备份自动清理。资源共3个文件&#xff0c;包含两个Shel…

作者头像 李华