☰
MySQL + Java Swing 宿舍管理系统:从建库到退宿的完整实现与避坑指南
2026/10/1 20:19:18 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的MySQL课程设计完整方案,以Java Swing构建桌面端学生宿舍管理系统,适合正在准备课程设计、毕业设计或需要Java+数据库综合实战练习的初学者与中级开发者。资源包共334个文件,约108.12MB,其中47个java源文件承载核心业务逻辑,202个class为编译产物,另有22个png界面素材、9个xml配置及若干cpp、h等辅助文件,并附带db数据库文件与jar依赖,开箱即可导入运行。内容覆盖管理员管理、宿舍管理、学生信息管理、学生宿舍分配及多角色权限接口等模块,目录结构清晰,便于按功能拆分阅读与二次开发。目前已有192人学习下载,可作为课程设计参考模板,帮助读者快速理解Swing界面搭建、JDBC数据库连接与增删改查实现思路,节省从零搭建的时间成本。

1. 从一份课程设计说起:MySQL + Java Swing 的宿舍管理系统到底能跑多远

每年毕业季,总有一批同学在搜索框里敲下「学生宿舍管理系统 源代码」,想找一个能跑、能改、能交差的完整项目。这个标题指向的东西很具体:用 MySQL 做数据存储,用 Java Swing 做桌面端界面,实现宿舍分配、学生入住、退宿、报修、来访登记这些日常事务。它适合两类人:一是正在做课程设计、需要一套可运行代码和数据库脚本的学生;二是刚学完 Java 基础、想找一个带完整 CRUD 和业务逻辑的练手项目的新手。我见过太多人卡在第一步——代码下载了,数据库脚本也有,但 MySQL 连不上、Swing 界面乱码、JDBC 驱动版本对不上,折腾两天就放弃了。这篇笔记就按我实际带人做这类项目的顺序,把环境、建库、核心模块、避坑和进阶技巧拆开讲,目标是让你照着能跑起来,并且知道每一处为什么这么写。

2. 环境准备与数据库设计:先把地基打对

2.1 JDK、MySQL、IDE 的版本选择与安装顺序

很多人一上来就打开 IDE 新建项目,结果 JDK 版本和 MySQL 驱动不匹配,报一堆No suitable driver found。我一般按这个顺序来:先装 JDK,再装 MySQL,最后配 IDE。JDK 选 8 或 11 都行,Swing 对高版本 JDK 兼容性没问题,但如果你后面要打包成 exe,JDK 8 的坑最少。MySQL 用 5.7 或 8.0 都可以,8.0 的默认认证插件是caching_sha2_password,老版本驱动连不上,这是新手翻车最多的地方。

安装 MySQL 时记住 root 密码,后面 JDBC 连接要用。Windows 下可以用 MSI 安装包,Linux 下用apt或yum都行。安装完先验证服务能不能起来:

# Windows 检查 MySQL 服务状态 net start | findstr MySQL # Linux 检查服务状态 systemctl status mysql # 登录测试 mysql -u root -p

如果登录成功,说明数据库服务正常。接下来建库建表,不要用 Navicat 手动点,直接写 SQL 脚本,方便版本管理和重复执行。

2.2 宿舍管理系统的表结构设计与建库脚本

这个系统的核心表不多,但关系要理清。学生表、宿舍表、入住记录表、报修表、管理员表,五张表就能撑起主要业务。学生和宿舍是多对一,入住记录是学生和宿舍的关联表,同时记录入住时间、退宿时间。报修表关联学生和宿舍。

