ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Swing实验室管理系统设计与实现:数据库到GUI全栈指南

Java Swing实验室管理系统设计与实现:数据库到GUI全栈指南 简介一套基于 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 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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-维护中 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备表;这段 SQL 里定义了四张表其中预约记录表用了复合外键关联用户和实验室。参数说明status字段统一用TINYINT不用字符串是为了减少冗余并方便在 Java 里用byte或int映射reserve_date和start_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.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/lab_manager?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.pool.maximumPoolSize10 jdbc.pool.minimumIdle2 jdbc.pool.connectionTimeout30000这里连接串中的characterEncodingutf8是关键参数不写全就可能在插入中文时变成乱码。serverTimezone指定时区是 MySQL 8.x 驱动必须带的参数否则会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized之类的错误。连接池参数中maximumPoolSize10对桌面应用完全足够connectionTimeout30000表示获取连接的最大等待时间超过 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 项目也要分 MVCSwing 本身是 MVC 架构的JTable有TableModelJList有ListModel。但很多课设代码习惯把所有东西堆在JFrame的构造函数里一个窗口几百行按钮事件里直接写 SQL。这种写法交作业没问题但答辩时被问“查询逻辑怎么复用”就会卡壳。我见过太多把Connection建在按钮事件里的代码一旦要加一个“导出当前列表为 CSV”的功能只能复制粘贴一大段 SQL 再改改表头。因此即使是用 Swing也应该分出三层界面层view只负责创建组件、摆放布局、给按钮添加监听器不写任何 SQL。业务层service接收界面传过来的参数调用 DAO处理事务和简单的业务规则比如预约时间不能冲突。数据访问层dao只用 JDBC 或连接池操作数据库返回ListUser、ListReservation之类的数据对象。这样分层之后JTable 的TableModel可以直接用 DAO 返回的ListBean来填充换数据库的时候只需要改 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 中避免界面卡死 SwingWorkerUser, 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 的绑定告别手工 setValueAtJTable 是 Swing 里表格展示的核心组件但很多人上来就table.setModel(new DefaultTableModel())再用addRow()填充。这种用法在数据量小、只做展示时没问题。一旦要支持“点击表头排序”、或“编辑某列后回写数据库”DefaultTableModel就暴露出缺点——它不知道数据对应的 Bean编辑后你得从表格的行号去反查对象代码很容易出错。更好的做法是自定义一个TableModel让它直接包装ListReservation。比如做一个ReservationTableModel继承AbstractTableModelimport javax.swing.table.AbstractTableModel; import java.util.List; public class ReservationTableModel extends AbstractTableModel { private final String[] columns {预约ID, 实验室编号, 日期, 开始时间, 结束时间, 用途, 状态}; private ListReservation data; public ReservationTableModel(ListReservation data) { this.data data; } public void setData(ListReservation 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查询按钮的响应代码就非常干净ListReservation list new ReservationService().queryByUser(currentUser.getId()); ReservationTableModel model new ReservationTableModel(list); table.setModel(model);这里table.setModel()会自动替换旧的列和数据不需要手动清空。若要支持排序用TableRowSorter包装模型TableRowSorterReservationTableModel 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所以可以直接调用setForeground和setText。这里要注意isSelected和hasFocus的默认行为——被选中时背景会变成选中色文字颜色往往会被覆盖所以要么先调用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 的MapString, Integer。聚合 SQLSELECT (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(htmlfont size14 color#333实验室总数/fontbrfont size24 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-8Maven 项目则在pom.xml的maven-compiler-plugin配置encodingUTF-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 里的characterEncodingutf8。注意这里的utf8是 MySQL 层面的 charset不是 Java 的 UTF-8但两者映射没问题。如果连接串里没写MySQL 驱动会默认用服务器变量服务器是latin1时中文必然乱码。5.2 数据库连接失败的常见错误对照桌面应用连接不上数据库的报错其实就那么几种。快速定位方法如下表报错信息关键字常见原因检查方向Access denied for user rootlocalhost用户名或密码错误检查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 认证在连接串加allowPublicKeyRetrievaltrueThe server time zone value ...未配置时区连接串加serverTimezoneAsia/Shanghai或修改 MySQL 全局时区参数[mysqld] default-time-zone08:00allowPublicKeyRetrievaltrue这个参数要特别说明。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-plugin或shade插件把所有依赖打进一个 JAR。pom.xml里关键配置build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.lab.Main/mainClass /transformer /transformers filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-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 程序按 WinR 运行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 和 WindowsCMD都支持的输出重定向。要恢复数据时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-statistics0参数但这只影响包含mysql系统库的备份对于用户库通常不出现这个报错。自动化备份可以在 Windows 下用计划任务执行一个批处理脚本命名backup.batecho 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 实例上直接跑通。这样即使演示现场主机崩溃换一台机器导入脚本也能恢复整个系统。这就是“源码数据库”压缩包里数据库脚本那份文件最重要的存在意义。本文还有配套的精品资源点击获取
返回列表