
简介这套学生选课及成绩管理系统采用Java与Swing开发使用MySQL存储数据面向课程设计、大作业或毕业设计场景。系统内置管理员、教师、学生三类角色覆盖选课管理、成绩录入查询、学生信息维护等功能模块实现登录验证、课程管理、成绩统计等操作适合初学者学习桌面图形界面与数据库编程。资源包约8.62MB共50个文件其中有16个Java源码文件、1个SQL数据库脚本、6个XML配置、20张功能界面截图、2张数据库E-R图另含MySQL驱动JAR包目录结构清晰方便导入开发工具运行。SQL脚本包含建表语句与初始登录数据。附带数据库连接配置说明修改账号密码后即可快速部署体验。系统结构图、E-R图及各操作界面截图均有收录便于理解整体设计。已有81人浏览学习可作为课程设计参考资料。1. 学生选课及成绩管理系统拿它当课设通宵会少一点期末前一个月很多人会被类似的题目砸中用 Java Swing MySQL 做一套学生选课及成绩管理系统。看上去不过是几个表单和按钮但真动手才发现拖界面最多占四成功夫剩下全耗在数据库表设计、JDBC 连接和一堆“玄学报错”上。这套系统实际要支撑四类操作学生信息维护、课程管理、学生选课与退选、成绩录入与统计。数据之间有多对多的关联所以比普通的图书管理系统更能训练数据库设计界面虽然是老派的 Swing但足够帮你看清桌面程序从事件触发到数据落库的完整链路。适合拿来当 java 课程设计题目也适合第一次接触 Swing MySQL 的初学者作为踏入“视图层、业务层、数据层”分层意识的第一个项目。它不难可依赖的细节不少下面把我会怎么做完整讲一遍。2. 数据库先行用三张表把选课和成绩放进同一个筐2.1 先定业务规则再决定要不要单独建成绩表学生和课程天然是多对多关系所以中间一定会有一张选课表。很多课程设计会拆出四张表学生表、课程表、选课表、成绩表。我的习惯是只建三张因为一次选课只会产生一条成绩记录成绩本质上就是选课记录的属性。如果拆出单独的成绩表就得额外维护选课ID和成绩ID的绑定关系查询时多一次连接还容易造成同一选课有多条成绩的数据不一致问题。什么时候拆当你要支持一次选课多次考试比如平时分、期末分、补考分时再拆成绩表才值得。课设阶段三张表已经足够这也是面试官愿意听的分析逻辑。三张表的核心关系是这样student 表和 course 表是数据源course_selection 表用来记录“谁选了哪门课以及考了多少分”。业务上的两个硬性规则必须用数据库约束兜住同一个学生不能重复选同一门课以及选课记录里的学生和课程必须来自对应主子表。2.2 建表SQL类型、默认值和唯一约束一次写对下面是我常用的建表脚本直接复制到 MySQL 的 Query 窗口执行即可。注意先建数据库和基础表最后建带外键的选课表。-- 打破那些临时表吧完整的库和表语句一次跑通 CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_db; -- 学生表内部自增主键 唯一学号避免学号变更时外键跟着拆链 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 内部主键, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号业务维度唯一, student_name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别男/女, class_name VARCHAR(50) COMMENT 班级名称, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表课程编号唯一容量字段可以作为选课时的校验依据 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分保留一位小数, teacher VARCHAR(50) COMMENT 任课教师, capacity INT DEFAULT 50 COMMENT 课程容量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课表联合唯一约束兜住重复选课成绩允许为空未考试就是NULL CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT 学生ID对应student.id, course_id INT NOT NULL COMMENT 课程ID对应course.id, score DECIMAL(5,2) DEFAULT NULL COMMENT 百分制成绩未录入时为空, selected_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, CONSTRAINT uk_student_course UNIQUE (student_id, course_id), CONSTRAINT fk_selection_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_selection_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段DDL里最值得说的是两个设计点第一是 student 和 course 都用了“自增主键 业务唯一编号”的组合。学号、课程编号虽然看起来唯一但你如果直接拿学号当主键将来一旦要做学号修改关联选课表的外键会全部跟着变。第二是 course_selection 表里唯一约束用的是联合唯一这样数据库层面就能拦截同一个人重复选同一门课不用等到 Java 代码里做一次“先查询再插入”。成绩字段我故意没有加 NOT NULL因为成绩本来就是在选课后才录入的没有考试前就是 NULL查询时配合 COALESCE 可以统一显示。2.3 造数据没有测试数据的系统没法演示表结构完成后手头至少要有三五个学生、三四门课和几条选课记录否则界面列表空空荡荡连按钮都试不出效果。下面的数据是我按常见课设要求造的样本。-- 造一组可以演示增删改查的数据 INSERT INTO student (student_no, student_name, gender, class_name) VALUES (2023001, 张明, 男, 软件工程2301), (2023002, 李丽, 女, 软件工程2301), (2023003, 王强, 男, 大数据2301); INSERT INTO course (course_no, course_name, credit, teacher, capacity) VALUES (C001, Java程序设计, 4.0, 陈老师, 60), (C002, 数据库原理, 3.0, 刘老师, 50), (C003, 操作系统, 3.5, 王老师, 40); -- 预先插入两条选课记录张明选Java李丽选数据库 INSERT INTO course_selection (student_id, course_id, score) VALUES (1, 1, 88.5), (2, 2, 91.0);插入时有一个容易忽略的点course_selection 里外键关联的是 student 表和 course 表的主键 id不是学号和课程编号。刚才的 INSERT 里学生 id 从 1 开始课程 id 从 1 开始所以 1、1 和 2、2 能对应上。如果你之前手动删过数据自增主键可能不是从 1 开始最好先用 SELECT 看看真实 id 再插。2.4 一个查询模板多表连接与成绩排序提前备好后面 Swing 界面上需要展示带学生名、课程名、成绩的综合列表我们先把这条 SQL 写出来也顺便练一下 mysql 里最常用的排序。-- 选课综合查询连接三张表按成绩从高到低排序 SELECT s.student_no, s.student_name, c.course_name, c.credit, cs.score, CASE WHEN cs.score IS NULL THEN 未录入 ELSE CAST(cs.score AS CHAR) END AS score_text FROM course_selection cs INNER JOIN student s ON cs.student_id s.id INNER JOIN course c ON cs.course_id c.id ORDER BY cs.score DESC;这段查询里INNER JOIN 只返回已经选课且学生、课程都存在的记录。如果你还要在“课程查询”页面把没人选的课也显示出来就需要把 INNER JOIN 换成 LEFT JOIN以 course 表为主表。ORDER BY cs.score DESC 是最直白的mysql排序应用不过要注意成绩为空时排序会把 NULL 放到最后这个特性演示时别当成 bug。3. JDBC连接层把Java和MySQL安全地接到一起3.1 驱动与连接串时区和编码参数别照抄Swing 界面的每次查询最终都要落到 JDBC 上。如果你是手动把 mysql-connector-java 的 jar 包加到项目里那么首先要保证驱动类能被找到。MySQL 8.x 的驱动类名是 com.mysql.cj.jdbc.DriverMySQL 5.x 则是 com.mysql.jdbc.Driver。很多同学连 MySQL 都还没装完就报 ClassNotFoundException第一步先看导入的 jar 包版本。连接串里我最常出现两个参数String url jdbc:mysql://localhost:3306/course_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String user root; String password 你的数据库密码;用 MySQL 8.x 时必须带 serverTimezone不写会提示时区异常characterEncodingutf8 是为了保证中文正确传输。这里我用的是 localhost如果你的 MySQL 装在另一台机器或者 Docker 容器里换成对应的 IP 和端口。顺便提醒如果你的 mysql 安装配置教程里用的是 XAMPP 或 phpMyAdmin端口通常还是 3306只要数据库服务启动着就能连上。3.2 JdbcUtil连接、关闭三件套是保命习惯桌面程序和 Web 程序一样不能只开不关。我习惯把连接获取和资源关闭收敛到一个工具类避免每个类里都重复写 try-catch 和 close。import java.sql.*; public class JdbcUtil { private static final String URL jdbc:mysql://localhost:3306/course_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }static 块里的 Class.forName 是为了让驱动自我注册这一行在 JDBC 4.0 以后其实可以省略但保留能兼容更老环境中手动加载的场景。close 方法里被 catch 后的异常我都刻意忽略因为关闭资源时出错不会影响业务主流程。这里有个经验关闭顺序和创建顺序相反ResultSet、PreparedStatement、Connection一层层来否则可能因为结果集没关闭导致下一次查询卡住。3.3 PreparedStatement比 Statement 安全也更好写参数执行查询时永远不要用字符串拼接 SQL特别是有用户输入的地方。登录页面如果写成“SELECT * FROM student WHERE student_no ” 学号 “”一个带单引号的学号就能把 SQL 语义改掉。PreparedStatement 用占位符先把 SQL 结构固定再把参数传入既防止注入又让代码整洁。public Student findStudentByNo(String studentNo) { String sql SELECT id, student_no, student_name, gender, class_name FROM student WHERE student_no ?; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, studentNo); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setStudentName(rs.getString(student_name)); s.setGender(rs.getString(gender)); s.setClassName(rs.getString(class_name)); return s; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里使用了 try-with-resources 语法Connection、PreparedStatement、ResultSet 会自动关闭不再需要手动调用 JdbcUtil.close。如果你用的 JDK 还是 7 以下要回到旧语法在 finally 里手动关。注意 setString 的下标从 1 开始这是 JDBC 的老规矩填错下标会立刻抛异常。3.4 事务选课是一组写操作不能只 insert 一半选课这个动作看起来只是往 course_selection 插一条记录但如果课程表带容量那就还涉及“更新剩余容量”。两步操作只要中间失败一次就会出现选课记录存在但容量没减、或者容量减了但选课没落库的错误。事务就是为这种情况准备的。public boolean selectCourse(int studentId, int courseId) { String insertSql INSERT INTO course_selection(student_id, course_id) VALUES(?, ?); String updateSql UPDATE course SET capacity capacity - 1 WHERE id ? AND capacity 0; Connection conn null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(insertSql); PreparedStatement ps2 conn.prepareStatement(updateSql)) { ps1.setInt(1, studentId); ps1.setInt(2, courseId); ps1.executeUpdate(); ps2.setInt(1, courseId); int rows ps2.executeUpdate(); if (rows 0) { throw new SQLException(课程容量已满或课程不存在); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } finally { conn.setAutoCommit(true); if (conn ! null) conn.close(); } } catch (SQLException e) { e.printStackTrace(); return false; } }这段代码的关键点是连接对象 conn 在整个 try 块外声明这样才能在 catch 里拿到同一个连接做 rollback。updateSql 里的条件“capacity 0”其实是一种乐观锁思路如果容量已经为 0这次 update 影响行数为 0代码就主动抛异常让事务回滚把 insert 一起撤销。事务的加分点是让老师看到你在业务动作上有意识保证一致性而不只是会建表。4. Swing界面与交互登录、选课、成绩录入的完整链路4.1 界面骨架JTabbedPane 把多个功能页装进一个窗口Swing 做课设最怕把所有按钮堆到一个面板上。我的推荐结构是用 JTabbedPane 把“选课管理”“成绩管理”“课程查询”分别放在不同标签页里主窗口只负责装载它们。JTabbedPane tabbedPane new JTabbedPane(); tabbedPane.addTab(选课管理, createSelectCoursePanel()); tabbedPane.addTab(成绩管理, createScorePanel()); tabbedPane.addTab(课程查询, createCoursePanel()); JFrame frame new JFrame(学生选课及成绩管理系统); frame.setContentPane(tabbedPane); frame.setSize(900, 600); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true);每个 createXxxPanel() 方法都返回一个 JPanel独立负责自己的布局和事件。这样即使后面要调整某个页面的字段也不会影响其他页面的结构。Swing 默认是流式布局实际做表格和表单时建议手工指定 BorderLayout 或者 GridBagLayout否则窗口缩放后控件会乱跑。4.2 JTable 别直接改用 DefaultTableModel 刷新数据表格是选课和成绩管理页的核心。许多人图省事直接把 TableModel 拿出来 addRow但数据一多、页面一刷新就容易出各种状态残留。正确做法是维护一个 DefaultTableModel 实例用 setRowCount(0) 清空再重新填充。private JTable table; private DefaultTableModel model; private void initTable() { String[] columns {选课ID, 学号, 学生姓名, 课程名, 学分, 成绩}; model new DefaultTableModel(columns, 0); table new JTable(model); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); } private void refreshTable() { model.setRowCount(0); String sql SELECT cs.id, s.student_no, s.student_name, c.course_name, c.credit, cs.score FROM course_selection cs JOIN student s ON cs.student_id s.id JOIN course c ON cs.course_id c.id ORDER BY cs.selected_time DESC; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { model.addRow(new Object[]{ rs.getInt(id), rs.getString(student_no), rs.getString(student_name), rs.getString(course_name), rs.getDouble(credit), rs.getObject(score) }); } } catch (SQLException ex) { JOptionPane.showMessageDialog(table, 查询失败 ex.getMessage()); } }这里 setRowCount(0) 清空表格但不会把列名删掉因为列名是在创建 model 时指定的。成绩列我用了 rs.getObject(score)这样 NULL 成绩在表格里直接显示成“null”不会因为 getDouble 碰到 NULL 时报错。每次操作完数据库后调用一次 refreshTable整个界面就能同步到最新状态。4.3 选课操作下拉框选人按钮提交失败要有提示选课页面最合理的交互是弹出一个模态对话框用下拉框选择学生和课程点击确定后写库并刷新表格。JButton selectBtn new JButton(选课); selectBtn.addActionListener(e - { JDialog dialog new JDialog(frame, 选择课程, true); JComboBoxStudent studentBox loadStudentComboBox(); JComboBoxCourse courseBox loadCourseComboBox(); JButton ok new JButton(确定选课); ok.addActionListener(ev - { Student s (Student) studentBox.getSelectedItem(); Course c (Course) courseBox.getSelectedItem(); if (s null || c null) { JOptionPane.showMessageDialog(dialog, 请先选择学生和课程); return; } boolean success new SelectionService().selectCourse(s.getId(), c.getId()); if (success) { JOptionPane.showMessageDialog(dialog, 选课成功); dialog.dispose(); refreshTable(); } else { JOptionPane.showMessageDialog(dialog, 选课失败可能已选过或容量不足); } }); // 这里仅示意布局实际要把组件加入 dialog 的 contentPane dialog.setVisible(true); });ComboBox 里放自定义对象时必须重写 Student 和 Course 类的 toString 方法否则下拉框里显示的是一串“com.xxx.Student123”。选课失败时弹窗提示比控制台打印更直观使用者不会去看 Console。如果业务空指针多半是下拉框没有默认选中项或者查询学生列表的 SQL 漏了排查从这两个点开始。4.4 成绩录入弹窗校验数字范围比表格直接编辑更稳成绩录入页经常有人尝试在 JTable 里双击单元格后直接改这在简单场景可行但需要重写 isCellEditable 和 setValueAt而且用户误编辑时很难拦截。课设阶段我推荐用一个输入弹窗来完成修改。private void openScoreDialog(int selectionId, Object currentScore) { JTextField scoreField new JTextField(currentScore null ? : currentScore.toString()); int option JOptionPane.showConfirmDialog(frame, scoreField, 录入/修改成绩, JOptionPane.OK_CANCEL_OPTION); if (option JOptionPane.OK_OPTION) { String text scoreField.getText().trim(); if (text.isEmpty()) { JOptionPane.showMessageDialog(frame, 成绩不能为空); return; } double score Double.parseDouble(text); if (score 0 || score 100) { JOptionPane.showMessageDialog(frame, 成绩必须是0到100之间的数字); return; } String sql UPDATE course_selection SET score ? WHERE id ?; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setDouble(1, score); ps.setInt(2, selectionId); ps.executeUpdate(); refreshTable(); } catch (SQLException ex) { JOptionPane.showMessageDialog(frame, 保存失败 ex.getMessage()); } } }这段代码展示了两个容易被忽略的点输入框为空时不要直接 parseDouble要提前判断Double.parseDouble 本身不限制范围所以必须自己做 0 到 100 的校验。如果用户填了字母parseDouble 会抛 NumberFormatException这正好提醒你课设里要加 try-catch 而不是让程序闪退。5. 课设最容易翻车的五个坑现象、原因、解决5.1 现象界面能打开但一点按钮就卡死选课点确定后整个窗口像冻住一样转圈圈鼠标点击没反应。原因是 Swing 的界面更新必须跑在事件调度线程EDT上而你在按钮回调里直接做了耗时的数据库查询EDT 被阻塞。解决方法是把耗时操作放到 SwingWorker 的后台线程再在 done 方法里刷新表格。SwingWorkerVoid, Void worker new SwingWorkerVoid, Void() { Override protected Void doInBackground() { // 这里执行数据库查询 return null; } Override protected void done() { // 这里刷新表格和提示 } }; worker.execute();以后一旦发现界面“假死”第一反应就是把数据库查询挪出事件回调而不是反复检查布局。5.2 现象查询出来的中文全是问号学生姓名显示成“”或者存进去之后读出来是乱码。原因通常是三层编码没有统一MySQL 库表字符集不是 utf8mb4、连接 URL 不带 characterEncodingutf8、Windows 控制台或 JVM 默认编码不是 UTF-8。解决方法是建库时带上 DEFAULT CHARSETutf8mb4 COLLATE utf8mb4_general_ci连接串里加 characterEncodingutf8在 IDE 的 VM 参数里加 -Dfile.encodingUTF-8。如果程序在命令行运行执行时也加上这个 JVM 参数。5.3 现象MySQL 服务没启动Java 狂报 Connection refused报 java.net.ConnectException: Connection refused 时很多人第一反应是去改代码里的 IP其实 90% 是 MySQL 服务根本没启动或者端口不是 3306。先检查服务状态Windows 下输入 net start mysql 看看报什么Linux 用 systemctl status mysqld。然后用 telnet 127.0.0.1 3306 测试端口连通性。如果 telnet 提示连接失败再确认 mysql 安装配置教程里的端口和你的连接串一致。我见过有人下载了 zip 版 MySQL 但从来没初始化和启动服务代码里却还在连数据库。5.4 现象程序关掉之后MySQL 连接数依然上涨跑一次课设演示MySQL Workbench 一看 processlist 全是你的 Java 程序留下的连接。原因是创建了 Connection 之后没有关闭特别是很多初学者喜欢把 Connection 定义为类的成员变量或者 static一次创建全程复用一直没有 close。解决方法是遵循“谁打开谁关闭”在每个 DAO 方法内部创建连接用 try-with-resources 或者 finally 中调用 JdbcUtil.close 三件套。关闭顺序是 ResultSet、PreparedStatement、Connection。如果你用了连接池也要保证归还连接而不是直接关。5.5 现象代码改了运行起来还是旧界面在 IDE 里点运行看到的还是之前的窗口和按钮或者在其它电脑上运行报 ClassNotFoundException。原因是项目没有被重新编译或者 jar 包没有真正打到类路径中。解决方法是先执行一次 clean再重新编译检查 mysql-connector-java 的 jar 是否在项目根目录下的 lib 文件夹里并且被加入构建路径。如果手动用 javac 编译命令行里要带上 -cp 指定 jar 位置。桌面程序打包成可执行 jar 时不要只打自己写的 class还要把第三方驱动一起打包进 MANIFEST 的 Class-Path或者使用 One-Jar 这类方案。6. 答辩前值得补上的三个进阶细节如果时间还够给系统加这三个小功能能让评分老师看到你不只是会拖 Swing 组件。第一个是登录权限。在 student 表或单独的设计员表里加一个 role 字段比如“管理员/学生”登录后用角色控制按钮的可用状态学生只能选课和看成绩管理员才能录入成绩和维护课程。这里只需要在每个窗体初始化时根据 role 调用 setEnabled(false)不需要动太多结构。第二个是组合查询。在课程查询标签页加一组可选的查询条件课程名、教师、学分范围任何一个为空就忽略。用 SQL 动态拼接 WHERE 条件时一定要用参数占位不能把用户输入直接拼进字符串。这是个很实用的加分点不少核心考点就是看你能不能安全处理动态查询。第三个是数据导出。JTable 显示的内容可以一键导出成 CSV导出时用 FileWriter 写 UTF-8 文件每行用英文逗号分隔。CSV 可以直接用 Excel 打开方便演示给老师看统计结果。Swing 桌面程序不需要再引入第三方 Excel 库一个 Writer 就够。我自己的一个教训是课设答辩前一晚演示的时候因为某条查询的 ResultSet 没有关闭数据库达到最大连接数程序直接连不上库。后来每次写完一个 DAO都要回头确认连接、语句、结果集是否全部关闭。这些东西看着不起眼但现场翻车时是最难解释的。希望这个避坑清单和代码结构能帮你把同样的尴尬挡在答辩之前也希望这套思路能给你一个真正可用的课设起点。祝顺利。本文还有配套的精品资源点击获取