MySQL+Java+Swing学生宿舍管理系统课程设计全解析

简介:面向高校数据库与Java课程设计场景,这套基于MySQL+Java+Swing开发的学生宿舍管理系统提供了完整的可运行工程与配套文档。资源以学生信息管理、宿舍分配管理、管理员后台等核心模块为主线,包含数据库表设计、界面交互与业务逻辑分层实现,适合需要完成课程设计、实训作业或毕设参考的计算机相关专业学生直接借鉴和二次开发。压缩包共334个文件,以47个Java源码与202个class编译文件为主体,另有22张png图表演示运行界面与数据关系、XML配置文件和工程构建文件等,整体体积112.72MB,目录结构完整清晰,便于按模块查找。已有268人浏览学习,可作为理解Java Swing界面开发、MySQL数据操作及分层设计的入门案例。

1. 这个课程设计在做什么:Mysql+Java+Swing组合为什么值得照着做

期末课程设计拿到“学生宿舍管理系统”这种题目,十个组里八个都选了差不多的路子,但真正能跑通、能答辩、能交出一整套源码加图表加文档的却不多。这个标题把技术栈写得很明确:Mysql负责持久化,Java+Swing负责桌面端界面,再加源码、图表、文档三件套。它解决的不只是“做一个能查宿舍的系统”,而是让你在有限时间内完成一个业务闭环——学生入住、退宿、换寝、报修、统计报表,每一条数据都能落到数据库里,每一个界面都有对应的SQL操作。适合谁?适合正在做课程设计或需要快速交付一个桌面管理系统的同学,也适合想把Java SE、JDBC、Swing、SQL这几块知识串成一体的人。别嫌Swing老,课程设计恰恰看重复现成本低、答辩好讲、图表好出。

2. 数据库设计:把宿舍业务拆成六张表,ER图与DDL一次到位

2.1 实体与关系梳理:先画清楚谁跟谁是一对多

宿舍管理系统的核心业务是“人”和“房间”的关系。动手写SQL之前,先把实体列出来:学生、宿舍、楼栋、入住记录、报修记录、管理员。它们之间的关系是典型的父子结构——一栋楼有多间宿舍,一间宿舍容量固定(常见4人间、6人间),一个学生一次只能有一条有效入住记录,一间宿舍可以有多条报修记录。画ER图时把这些关系标成一对多,外键就落在了“多”的那一侧:学生表的dorm_id、入住记录表的student_no和dorm_id、报修记录表的dorm_id。

这里有个常见的建模分歧:要不要把“楼栋”和“宿舍”拆成两张表。常见做法是拆,因为宿舍编号通常带楼栋信息,比如“A栋201”,如果不拆表,楼栋的层数、每层房间数这些属性就没地方放。我一般会把楼栋表做得极简,只有id、名称、楼层数、每层房间数。宿舍表通过building_id关联楼栋,宿舍自己的属性是房间号、容量、当前已住人数。注意“当前已住人数”这个字段是冗余的,它可以通过入住记录实时统计出来,但课程设计阶段保留它能让查询界面少写一条聚合SQL,代价是入住和退宿时要做两步更新,这个取舍要心里有数。

字段命名上,建议主键统一用id,外键用xxx_id,字符串统一用varchar,与业务无关的纯数字状态位用tinyint。不要用中文做字段名,不要用status这类单词做列名时不加注释——给每个字段加上COMMENT,后续写文档和答辩都会轻松很多。Mysql 8.0默认字符集是utf8mb4,如果建库时图省事用默认配置,后面界面里插入emoji或者生僻字会报错,建库语句显式指定utf8mb4是最省心的做法。

2.2 单机跑通的最小数据集:六张表的完整DDL与说明

下面这套DDL是能直接落到Mysql 8.0里执行的,覆盖了楼栋、宿舍、学生、入住记录、报修记录、管理员六张表。为了课程设计演示方便,我在每张表里都保留了冗余字段或状态字段,比如宿舍表的current_count、入住记录的status,它们让界面展示和统计查询都更直观。