-- 创建数据库,字符集用 utf8mb4,避免中文乱码 CREATE DATABASE dormitory_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dormitory_db; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender VARCHAR(4) COMMENT '性别', class_name VARCHAR(50) COMMENT '班级', phone VARCHAR(20) COMMENT '联系电话' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 宿舍表 CREATE TABLE dorm ( id INT PRIMARY KEY AUTO_INCREMENT, dorm_no VARCHAR(10) NOT NULL UNIQUE COMMENT '宿舍号', building VARCHAR(20) COMMENT '楼栋', capacity INT DEFAULT 4 COMMENT '床位容量', occupied INT DEFAULT 0 COMMENT '已住人数' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入住记录表 CREATE TABLE checkin ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, dorm_id INT NOT NULL, checkin_date DATE, checkout_date DATE, status VARCHAR(10) DEFAULT '在住' COMMENT '在住/已退宿', FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (dorm_id) REFERENCES dorm(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 报修表 CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, dorm_id INT NOT NULL, content VARCHAR(255), report_date DATE, status VARCHAR(10) DEFAULT '待处理', FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (dorm_id) REFERENCES dorm(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入默认管理员,密码存明文仅用于课程设计演示 INSERT INTO admin (username, password) VALUES ('admin', '123456');

建表时几个参数要留意:utf8mb4是必须的,不然学生姓名里的生僻字会变问号;InnoDB引擎支持外键和事务,退宿时同时更新入住记录和宿舍已住人数,需要事务保证一致性;occupied字段是冗余设计,避免每次查已住人数都去 count 入住记录,但更新时要和入住记录同步,这是后面容易出 bug 的点。

2.3 JDBC 连接封装与驱动版本匹配

Swing 项目里数据库连接不能每次操作都DriverManager.getConnection,那样连接数会爆。常见做法是写一个DBUtil工具类,用静态方法返回连接,或者用简单的连接池。课程设计级别用静态工具类就够了,但要注意关闭资源。

import java.sql.*; public class DBUtil { // MySQL 8.0 驱动类名是 com.mysql.cj.jdbc.Driver // MySQL 5.x 是 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/dormitory_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

URL 里的参数逐个说:useSSL=false关掉 SSL 警告,本地开发不需要加密连接;serverTimezone=Asia/Shanghai解决时区报错,MySQL 8.0 不配这个会抛The server time zone value异常;characterEncoding=utf8保证中文不乱码。驱动 jar 包用mysql-connector-java-8.0.x.jar,放到项目的lib目录并添加到 classpath。如果你用 Maven,加依赖就行,但课程设计通常直接拷 jar 包。

3. Swing 界面与核心业务模块:从登录到退宿的完整链路

3.1 登录窗口与主界面布局的代码骨架

Swing 的布局管理器是新手最头疼的地方,null布局虽然能绝对定位,但窗口一拉伸就乱。我一般用BorderLayout做整体,中间放JTabbedPane分页,每个 Tab 里用GridBagLayout或BoxLayout排表单。登录窗口用JDialog或JFrame都行,关键是密码框用JPasswordField,登录按钮绑定ActionListener。

import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.sql.*; public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; public LoginFrame() { setTitle("宿舍管理系统 - 登录"); setSize(350, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 居中显示 JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(8, 8, 8, 8); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.gridx = 0; gbc.gridy = 0; panel.add(new JLabel("用户名:"), gbc); gbc.gridx = 1; userField = new JTextField(15); panel.add(userField, gbc); gbc.gridx = 0; gbc.gridy = 1; panel.add(new JLabel("密码:"), gbc); gbc.gridx = 1; passField = new JPasswordField(15); panel.add(passField, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(this::doLogin); panel.add(loginBtn, gbc); add(panel); setVisible(true); } private void doLogin(ActionEvent e) { String username = userField.getText().trim(); String password = new String(passField.getPassword()); String sql = "SELECT * FROM admin WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery(); if (rs.next()) { dispose(); // 关闭登录窗 new MainFrame(); // 打开主界面 } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "数据库连接失败:" + ex.getMessage()); } } public static void main(String[] args) { // 在事件调度线程中启动 Swing,避免线程安全问题 SwingUtilities.invokeLater(LoginFrame::new); } }

这里用PreparedStatement而不是Statement,一是防 SQL 注入,二是参数里有中文时不用手动加引号。SwingUtilities.invokeLater是 Swing 的规矩,所有界面创建都放在事件调度线程里,不然偶尔会出界面卡死或组件不显示的玄学问题。登录成功后dispose()关掉登录窗,再 new 主界面,不要用System.exit,否则整个程序都退了。

3.2 学生信息管理:增删改查与表格刷新

主界面里学生管理是最典型的 CRUD 模块。左边放JTable显示数据,上面放搜索框和按钮,下面放表单。表格数据用DefaultTableModel,查询后先setRowCount(0)清空再逐行添加,不然会重复叠加。

import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.sql.*; public class StudentPanel extends JPanel { private JTable table; private DefaultTableModel model; private JTextField searchField; public StudentPanel() { setLayout(new BorderLayout()); // 顶部搜索栏 JPanel top = new JPanel(); searchField = new JTextField(15); JButton searchBtn = new JButton("查询"); JButton addBtn = new JButton("新增"); JButton delBtn = new JButton("删除"); top.add(new JLabel("学号/姓名:")); top.add(searchField); top.add(searchBtn); top.add(addBtn); top.add(delBtn); add(top, BorderLayout.NORTH); // 表格 model = new DefaultTableModel(new String[]{"ID", "学号", "姓名", "性别", "班级", "电话"}, 0) { @Override public boolean isCellEditable(int row, int column) { return false; // 表格不可直接编辑 } }; table = new JTable(model); add(new JScrollPane(table), BorderLayout.CENTER); searchBtn.addActionListener(e -> loadData(searchField.getText().trim())); delBtn.addActionListener(e -> deleteSelected()); addBtn.addActionListener(e -> showAddDialog()); loadData(""); } private void loadData(String keyword) { model.setRowCount(0); String sql = "SELECT * FROM student WHERE student_no LIKE ? OR name LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ResultSet rs = ps.executeQuery(); while (rs.next()) { model.addRow(new Object[]{ rs.getInt("id"), rs.getString("student_no"), rs.getString("name"), rs.getString("gender"), rs.getString("class_name"), rs.getString("phone") }); } } catch (SQLException ex) { ex.printStackTrace(); } } private void deleteSelected() { int row = table.getSelectedRow(); if (row == -1) { JOptionPane.showMessageDialog(this, "请先选中一行"); return; } int id = (int) model.getValueAt(row, 0); int confirm = JOptionPane.showConfirmDialog(this, "确定删除该学生?", "确认", JOptionPane.YES_NO_OPTION); if (confirm != JOptionPane.YES_OPTION) return; String sql = "DELETE FROM student WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); ps.executeUpdate(); loadData(searchField.getText().trim()); } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "删除失败,可能该学生有入住记录"); } } private void showAddDialog() { // 弹窗表单省略,核心是收集输入后执行 INSERT JTextField noField = new JTextField(); JTextField nameField = new JTextField(); Object[] message = {"学号:", noField, "姓名:", nameField}; int option = JOptionPane.showConfirmDialog(this, message, "新增学生", JOptionPane.OK_CANCEL_OPTION); if (option == JOptionPane.OK_OPTION) { String sql = "INSERT INTO student (student_no, name) VALUES (?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, noField.getText().trim()); ps.setString(2, nameField.getText().trim()); ps.executeUpdate(); loadData(""); } catch (SQLException ex) { ex.printStackTrace(); } } } }

DefaultTableModel重写isCellEditable返回 false,防止用户直接双击改表格导致数据没进库。删除时先弹确认框,这是基本交互。如果学生已经有入住记录,外键约束会阻止删除,catch 里给个友好提示,而不是把堆栈打到控制台就完事。查询用LIKE加%实现模糊匹配,参数化查询避免拼接字符串。

3.3 宿舍分配与退宿:事务处理与并发更新

宿舍分配是业务核心。分配时要做三件事:检查宿舍是否满员、插入入住记录、更新宿舍已住人数。这三步必须在一个事务里,否则可能出现宿舍满了但记录没插进去,或者记录插了但人数没加。退宿同理,更新记录状态、减少已住人数。

public class CheckinService { // 分配宿舍,返回 null 表示成功,否则返回错误信息 public String assignDorm(int studentId, int dormId) { String checkSql = "SELECT capacity, occupied FROM dorm WHERE id=? FOR UPDATE"; String insertSql = "INSERT INTO checkin (student_id, dorm_id, checkin_date, status) VALUES (?, ?, CURDATE(), '在住')"; String updateSql = "UPDATE dorm SET occupied = occupied + 1 WHERE id=?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 锁定宿舍行,防止并发超员 try (PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setInt(1, dormId); ResultSet rs = ps.executeQuery(); if (!rs.next()) { conn.rollback(); return "宿舍不存在"; } int capacity = rs.getInt("capacity"); int occupied = rs.getInt("occupied"); if (occupied >= capacity) { conn.rollback(); return "该宿舍已满员"; } } try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setInt(1, studentId); ps.setInt(2, dormId); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setInt(1, dormId); ps.executeUpdate(); } conn.commit(); return null; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return "分配失败:" + e.getMessage(); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } // 退宿 public String checkout(int checkinId) { String querySql = "SELECT dorm_id FROM checkin WHERE id=? AND status='在住'"; String updateCheckinSql = "UPDATE checkin SET status='已退宿', checkout_date=CURDATE() WHERE id=?"; String updateDormSql = "UPDATE dorm SET occupied = occupied - 1 WHERE id=?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); int dormId; try (PreparedStatement ps = conn.prepareStatement(querySql)) { ps.setInt(1, checkinId); ResultSet rs = ps.executeQuery(); if (!rs.next()) { conn.rollback(); return "记录不存在或已退宿"; } dormId = rs.getInt("dorm_id"); } try (PreparedStatement ps = conn.prepareStatement(updateCheckinSql)) { ps.setInt(1, checkinId); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(updateDormSql)) { ps.setInt(1, dormId); ps.executeUpdate(); } conn.commit(); return null; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return "退宿失败:" + e.getMessage(); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }

FOR UPDATE是行级锁,在事务里锁住宿舍记录,防止两个管理员同时给同一个宿舍分配导致超员。setAutoCommit(false)开启事务,所有操作成功后commit,任何一步失败就rollback。finally 里恢复自动提交并关闭连接,避免连接池里的连接状态被污染。退宿时先查记录状态,只有「在住」的才能退,防止重复退宿把人数减成负数。

4. 避坑与排查:那些让你熬夜的报错和乱码

4.1 数据库连接失败:从 error 2002 到 SSL 警告

现象:运行程序报Communications link failure或error 2002 (HY000): Can't connect to local MySQL server through socket。原因通常是 MySQL 服务没启动,或者 JDBC URL 里的端口不对。解决:先net start mysql或systemctl start mysql,再用mysql -u root -p确认能登录。如果报 SSL 警告,在 URL 后面加useSSL=false。如果报时区错误,加serverTimezone=Asia/Shanghai。还有一个隐蔽的坑:MySQL 8.0 默认认证插件是caching_sha2_password,老版本驱动连不上,要么换 8.0 的驱动 jar,要么改用户认证方式:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码';。

4.2 中文乱码:数据库、连接、Swing 三处都要查

现象:学生姓名显示成问号或乱码。原因可能在三处:数据库字符集不是 utf8mb4、JDBC URL 没加 characterEncoding、Swing 组件字体不支持中文。解决:建库时指定DEFAULT CHARACTER SET utf8mb4,JDBC URL 加characterEncoding=utf8,Swing 里如果中文显示成方块,设置全局字体:UIManager.put("Label.font", new Font("微软雅黑", Font.PLAIN, 14));。Linux 下如果没有微软雅黑,用WenQuanYi Micro Hei或Noto Sans CJK SC。

4.3 表格数据重复叠加:setRowCount(0) 的位置

现象:每次查询后表格数据翻倍。原因:DefaultTableModel是累加的,查询前没清空。解决:在loadData方法开头调用model.setRowCount(0),再执行查询和添加。注意setRowCount(0)要放在查询之前,不要放在while循环里,否则每次添加一行就清空一次,最后只剩最后一行。

4.4 外键约束导致删除失败:先查关联再删

现象:删除学生时报Cannot delete or update a parent row: a foreign key constraint fails。原因:该学生在checkin或repair表里有记录,外键阻止删除。解决:要么先删关联记录,要么在删除前查询并提示用户「该学生有入住记录,请先退宿」。课程设计里更稳妥的做法是逻辑删除,加is_deleted字段,而不是物理删除。

4.5 界面卡死:数据库操作别放在事件调度线程

现象:点击查询按钮后界面卡住几秒,按钮点不动。原因:Swing 的事件调度线程被 JDBC 阻塞了。解决:把数据库操作放到SwingWorker的doInBackground里,完成后在done方法里更新表格。课程设计数据量小,偶尔卡一下能忍,但如果要做得像样,SwingWorker是必须掌握的。

5. 进阶技巧:让课程设计多拿几分

如果你已经把基本功能跑通了,想让这个项目在答辩时更有说服力,有几个方向可以加。第一,把密码明文存储改成 MD5 或 SHA-256 加盐哈希,登录时比对哈希值,这是安全意识的体现。第二,给表格加排序和过滤功能,TableRowSorter一行代码就能实现点击表头排序。第三,导出 Excel,用 Apache POI 把学生列表写成.xlsx,答辩时演示「一键导出」很加分。第四,加一个简单的日志记录,用java.util.logging把登录、分配、退宿操作写到文件里,出问题能追溯。

// 表格排序示例 TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(model); table.setRowSorter(sorter); // 密码哈希示例 import java.security.MessageDigest; public static String sha256(String input) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(input.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : hash) { sb.append(String.format("%02x", b)); } return sb.toString(); }

我自己的习惯是,每做完一个模块就手动跑一遍完整流程:新增学生、分配宿舍、退宿、删除学生,看数据对不对、界面刷不刷新、异常有没有提示。不要等全部写完再测,那时候报错堆在一起,排查成本翻倍。数据库脚本和代码分开管理,脚本放sql目录,每次改表结构就加一个alter脚本,别直接改原始建表语句,不然别人拿到你的项目不知道执行哪个版本。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询