
简介这是一套面向Java初学者与课程设计学生的学生选课管理系统完整源码采用Java Swing构建GUI界面后端以MySQL存储数据适合作为课设参考或自学练手项目。压缩包共165个文件约58.82MB包含23个java源文件、24个form窗体文件、63个class编译文件以及41个png与5个jpg界面素材、3个jar依赖包另附1个sql数据库脚本、1个doc说明文档和1个avi项目演示视频覆盖登录、选课、退课、课程管理、学生信息维护、密码修改等模块窗体与源码一一对应便于对照理解Swing事件驱动与JDBC数据库操作。已有549人学习下载可直接导入Eclipse并配合Navicat导入数据运行帮助读者快速掌握GUI界面搭建、数据库连接配置与增删改查逻辑是兼顾完整度与可运行性的课程设计参考方案。1. Java Swing 选课系统为什么课设 95 分以上关键不在界面而在数据流很多同学做 Java Swing 学生选课管理系统第一反应是拖控件、调颜色、把登录页做得花里胡哨结果答辩时老师问一句“选课冲突怎么判的”就卡住了。这个标题里的“95 分以上课设”不是靠 GUI 好看拿到的而是靠一条完整、可验证的数据流学生登录后能选课、退课、查已选学分管理员能开课、删课、看选课名单数据库里外键和唯一约束兜底。Swing 只是壳真正决定分数的是 JDBC 事务、表结构设计和边界判断。适合正在做 Java 课程设计、需要一套能跑通且能讲清楚源码逻辑的本科生也适合想用 Swing 练手数据库增删改查的 Java 初学者。下面按“能复现、能答辩、能改需求”三条线拆开讲。2. 选课系统的表结构与 Swing 界面分层先定数据库再画窗口2.1 四张核心表怎么设计才不返工学生选课管理系统的数据库不需要多复杂但四张表必须一开始就定清楚否则写到一半改表结构Swing 里的实体类和 DAO 全得跟着翻车。常见做法是学生表、课程表、选课记录表、管理员表。选课记录表是核心它把学生和课程连起来同时承担“已选人数”和“是否重复选课”的约束。-- 学生表学号做主键姓名和密码必填 CREATE TABLE student ( stu_id VARCHAR(20) PRIMARY KEY, stu_name VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, major VARCHAR(50), total_credit INT DEFAULT 0 -- 已选总学分冗余字段方便查询 ); -- 课程表课程号主键容量和已选人数分开存 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher VARCHAR(50), credit INT NOT NULL, capacity INT NOT NULL, -- 课容量上限 selected INT DEFAULT 0, -- 当前已选人数 time_slot VARCHAR(50) -- 上课时间段用于冲突判断 ); -- 选课记录表联合主键防止同一学生重复选同一门课 CREATE TABLE sc ( stu_id VARCHAR(20), course_id VARCHAR(20), select_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (stu_id, course_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id), FOREIGN KEY (course_id) REFERENCES course(course_id) );逻辑说明sc表的联合主键是防重复选课的第一道锁比在 Java 里查一遍再插入更可靠。course.selected是冗余字段每次选课成功加一退课减一避免每次查已选人数都去COUNT(*)。time_slot存类似“周一1-2节”的字符串冲突判断时直接比较字符串是否相等简单但够用。参数说明capacity和selected都用 INT选课时先判断selected capacity。total_credit在选课成功后累加对应课程学分退课时减去。如果学校要求学分上限再加一个max_credit字段到学生表。2.2 Swing 界面按角色拆成三个 JFrameSwing 带 GUI 的学生选课管理系统界面不要全塞进一个类。我一般拆成登录窗、学生主窗、管理员主窗三个 JFrame每个窗里用 JTabbedPane 分页。登录窗只做一件事查数据库匹配账号密码然后根据角色打开对应主窗。// LoginFrame.java 核心登录逻辑 public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; private JComboBoxString roleBox; private void loginAction() { String id userField.getText().trim(); String pwd new String(passField.getPassword()); String role (String) roleBox.getSelectedItem(); // 根据角色查不同表管理员表和学生表分开 String sql student.equals(role) ? SELECT * FROM student WHERE stu_id? AND password? : SELECT * FROM admin WHERE admin_id? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, id); ps.setString(2, pwd); ResultSet rs ps.executeQuery(); if (rs.next()) { dispose(); // 关掉登录窗 if (student.equals(role)) { new StudentFrame(id).setVisible(true); } else { new AdminFrame(id).setVisible(true); } } else { JOptionPane.showMessageDialog(this, 账号或密码错误); } } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明用PreparedStatement而不是字符串拼接防止 SQL 注入这也是答辩时老师常问的点。登录成功后dispose()关掉当前窗再 new 出主窗避免窗口堆叠。角色用下拉框选比自动判断更直观也方便演示。参数说明DBUtil.getConnection()是工具类里面写死 JDBC URL、用户名、密码。MySQL 8 以上要加serverTimezoneAsia/Shanghai否则时间字段会报错。userField.getText().trim()去空格防止学生误输入空格导致登录失败。2.3 学生选课面板的表格模型与刷新机制学生主窗里最核心的是“可选课程”和“已选课程”两个 JTable。JTable 本身不存数据靠 TableModel 驱动。我一般用DefaultTableModel选课成功后重新查库并fireTableDataChanged()而不是手动往表格里加行这样数据永远和数据库一致。// StudentFrame.java 中加载可选课程的片段 private void loadAvailableCourses() { String sql SELECT course_id, course_name, teacher, credit, capacity, selected, time_slot FROM course WHERE selected capacity AND course_id NOT IN (SELECT course_id FROM sc WHERE stu_id?); DefaultTableModel model (DefaultTableModel) courseTable.getModel(); model.setRowCount(0); // 清空旧数据 try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuId); ResultSet rs ps.executeQuery(); while (rs.next()) { model.addRow(new Object[]{ rs.getString(course_id), rs.getString(course_name), rs.getString(teacher), rs.getInt(credit), rs.getInt(capacity) - rs.getInt(selected), rs.getString(time_slot) }); } } catch (SQLException e) { e.printStackTrace(); } }逻辑说明SQL 里用NOT IN子查询排除已选课程用selected capacity排除已满课程。model.setRowCount(0)先清空再填避免重复加载时数据翻倍。剩余名额用capacity - selected算出来显示比直接显示已选人数更直观。参数说明stuId从登录窗传进来是当前学生的学号。表格列顺序要和addRow里的顺序一致否则数据错位。如果课程很多可以加分页或搜索框但课设阶段全量加载足够。3. 选课与退课的事务控制把“抢课”逻辑写对3.1 选课按钮背后的三条 SQL 和一次事务选课不是简单插一条记录。正确顺序是先查课程是否已满、是否重复、时间是否冲突再插选课记录、更新课程已选人数、更新学生总学分。这三步必须在一个事务里否则插了记录但人数没加或者人数加了但记录没插数据就乱了。// CourseDAO.java 选课方法 public boolean selectCourse(String stuId, String courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查课程容量和时间 String checkSql SELECT capacity, selected, time_slot, credit FROM course WHERE course_id?; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setString(1, courseId); ResultSet rs checkPs.executeQuery(); if (!rs.next()) return false; int capacity rs.getInt(capacity); int selected rs.getInt(selected); String timeSlot rs.getString(time_slot); int credit rs.getInt(credit); if (selected capacity) { JOptionPane.showMessageDialog(null, 课程已满); return false; } // 2. 查时间冲突该学生已选课程中是否有同一时间段 String conflictSql SELECT c.time_slot FROM sc s JOIN course c ON s.course_idc.course_id WHERE s.stu_id? AND c.time_slot?; PreparedStatement conflictPs conn.prepareStatement(conflictSql); conflictPs.setString(1, stuId); conflictPs.setString(2, timeSlot); if (conflictPs.executeQuery().next()) { JOptionPane.showMessageDialog(null, 时间冲突); return false; } // 3. 插入选课记录 String insertSql INSERT INTO sc(stu_id, course_id) VALUES(?,?); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setString(1, stuId); insertPs.setString(2, courseId); insertPs.executeUpdate(); // 4. 更新课程已选人数 String updateCourseSql UPDATE course SET selectedselected1 WHERE course_id?; PreparedStatement updateCoursePs conn.prepareStatement(updateCourseSql); updateCoursePs.setString(1, courseId); updateCoursePs.executeUpdate(); // 5. 更新学生总学分 String updateStuSql UPDATE student SET total_credittotal_credit? WHERE stu_id?; PreparedStatement updateStuPs conn.prepareStatement(updateStuSql); updateStuPs.setInt(1, credit); updateStuPs.setString(2, stuId); updateStuPs.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }逻辑说明setAutoCommit(false)开启事务任何一步抛异常都rollback()保证要么全成功要么全回滚。时间冲突查询用 JOIN 把sc和course连起来只看同一学生同一时间段。插入记录用INSERT如果联合主键冲突会抛异常自动回滚相当于第二道防重复锁。参数说明stuId和courseId从界面表格选中行获取。credit从课程表查出用于更新学生总学分。如果学校有学分上限在更新学生表前先查total_credit credit max_credit不满足就回滚。3.2 退课为什么比选课更容易出 bug退课逻辑看起来是选课的反操作但坑更多。常见翻车点退课后课程已选人数没减、学生总学分没减、退课记录删了但界面没刷新。更隐蔽的是如果学生先退了课再选同一门课联合主键不会冲突但人数和学分必须正确回滚。// CourseDAO.java 退课方法 public boolean dropCourse(String stuId, String courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 查课程学分 String creditSql SELECT credit FROM course WHERE course_id?; PreparedStatement creditPs conn.prepareStatement(creditSql); creditPs.setString(1, courseId); ResultSet rs creditPs.executeQuery(); if (!rs.next()) return false; int credit rs.getInt(credit); // 2. 删除选课记录 String deleteSql DELETE FROM sc WHERE stu_id? AND course_id?; PreparedStatement deletePs conn.prepareStatement(deleteSql); deletePs.setString(1, stuId); deletePs.setString(2, courseId); int rows deletePs.executeUpdate(); if (rows 0) { JOptionPane.showMessageDialog(null, 未选该课程); return false; } // 3. 课程已选人数减一 String updateCourseSql UPDATE course SET selectedselected-1 WHERE course_id? AND selected0; PreparedStatement updateCoursePs conn.prepareStatement(updateCourseSql); updateCoursePs.setString(1, courseId); updateCoursePs.executeUpdate(); // 4. 学生总学分减掉 String updateStuSql UPDATE student SET total_credittotal_credit-? WHERE stu_id?; PreparedStatement updateStuPs conn.prepareStatement(updateStuSql); updateStuPs.setInt(1, credit); updateStuPs.setString(2, stuId); updateStuPs.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }逻辑说明先删sc记录根据executeUpdate()返回行数判断是否真的选了这门课。如果返回 0说明没选直接提示并返回避免后续误减人数。更新课程人数时加selected0条件防止减成负数。学生总学分减去对应学分同样在事务里。参数说明credit从课程表查不要从界面传防止界面数据过期。selected0是防御性条件正常流程不会触发但能兜住异常数据。3.3 管理员开课与删课的级联处理管理员开课就是往course表插记录删课则要小心如果课程已经被学生选了直接删course记录会触发外键约束报错。常见做法是删课前先查sc表有没有关联记录有就提示“已有学生选课不能删除”或者先删sc再删course但后者要同步更新学生总学分。// AdminDAO.java 删课方法 public boolean deleteCourse(String courseId) { String checkSql SELECT COUNT(*) FROM sc WHERE course_id?; try (Connection conn DBUtil.getConnection(); PreparedStatement checkPs conn.prepareStatement(checkSql)) { checkPs.setString(1, courseId); ResultSet rs checkPs.executeQuery(); if (rs.next() rs.getInt(1) 0) { JOptionPane.showMessageDialog(null, 已有学生选课不能删除); return false; } String deleteSql DELETE FROM course WHERE course_id?; PreparedStatement deletePs conn.prepareStatement(deleteSql); deletePs.setString(1, courseId); return deletePs.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }逻辑说明先COUNT(*)查选课记录大于 0 就拒绝删除。这样避免外键报错也避免学生已选课程突然消失。如果业务允许强制删课就要在事务里先删sc、再更新学生学分、最后删course但课设阶段建议用拒绝策略逻辑简单且不容易出错。参数说明courseId从管理员界面表格选中行获取。COUNT(*)返回 int判断大于 0 即可。4. 避坑与排查Swing 选课系统最常见的 5 个翻车点4.1 中文乱码数据库、JDBC、Swing 三处都要设 UTF-8现象学生姓名或课程名在 Swing 表格里显示成问号或乱码。原因MySQL 建库时没指定字符集或者 JDBC URL 没加characterEncodingutf8。解决建库用CREATE DATABASE choose_course DEFAULT CHARSET utf8mb4;JDBC URL 写成jdbc:mysql://localhost:3306/choose_course?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。Swing 本身用默认字体即可乱码基本都出在数据库连接层。4.2 选课成功但表格没刷新现象点选课按钮后弹窗提示成功但“可选课程”里课程还在“已选课程”里没出现。原因只更新了数据库没重新调用loadAvailableCourses()和loadSelectedCourses()。解决选课成功后立刻调这两个方法或者用fireTableDataChanged()通知表格模型。我一般把刷新逻辑写在选课按钮的actionPerformed最后确保界面和数据库一致。4.3 时间冲突判断漏掉“同一时间段不同课程”现象学生选了“周一1-2节”的 A 课还能选“周一1-2节”的 B 课。原因冲突查询只比了课程号没比time_slot。解决用SELECT c.time_slot FROM sc s JOIN course c ON s.course_idc.course_id WHERE s.stu_id? AND c.time_slot?把当前要选课程的time_slot传进去。如果time_slot存的是“周一1-2节”这种字符串直接相等比较就行如果存的是结构化时间就要做区间重叠判断。4.4 删课导致外键约束报错现象管理员删一门已经有人选的课控制台抛Cannot delete or update a parent row: a foreign key constraint fails。原因sc表有外键指向course直接删course违反约束。解决删课前先查sc表有记录就提示不能删。如果确实要删就在事务里先删sc、再更新学生总学分、最后删course。课设答辩时老师常问这个答“先查再删”比“级联删除”更稳妥。4.5 登录后关闭窗口但程序没退出现象关掉学生主窗后Java 进程还在后台跑任务管理器里能看到。原因Swing 默认关闭 JFrame 只是隐藏不是退出程序。解决主窗设置setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)或者在登录窗被关闭时调System.exit(0)。如果用了多线程加载数据还要确保线程是守护线程或手动停止。5. 从课设到能讲清楚用“选课冲突”做一次可验证的答辩演示课设拿高分的关键不是功能多而是你能当着老师面把一条边界逻辑讲透。我一般会准备一个“选课冲突”的演示脚本先登录学生账号选一门“周一1-2节”的课再选另一门同样时间的课让系统弹出“时间冲突”。然后打开数据库展示sc表里只有一条记录course.selected只加了 1。这个演示能同时证明事务、冲突判断和界面刷新都是对的。如果你想让系统更耐问可以加一个“选课日志”表每次选课/退课都记一条包含操作时间、学号、课程号、操作类型。答辩时老师问“怎么追溯选课记录”直接查日志表就行。日志表不需要外键字段也简单CREATE TABLE select_log ( id INT AUTO_INCREMENT PRIMARY KEY, stu_id VARCHAR(20), course_id VARCHAR(20), action VARCHAR(10), -- select 或 drop log_time DATETIME DEFAULT CURRENT_TIMESTAMP );在选课和退课的事务里各加一条INSERT INTO select_log不影响主流程但多了一层可验证的痕迹。参数说明action用固定字符串方便按类型筛选log_time默认当前时间不用 Java 传。另一个进阶技巧是把“已选学分”的校验做成可配置。学生表加max_credit字段默认 25选课时判断total_credit credit max_credit超过就回滚并提示。这样老师问“学分上限怎么控制”时你能直接指出代码位置和数据库字段。最后说个血泪经验Swing 的JTable在数据量大时不要用DefaultTableModel全量刷新课设阶段课程一般几十条没问题但如果想显得专业可以换成AbstractTableModel自定义模型只刷新变化行。不过课设时间有限先把事务和冲突判断写对界面流畅度是次要的。希望帮到你。本文还有配套的精品资源点击获取