CREATE DATABASE IF NOT EXISTS dormitory_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dormitory_db; -- 楼栋表 CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '楼栋编号', name VARCHAR(50) NOT NULL COMMENT '楼栋名称,如A栋', floors INT NOT NULL DEFAULT 6 COMMENT '楼层数', rooms_per_floor INT NOT NULL DEFAULT 8 COMMENT '每层房间数' ) COMMENT '宿舍楼栋表'; -- 宿舍表 CREATE TABLE dormitory ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '宿舍编号', building_id INT NOT NULL COMMENT '所属楼栋id', room_no VARCHAR(20) NOT NULL COMMENT '房间号,如A201', capacity INT NOT NULL DEFAULT 4 COMMENT '可住人数', current_count INT NOT NULL DEFAULT 0 COMMENT '当前已住人数', UNIQUE KEY uk_room (building_id, room_no), CONSTRAINT fk_dorm_building FOREIGN KEY (building_id) REFERENCES building(id) ) COMMENT '宿舍房间表'; -- 学生表 CREATE TABLE student ( student_no VARCHAR(20) PRIMARY KEY COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT NOT NULL DEFAULT 1 COMMENT '性别,1男2女', phone VARCHAR(11) COMMENT '联系电话', dorm_id INT COMMENT '当前所在宿舍id', major VARCHAR(50) COMMENT '专业', enroll_year YEAR COMMENT '入学年份', CONSTRAINT fk_student_dorm FOREIGN KEY (dorm_id) REFERENCES dormitory(id) ) COMMENT '学生表'; -- 入住记录表 CREATE TABLE checkin ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '入住记录id', student_no VARCHAR(20) NOT NULL COMMENT '学号', dorm_id INT NOT NULL COMMENT '宿舍id', checkin_date DATE NOT NULL COMMENT '入住日期', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态,1当前有效,0已退宿', CONSTRAINT fk_checkin_student FOREIGN KEY (student_no) REFERENCES student(student_no), CONSTRAINT fk_checkin_dorm FOREIGN KEY (dorm_id) REFERENCES dormitory(id) ) COMMENT '入住记录表'; -- 报修记录表 CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '报修单号', dorm_id INT NOT NULL COMMENT '宿舍id', description VARCHAR(255) NOT NULL COMMENT '报修内容', report_date DATE NOT NULL COMMENT '报修日期', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态,0待处理,1已处理', CONSTRAINT fk_repair_dorm FOREIGN KEY (dorm_id) REFERENCES dormitory(id) ) COMMENT '报修记录表'; -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员id', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT '登录密码', real_name VARCHAR(50) COMMENT '真实姓名' ) COMMENT '管理员表';

DDL的逻辑说明:student表的主键直接用学号,这是真实业务里最常见的做法,避免了在系统内部再造一套学生id。dorm_id作为外键放在学生表里,表示“当前住在哪”,但要注意它和checkin表存在数据一致性——某条checkin记录状态还是1时,student.dorm_id必须指向同一个宿舍。foreign key的作用是保证引用的完整性,Mysql在InnoDB引擎下才生效,如果你是MyISAM引擎,外键约束会被静默忽略,这是一个隐蔽坑,后面的避坑章节会细说。

参数调整建议:宿舍容量按学校实际情况改,默认4人间,如果某栋楼有6人间,直接在capacity列上改值即可。status字段我建议默认值给1(有效),这样新插入的入住记录天然是“当前有效”的,不需要上层代码单独赋值。假如你的课程设计要求支持“学生中途换宿舍”,checkin表里就会出现同一个学生多条status=1的记录,这时候更严谨的模型应当增加一个有效时间区间,但课程设计阶段用status标记就够了,文档里说明一下这个设计取舍,答辩时反而加分。

2.3 图表交付:ER图、用例图与流程图的生成方式

标题里写了“图表”,课程设计报告通常要求三张图:ER图、用例图、系统流程图。ER图可以直接用Mysql Workbench连接数据库后选择Reverse Engineer生成,也可以把上面的DDL粘贴到draw.io里手动连线。更推荐的是先建库建表,再用Workbench逆向,因为导出的图是“真实存在的表结构”,和你交的报告完全对得上,评委如果现场打开数据库对照也不会有出入。

用例图描述的是角色和功能的边界:管理员可以登录、管理学生信息、分配宿舍、登记退宿、查看统计报表;学生(如果做成两个角色)可以查询自己的宿舍信息和报修进度。课程设计阶段通常会简化角色,只保留管理员一个角色,把学生相关的操作都收进管理员的权限范围内。流程图则画登录验证、入住办理、退宿办理三条主链路,每一条3到5个节点就够了。

如果不想装重工具,有一个更快的做法:在draw.io里按表结构把六张表画成矩形,主键加钥匙图标,外键连线并标注1:N,十分钟就能导出一张能放进文档的ER图。图标和配色统一用黑白灰,打印出来也清晰。记住图表的作用是让评委快速理解你的设计,不是展示绘图技巧,越朴素越好。

3. 搭出可运行的Swing工程:分层、登录与数据库连接

3.1 目录结构与包命名:DAO层隔离SQL,UI层只管画界面

Java+Swing课程设计最常见的翻车点不是功能写不出来,而是所有代码堆在一个类里,最后文档没法拆,答辩也讲不清。推荐的分层结构是五个包:entity放实体类,dao放数据库访问,service放业务逻辑,ui放窗口界面,util放工具类。实体类的字段和数据库表一一对应,比如Student类有studentNo、name、gender、phone、dormId这些属性;DAO层只负责“执行SQL并返回结果”;UI层只创建组件和绑定事件,不直接写SQL。

这样一个分层的直接收益是:界面改动不影响数据层,换数据库连接方式也不用重写窗口。后面每做一个功能,遵循“实体类写属性 -> DAO写SQL -> Service写业务规则 -> UI调用Service”的顺序,思路会非常顺。我见过同学把SQL字符串直接写在按钮事件里,功能是能跑的,但一旦要做报表统计,同样的条件查询就要复制粘贴三遍,维护成本立刻上来。

以下是一个项目骨架,用Maven或纯javac都能编译,但建议直接用你顺手的IDE创建普通Java项目,手动建这五个包:

