
简介基于Java及MySQL的学生选课与成绩管理系统课程设计源码包采用Swing技术构建桌面端图形界面实现登录注册、学生选课、课程信息维护、成绩录入与查询、密码修改等教务管理核心功能。项目内置管理员、教师、学生三类演示账号初始密码均为123导入SQL脚本即可体验不同角色的权限操作适合作为Java课程设计、毕业设计或初学者练手项目。压缩包共50个文件包含16个Java源代码文件、6个XML配置、1个SQL建库脚本及MySQL驱动JAR包同时提供20余张系统运行界面截图、数据库E-R图与说明文档整体大小约8.62MB目录区分源码、资源与文档便于按需查阅。数据库连接信息集中在配置类中支持修改用户名和密码配合注释可快速完成环境搭建。当前已有80人学习下载对于想掌握Swing界面交互与MySQL数据持久化联动的学习者是一份可直接运行并加以改造的完整参考资料。1. 学生选课和成绩管理系统用JavaSwing写课设到底在练什么很多同学的Java课程设计题目里都写着这么一句基于JavaSwingMySQL的学生选课及成绩管理系统。拿到题目时第一反应是打开IDE把登录窗口画出来等到上机演示才发现最花时间的不是界面而是表结构、JDBC连接和事务。这个题目看着像一个普通的增删改查实际上它覆盖了数据库建模、PreparedStatement、事务回滚、Swing事件线程这几条主线。对于刚学完Java基础的人来说它是把Java语言、面向对象编程和MySQL真正拧在一起的小项目对于要答辩的同学这套系统的评分重点通常不是花哨的界面而是你能否讲清楚选课记录表的唯一约束和容量扣减的原子性。下面按建表、DAO、Swing界面、避坑和答辩加分的顺序来展开复制到Eclipse或IDEA都能跟着做。2. 先设计表结构再动手写界面选课与成绩模块的数据库建模很多课设翻车的共同点是表结构边写边改。我先建表因为Swing界面上的每个下拉框、每个表格列最后都要落在表的字段上如果先画界面后面增删字段DAO和界面都要跟着返工。所以第一步永远是ER图和建表SQL。这个项目里我会用四张表user、student、course、sc。注意这里的成绩不单独建表而是放在sc这个选课关系表上后面解释为什么。2.1 四张核心表user、student、course、sc 的建表语句先给建库和建表SQL-- 建库字符集必须用 utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARSET utf8mb4; USE course_db; -- 账号表管理员和学生都走这张表登录 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role ENUM(admin,student) NOT NULL DEFAULT student ); -- 学生信息表user_id 关联 user.id CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, user_id INT NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(2), major VARCHAR(80), class_name VARCHAR(50), CONSTRAINT fk_student_user FOREIGN KEY (user_id) REFERENCES user(id) ); -- 课程表capacity 是容量selected_count 是已选人数 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(80) NOT NULL, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0, capacity INT NOT NULL DEFAULT 60, selected_count INT NOT NULL DEFAULT 0, teacher_name VARCHAR(50) ); -- 选课成绩表成绩 score 允许为空空代表还没有录入成绩 CREATE TABLE sc ( id INT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20) NOT NULL, course_id INT NOT NULL, semester VARCHAR(20) NOT NULL, score DECIMAL(5,2) NULL, UNIQUE KEY uk_sc (student_id, course_id, semester), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) );user表用role字段区分管理员和学生避免给每个角色单独做一张登录表登录代码只需要查一张表。student表用学号做业务主键user_id加UNIQUE约束保证一个学生只对应一个账号。course表里的selected_count是后面选课事务的关键字段默认值设为0表示还没有人选。sc表通过UNIQUE KEY uk_sc防止同一学生同一学期重复选同一门课score是DECIMAL(5,2)最大999.99课程设计里足够用。2.2 成绩字段放哪个表放在选课记录而不是课程表最常见的设计错误是把“成绩”字段加到course表里。这样一来一门课程只有一个分数完全不符合“一个学生一个分数”的真实场景。成绩是“学生-课程”这个关系上的属性而不是课程本身的属性所以正确的落点是sc表。那为什么不单独建一张score表因为score表本质上是复制sc表的结构score(id, student_id, course_id, semester, score)。既然sc表已经记录了哪位学生选了哪门课成绩直接作为sc表的字段即可如果成绩还没录入就让score保持NULL表示“已选课但未出分”。这样的设计在答辩时一句话就能讲清楚单独建表反而会让联表查询变多增加不必要的复杂度。semester字段值得单独说明同一学生可以在不同学期选同一门课程所以“学生课程”不能作为唯一约束只有“学生课程学期”才能避免重复选课。如果题目不涉及学期概念可以去掉semester但成绩管理一般都需要按学期统计保留这个字段是更稳妥的方案。2.3 初始化数据管理员账号、测试学生和课程容量表结构确定后先插入几条测试数据保证后面登录和选课有真实数据可看-- 初始化测试账号管理员和两个学生 INSERT INTO user (username, password, role) VALUES (admin, admin123, admin), (2021001, 123456, student), (2021002, 123456, student); INSERT INTO student (student_id, user_id, name, gender, major, class_name) VALUES (2021001, 2, 张三, 男, 计算机科学与技术, CS2101), (2021002, 3, 李四, 女, 软件工程, SE2102); -- 初始化两门课程容量分别 60 和 40 INSERT INTO course (course_no, course_name, credit, capacity, selected_count, teacher_name) VALUES (C001, Java程序设计, 4.0, 60, 0, 王老师), (C002, MySQL数据库, 3.0, 40, 0, 刘老师); -- 张三已经选了一门课成绩还没录 INSERT INTO sc (student_id, course_id, semester) VALUES (2021001, 1, 2024-2025-1);这组数据让登录、选课、成绩录入都有初始状态。管理员账号admin能进入管理端两个学生账号用来测试选课和成绩查询。注意如果使用MySQL Workbench或Navicat重复执行这些INSERT会触发唯一键冲突可以在执行前先把旧数据DELETE掉或者把INSERT改成INSERT IGNORE。课程表里的selected_count没有手工写成1而是依赖后面的选课事务去更新这样演示时能看到“已选人数从0变成1”的过程。3. 用JDBC连接MySQL写一个不被并发选课打爆的数据库访问层表建好后下一步别急着画界面先把数据库访问层写好。这里的访问层指的是负责拿连接、执行SQL、关闭资源的“底座”。很多同学图省事直接在需要查数据库的地方写Class.forName每次调用都新建连接结果演示时频繁卡顿。这一章用一个课设能接受的方式写ConnectionUtil再写一个使用PreparedStatement的DAO最后把选课逻辑做成事务。3.1 连接参数与连接工具类从DriverManager到连接池的选型理由先给出工具类import java.sql.*; public class ConnectionUtil { // 注意 URL 里的 5 个参数缺一个都可能踩坑 private static final String URL jdbc:mysql://localhost:3306/course_db ?useSSLfalse allowPublicKeyRetrievaltrue serverTimezoneAsia/Shanghai characterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { // MySQL 8.x 的驱动类是 com.mysql.cj.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败检查项目是否引入 mysql-connector-j 依赖, e); } } 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 e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里需要解释一下连接串里的几个参数。useSSLfalse是因为本地开发不需要SSL认证MySQL 8.0默认会尝试SSL不加会报SSL连接警告或直接失败。allowPublicKeyRetrievaltrue是因为MySQL 8.0默认使用caching_sha2_password认证不加会报“Public Key Retrieval is not allowed”。serverTimezoneAsia/Shanghai解决时区报错。characterEncodingutf8配合建库的utf8mb4保证中文不乱码。驱动类名也容易搞混MySQL 5.x用com.mysql.jdbc.Driver8.x用com.mysql.cj.jdbc.Driver很多ClassNotFoundException就是旧包名导致的。关于连接池选型课程设计通常不需要引入太多第三方依赖用DriverManager足够。如果有余力可以把getConnection()换成HikariCP数据源核心参数是maximumPoolSize、minimumIdle和connectionTimeout。这个方案在生产环境更稳但会引入jar包依赖。对课设来说能说清楚“连接池复用连接而不是反复创建”这个理由要比代码本身更加分。这类问题也是Java面试八股文里常问的做完这个项目再去面试会有真实体感。3.2 PreparedStatement防注入查询课程列表和成绩列表的写法不要在DAO里用Statement拼接SQL比如SELECT * FROM course WHERE course_name name 这种写法只要在输入框里放一个单引号就能改变SQL结构。应该用PreparedStatement占位符参数交给驱动处理。public ListCourse findAvailableCourses(String keyword) throws SQLException { String sql SELECT id, course_no, course_name, credit, capacity, selected_count, teacher_name FROM course WHERE selected_count capacity AND (course_name LIKE ? OR course_no LIKE ?); ListCourse list new ArrayList(); try (Connection conn ConnectionUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Course course new Course(); course.setId(rs.getInt(id)); course.setCourseNo(rs.getString(course_no)); course.setCourseName(rs.getString(course_name)); course.setCredit(rs.getBigDecimal(credit)); course.setCapacity(rs.getInt(capacity)); course.setSelectedCount(rs.getInt(selected_count)); course.setTeacherName(rs.getString(teacher_name)); list.add(course); } } } return list; }LIKE查询有个坑不能写成%?%因为占位符会变成字符串的一部分正确做法是在setString时拼好%关键字%。try-with-resources会按创建顺序的逆序关闭ResultSet、PreparedStatement、Connection比finally手动关闭更省心。查询条件里加上selected_count capacity这样学生端表格里只展示还有容量的课程管理员端如果也要看所有课程就另外写一个不带这条件的查询。成绩查询的SQL也类似SELECT c.course_name, sc.score FROM sc JOIN course c ON c.id sc.course_id WHERE sc.student_id ? AND sc.score IS NOT NULL查出来以后在Swing里组装成成绩单表格。3.3 用事务保证“选课扣容量插入选课记录”的原子性选课从安全角度看至少两个步骤更新课程的已选人数、插入一条选课记录。如果第一步成功、第二步失败已选人数会多算一次如果两个人同时选同一门课就可能超容量。用事务把两步包起来同时把“检查容量”和“扣减容量”合成一条SQL。public boolean selectCourse(String studentId, int courseId, String semester) throws SQLException { Connection conn null; PreparedStatement updateCap null; PreparedStatement insertSc null; try { conn ConnectionUtil.getConnection(); conn.setAutoCommit(false); // 条件更新成功返回 1失败返回 0避免并发超卖 String updateSql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; updateCap conn.prepareStatement(updateSql); updateCap.setInt(1, courseId); int affected updateCap.executeUpdate(); if (affected 0) { conn.rollback(); return false; } String insertSql INSERT INTO sc (student_id, course_id, semester) VALUES (?, ?, ?); insertSc conn.prepareStatement(insertSql); insertSc.setString(1, studentId); insertSc.setInt(2, courseId); insertSc.setString(3, semester); insertSc.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) { } } throw e; } finally { if (updateCap ! null) { updateCap.close(); } if (insertSc ! null) { insertSc.close(); } if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }setAutoCommit(false)开启事务commit提交rollback回滚。关键是UPDATE语句里的WHERE id ? AND selected_count capacity它把“检查容量”和“扣减容量”合并成一条原子操作。数据库对单条UPDATE会加行锁两个并发线程只有一个能成功更新从而解决超卖问题。如果插入sc时重复比如同一学期重复选同一门课唯一约束uk_sc会抛异常事务回滚课程容量自动恢复。这个设计比“先SELECT检查、再UPDATE”更可靠答辩时老师问并发问题直接讲这个点就够了。成绩录入的SQL相对简单UPDATE sc SET score ? WHERE student_id ? AND course_id ? AND semester ?单条UPDATE不需要事务如果要做成绩修改日志再考虑用事务包住INSERT和UPDATE。4. Swing界面与事件监听把登录、选课、成绩录入串起来访问层写完界面层的任务就简单了界面从DAO拿数据事件触发时调用DAO不要在事件里拼SQL。很多课设的代码把Connection写在按钮监听里看着能跑但无法复用。这一章用CardLayout切面板、DefaultTableModel刷表格的方式实现一个多角色系统。4.1 Swing线程规则界面卡死的根源和SwingWorker用法Swing是单线程模型所有UI更新必须在事件分发线程EDT上执行。如果直接在按钮事件里调用JDBC查询数据库一慢EDT就被阻塞窗口拖动没反应在Windows上还会显示“未响应”。SwingWorker可以在后台线程执行耗时任务完成后自动回到EDT更新UI。loginBtn.addActionListener(e - { loginBtn.setEnabled(false); // 防止重复点击 new SwingWorkerUser, Void() { Override protected User doInBackground() throws Exception { // 在后台线程中连接数据库不阻塞界面 return userDao.login(usernameField.getText(), new String(passwordField.getPassword())); } Override protected void done() { loginBtn.setEnabled(true); try { User user get(); if (user ! null) { currentUser user; if (admin.equals(user.getRole())) { cardLayout.show(mainPanel, adminPanel); } else { cardLayout.show(mainPanel, studentPanel); } } else { JOptionPane.showMessageDialog(frame, 用户名或密码错误); } } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(frame, 连接数据库失败 ex.getMessage()); } } }.execute(); });doInBackground在后台线程执行done回到EDT。这里有个人人踩的坑在doInBackground里直接操作UI控件比如调用cardLayout.showSwing不会立即报错但会出现界面状态不稳定的情况。所以切换面板、更新表格这种事必须放在done里做。登录按钮用setEnabled(false)避免后台查询期间用户重复点击。4.2 用CardLayout管理多角色界面登录成功后切换卡片不要为管理员和学生各开一个新JFrame一个主窗口内容切换就够了。CardLayout可以把多个JPanel叠在同一块区域通过字符串名称切换显示哪一张。JFrame frame new JFrame(学生选课及成绩管理系统); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(1080, 720); frame.setLocationRelativeTo(null); CardLayout cardLayout new CardLayout(); JPanel mainPanel new JPanel(cardLayout); mainPanel.add(createLoginPanel(), loginPanel); mainPanel.add(createStudentPanel(), studentPanel); mainPanel.add(createAdminPanel(), adminPanel); frame.setContentPane(mainPanel); cardLayout.show(mainPanel, loginPanel); frame.setVisible(true);createLoginPanel、createStudentPanel这些方法返回JPanel把界面构建拆到独立方法里避免构造函数里堆几百行代码。退出登录时调用cardLayout.show(mainPanel, loginPanel)同时清空密码框、刷新一次表格数据。4.3 JTable绑定数据刷新课程列表和成绩表的正确方式JTable配合DefaultTableModel使用刷新时先清空行再填新数据。直接修改JTable的setValueAt容易引发类型转换问题我一般会写一个通用刷新方法。public void refreshCourseTable(JTable table, ListCourse courses) { DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (Course c : courses) { model.addRow(new Object[]{ c.getId(), c.getCourseNo(), c.getCourseName(), c.getCredit(), c.getSelectedCount() / c.getCapacity(), c.getTeacherName() }); } }setRowCount(0)是重置表格最稳的方式比model.clear()少一个空指针。表格里显示“已选/容量”这一列而不是把selected_count和capacity分成两列看起来更直观。id列是选课按钮用来定位课程的隐藏字段可以把该列宽度设置为0或放到最后但必须保留在数据模型里。选课按钮的监听事件通常长这样int row courseTable.getSelectedRow(); if (row 0) { JOptionPane.showMessageDialog(frame, 请先选择一门课程); return; } int courseId Integer.parseInt(courseTable.getValueAt(row, 0).toString()); String semester 2024-2025-1; try { boolean ok scDao.selectCourse(currentUser.getStudentId(), courseId, semester); if (ok) { JOptionPane.showMessageDialog(frame, 选课成功); ListCourse courses courseDao.findAvailableCourses(); refreshCourseTable(courseTable, courses); } else { JOptionPane.showMessageDialog(frame, 选课失败可能课容量已满); } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(frame, 选课失败 ex.getMessage()); }这个事件里只做三件事获取选中行、调用DAO、刷新表格。getValueAt返回的是Object需要toString再parseInt直接强转Integer有可能因为列类型不同而失败。如果想在成绩管理界面做“录入成绩”“退课”这类操作可以用JOptionPane弹窗输入数据课程设计阶段不建议一开始就做内嵌按钮列那会消耗大量时间在JTable渲染器上。5. 避坑指南SwingMySQL课程设计的5个血泪教训数据库访问层和界面层写好后剩下的就是调整细节。这里把我自己做课设和带人时遇到的5个高频坑列一下每一条都按“现象→原因→解决”的思路复盘。5.1 MySQL 8.0连接报错SSL、时区和连接耗尽现象第一次跑程序启动后立刻报Establishing SSL connection without server verification is not recommended或者直接报Public Key Retrieval is not allowed随后又出现The server time zone value CST is unrecognized。原因MySQL 8.0默认使用caching_sha2_password认证驱动默认要求安全连接并校验服务器公钥服务器时区没有设置驱动也无法识别。解决在JDBC URL后面追加useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai再补一个characterEncodingutf8。如果报ClassNotFoundException检查驱动包是否引入类名是否正确。现象程序连续操作几次后数据库连接报Too many connections界面卡住。原因每次getConnection都新建连接用完没有关闭连接泄漏。解决在finally或try-with-resources中关闭ResultSet、Statement、Connection。关闭顺序有讲究先关闭ResultSet再Statement最后Connection这个顺序能避免部分MySQL版本在连接关闭时出现清理不完整的问题。5.2 选课超卖容量被“查-改”两步操作弄丢现象课程容量还剩1两个学生几乎同时点选课结果两端都显示选课成功课程已选人数变成2超过了capacity。原因常见实现是先SELECT selected_count, capacity FROM course WHERE id ?判断已选人数小于容量再执行UPDATE course SET selected_count selected_count 1。两个请求同时通过第一步判断然后都执行UPDATE于是超卖。解决把检查容量和扣减容量合并为一条条件UPDATEUPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity。这条SQL只有一行受影响时才算选课成功再配合事务插入选课记录。这是第3章写过的方案也是答辩时的隐藏加分点。5.3 Swing界面卡死查询写在了EDT线程现象点击“查询课程”按钮界面像冻住一样窗口拖动很卡标题栏出现“未响应”。原因按钮的ActionListener运行在EDT线程直接在这里执行JDBC的executeQuery数据库等待会阻塞整个界面事件分发。解决用SwingWorker把耗时数据库操作放到后台线程完成后在done里更新UI。注意更新UI要放到done方法里不要在doInBackground里调用SwingUtilities.invokeLater那样会让线程模型变得更复杂。后台查询期间可以把按钮setEnabled(false)避免重复点击。5.4 中文乱码从建库到JTable都要对齐现象数据库里显示正常Swing表格里中文却是问号反过来界面正常数据库里存的是??。原因字符集不一致。建库用了latin1连接串没指定编码或者代码文件保存的编码和JVM默认编码不一致。解决建库建表统一定义DEFAULT CHARSET utf8mb4连接URL加characterEncodingutf8代码文件在IDEA里设置成UTF-8。如果控制台仍然乱码在VM参数里加上-Dfile.encodingUTF-8。JTable在Windows下默认字体能显示中文如果界面用自定义字体建议用new Font(Microsoft YaHei, Font.PLAIN, 14)。5.5 外键删除顺序先删子表再删主表现象删除课程或学生时MySQL报Cannot delete or update a parent row: a foreign key constraint fails。原因sc表有外键引用student和course直接删主表数据外键约束不让删。解决删除前先删sc记录或者在程序里显式分两步先DELETE FROM sc WHERE course_id ?再DELETE FROM course WHERE id ?。也可以把外键设为ON DELETE CASCADE但CASCADE会连成绩记录一起删除。如果希望保留成绩历史就不要偷懒用CASCADE而是用事务控制两步删除。6. 课设答辩加分用一条SQL把成绩统计和选课报表做出来大多数课设做到登录、选课、录入成绩就可以运行了但答辩时最容易被问的一句话是“你的系统能不能统计成绩分布”与其现场写循环计算不如在数据库里用一条GROUP BY解决。先做成绩分布统计SELECT SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS excellent, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS good, SUM(CASE WHEN score 60 AND score 80 THEN 1 ELSE 0 END) AS pass, SUM(CASE WHEN score IS NULL OR score 60 THEN 1 ELSE 0 END) AS fail FROM sc WHERE course_id ?;这条SQL把分数段统计成四个字段在Swing里用JTable或JLabel显示都很方便。注意score IS NULL也要算成未录入不要让它参与平均分计算。想统计所有课程去掉WHERE条件再按course_id分组即可。再做选课报表SELECT c.course_no, c.course_name, c.teacher_name, COUNT(sc.id) AS student_count, COALESCE(AVG(sc.score), 0) AS avg_score FROM course c LEFT JOIN sc ON sc.course_id c.id GROUP BY c.id;LEFT JOIN可以保留还没有人选修的课程COUNT统计每门课的实际选课人数COALESCE把无成绩的课程平均分显示为0而不是NULL。这个查询可以直接放到管理员的“课程统计”按钮里展示全部课程的选课热度。答辩前先把这两条SQL在MySQL Workbench或Navicat里跑一遍确认结果和程序里展示的一致不要等到演示现场再敲SQL。我这个课设做下来最深的体会是把JDBC和事务放在界面之前设计后面会顺畅很多答辩时把预编译、条件更新、SwingWorker这几个点讲出来老师一般不会再追问太多。这个习惯后来我带进了项目里做任何功能都先想数据再想界面希望帮到你。本文还有配套的精品资源点击获取