ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL+Java Swing宿舍管理系统课设实战:从建库到床位分配事务

MySQL+Java Swing宿舍管理系统课设实战:从建库到床位分配事务

简介:这份资源面向高校计算机相关专业学生与Java初学者,提供一套可直接运行的MySQL课程设计项目——基于MySQL数据库、Java语言与Swing图形界面开发的学生宿舍管理系统,适合用于课程设计参考、期末项目提交或数据库与桌面应用开发的入门练手。压缩包共334个文件,约112.72MB,以编译后的class文件与47个java源码为主,另含png界面截图、xml配置、db数据库文件及jar依赖等,源码与运行产物齐全,便于对照阅读与二次修改。目前已有268人学习下载,具备一定的参考基础。项目围绕管理员管理、宿舍管理、学生信息管理、学生宿舍分配等模块展开,配套图表与文档说明,可帮助读者理解Swing界面布局、JDBC连接MySQL、增删改查逻辑与表结构设计,快速搭建本地环境并梳理功能实现思路,适合作为课程设计模板与学习脚手架。

1. 从一份“能跑起来”的课设说起:MySQL+Java Swing 宿舍管理系统到底能干什么

每年到了课程设计季,后台总有人问同一个问题:有没有那种下载下来、改个数据库密码就能跑、还能顺手交文档的 Java 课设?这份基于 MySQL + Java + Swing 的学生宿舍管理系统就是冲着这个场景来的。它不是那种只贴了几段增删改查的“半成品”,而是把宿舍楼、房间、学生入住、床位分配、报修记录、管理员权限这些真实业务串成了一条完整链路,前端用 Swing 做桌面端界面,后端用 JDBC 直连 MySQL,配套还给了数据库图表和设计文档。适合两类人:一类是刚学完 Java 和数据库、需要一份结构完整参考的新手;另一类是想拿它当骨架,改成自己学校业务逻辑的熟手。下面我按“它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改”的顺序拆一遍,尽量让你少走弯路。

2. 技术栈与目录结构:Swing 桌面端 + JDBC 直连 MySQL 的分层套路

2.1 为什么是 Swing 而不是 Web

课程设计这个场景有个很现实的约束:交付周期短、部署环境简单、演示时不能依赖浏览器和服务器。Swing 是 JDK 自带的 GUI 工具包,不需要额外装前端框架,双击 jar 或者直接在 IDE 里 Run 就能出界面,这对答辩演示非常友好。代价是界面风格偏老、布局要靠手写GridBagLayout或BorderLayout调,但换来的是零前端依赖。

后端没有用 Spring 那一套,而是 JDBC 直连。原因也简单:课设的评分点通常在“数据库设计是否合理、CRUD 是否完整、事务有没有处理”,而不是“有没有用微服务”。直连的好处是链路短、调试直观,一条 SQL 出错能立刻定位到具体 DAO 方法;坏处是连接管理要自己写,后面避坑章节会专门讲。

常见做法是把项目分成四层,这份源码基本也是这个结构:

层次包名示例职责
实体层entityStudent、Room、DormBuilding、RepairRecord 等 POJO
数据访问层dao每个实体一个 DAO,封装增删改查 SQL
业务/工具层service/util床位分配逻辑、数据库连接工具类
界面层ui/view登录窗、主窗体、各管理面板

2.2 目录结构长什么样

一份典型的工程目录大致如下(不同人整理会有出入,但核心包不会变):

DormitorySystem/ ├── src/ │ ├── entity/ │ │ ├── Student.java │ │ ├── Room.java │ │ └── RepairRecord.java │ ├── dao/ │ │ ├── StudentDao.java │ │ ├── RoomDao.java │ │ └── RepairDao.java │ ├── service/ │ │ └── BedAllocationService.java │ ├── util/ │ │ └── DBUtil.java │ └── ui/ │ ├── LoginFrame.java │ ├── MainFrame.java │ └── panel/ │ ├── StudentPanel.java │ └── RoomPanel.java ├── sql/ │ └── dormitory.sql ├── lib/ │ └── mysql-connector-java.jar └── docs/ ├── 数据库设计说明.docx └── ER图.png

这个结构的好处是职责清晰:改界面只动ui,改 SQL 只动dao,业务规则集中在service。新手拿到手先看sql/dormitory.sql建库,再看util/DBUtil.java改连接参数,最后从LoginFrame的main方法启动,路径就通了。

2.3 数据库表设计的关键几张表