src/ ├── main/java/com/example/dorm/ │ ├── entity/ Student.java Dormitory.java Checkin.java Repair.java Admin.java │ ├── dao/ StudentDao.java DormitoryDao.java CheckinDao.java RepairDao.java AdminDao.java │ ├── service/ StudentService.java DormitoryService.java CheckinService.java │ ├── ui/ LoginFrame.java MainFrame.java CheckinPanel.java ReportPanel.java │ └── util/ DBUtil.java

这里不纠结包名的企业规范,课程设计用com.xxx.dorm即可,但包名要大于等于三层,某些学校的检查工具会看这个。Service层的意义可以这样理解:DAO是“对单张表的操作”,Service是“跨表的业务规则”,比如办理入住时要同时往checkin表插记录、更新student表的dorm_id、更新dormitory表的current_count,三个DAO调用组合起来才是完整的“入住业务”,这层逻辑放在UI里会让窗口类膨胀到几百行,放在Service里则清晰可控。

3.2 数据库连接工具类:JDBC参数含义与连接池取舍

数据库连接部分,课程设计不一定要上连接池,用最简单的DriverManager也能跑,但前提是封装好。下面是一个可以直接用的DBUtil,我习惯把连接参数做成常量放在类顶部,方便换库或改密码时只改一处。

package com.example.dorm.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 注意characterEncoding和serverTimezone这两个参数不能省 private static final String URL = "jdbc:mysql://localhost:3306/dormitory_db" + "?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("加载JDBC驱动失败,请检查mysql-connector-java依赖", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

代码里最关键的是URL参数。useSSL=false是避免Mysql 8.0对SSL的默认校验在本地连接时报warning和握手错误;characterEncoding=utf8保证中文字段从数据库到Swing界面不乱码;serverTimezone=Asia/Shanghai解决Mysql 8.0和JDBC驱动之间关于时区的报错。如果你用的是Mysql 5.7,驱动仍然用com.mysql.jdbc.Driver(不带cj),URL也基本兼容,但建议统一用8.0驱动和8.0数据库,省得排查版本差异。

静态代码块里的Class.forName在现代JDBC驱动里其实可以省略,驱动会自动注册,但保留它能让代码更直观地“显式加载驱动”,课程设计文档里也好解释。关闭资源时用倒序关闭,ResultSet最先关,Connection最后关,这个习惯能避免连接泄漏。如果你的机器上MySQL端口不是默认的3306,把URL里的端口改掉即可。

连接池方面,如果追求工程上的严谨,可以引入HikariCP并把DBUtil改造成从连接池取连接,课程设计做这个属于“超出要求的加分项”。但需要注意HikariCP需要额外引入slf4j日志依赖,有同学在集成时被一堆no provider的报错卡住,所以我建议先用纯JDBC把系统跑通,文档里写一句“生产环境建议引入连接池”就足够,不要为了炫技给自己挖坑。

3.3 登录界面落地:组件布局、密码框与事件监听

登录界面是系统的门面,也是第一个要演示的界面,它的实现套路是固定的:一个JFrame放用户名输入框、密码输入框、登录按钮,点击按钮后从数据库的admin表校验账号密码。下面这段代码展示了一个完整可运行的登录窗口,重点不是组件本身,而是三个容易忽略的细节:密码框用JPasswordField、回车键触发登录、登录成功后的跳转。

package com.example.dorm.ui; import com.example.dorm.util.DBUtil; import javax.swing.*; import java.awt.*; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle("学生宿舍管理系统 - 登录"); setSize(380, 240); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(8, 8, 8, 8); // 组件间距 gbc.gridx = 0; gbc.gridy = 0; panel.add(new JLabel("账号:"), gbc); gbc.gridx = 1; gbc.gridy = 0; usernameField = new JTextField(15); panel.add(usernameField, gbc); gbc.gridx = 0; gbc.gridy = 1; panel.add(new JLabel("密码:"), gbc); gbc.gridx = 1; gbc.gridy = 1; passwordField = new JPasswordField(15); panel.add(passwordField, gbc); JButton loginBtn = new JButton("登录"); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; panel.add(loginBtn, gbc); loginBtn.addActionListener(e -> doLogin()); // 支持在密码框内直接按回车登录 passwordField.addActionListener(e -> doLogin()); setContentPane(panel); setVisible(true); } private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "账号和密码不能为空"); return; } String sql = "SELECT id FROM admin WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 登录成功,关闭登录窗口并打开主界面 dispose(); new MainFrame(username).setVisible(true); } else { JOptionPane.showMessageDialog(this, "账号或密码错误"); } } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "数据库连接失败,请检查数据库服务"); } } public static void main(String[] args) { SwingUtilities.invokeLater(LoginFrame::new); } }

登录逻辑说明:doLogin方法里先用new String(passwordField.getPassword())取出密码,这里不用getText()是安全考虑,虽然课程设计不真的在乎安全,但写成这样在文档里能多一句解释。PreparedStatement用?占位符,避免拼接SQL字符串,这既是防SQL注入的规范,也是处理字符串引号的一层保障。查询用的是精确匹配,admin表里预先插入一条账号密码即可,比如root/123456,建表后手工INSERT进去就好。

