Java Swing实验室管理系统设计与实现:数据库到GUI全栈指南
2026/9/15 21:08:44 网站建设 项目流程

简介:一套基于 Java Swing 的实验室管理系统课程设计项目,面向 Java 初学者及高校课程设计/毕业设计场景,可用于快速理解 Swing 图形界面开发与实验室业务流程实现。系统内置文件、维修管理、管理员助理信息、实验室课程查询预约、系统设置、帮助六大功能模块,其中维修管理涵盖设备报修、故障表、状况登记与运行状况查询,助理信息包含楼层分工、规章制度和值日排班表,能够覆盖实验室日常管理的主要环节。整个压缩包共 1344 个文件,约 6.35MB,包含 42 个 Java 源文件、67 个 class 字节码文件、大量 htm 说明页面以及 gif/jpg 界面素材,同时附带 SQL Server 数据库文件(mdf/ldf)和 SQL 脚本,方便还原数据库环境。资源还带有完整 GUI 界面与项目配置文件,导入开发环境后即可运行调试,适合参考模块划分、数据库设计和界面布局。从内容预览可见,系统核心业务围绕课程预约、查询、设备维修与故障登记展开,便于学习者对照源码理解每个功能的实现路径;目前已有 122 人学习下载。

1. 从课设标题说起:Java Swing 实验室管理系统到底在练什么

很多计算机专业的学生拿到“基于Java Swing带GUI界面的实验室管理系统(源码+数据库)”这个题目时,第一反应是“又要写增删改查”。这个判断对了一半:系统核心确实是数据库的 CRUD,但另一半容易被忽略——Swing 界面驱动下的事件分发、表格模型、数据库连接池和事务边界,才是这道课程设计真正想考察的东西。它不像 Spring Boot 那样有现成的脚手架,所有组件要自己 new、自己布局、自己监听,数据要自己从 ResultSet 里搬进 JTable 的 TableModel。也就是说,这个题目让你用最朴素的方式把 Java SE 的 GUI 编程和 JDBC 编程串起来,练的是“没有框架加持时,你还能不能把业务写清楚”。

本文要讲的,是围绕这个标题最常见也最可靠的一套落地方案:从界面分层、数据库表设计,到用 Swing + JDBC 实现登录、预约、设备管理,再到打包发布和数据备份。内容面向两类人:一是正在做课设、需要一份能讲清楚原理的代码结构的学生;二是工作中偶尔要维护老式桌面系统、想快速找回 Swing 组件和 JDBC 调优手感的开发者。标题里的“源码+数据库”不是让你去网上找一个压缩包直接交差,而是让你理解——源码拆开是三层结构,数据库导出的 SQL 脚本是唯一能证明系统可跑的物证。这两样东西,缺一不可。

2. 系统设计与数据库建模:先定表,再写界面

2.1 从标题反推出来的三个核心实体

实验室管理系统的典型业务场景包括:管理员维护实验室信息、教师或学生预约空闲实验室、实验员管理设备状态、系统记录每次使用历史。因此,即使标题只给了“实验室管理系统”六个字,正常从业者也会默认至少设计四张表:用户表、实验室表、预约记录表、设备表。如果还要扩展,可以加公告表或维修记录表,但课设规模下四张表足够撑起完整的业务闭环。

在开始写任何 Java 代码之前,我一般会先用 SQL 脚本把表结构定死。原因很简单:Swing 界面上的每一个输入框、下拉框、表格列,最终都要映射到数据库表的字段上。如果表结构在编码中频繁改动,JTable 的列模型和 Bean 的属性都要跟着改,返工成本非常高。表结构设计遵循第三范式,但要考虑查询效率——预约记录表里冗余一个实验室名称字段,可以避免每次渲染历史记录时都要 JOIN 实验室表。这个冗余在桌面应用的体量下完全值得。

2.2 建表 SQL 脚本与字段说明

下面是一个可以直接导入 MySQL 的建表脚本,覆盖用户、实验室、预约、设备四个核心实体。注意字符集和存储引擎的设定,MySQL 5.7 以上建议用 utf8mb4,避免中文乱码。