宿舍系统的核心是“人—房—床”的关系。常见设计是学生表、房间表、楼栋表,再加一张入住记录表来解耦“学生当前住哪”这个动态状态。下面是我按这个项目场景整理的建表片段,字段命名和类型是这类课设里最稳妥的写法:

-- 楼栋表 CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, -- 楼栋名,如 1 号楼 gender CHAR(1) NOT NULL -- M 男 / F 女,用于分配校验 ); -- 房间表 CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(10) NOT NULL, -- 房间号 capacity INT DEFAULT 4, -- 床位上限 occupied INT DEFAULT 0, -- 已住人数,冗余字段便于快速判断满员 FOREIGN KEY (building_id) REFERENCES building(id) ); -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) UNIQUE NOT NULL, -- 学号 name VARCHAR(30) NOT NULL, gender CHAR(1) NOT NULL, room_id INT, -- 当前房间,NULL 表示未分配 FOREIGN KEY (room_id) REFERENCES room(id) );

room.occupied这个冗余字段是血泪经验:如果每次判断满员都去COUNT(*)学生表,列表页一刷新就是几十条子查询,界面会明显卡顿。用冗余字段加事务内同步更新,读多写少的场景下体验好很多。代价是分配和退宿时必须保证occupied和学生表在同一个事务里改,否则会出现“显示满员但实际有空床”的玄学问题。

3. 跑起来:从建库到登录界面的完整启动链路

3.1 环境准备与依赖

跑这个项目需要三样东西:JDK(8 或 11 都行,Swing 对版本不挑)、MySQL(5.7 或 8.0)、一个 IDE(IDEA 或 Eclipse)。JDBC 驱动是必须的,lib目录里通常已经放了mysql-connector-java.jar,如果没有就自己下一个对应 MySQL 版本的驱动,注意 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,5.x 是com.mysql.jdbc.Driver,写错了启动就报ClassNotFoundException。

在 IDEA 里的操作顺序是:File → Project Structure → Modules → Dependencies,把 jar 加进去并勾选 Export;Eclipse 则是右键项目Build Path → Add External JARs。这一步漏了,编译能过但运行时报No suitable driver,是新手最常见的翻车点之一。

3.2 导入 SQL 并核对字符集

打开命令行或 Navicat,先建库再导表:

mysql -u root -p -e "CREATE DATABASE dormitory DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p dormitory < sql/dormitory.sql

字符集一定要用utf8mb4,不要用utf8。学生姓名里偶尔有生僻字,utf8三字节存不下,插入时会报Incorrect string value,而且这个错误在 Swing 里往往只弹一个笼统的“保存失败”,排查起来很费时间。建库时定好字符集,后面省一堆事。

3.3 改连接配置并启动

连接参数集中在DBUtil里,这是启动前唯一必须改的地方:

public class DBUtil { // 8.0 驱动用 cj,5.x 去掉 cj private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // useSSL=false 避免本地连接告警,serverTimezone 必须显式指定 private static final String URL = "jdbc:mysql://localhost:3306/dormitory?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws SQLException { try { Class.forName(DRIVER); // 加载驱动,只需一次但重复调用无害 } catch (ClassNotFoundException e) { throw new SQLException("驱动未找到,检查 jar 是否加入 classpath", e); } return DriverManager.getConnection(URL, USER, PASSWORD); } }

serverTimezone不写的话,MySQL 8.0 驱动会抛The server time zone value is unrecognized,这是版本升级后最典型的报错。characterEncoding=utf8配合库的utf8mb4,能覆盖绝大多数中文场景。

改完配置,找到LoginFrame里的main方法直接运行。默认管理员账号一般在 SQL 脚本的admin表里有初始数据,常见是admin/123456,登录后进主窗体,左侧或顶部是功能菜单,点“学生管理”“房间管理”就能看到表格数据。如果登录后表格空白但数据库里有数据,先查 DAO 里的查询 SQL 有没有写死条件,再查JTable的TableModel有没有正确fireTableDataChanged。

3.4 打包成可执行 jar

演示时不想开 IDE,可以打成 jar。IDEA 里Project Structure → Artifacts → JAR → From modules with dependencies,主类选LoginFrame,构建后在out/artifacts下拿到 jar。命令行运行:

java -jar DormitorySystem.jar

注意依赖 jar 要么一起打进去(选 extract to the target JAR),要么放同目录用-cp指定。只打主类不打驱动,双击就是一闪而过,控制台都看不到错误——这种“黑匣子”式的失败最劝退,建议第一次打包时在命令行跑,把异常打出来。

4. 核心模块拆解:DAO 层、床位分配事务与 Swing 表格联动

4.1 DAO 层的增删改查怎么写才不啰嗦

DAO 层最容易写成一大坨重复代码。合格的写法是把“取连接、建语句、设参数、关资源”抽出来,每个方法只关心 SQL 和参数映射。以学生查询为例:

public class StudentDao { // 按姓名模糊查询,返回列表供 JTable 渲染 public List<Student> searchByName(String keyword) { List<Student> list = new ArrayList<>(); String sql = "SELECT s.id, s.sno, s.name, s.gender, r.room_no " + "FROM student s LEFT JOIN room r ON s.room_id = r.id " + "WHERE s.name LIKE ?"; // try-with-resources 自动关闭 Connection/PreparedStatement/ResultSet try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); // 参数化,防注入 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student stu = new Student(); stu.setId(rs.getInt("id")); stu.setSno(rs.getString("sno")); stu.setName(rs.getString("name")); stu.setGender(rs.getString("gender")); stu.setRoomNo(rs.getString("room_no")); // 关联查出的房间号 list.add(stu); } } } catch (SQLException e) { e.printStackTrace(); // 实际项目应转成业务异常向上抛 } return list; } }

几个要点:LEFT JOIN保证未分配房间的学生也能查出来,room_no为 null 时界面显示“未分配”;PreparedStatement的?占位符不只是防注入,还能让 MySQL 复用执行计划;try-with-resources是 JDBC 资源管理的标准姿势,比手写finally里一堆close干净得多。参数说明上,keyword传空字符串会匹配全部,界面上的“查询”按钮不填条件时正好走这个逻辑。

4.2 床位分配:一个必须用事务兜住的业务

分配床位是这个系统里唯一有“业务规则”的地方:房间不能超员、男女生不能混楼、学生不能重复分配。这三条任何一条被并发或异常打断,数据就脏了。核心逻辑放在BedAllocationService:

public boolean allocate(int studentId, int roomId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 查房间容量与已住人数,FOR UPDATE 锁行防并发超员 String checkSql = "SELECT capacity, occupied FROM room WHERE id = ? FOR UPDATE"; int capacity, occupied; try (PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setInt(1, roomId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) return false; capacity = rs.getInt("capacity"); occupied = rs.getInt("occupied"); } } if (occupied >= capacity) { conn.rollback(); return false; // 满员,回滚 } // 2. 更新学生房间 try (PreparedStatement ps = conn.prepareStatement( "UPDATE student SET room_id = ? WHERE id = ?")) { ps.setInt(1, roomId); ps.setInt(2, studentId); ps.executeUpdate(); } // 3. 房间已住人数 +1 try (PreparedStatement ps = conn.prepareStatement( "UPDATE room SET occupied = occupied + 1 WHERE id = ?")) { ps.setInt(1, roomId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }

FOR UPDATE是关键:两个管理员同时给最后一个床位分配学生时,没有行锁就会双双通过容量检查,最后occupied变成 5 而capacity是 4。加上行锁后第二个事务会等第一个提交再读,读到满员直接回滚。setAutoCommit(false)和commit/rollback的配对不能漏,finally里恢复自动提交是为了连接池复用时状态干净——虽然这个项目用的是直连,但养成习惯没坏处。

4.3 Swing 表格与 DAO 的联动

界面层最典型的是“查询 → 填充表格 → 选中行 → 操作”这条链路。以学生面板为例:

// 查询按钮事件 searchBtn.addActionListener(e -> { String kw = keywordField.getText().trim(); List<Student> data = studentDao.searchByName(kw); // DefaultTableModel 是 JTable 的数据模型,先清空再逐行加 DefaultTableModel model = (DefaultTableModel) table.getModel(); model.setRowCount(0); for (Student s : data) { model.addRow(new Object[]{ s.getId(), s.getSno(), s.getName(), "M".equals(s.getGender()) ? "男" : "女", s.getRoomNo() == null ? "未分配" : s.getRoomNo() }); } });

setRowCount(0)清空旧数据这一步经常被忘,结果每查一次表格就多一批重复行,看着像“数据翻倍”的玄学 bug。DefaultTableModel默认所有单元格可编辑,如果不想让用户直接改表格,重写isCellEditable返回false即可。选中行取id用table.getSelectedRow()拿视图索引,再convertRowIndexToModel转模型索引——排序后这两个索引会不一致,直接混用会取错行,这是 Swing 表格的经典坑。

5. 避坑与排查:启动失败、中文乱码、事务不生效的现场记录

5.1 启动报 No suitable driver 或 ClassNotFound

现象是程序一运行就抛异常,登录窗都出不来。原因通常是驱动 jar 没进 classpath,或者驱动类名和 MySQL 版本对不上。解决:确认lib下的 jar 已在 IDE 依赖里且勾选导出;MySQL 8.0 用com.mysql.cj.jdbc.Driver,5.x 用com.mysql.jdbc.Driver;打包成 jar 时确认驱动被一起打进去,用java -jar在命令行跑一遍看完整堆栈。

5.2 中文姓名存进去变问号

现象是界面输入正常,存库后查出来是???。原因多半是建库时用了utf8或默认latin1,或者连接串没带characterEncoding。解决:库和表统一utf8mb4,连接 URL 加characterEncoding=utf8,DBUtil里不要用DriverManager之外的方式绕开编码设置。已经存坏的数据改不回来,只能重导。

5.3 分配床位后人数对不上

现象是房间显示满员但学生表里没几个人,或者反过来。原因是student.room_id和room.occupied没在同一个事务里更新,中途异常导致只改了一边。解决:按 4.2 的写法把两步包进事务,异常统一rollback;再写一条对账 SQL 定期核对:

-- 找出 occupied 与实际入住人数不一致的房间 SELECT r.id, r.occupied, COUNT(s.id) AS real_cnt FROM room r LEFT JOIN student s ON s.room_id = r.id GROUP BY r.id, r.occupied HAVING r.occupied <> COUNT(s.id);

5.4 表格查询结果重复翻倍

现象是每点一次查询,表格行数就多一倍。原因是没在填充前清空TableModel。解决:model.setRowCount(0)放在循环前;如果用了自定义 Model,确认没有在addRow之外重复fireTableDataChanged触发重绘。

5.5 打包后双击没反应

现象是 IDE 里能跑,双击 jar 一闪而过。原因是异常被吞了或者主类清单没配对。解决:命令行java -jar跑,看控制台报错;检查MANIFEST.MF里的Main-Class是否指向带main的类;依赖没打进去时改用-cp指定。这类问题没有捷径,把错误打出来就解决一半了。

6. 二次开发:把课设改成能讲出业务价值的版本

拿到这份源码,最忌讳的是只改个标题就交。答辩时老师一问“你的分配逻辑怎么防超员”“退宿后床位怎么释放”,答不上来就露馅。我的建议是挑两三个点做深,而不是铺开改界面。

第一个改造点是退宿流程。原版多半只有分配没有释放,补一个release(int studentId):事务内把student.room_id置 null、room.occupied减 1,并加一条CHECK (occupied >= 0)约束兜底。验证方法是连续分配再退宿十次,跑 5.3 的对账 SQL 确认始终一致。

第二个改造点是操作日志。新建operation_log表,字段id, operator, action, target_id, create_time,在 Service 层每次分配、退宿、删除后插一条。这一步能让你的项目从“能跑”变成“有审计意识”,答辩时是个加分项,代码量却不大:

// 在 allocate 成功 commit 之后调用 private void log(Connection conn, String operator, String action, int targetId) throws SQLException { String sql = "INSERT INTO operation_log(operator, action, target_id, create_time) VALUES(?,?,?,NOW())"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, operator); ps.setString(2, action); ps.setInt(3, targetId); ps.executeUpdate(); } }

注意日志插入要和业务在同一个事务里,否则业务回滚了日志还在,审计就失真了。

第三个改造点是查询性能。学生表数据量上来后,按姓名LIKE '%kw%'走不了索引,列表页会变慢。可以给sno加唯一索引、给room_id加普通索引,姓名查询改成前缀匹配kw%或者加个拼音首字母字段。改完用EXPLAIN看执行计划,确认type从ALL变成range或ref,这个对比截图放进文档里很有说服力。

最后说个习惯。我早期做这类课设,改完代码直接交,结果答辩现场数据库连不上、演示机没装 MySQL,当场翻车。从那以后我每次交付前都强制走一遍“干净环境复现”:新建一个空库、只按文档步骤导入、从零启动,确认每一步都能复现才敢交。你也可以在docs里补一份“从零启动检查清单”,把建库、改配置、启动、登录、分配、退宿六个动作列成勾选项,自己跑一遍再交给别人。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进