简介:这份资源是Java Swing+JDBC+MySQL实现的毕业设计选题管理系统完整项目包,面向计算机相关专业学生、Java桌面开发初学者及需要课程设计或毕设参考的开发者,帮助解决选题分配流程信息化的问题。系统覆盖学生选题、教师命题、管理员审核等环节,包含用户权限管理、题目发布、选题申请、审核流程、查询统计与数据备份恢复等核心功能。压缩包共18个文件,约4.61MB,以12个java源码文件为主体,另含sql建库脚本、doc与md格式的数据库表设计文档、jar依赖包及iml工程配置,结构清晰便于导入运行与二次开发。目前已有1532人学习下载,适合作为Java桌面应用与数据库综合实践的参考案例,可据此理解Swing界面搭建、JDBC数据交互与MySQL表结构设计,并借鉴异常处理、代码复用等编程实践,快速完成类似系统的搭建与调试。
1. 从一张选题志愿表说起:Swing+JDBC+MySQL 的选题系统到底解决什么问题
每年毕业季,教务老师手里都会堆起一摞选题志愿表,学生填三个志愿,导师再逐个确认,中间只要有一轮调剂,Excel 就开始打架:谁被谁选了、哪个题目还剩名额、哪个导师超了指标,全靠人工核对。用 Java Swing+JDBC+MySQL 做一套毕业设计选题管理系统,本质就是把这套「志愿填报—导师确认—名额调剂」的流程搬进一个带界面的桌面程序里。它适合两类人:一类是正在做 Java 课程设计、需要一套能跑通 CRUD 和事务的完整案例;另一类是想练手 JDBC 连接 MySQL、Swing 事件驱动和表关联查询的初学者。这套东西不追求高并发,追求的是逻辑闭环和数据库操作规范,把选题、导师、学生三张表的关系理清楚,比堆界面控件重要得多。
2. 选题系统的数据模型与 JDBC 连接层:先把三张表和一条连接做扎实
2.1 学生、导师、选题三张表怎么设计才不返工
很多同学一上来就画界面,结果做到一半发现字段不够用,回头改表结构,JDBC 代码全得重写。血泪经验是先把表定死。核心就三张:student、teacher、topic,再加一张selection记录选题关系。topic表里要有teacher_id外键和max_students名额字段,selection表用student_id和topic_id做联合唯一约束,防止一个学生重复选同一个题。
CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, title VARCHAR(20), max_topics INT DEFAULT 3 ); CREATE TABLE topic ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, max_students INT DEFAULT 1, selected_count INT DEFAULT 0, FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); CREATE TABLE selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, topic_id INT NOT NULL, status TINYINT DEFAULT 0, -- 0待确认 1已确认 2已拒绝 UNIQUE KEY uk_stu_topic (student_id, topic_id) );selected_count这个冗余字段是故意的,每次选课成功就加一,避免每次查名额都去selection表 count,Swing 表格刷新时性能差别很明显。status用 TINYINT 而不是字符串,省空间也方便做状态流转判断。
2.2 JDBC 连接 MySQL 的三种写法与连接池取舍
初学者最容易在jdbc连接mysql这一步翻车,要么驱动没加载,要么时区报错。最稳的写法是把连接参数抽到配置文件,别硬编码在 Java 里。
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/topic_sys?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PWD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动未找到", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }useSSL=false是为了绕开本地开发时的mysql ssl连接错误,生产环境不该这么写,但课程设计阶段这样最省事。serverTimezone=Asia/Shanghai不加的话,MySQL 8.0 会直接抛时区异常。如果想让 Swing 界面在频繁刷新表格时不卡,可以引入 Druid 或 HikariCP 连接池,把getConnection换成从池里取,但课程设计用DriverManager足够,别为了炫技把依赖搞复杂。
2.3 用 PreparedStatement 封装通用增删改查
Swing 里每个按钮背后都是一次数据库操作,如果每个都写一遍Statement,代码会又臭又长。封装一个通用方法,参数用Object...传。
public static int update(String sql, Object... params) { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return -1; } }setObject能自动处理 String、Integer、Date 等常见类型,省去一堆if-else判断。返回值 -1 表示失败,Swing 里拿到 -1 就弹JOptionPane提示,别让异常静默吞掉。查询方法同理,返回List<Map<String,Object>>或者直接映射成实体类,看你对面向对象编程java的掌握程度,实体类更规范,Map 更快出活。
3. Swing 界面与选题业务逻辑:从登录到选课的事务控制
3.1 登录窗口与角色分流的最小实现
Swing 做登录界面,核心是JFrame+JPanel+GridBagLayout,别用绝对定位,否则换个分辨率就错位。登录成功后根据角色打开不同主窗口,学生看到选题列表,导师看到自己发布的题目和待确认列表。
loginBtn.addActionListener(e -> { String sql = "SELECT id, role FROM user WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, userField.getText()); ps.setString(2, new String(pwdField.getPassword())); ResultSet rs = ps.executeQuery(); if (rs.next()) { int role = rs.getInt("role"); dispose(); if (role == 1) new StudentFrame(rs.getInt("id")).setVisible(true); else new TeacherFrame(rs.getInt("id")).setVisible(true); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } catch (SQLException ex) { ex.printStackTrace(); } });密码用JPasswordField拿到的char[]转 String,别直接存明文到数据库,课程设计至少做个 MD5 或者直接存明文但心里要有数。角色字段role用 1 和 2 区分,分流逻辑写在监听器里,别在构造函数里查数据库,Swing 的 UI 构建和业务查询要分开。
3.2 选题事务:名额扣减和重复选择的原子操作
选课按钮点下去,背后要做三件事:检查名额、检查是否已选、插入记录并更新selected_count。这三步必须在一个事务里,否则并发下会出现名额超卖。JDBC 事务默认自动提交,要手动关掉。
public boolean selectTopic(int studentId, int topicId) { String checkSql = "SELECT max_students, selected_count FROM topic WHERE id=? FOR UPDATE"; String insertSql = "INSERT INTO selection(student_id, topic_id) VALUES(?,?)"; String updateSql = "UPDATE topic SET selected_count=selected_count+1 WHERE id=?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement check = conn.prepareStatement(checkSql)) { check.setInt(1, topicId); ResultSet rs = check.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } if (rs.getInt("selected_count") >= rs.getInt("max_students")) { conn.rollback(); return false; } } try (PreparedStatement ins = conn.prepareStatement(insertSql); PreparedStatement upd = conn.prepareStatement(updateSql)) { ins.setInt(1, studentId); ins.setInt(2, topicId); ins.executeUpdate(); upd.setInt(1, topicId); upd.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} return false; } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }FOR UPDATE是行级锁,防止两个学生同时读到同一个剩余名额。setAutoCommit(false)之后所有操作要么全成功要么全回滚,这就是jdbc transaction的典型用法。注意finally里要把autoCommit恢复,否则连接放回池里会带着脏状态。如果插入时触发唯一约束异常,说明学生重复选了,catch 里回滚即可,不用额外查一次。
3.3 用 JTable 展示选题列表并实时刷新
Swing 的JTable配DefaultTableModel是最顺手的组合,数据从数据库查出来直接塞进二维数组。导师确认选课后,学生的表格要能刷新,别每次关窗口重开。
String[] cols = {"题目ID", "题目名称", "导师", "已选/上限", "状态"}; DefaultTableModel model = new DefaultTableModel(cols, 0); JTable table = new JTable(model); public void refreshTable() { model.setRowCount(0); String sql = "SELECT t.id, t.title, te.name, t.selected_count, t.max_students " + "FROM topic t JOIN teacher te ON t.teacher_id=te.id"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { model.addRow(new Object[]{ rs.getInt(1), rs.getString(2), rs.getString(3), rs.getInt(4) + "/" + rs.getInt(5), "可选" }); } } catch (SQLException e) { e.printStackTrace(); } }setRowCount(0)清空旧数据再重新填,比fireTableDataChanged更直接。状态列可以根据selected_count和max_students动态算,满了就显示「已满」。刷新时机放在选课成功回调里,别用定时器轮询,Swing 的单线程模型经不起折腾。
4. 避坑与排查:Swing+JDBC 选题系统最常见的五个翻车点
4.1 现象:界面卡死,按钮点不动
原因:在事件监听器里直接执行了耗时查询,Swing 的事件分发线程被阻塞。解决:把数据库操作放到SwingWorker的doInBackground里,done方法里再更新 UI。课程设计数据量小,但养成习惯没坏处。
4.2 现象:中文乱码,数据库里存的是问号
原因:连接 URL 没加characterEncoding=utf8,或者 MySQL 建库时字符集是 latin1。解决:URL 补上参数,建库语句用DEFAULT CHARSET=utf8mb4,两边对齐。
4.3 现象:选课成功但名额没变
原因:selected_count更新和插入不在同一个事务,或者autoCommit没关。解决:按 3.2 的写法把两步包进事务,确认commit执行了。
4.4 现象:MySQL 8.0 连接报Public Key Retrieval is not allowed
原因:MySQL 8.0 默认认证插件是caching_sha2_password,旧驱动不兼容。解决:URL 加allowPublicKeyRetrieval=true,或者把用户认证方式改成mysql_native_password。
4.5 现象:导师删除题目后,学生端还能看到
原因:查询没加WHERE条件过滤已删除状态,或者用了软删除但查询没带is_deleted=0。解决:要么物理删除并级联清理selection,要么统一在查询里加状态过滤,别两套逻辑混用。
5. 进阶技巧:用存储过程做名额校验和状态流转
课程设计做到后面,导师端要批量确认选课,一条条 update 太慢。可以把名额校验和状态流转写成 MySQL 存储过程,Java 端只调一次CallableStatement。这样逻辑集中在数据库层,Swing 端代码更薄。
DELIMITER // CREATE PROCEDURE confirm_selection(IN sel_id INT, OUT result INT) BEGIN DECLARE v_topic_id INT; DECLARE v_status TINYINT; SELECT topic_id, status INTO v_topic_id, v_status FROM selection WHERE id = sel_id; IF v_status = 0 THEN UPDATE selection SET status = 1 WHERE id = sel_id; SET result = 1; ELSE SET result = 0; END IF; END // DELIMITER ;Java 调用时用CallableStatement注册出参:
CallableStatement cs = conn.prepareCall("{call confirm_selection(?, ?)}"); cs.setInt(1, selectionId); cs.registerOutParameter(2, Types.INTEGER); cs.execute(); int result = cs.getInt(2);result=1表示确认成功,0表示状态不对。存储过程的好处是把判断逻辑收拢,坏处是调试麻烦,MySQL 里DECLARE必须写在最前面,顺序错了直接报语法错误。我一般会在selection表上加一个confirm_time字段,确认时顺手写入时间戳,后面统计用得上。
验证方法很简单:开两个学生账号同时选同一个只剩一个名额的题目,看是否只有一个成功。如果两个都成功,说明事务隔离级别或者FOR UPDATE没生效,回去检查setAutoCommit和锁的范围。另一个习惯是每次改完 JDBC 代码,先把 SQL 拿到 MySQL 命令行里跑一遍,确认语句本身没问题,再排查 Java 层。这套系统不大,但把事务、连接、界面刷新这三块吃透,后面做任何 JavaWeb 项目都能迁移过去。希望帮到你。
本文还有配套的精品资源,点击获取