CREATE DATABASE IF NOT EXISTS lab_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE lab_manager; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码(建议存储SHA-256摘要)', real_name VARCHAR(50) NOT NULL COMMENT '姓名', role TINYINT NOT NULL DEFAULT 2 COMMENT '角色: 1-管理员, 2-教师, 3-学生', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE t_lab ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '实验室ID', lab_no VARCHAR(20) NOT NULL UNIQUE COMMENT '实验室编号,如A301', name VARCHAR(100) NOT NULL COMMENT '实验室名称', location VARCHAR(100) DEFAULT NULL COMMENT '位置描述', capacity INT NOT NULL DEFAULT 30 COMMENT '容纳人数', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1-可用, 0-维护中' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实验室表'; CREATE TABLE t_reservation ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '预约ID', user_id INT NOT NULL COMMENT '预约人ID', lab_id INT NOT NULL COMMENT '实验室ID', reserve_date DATE NOT NULL COMMENT '预约日期', start_time TIME NOT NULL COMMENT '开始时间', end_time TIME NOT NULL COMMENT '结束时间', purpose VARCHAR(255) DEFAULT NULL COMMENT '预约用途', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1-已预约, 2-已取消, 3-已完成', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_res_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_res_lab FOREIGN KEY (lab_id) REFERENCES t_lab(id), INDEX idx_res_lab_date (lab_id, reserve_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约记录表'; CREATE TABLE t_device ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '设备ID', lab_id INT NOT NULL COMMENT '所属实验室ID', device_name VARCHAR(100) NOT NULL COMMENT '设备名称', device_type VARCHAR(50) DEFAULT NULL COMMENT '设备类型', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1-正常, 0-故障, 2-报废', buy_date DATE DEFAULT NULL COMMENT '购置日期', CONSTRAINT fk_dev_lab FOREIGN KEY (lab_id) REFERENCES t_lab(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备表';

这段 SQL 里定义了四张表,其中预约记录表用了复合外键关联用户和实验室。参数说明status字段统一用TINYINT,不用字符串,是为了减少冗余并方便在 Java 里用byteint映射;reserve_datestart_time分开存,因为后续查询“某天哪个时间段可用”时,日期过滤和时刻比较可以分别走索引。外键约束在课设里保留是合理的,它能保证预约记录不指向不存在的用户或实验室,避免出现逻辑脏数据。但要注意,InnoDB引擎下外键约束对插入性能有轻微影响,如果预约表数据量很大(百万级),生产环境通常会去掉外键改由应用层校验——课设规模不需要担心这个。

2.3 数据源初始化与连接池选型

标题里写了“源码+数据库”,数据库不能只给一个建表脚本,还需要初始化数据。至少插入一个管理员账号、几个教师和学生账号、若干实验室和少量设备,这样界面一打开就有数据可显示,演示时也好看。初始化数据脚本放在init_data.sql里,与建表脚本分开,方便于区分“结构文件”和“演示数据”。

Java 代码侧,JDBC 连接方式有两种常见选择:一是最简单的DriverManager.getConnection()每次获取连接,用完关闭;二是用连接池(如 HikariCP 或 Druid)管理连接。很多人课设图省事直接用DriverManager,但如果你在界面上频繁点击“查询”按钮,每次查询都新建连接,MySQL 默认的max_connections很容易被打满,尤其在演示现场反复操作时会出现“Too many connections”错误。所以我会建议课设阶段也用一个轻量连接池。HikariCP 配置简单,一个HikariConfig对象加两行代码就能集成。下面是一个典型的配置文件:

db.properties文件内容:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/lab_manager?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 jdbc.pool.maximumPoolSize=10 jdbc.pool.minimumIdle=2 jdbc.pool.connectionTimeout=30000

这里连接串中的characterEncoding=utf8是关键参数,不写全就可能在插入中文时变成乱码。serverTimezone指定时区,是 MySQL 8.x 驱动必须带的参数,否则会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized之类的错误。连接池参数中,maximumPoolSize=10对桌面应用完全足够,connectionTimeout=30000表示获取连接的最大等待时间,超过 30 秒抛异常,这样可以配合 Swing 的事件回调做超时提示,不会让界面无限卡死。

初始化连接池的工具类代码如下:

import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static HikariDataSource dataSource; static { try (InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(is); HikariConfig config = new HikariConfig(); config.setDriverClassName(props.getProperty("jdbc.driver")); config.setJdbcUrl(props.getProperty("jdbc.url")); config.setUsername(props.getProperty("jdbc.username")); config.setPassword(props.getProperty("jdbc.password")); config.setMaximumPoolSize(Integer.parseInt(props.getProperty("jdbc.pool.maximumPoolSize"))); config.setMinimumIdle(Integer.parseInt(props.getProperty("jdbc.pool.minimumIdle"))); config.setConnectionTimeout(Long.parseLong(props.getProperty("jdbc.pool.connectionTimeout"))); dataSource = new HikariDataSource(config); } catch (Exception e) { throw new ExceptionInInitializerError("数据库连接池初始化失败: " + e.getMessage()); } } private DBUtil() {} public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

这段静态代码块在类加载时执行,只初始化一次连接池。参数说明setDriverClassName用的是 MySQL 8.x 的驱动类,注意com.mysql.cj.jdbc.Driver是老驱动com.mysql.jdbc.Driver的替代,后者的包名已经被新版驱动移除了。getResourceAsStream从 classpath 加载配置文件,因此db.properties要放在src/main/resources目录下(普通课设项目就是src目录下创建resources文件夹)。使用try-with-resources方式读取属性文件,避免忘记关闭流。

有的同学可能想用纯 JDBC 而不是连接池,那也可以。只是后续每个DAO类里都要写一遍DriverManager.getConnection(),代码冗余不说,如果数据库密码写在多个类里,改密码时要改几十个文件。连接池的好处是配置集中、连接复用,而且HikariDataSource本身实现了Closeable,程序退出时可以调用DBUtil.getDataSource().close()释放资源。

3. Swing 界面分层实现:登录、主窗口与表格数据绑定

3.1 为什么 Swing 项目也要分 MVC

Swing 本身是 MVC 架构的,JTableTableModelJListListModel。但很多课设代码习惯把所有东西堆在JFrame的构造函数里,一个窗口几百行,按钮事件里直接写 SQL。这种写法交作业没问题,但答辩时被问“查询逻辑怎么复用”就会卡壳。我见过太多把Connection建在按钮事件里的代码,一旦要加一个“导出当前列表为 CSV”的功能,只能复制粘贴一大段 SQL 再改改表头。

因此,即使是用 Swing,也应该分出三层:

  • 界面层(view):只负责创建组件、摆放布局、给按钮添加监听器,不写任何 SQL。
  • 业务层(service):接收界面传过来的参数,调用 DAO,处理事务和简单的业务规则(比如预约时间不能冲突)。
  • 数据访问层(dao):只用 JDBC 或连接池操作数据库,返回List<User>List<Reservation>之类的数据对象。

这样分层之后,JTable 的TableModel可以直接用 DAO 返回的List<Bean>来填充,换数据库的时候只需要改 DAO 实现,界面代码一行不动。

3.2 登录窗口的事件处理与角色跳转

登录窗口是入口,包含用户名、密码输入框、登录和取消按钮。用户点击登录按钮后,监听器里调用业务层的login(username, password)方法,返回当前用户对象和角色。角色不同,进入主窗口后可见的菜单不同——管理员能看到“用户管理”和“设备管理”,教师/学生只看到“实验室预约”和“我的记录”。这就是最简单的权限控制,不需要 Spring Security,一个if分支即可。

下面给出一个简化版的登录按钮事件代码,用SwingWorker避免查询阻塞事件线程:

btnLogin.addActionListener(e -> { String username = txtUsername.getText().trim(); String password = new String(txtPassword.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空", "错误", JOptionPane.ERROR_MESSAGE); return; } // 耗时操作放到 SwingWorker 中,避免界面卡死 SwingWorker<User, Void> worker = new SwingWorker<>() { @Override protected User doInBackground() { return new UserService().login(username, password); } @Override protected void done() { try { User user = get(); if (user != null) { new MainFrame(user).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(LoginFrame.this, "用户名或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(LoginFrame.this, "系统异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } }; worker.execute(); });

这段代码体现了 Swing 编程中一个最常见的坑:不要在事件分发线程(EDT)里做数据库操作。如果直接在ActionListener里执行查询,查询期间界面会失去响应,窗口不能拖动、按钮点不动。SwingWorker会在后台线程执行doInBackground(),完成后自动回到 EDT 调用done(),可以安全地更新界面。参数说明new String(txtPassword.getPassword())获取密码框内容,getPassword()返回char[],用new String转成 String 时,有些安全敏感场景会先让数组填满空字符再置为 null,但课设不需要这么讲究。SwingWorker的第一个泛型参数是后台计算结果的类型,第二个泛型参数是中间进度类型,这里只用到第一个。

登录成功后的跳转逻辑也简单:MainFrame构造器接收一个User对象,根据user.getRole()决定是否显示“用户管理”标签页。这里有一个细节——密码存储不能明文。课设虽然演示用,但答辩老师大概率会问安全措施,所以建议在注册用户或插入初始数据时,用MessageDigest做 SHA-256 哈希。初始化数据脚本里可以直接写预计算好的哈希值,或者写一段 Java 工具类生成。

3.3 JTable 与 TableModel 的绑定:告别手工 setValueAt

JTable 是 Swing 里表格展示的核心组件,但很多人上来就table.setModel(new DefaultTableModel())再用addRow()填充。这种用法在数据量小、只做展示时没问题。一旦要支持“点击表头排序”、或“编辑某列后回写数据库”,DefaultTableModel就暴露出缺点——它不知道数据对应的 Bean,编辑后你得从表格的行号去反查对象,代码很容易出错。

更好的做法是自定义一个TableModel,让它直接包装List<Reservation>。比如做一个ReservationTableModel继承AbstractTableModel

import javax.swing.table.AbstractTableModel; import java.util.List; public class ReservationTableModel extends AbstractTableModel { private final String[] columns = {"预约ID", "实验室编号", "日期", "开始时间", "结束时间", "用途", "状态"}; private List<Reservation> data; public ReservationTableModel(List<Reservation> data) { this.data = data; } public void setData(List<Reservation> data) { this.data = data; fireTableDataChanged(); } @Override public int getRowCount() { return data == null ? 0 : data.size(); } @Override public int getColumnCount() { return columns.length; } @Override public String getColumnName(int column) { return columns[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Reservation r = data.get(rowIndex); return switch (columnIndex) { case 0 -> r.getId(); case 1 -> r.getLabNo(); case 2 -> r.getReserveDate().toString(); case 3 -> r.getStartTime().toString(); case 4 -> r.getEndTime().toString(); case 5 -> r.getPurpose(); case 6 -> r.getStatus() == 1 ? "已预约" : (r.getStatus() == 2 ? "已取消" : "已完成"); default -> null; }; } }

这个模型的好处是:表格每一行直接对应一个Reservation对象,要取整行数据时data.get(rowIndex)即可,不用再做“从表格值重新组装对象”的工作。参数说明getColumnName设置列头名称,fireTableDataChanged()是通知表格刷新数据的关键方法,当你删掉某一行或修改集合后调用它,表格 UI 才会重绘。列内容里对status做了映射,显示中文而不是数字,更直观。switch表达式是 Java 14 引入的,如果课设环境是 Java 8,需要改写成传统的switch语句或者if-else

有了这个TableModel,查询按钮的响应代码就非常干净:

List<Reservation> list = new ReservationService().queryByUser(currentUser.getId()); ReservationTableModel model = new ReservationTableModel(list); table.setModel(model);

这里table.setModel()会自动替换旧的列和数据,不需要手动清空。若要支持排序,用TableRowSorter包装模型:

TableRowSorter<ReservationTableModel> sorter = new TableRowSorter<>(model); table.setRowSorter(sorter);

排序器会让 JTable 表头可点击,点击后视图行序改变,但TableModel里行的顺序不变。注意:当表格经过排序后,table.getValueAt(row, col)row是视图行索引,必须用table.convertRowIndexToModel(row)转换成模型行索引,否则取到的数据对不上。这是 Swing 表格应用里最容易忽视的细节,属于面过的人秒懂、新手一脸懵的典型问题。

3.4 预约界面怎么校验“同一实验室时间冲突”

预约功能的核心业务规则:同一实验室在同一个时间段只能有一条有效预约。如果其他学生已经预约了周三 14:00–16:00 的 A301,那新的预约申请如果同样选 A301、周三 14:00–16:00 就要被拒绝。实现这一点有两种方式:一种是在插入前查一遍是否有重叠时间段的记录;另一种是给表加约束,比如用UNIQUE KEY (lab_id, reserve_date, start_time)——但这样只能防止完全相同的起始时间,无法防住“14:00-16:00 与 14:30-16:30 部分重叠”的情况。因此业务层校验必须写。

查询重叠预约的 SQL 可以这样写:

SELECT COUNT(*) FROM t_reservation WHERE lab_id = ? AND reserve_date = ? AND status = 1 AND start_time < ? AND end_time > ?;

四个参数依次是:实验室 ID、日期、新预约的结束时间、新预约的开始时间。这个条件解释起来是:只要已有预约的开始时间早于新预约的结束时间,并且已有预约的结束时间晚于新预约的开始时间,两者就存在时间交集。这是区间重叠判断的标准写法,比反过来判断“新的开始小于旧的结束 AND 新的结束大于旧的开始”更容易理解。

业务层的ReservationService.reserve()方法执行逻辑如下:

public boolean reserve(int userId, int labId, String date, String startTime, String endTime, String purpose) { if (startTime.compareTo(endTime) >= 0) { throw new IllegalArgumentException("开始时间必须早于结束时间"); } try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); ReservationDAO dao = new ReservationDAO(); if (dao.countConflict(conn, labId, date, startTime, endTime) > 0) { conn.rollback(); return false; // 存在冲突,预约失败 } dao.insert(conn, userId, labId, date, startTime, endTime, purpose); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} throw new RuntimeException("预约失败: " + e.getMessage(), e); } }

这里用conn.setAutoCommit(false)开启手动事务,把冲突检查和插入操作包在同一个事务里。如果没有事务,两个用户同时预约同一时间段时,可能两个请求都查询不到冲突,然后都插入成功,产生脏数据。虽然桌面应用并发量低,但写清楚事务边界能体现你对数据库基本原理的理解。参数说明try-with-resources释放 Connection,事务回滚放在catch块中,防止出现部分写入。

界面端,预约按钮点击后,从表单收集输入,调用reserve()方法,返回true则提示“预约成功”并刷新预约表格,返回false则提示“该时间段已被占用”。如果抛出IllegalArgumentException,则用JOptionPane显示“开始时间必须早于结束时间”等提示。整个交互闭环才算完整。

4. 后台管理功能:用户、设备、预约审核的完整实现

4.1 用户管理面板的 CRUD 与表格刷新

管理员登录后,进入用户管理页。这个页面需要一个 JTable 展示所有用户,上方是“添加用户”“编辑用户”“删除用户”三个按钮,以及一个搜索框。搜索框的监听器里调用 DAO 的按用户名模糊查询方法,输入关键字即可过滤。这里不直接用 Statement 拼 SQL,因为用户输入的内容可能包含单引号,拼接容易构成 SQL 注入。用PreparedStatement是底线,这也是答辩时的加分项。

添加用户对话框用一个小窗口(JDialog)实现,包含用户名、姓名、密码、角色下拉框、电话。点击确定后,校验用户名是否唯一(DAO 里执行SELECT COUNT(*)),如果不唯一则提示重新输入。密码加 SHA-256 哈希后存储。添加成功或失败都弹出消息框,成功后关闭对话框并调用JTable的模型setData()刷新。

更新用户时,表格双击行可以回填对话框内容。删除用户前要检查该用户是否有关联的预约记录。如果用户已经产生预约,直接删除用户会导致预约记录变成孤儿数据(外键约束会阻止删除,但报错信息不友好)。所以业务层要先查t_reservation表有没有user_id等于该用户的记录,有则提示“该用户存在预约记录,不能删除,可改为禁用”。这看起来是复杂逻辑,但才是真实系统需要的。给用户表加一个enabled字段会更合理,但课设通常时间有限,用“有预约不可删”这个规则也算能自圆其说。

4.2 设备管理:状态联动实验室编号

设备管理面板和用户管理类似,不同之处在于设备表格有“所属实验室编号”列,通常是一个下拉框,数据从t_lab表实时加载。设备状态有“正常/故障/报废”,当设备故障时,要能在设备列表里明显标识出来——比如状态列用不同颜色字体。Swing 里实现单元格渲染器TableCellRenderer并不复杂,但很多人不知道。下面是一个给状态列染色的渲染器:

class DeviceStatusRenderer extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if ("故障".equals(value)) { setForeground(Color.RED); } else if ("报废".equals(value)) { setForeground(Color.GRAY); } else if ("正常".equals(value)) { setForeground(new Color(0, 128, 0)); } else { setForeground(Color.BLACK); } return this; } } table.getColumnModel().getColumn(2).setCellRenderer(new DeviceStatusRenderer());

这段代码的setForeground设置了字体颜色,getColumn(2)假设状态列是第 3 列(下标 2)。参数说明DefaultTableCellRenderer继承自JLabel,所以可以直接调用setForegroundsetText。这里要注意isSelectedhasFocus的默认行为——被选中时背景会变成选中色,文字颜色往往会被覆盖,所以要么先调用super的默认绘制再设置前景色,要么在选中状态下也使用同样的颜色逻辑。经验是:渲染器里先调用父类方法,再覆盖前景色,否则选中的行文字颜色会看不清。

设备管理还有一个典型功能:按状态过滤。一个JComboBox下拉选择“全部/正常/故障/报废”,选中项变化时刷新表格。JComboBox 的监听器用addActionListener即可,不要在addItemListener里做过多处理,因为ActionListener更符合“用户选择完成”的语义。

4.3 预约审核:状态流转与时间冲突复查

管理员对预约记录的处理一般有两种模式:一种是学生提交预约后管理员确认;另一种是教师或学生在界面上直接预约成功,管理员只能查看或取消。课设题目没有明确要求,我默认走“教师/学生直接预约,管理员可查看全部记录、取消未开始的预约”这条简单路线。状态流转为“已预约(1)→ 已完成(3)”或“已预约(1)→ 已取消(2)”。

管理员查看全部预约时,列表可以按照预约日期排序。SQL 里加上ORDER BY reserve_date DESC, start_time DESC,配合TableRowSorter让用户自行点击列头排序。取消预约时要注意:只允许取消状态为“已预约”且预约时间在未来的记录。在业务层加这个判断比在界面层加更可靠,因为如果有多个窗口都打开着同一份数据,界面上的状态可能过期。事务层用乐观锁的思路:

public boolean cancelReservation(int reservationId, int operatorRole) { try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); ReservationDAO dao = new ReservationDAO(); Reservation r = dao.findById(conn, reservationId); if (r == null) { conn.rollback(); return false; } if (r.getStatus() != 1) { conn.rollback(); throw new IllegalStateException("该预约当前状态不可取消"); } if (r.getReserveDate().before(java.sql.Date.valueOf(LocalDate.now()))) { conn.rollback(); throw new IllegalStateException("预约已过期,不能取消"); } dao.updateStatus(conn, reservationId, 2); conn.commit(); return true; } catch (SQLException e) { throw new RuntimeException(e); } }

这里的operatorRole参数目前只用于校验管理员权限,实际上如果你是管理员,可以取消任何人的预约;如果是普通用户,只能取消自己的预约,这需要额外传入userId,在findById后比较r.getUserId()是否等于当前用户。教务系统里这一层校验必不可少,不然用户可以取消别人预约。加了这段判断,评审老师会认为你有完整的多用户权限意识。

4.4 用统计标签页展示数据报表

一个进阶的功能是首页展示统计卡片:实验室总数、设备总数、今日预约数、可用实验室数量。这些数字可以从数据库聚合查询得到。放在主界面的上方,用四个JLabel加边框形成卡片效果。刷新逻辑是:每次窗体重置或点击“刷新”按钮时,调用一个DashboardService.getStats()方法,返回一个包含四个 int 的Map<String, Integer>。聚合 SQL:

SELECT (SELECT COUNT(*) FROM t_lab) AS lab_count, (SELECT COUNT(*) FROM t_device WHERE status != 2) AS device_count, (SELECT COUNT(*) FROM t_reservation WHERE reserve_date = CURDATE() AND status = 1) AS today_res_count, (SELECT COUNT(*) FROM t_lab WHERE status = 1) AS avail_lab_count;

用四段子查询一次性查出来,比执行四条 SQL 再拼结果效率高,而且关注点更集中。返回的Map再通过setText摆到界面上。如果统计卡片的字体想做得美观,可以给每个JLabel设置不同的 HTML 样式,比如:

JLabel labCountLabel = new JLabel("<html><font size='14' color='#333'>实验室总数</font><br><font size='24' color='#1e88e5'>" + stats.get("lab_count") + "</font></html>");

Swing 的JLabel默认支持简单 HTML,用这种方式可以在一行里展示标题和数字,而不必再嵌一个Box布局。注意 HTML 文本的开头必须有<html>标签才能触发渲染。

5. 排错与打包:从“中文乱码”到“双击可运行”的最后一公里

5.1 中文乱码的三个排查位置

Java Swing 程序运行后中文显示为方块或问号,通常是三个位置出问题:源代码文件编码、数据库字符集、JDBC 连接参数。源代码编译时,javac默认平台编码,如果 IDE 里项目编码设为 GBK,另一位同学用 UTF-8 打开就乱码。解决办法统一使用 UTF-8,在JFrame构造器里可以加一句System.setProperty("file.encoding", "UTF-8")但要注意这行代码必须在任何字符串输出前执行,实际效果有限。更靠谱的是在 IDE 的编译参数里设置-encoding UTF-8,Maven 项目则在pom.xmlmaven-compiler-plugin配置<encoding>UTF-8</encoding>

数据库侧,MySQL 建库时指定utf8mb4后,如果表中已有数据但出现乱码,可以用以下语句修改并验证:

ALTER DATABASE lab_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; SHOW VARIABLES LIKE 'character_set%';

第三处是 JDBC URL 里的characterEncoding=utf8。注意这里的utf8是 MySQL 层面的 charset,不是 Java 的 UTF-8,但两者映射没问题。如果连接串里没写,MySQL 驱动会默认用服务器变量,服务器是latin1时中文必然乱码。

5.2 数据库连接失败的常见错误对照

桌面应用连接不上数据库的报错,其实就那么几种。快速定位方法如下表:

报错信息关键字常见原因检查方向
Access denied for user 'root'@'localhost'用户名或密码错误检查db.properties,确认 MySQL 的 root 密码
Unknown database 'lab_manager'数据库名不存在执行SHOW DATABASES;确认库名
Communications link failure/Connection refusedMySQL 端口不对、服务没启动、防火墙拦截使用telnet 127.0.0.1 3306测试端口连通性
Public Key Retrieval is not allowedMySQL 8.0 使用 caching_sha2_password 认证在连接串加allowPublicKeyRetrieval=true
The server time zone value '...'未配置时区连接串加serverTimezone=Asia/Shanghai或修改 MySQL 全局时区参数[mysqld] default-time-zone='+08:00'

allowPublicKeyRetrieval=true这个参数要特别说明。MySQL 8.x 默认认证插件是caching_sha2_password,当客户端连接时如果服务器端还没有缓存密码哈希,第一次连接会要求客户端请求公钥进行 RSA 加密,而这个请求默认被禁用,于是报错。开发环境可以直接放开,但生产环境建议创建专用用户并使用mysql_native_password插件来规避安全问题。课设阶段直接加上无碍。

5.3 打一个双击运行的 JAR 包

与标题配套的源码包通常给人用 IDE 运行,但作为课程设计,最好提供一个可以直接运行的 JAR 包,演示时省去配置 IDE 的时间。打 JAR 包的方式有几种:IDEA 自带 Artifacts 打包、Mavenshade插件打 fat JAR、命令行jar命令。最容易成功的是用 Maven +maven-assembly-pluginshade插件,把所有依赖打进一个 JAR。pom.xml里关键配置:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.lab.Main</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build>

上面的配置中mainClass指向入口类。参数说明<filters>里排除签名文件,因为如果不排除,shade 插件合并多个依赖 JAR 时会产生签名冲突,双击运行会报SecurityException: Invalid signature file digest<transformer>告诉插件在生成MANIFEST.MF时写入Main-Class属性。运行mvn clean package后,target目录下会生成xxx-shaded.jar,双击即可启动。如果系统没有关联默认的 JAR 程序,按 Win+R 运行java -jar xxx-shaded.jar也能打开。

记得把数据库导出脚本lab_manager.sql和操作说明README.txt放在 JAR 包旁边,注明“先导入 SQL,再修改db.properties中的数据库账号密码”。由于db.properties在 JAR 包内部,修改起来麻烦,一个常用技巧是让程序启动时优先加载外部配置文件。做法是在DBUtil里先尝试读取./config/db.properties,找不到再读类路径下的db.properties。这样你交付的时候,外部config目录和 JAR 包放在一起,演示现场改配置不需要重新打包。实现代码只需要修改DBUtil静态块的读取路径,这里不再赘述。

6. 数据备份与恢复:用 MySQL 命令行给课设加最后一道防护

实验室管理系统的数据是宝贵的课设证明,包括用户记录、预约历史、设备状态。答辩前或演示前手动备份一次完整数据库,既防止现场操作导致数据损坏,也方便重置。备份用 MySQL 自带的mysqldump工具,不需要额外依赖。命令如下:

mysqldump -h localhost -u root -p123456 lab_manager > lab_manager_backup.sql

这个命令导出的 SQL 文件包含建表和 INSERT 语句。参数说明-p123456是密码紧跟在-p后面,中间不能有空格,如果要避免密码出现在 shell 历史记录里,可以写-p然后回车再输入。>是 Linux 和 Windows(CMD)都支持的输出重定向。要恢复数据时:

mysql -u root -p123456 lab_manager < lab_manager_backup.sql

<会把备份文件内容作为输入送给mysql客户端。恢复前需要确保lab_manager数据库存在,不存在则先CREATE DATABASE lab_manager CHARACTER SET utf8mb4;。如果备份时报错Unknown table 'COLUMN_STATISTICS',说明 MySQL 8.x 备份旧版数据库时存在兼容问题,可以加--column-statistics=0参数,但这只影响包含mysql系统库的备份,对于用户库通常不出现这个报错。

自动化备份可以在 Windows 下用计划任务执行一个批处理脚本,命名backup.bat

@echo off set timestamp=%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2% mysqldump -h localhost -u root -p123456 lab_manager > D:\backup\lab_manager_%timestamp%.sql echo Backup done: %timestamp%

脚本里用日期时间拼接文件名,生成lab_manager_20250602_1430.sql这种带时间戳的备份,方便保留多个版本。注意%date%的格式依赖系统区域设置,中文 Windows 下%date:~0,4%取到的是月份还是年份要看具体字符串,保险起见可以用 Python 或 Java 生成时间戳,但批处理优点是零依赖。定期备份后,如果课设做完后又改乱了数据,直接恢复即可,不必重新从脚本初始化。

一个更进阶的做法是给程序添加“导出备份”按钮,在 Swing 里调用Runtime.getRuntime().exec()执行 mysqldump,并把导出文件保存到用户选择的目录。但要注意,Swing 程序所在机器可能没安装 MySQL 的 bin 目录,mysqldump找不到可执行文件。所以在外部命令里需要做两次尝试:先执行mysqldump,找不到再用where mysqldump或者让用户在配置文件中指定路径。课设阶段用外部命令行备份就足够了,不要在系统内部实现一个完整的数据导出器,那是另一个项目的工作量。

最后,如果数据库文件本身损坏或误删,重建的方式是导入完整备份。如果没有备份,只能重新执行建表脚本和初始数据。这个教训说明:任何课程设计项目,数据库目录下永远保留一份初始lab_manager.sql,并且在答辩前做一次干净环境的导入测试,确认 SQL 脚本能在全新的 MySQL 实例上直接跑通。这样即使演示现场主机崩溃,换一台机器导入脚本也能恢复整个系统。这就是“源码+数据库”压缩包里,数据库脚本那份文件最重要的存在意义。

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

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

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

立即咨询