参数和习惯说明:GridBagLayout虽然写起来繁琐,但它是Swing里最能控制对齐的布局管理器,比绝对定位( setLayout(null) )要靠谱得多——绝对定位在窗口大小稍微变化时组件会重叠错乱,网格布局不会。SwingUtilities.invokeLater把窗口创建放到事件分发线程(EDT),这是Swing的单线程规则的起点,如果直接在main线程里new窗口,虽然多数情况下看起来正常,但遇到并发刷新时会出诡异的界面问题。main方法保留在登录窗口里方便本地测试,真正交付时用一个单独的主入口类来启动。

4. 核心功能逐个实现:入住、退宿、查询与统计报表

4.1 入住登记:插入记录的同时维护宿舍占用状态

入住登记是系统的核心操作,它的业务规则比表面看起来多一层:不能只往checkin表插入一条记录,还要检查宿舍有没有床位、更新student表的dorm_id、更新dormitory表的current_count。这三个动作必须偶合在一次事务里,否则会出现“入住记录插进去了,但宿舍人数没变”的数据不一致。

下面这段代码展示的是事务版本的入住操作,放在CheckinService里。事务的关键是在同一个Connection上手动关闭自动提交,最后统一commit。

package com.example.dorm.service; import com.example.dorm.util.DBUtil; import java.sql.*; public class CheckinService { public boolean checkIn(String studentNo, int dormId, Date checkinDate) { // 第一步:检查宿舍是否还有空位 int capacity = 0; int currentCount = 0; String checkSql = "SELECT capacity, current_count FROM dormitory WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setInt(1, dormId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { capacity = rs.getInt("capacity"); currentCount = rs.getInt("current_count"); } else { return false; // 宿舍不存在 } } } catch (SQLException e) { e.printStackTrace(); return false; } if (currentCount >= capacity) { return false; // 宿舍已满 } // 第二步:同一个事务里完成多表更新 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String insertCheckin = "INSERT INTO checkin(student_no, dorm_id, checkin_date, status) VALUES(?,?,?,1)"; try (PreparedStatement ps = conn.prepareStatement(insertCheckin)) { ps.setString(1, studentNo); ps.setInt(2, dormId); ps.setDate(3, checkinDate); ps.executeUpdate(); } String updateStudent = "UPDATE student SET dorm_id=? WHERE student_no=?"; try (PreparedStatement ps = conn.prepareStatement(updateStudent)) { ps.setInt(1, dormId); ps.setString(2, studentNo); ps.executeUpdate(); } String updateDorm = "UPDATE dormitory SET current_count=current_count+1 WHERE id=?"; try (PreparedStatement ps = conn.prepareStatement(updateDorm)) { ps.setInt(1, dormId); ps.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(); } } } } }

这个实现里有三个值得记住的点。第一,操作顺序很重要:先查容量,再插入记录,再更新学生表,最后更新宿舍人数。如果把更新宿舍人数放在插入入住记录之前,万一插入失败,人数已经被加一,就会造成虚占床位。第二,事务边界要覆盖所有写操作,conn.setAutoCommit(false)之后的语句要么全部成功,要么全部回滚。finally里必须把连接归还给连接池或关闭,并恢复autoCommit状态,这是连接复用的基本素养。第三,current_count的更新用的current_count=current_count+1而不是读取后再写回,这是为了防止并发下的覆盖,课程设计虽然是单机演示,但这样写能在文档里体现你对并发问题的意识。

4.2 退宿与换寝:先改入住记录,再改宿舍人数

退宿的逻辑是入住的逆操作,但顺序上有个容易踩的坑:先从checkin表把对应记录的状态改为0,再执行“宿舍人数减一”。如果你先减人数,再更新记录状态,中途程序抛异常的话,宿舍人数少了但学生仍然住在里面,数据就对不上了。退宿操作还需要一个前置判断:该学生当前必须在checkin表里有一条status=1的记录。

public boolean checkOut(String studentNo) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 查出当前有效入住记录对应的宿舍 String findSql = "SELECT dorm_id FROM checkin WHERE student_no=? AND status=1"; int dormId = -1; try (PreparedStatement ps = conn.prepareStatement(findSql)) { ps.setString(1, studentNo); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { dormId = rs.getInt("dorm_id"); } else { conn.rollback(); return false; // 没有有效入住记录 } } } // 置为退宿状态 String updateCheckin = "UPDATE checkin SET status=0 WHERE student_no=? AND status=1"; try (PreparedStatement ps = conn.prepareStatement(updateCheckin)) { ps.setString(1, studentNo); ps.executeUpdate(); } // 宿舍人数减一 String updateDorm = "UPDATE dormitory SET current_count=current_count-1 WHERE id=?"; try (PreparedStatement ps = conn.prepareStatement(updateDorm)) { ps.setInt(1, dormId); ps.executeUpdate(); } // 学生表清除宿舍关联 String clearStudent = "UPDATE student SET dorm_id=NULL WHERE student_no=?"; try (PreparedStatement ps = conn.prepareStatement(clearStudent)) { ps.setString(1, studentNo); ps.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(); } } } }

退宿流程里清空student.dorm_id这步很容易被遗漏。入住时更新了学生表,退宿时如果只改入住记录和宿舍人数,学生表里会残留一个指向旧宿舍的dorm_id,列表展示时就会出现“这个人明明退宿了,却还挂在某间宿舍名下”的假象。所以在退宿里补一句UPDATE student SET dorm_id=NULL很有必要。

换寝操作在课程设计里可以复用这两个函数:先checkOut旧宿舍,再checkIn新宿舍。虽然会生成两条入住记录,但status字段能区分出历史记录和当前记录。在界面上把一个“换寝”按钮做成两步调用的组合,而不是单独写一套逻辑,文档里解释起来也更简单。

4.3 查询与统计:JTable配合聚合SQL展示入住率

宿舍查询是使用频率最高的功能,界面上通常是一个JTable加一个搜索框,按楼栋或按房间号过滤。JTable的数据不是手动一行行添加的,而是通过TableModel来管理。下面是按楼栋查询宿舍并显示入住率的核心代码,包含SQL和Swing两部分。

package com.example.dorm.dao; import com.example.dorm.util.DBUtil; import com.example.dorm.entity.Dormitory; import java.sql.*; import java.util.ArrayList; import java.util.List; public class DormitoryDao { public List<Dormitory> findByBuilding(int buildingId) { List<Dormitory> list = new ArrayList<>(); String sql = "SELECT id, building_id, room_no, capacity, current_count " + "FROM dormitory WHERE building_id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, buildingId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Dormitory d = new Dormitory(); d.setId(rs.getInt("id")); d.setBuildingId(rs.getInt("building_id")); d.setRoomNo(rs.getString("room_no")); d.setCapacity(rs.getInt("capacity")); d.setCurrentCount(rs.getInt("current_count")); list.add(d); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public List<Dormitory> findAllWithStatus() { List<Dormitory> list = new ArrayList<>(); // 聚合查询:关联楼栋名称,并计算出空余床位 String sql = "SELECT d.id, d.room_no, d.capacity, d.current_count, " + "b.name AS building_name, (d.capacity - d.current_count) AS free_count " + "FROM dormitory d JOIN building b ON d.building_id = b.id"; try (Connection conn = DBUtil.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { Dormitory d = new Dormitory(); d.setId(rs.getInt("id")); d.setRoomNo(rs.getString("room_no")); d.setCapacity(rs.getInt("capacity")); d.setCurrentCount(rs.getInt("current_count")); // free_count 和 building_name 可以通过属性或者额外Map返回 d.setBuildingName(rs.getString("building_name")); d.setFreeCount(rs.getInt("free_count")); list.add(d); } } catch (SQLException e) { e.printStackTrace(); } return list; } }

UI层拿到List之后,用DefaultTableModel把数据灌进表格即可。注意JTable本身不推荐直接调用addRow之外的方法来频繁增删数据,推荐的做法是每次查询后重建String[][]和列名数组,再重新setModel。下面的代码展示如何在查询按钮的事件里刷新表格:

String[] columns = {"宿舍编号", "楼栋", "房间号", "容量", "已住人数", "空余床位"}; List<Dormitory> list = dormitoryDao.findAllWithStatus(); String[][] data = new String[list.size()][columns.length]; for (int i = 0; i < list.size(); i++) { Dormitory d = list.get(i); data[i][0] = String.valueOf(d.getId()); data[i][1] = d.getBuildingName(); data[i][2] = d.getRoomNo(); data[i][3] = String.valueOf(d.getCapacity()); data[i][4] = String.valueOf(d.getCurrentCount()); data[i][5] = String.valueOf(d.getFreeCount()); } table.setModel(new DefaultTableModel(data, columns));

这里有个使用习惯:不要在每次查询后table.repaint()。repaint只是重绘,不会重新读取数据模型。数据更新的核心是setModel,传入一个新的TableModel,JTable会自动刷新。如果只是局部数据变化,也可以直接操作现有TableModel再调用fireTableDataChanged(),但简单场景下重建模型最不容易出错。聚合SQL里用了JOIN把楼栋名称带到列表里,这就是为什么建表时要把楼栋独立出来的原因——否则房间号前面得拼楼栋名,数据冗余且不优雅。

4.4 图表面板与导出:JFreeChart画柱状图,结果另存CSV

“图表”是标题里明确写的交付物,运行时图表比文档里的静态图更能体现工作量。最常见的做法是用JFreeChart画一张宿舍入住率柱状图,再配一张楼栋床位占比饼图。JFreeChart的集成方式不复杂,核心是把DataSet准备好,ChartFactory创建图表对象,再用ChartPanel嵌入Swing面板。

package com.example.dorm.ui; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.PlotOrientation; import org.jfree.data.category.DefaultCategoryDataset; import javax.swing.*; import java.util.List; public class ReportPanel extends JPanel { public ReportPanel(List<Object[]> buildingStats) { setLayout(new BorderLayout()); // buildingStats 里每个元素是 {楼栋名称, 总床位, 已住人数} DefaultCategoryDataset dataset = new DefaultCategoryDataset(); for (Object[] row : buildingStats) { String buildingName = (String) row[0]; int total = (int) row[1]; int used = (int) row[2]; dataset.addValue(used, "已住人数", buildingName); dataset.addValue(total - used, "空余床位", buildingName); } JFreeChart chart = ChartFactory.createBarChart( "各楼栋入住情况", // 图表标题 "楼栋", // X轴标签 "人数", // Y轴标签 dataset, // 数据 PlotOrientation.VERTICAL, true, // 显示图例 true, // 生成tooltip false // 不生成URL ); ChartPanel chartPanel = new ChartPanel(chart); chartPanel.setPreferredSize(new java.awt.Dimension(700, 400)); add(chartPanel, BorderLayout.CENTER); } }

JFreeChart依赖两个jar包:jfreechart和jcommon。IDE项目里把jar加到classpath即可,Maven项目在pom里加对应依赖。需要注意版本匹配,旧教程里常用的JFreeChart 1.0.x和Java 17以上偶有兼容问题,建议直接取最新的1.5.x版本,构建方式差异不大。如果完全不想引第三方库,可以退而求其次在JPanel里用Graphics2D手绘柱状图,但显示效果和答辩观感会差很多,不建议省这一步。

图表之外,统计结果导出成CSV或者Excel是课程设计的加分动作。用纯Java写CSV最简单,逗号分隔加换行,Excel打开时选逗号分隔即可。下面这行代码就是这个思路的核心,把查询结果拼成CSV字符串再写文件:

public void exportToCsv(List<Dormitory> list, String filePath) { try (java.io.PrintWriter writer = new java.io.PrintWriter(filePath, "UTF-8")) { writer.println("房间号,容量,已住人数,空余床位"); for (Dormitory d : list) { writer.println(d.getRoomNo() + "," + d.getCapacity() + "," + d.getCurrentCount() + "," + d.getFreeCount()); } } catch (Exception e) { e.printStackTrace(); } }

导出Excel的正规做法是用Apache POI,但对于课程设计,CSV方案更轻量且不会引入大量依赖,放在文档里说清楚“用Excel打开CSV查看报表”即可。导出功能的价值在于:它能证明你的统计结果不只是停留在内存里的临时数据,而是可以落盘交付的报表文件,这一点在答辩时比单纯的多画一张图更有说服力。

5. 避坑指南:Swing线程、中文乱码与MySQL时区的五个高频问题

5.1 界面卡死:耗时操作全部堆在事件线程

现象:点击查询按钮后,整个窗口像是被冻结了,拖不动也点不了其他按钮,几秒甚至十几秒后才恢复。

原因:查询、报表、导出这类耗时操作直接写在ActionListener里,全部运行在Swing的事件分发线程EDT上。EDT被阻塞时,界面绘制事件排不上队,窗口就表现为“假死”。

解决:把耗时操作放到SwingWorker后台线程里执行,完成后通过done方法回到EDT更新界面。下面是一个最小改造示例:

loginBtn.addActionListener(e -> { loginBtn.setEnabled(false); // 防止重复点击 new SwingWorker<Boolean, Void>() { @Override protected Boolean doInBackground() { return doLoginInner(); // 包含数据库操作 } @Override protected void done() { loginBtn.setEnabled(true); try { if (get()) { dispose(); new MainFrame().setVisible(true); } } catch (Exception ex) { JOptionPane.showMessageDialog(LoginFrame.this, "登录失败:" + ex.getMessage()); } } }.execute(); });

注意不要在doInBackground里直接弹JOptionPane或者更新JTable,UI操作必须回到done或process里。这个坑之所以是血泪经验,是因为本地测试时数据量小看不出问题,等库里插入几百条数据再查询时才会卡,而那时已经快要答辩了。从一开始就养成“所有数据库操作放SwingWorker”的习惯,后面几乎不会遇到界面假死。

5.2 中文乱码:连接串漏了characterEncoding

现象:数据库里明明存的“张三”,Swing界面显示“å¼ ä¸‰”或者“???”。

原因:JDBC连接串没有指定字符编码,Mysql驱动在连接时用的默认字符集和界面、数据库的字符集不一致。数据库建库时如果用了utf8mb4而连接串没有characterEncoding=utf8,中文就搭错车了。

解决:在DBUtil的URL末尾补上characterEncoding=utf8,同时保证数据库表字符集是utf8mb4。还有一个容易被忽略的点:用ps.setString(1, name)传中文参数没问题,但如果你手动拼接SQL,比如"SELECT * FROM student WHERE name='" + name + "'",中文经IDE默认编码(通常是UTF-8)编译后,运行时报错可能指向“字符串不正确”。杜绝这个问题的办法是统一用PreparedStatement,任何用户输入都用?占位符。

5.3 数据库连接超时:先查驱动版本、时区与服务状态

现象:启动程序点击登录后弹出“数据库连接失败”,控制台抛出类似Communications link failure或The server time zone value is unrecognized的异常。

原因:常见的有三种。一是Mysql驱动和数据库版本不匹配,Mysql 8.0必须用com.mysql.cj.jdbc.Driver,5.7以前用com.mysql.jdbc.Driver,混用会ClassNotFoundException或连接协议异常。二是8.0以后Mysql默认时区和JDBC的UTC对不上,必须显式指定serverTimezone=Asia/Shanghai。三是你的Mysql服务根本没有启动,Windows下服务名叫Mysql80或类似,打开服务管理器确认状态。

解决:按三个方向依次排查。先打开命令行执行mysql -uroot -p确认数据库能连;再看驱动jar包是否在classpath里,在IDE里检查External Libraries;最后确认连接串完整。如果本机还跑着MariaDB之类服务占用3306端口,也要处理,换端口或停止冲突服务。

5.4 JTable点了查询不刷新:改TableModel而不是改表格

现象:第一次点查询有数据,但插入了新学生后再点查询,表格内容没变化,重启程序才看到新数据。

原因:界面上的JTable用的还是最初创建的DefaultTableModel,查询按钮只是更新了后端数据或者调用了repaint()。repaint只负责重绘像素,不重新执行查询,数据源根本没有更新。

解决:每次查询后都重新构造数据数组和列名数组,调用table.setModel(new DefaultTableModel(data, columns))。如果你需要在已有模型上追加数据,调用模型的方法后记得model.fireTableDataChanged()通知监听器。这两个方法选一个即可,不要两个都做。另外,查询前后注意把JTable放进JScrollPane里使用,否则表头显示异常会让人误以为刷新失败。

5.5 删除宿舍报外键错误:先处理入住记录和维修单

现象:在宿舍管理界面删除一间宿舍时,Mysql抛出类似Cannot delete or update a parent row: a foreign key constraint fails的报错。

原因:building和dormitory、dormitory和checkin之间建立了外键约束。只要该宿舍还有关联的入住记录或报修单,删除操作就会被数据库拒绝,这是InnoDB外键的默认行为,整行直接删会触发约束校验。

解决:删除宿舍前先处理关联数据。常见做法是改成“逻辑删除”,给dormitory表加一个status字段,删除时置为0,界面查询默认过滤status=1;如果必须物理删除,需要先DELETE掉该宿舍的所有checkin和repair记录。课程设计里直接物理删除的风险是误删入住历史,导致统计报表数据不准,所以推荐逻辑删除,并在文档里说明“考虑到入住记录是历史数据,系统采用逻辑删除保留痕迹”,这是一个可以主动说的设计决策。

6. 文档与答辩:把图表和源码讲成闭环,用验证清单收尾

6.1 文档组织结构与图表对应关系

课程设计文档通常按这个顺序写:需求分析、概念结构设计(ER图)、逻辑结构设计(表结构说明)、物理实现与界面展示、测试报告、总结。每一章都要能和源码对应上。ER图对应第二章的六张表;用例图对应登录、入住、退宿、查询、报修、统计这几个功能;系统流程图对应你在界面上点按钮的完整路径。写文档的一个省力技巧是:每写完一个功能模块,顺手截两张运行图放进对应的“功能实现”小节,标注窗口入口和操作步骤。答辩时评委翻文档,能看到图、看到表、看到运行截图,不一定看你的源码,但一定会问“你说的这个功能在哪里”。

6.2 答辩演示路径与一句式讲解

答辩演示不要跳着点,按业务顺序走:先登录,然后查学生信息,再办理一次入住,展示宿舍人数变化,再做一次退宿,展示床位恢复,最后打开统计报表导出文件。每一步配一句背景说明,比如“这是A栋宿舍的入住情况,通过JOIN查询把楼栋名称和空余床位一次带出来,JFreeChart根据聚合结果生成柱状图”。这个路径覆盖了登录、CRUD、一对一关联、聚合统计、图表、文件导出全部知识点,自然流畅。如果评委问“你遇到过什么问题”,不要答“没遇到过”,从第五章避坑记录里挑一个讲,比如中文字符集问题的排查过程,反而更能体现你踩过坑、会排查。

6.3 进阶验证:功能覆盖度检查清单

交稿前最后一步做覆盖度检查,把需求列表逐项和界面窗口、SQL语句、文档章节对应起来。比如“学生信息管理”对应StudentDao里的增删改查、主界面的学生管理Tab、文档第4.2节;“宿舍分配与退宿”对应CheckinService的两个方法和界面按钮。我有一个习惯:在文档末尾放一张覆盖度表格,列出功能点、界面位置、涉及DAO方法、是否通过测试四列,这一页虽然简单,但往往能让评委觉得你的系统不是“demo”,而是认真验证过的作品。我第一次写这类管理系统时,把表结构和界面代码同步推进,结果每改一次字段就要改DAO、改实体类、改表格列,来回折腾了两天,后来养成“先定表再定界面”的习惯,整个过程顺了很多。希望这篇笔记能帮你少走这段弯路,把课程设计做得省力又完整。

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

相关新闻

Wireshark抓包实战:解剖TCP三次握手与四次挥手的真实流量

Wireshark抓包实战:解剖TCP三次握手与四次挥手的真实流量

1. 为什么抓包看三次握手比背定义管用十倍我带过不少刚学网络的新人&#xff0c;发现一个特别普遍的现象&#xff1a;他们能把TCP三次握手的SYN、SYN-ACK、ACK背得滚瓜烂熟&#xff0c;也能默写出四次挥手的FIN、ACK、FIN、ACK顺序&#xff0c;但一遇到线上服务连接超时、接口偶…

2026/10/20 22:38:46 阅读更多 →
Docker常用命令实战:容器、镜像、网络与数据卷全攻略

Docker常用命令实战:容器、镜像、网络与数据卷全攻略

我最早开始系统性整理 Docker 指令&#xff0c;是因为团队里连续几次有人把容器目录挂载错了&#xff0c;还有人分不清docker restart和docker stop配合docker start在行为上的区别。这些看起来都是小问题&#xff0c;但在生产环境里&#xff0c;随便一个误操作都可能把服务搞得…

2026/10/20 22:38:46 阅读更多 →
artcraft:轻量可组合的创意生成框架,让设计文案手工音乐一键落地

artcraft:轻量可组合的创意生成框架,让设计文案手工音乐一键落地

1. 项目缘起与整体设计思路1.1 为什么会有 artcraft 这个想法做创意工具这些年&#xff0c;我最大的感受就是&#xff1a;大部分所谓的“创作辅助工具”&#xff0c;其实都在帮倒忙。你要么被一堆参数面板淹没&#xff0c;要么被强制绑定某个平台的账号体系&#xff0c;要么生成…

2026/10/20 22:38:46 阅读更多 →

最新新闻

文献综述智能生成全流程实战:从检索式到初稿的AI辅助指南

文献综述智能生成全流程实战:从检索式到初稿的AI辅助指南

写文献综述写到想摔电脑的人&#xff0c;估计不止我一个。我见过不少同学&#xff0c;实验做得有模有样&#xff0c;最后卡在论文绪论的“国内外研究现状”上&#xff0c;参考文献下载了两三百篇&#xff0c;打开文档却一句话都憋不出来。这种痛苦我太熟悉了。这几年&#xff0…

2026/10/20 23:19:50 阅读更多 →
PPT-Master:技术文档可视化与高效沟通的方法论

PPT-Master:技术文档可视化与高效沟通的方法论

1. “ppt-master”不是软件&#xff0c;而是一类人的隐性能力标签最近在几个技术社区和设计协作群聊里&#xff0c;频繁看到有人发“求个ppt-master带飞”“有没有靠谱的ppt-master接单”“这个需求找ppt-master比找前端还快”。起初我以为是某个新出的AI插件或SaaS工具&#x…

2026/10/20 23:19:50 阅读更多 →
采样终结,演绎之始:虚拟乐器 Instrument X 深度体验

采样终结,演绎之始:虚拟乐器 Instrument X 深度体验

在虚拟乐器圈子里泡久了&#xff0c;你会养成一种本能&#xff1a;听任何一首歌&#xff0c;先猜里面用的是什么音色&#xff0c;是采样库还是合成器&#xff0c;是预制的“万金油”钢琴还是真的为这个编曲调过力度层的音源。最近我拿到的这套名为 Instrument X 的软件&#xf…

2026/10/20 23:19:50 阅读更多 →
视频混剪特效全攻略:转场、调色、文字动效与风格化

视频混剪特效全攻略:转场、调色、文字动效与风格化

项目标题: 视频混剪-特效篇入行这几年&#xff0c;我剪过的混剪项目少说也有上百个。从早期狂拖特效模板、把画面整得眼花缭乱&#xff0c;到后来回归克制、追求“看不见的特效”&#xff0c;最大的感悟是&#xff1a;视频混剪里的特效&#xff0c;从来不是为了让画面花哨&…

2026/10/20 23:19:50 阅读更多 →
学生报名系统源码解析:PHP与MySQL数据库设计及改造实践

学生报名系统源码解析:PHP与MySQL数据库设计及改造实践

简介&#xff1a;一份面向学校、培训机构等场景的PHP学生报名管理系统源码&#xff0c;基于常规PHPMySQL开发&#xff0c;功能覆盖报名全流程&#xff1a;分区管理、学生信息登记&#xff08;含报名号、姓名、性别、出生年月、毕业学校、身份证号、家庭住址、父母姓名、联系电话…

2026/10/20 23:19:50 阅读更多 →
NAO机器人舞蹈开发:Choregraphe工程与Python关节轨迹插值实战

NAO机器人舞蹈开发:Choregraphe工程与Python关节轨迹插值实战

简介&#xff1a;NAO机器人重现迈克尔杰克逊经典舞蹈的Choregraphe项目工程资源&#xff0c;面向机器人爱好者、高校教学者以及正在入门社交机器人编程的开发者&#xff0c;聚焦于如何用可视化方式编排舞蹈动作&#xff0c;适合课程设计、课题研究或创客展示场景。包内共5个文件…

2026/10/20 23:18:49 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →