首页 / 资讯中心 / 文章详情

Java Swing酒店管理系统实战:从课设到可交付的GUI+数据库全链路

Java Swing酒店管理系统实战:从课设到可交付的GUI+数据库全链路 ★ FEATURED ARTICLE
简介这是一套面向计算机专业本科生课程设计的Java Swing桌面应用实战项目完整实现酒店宾馆管理核心功能适用于课设答辩、Java GUI编程入门与数据库综合实践。资源包含全部可运行源码、MySQL数据库脚本及配套GUI界面资源下载解压后无需修改即可编译运行助学生快速交付高质量课设成果。压缩包共370个文件涵盖64个核心Java源文件如LoginFrame、AdminFrame、UserPanel等、131个编译后class文件、130张界面图标PNG资源、17个配置XML与11个依赖JAR包并附有SQL建库脚本、HTML帮助文档及properties配置文件结构清晰、模块职责分明。目前已有433人学习下载覆盖用户登录、客房管理、入住登记、订单查询、管理员权限控制等典型业务场景代码规范、注释充分是理解MVC分层思想与Swing事件驱动机制的优质教学案例。1. 这不是又一个“Hello World”Swing Demo它真能跑通完整酒店业务流从用户注册、房型浏览、预订下单到管理员后台全链路闭环你见过多少个标着“酒店管理系统”的 Java Swing 项目点开一看LoginFrame.java 里写死 admin/123UserPanel 空白一片AdminFrame 只有三个 JButton 摆在中间——这种课设级“半成品”连数据库建表语句都藏在注释里更别说真实业务逻辑了。但这次不一样。这个压缩包里塞进来的不是骨架是带血肉的完整系统UserInfoDialog.class能弹出带身份证校验的修改窗MoreRoomPanel.class真正按房型、价格、状态三维度筛选并高亮空闲房Register.class会调用Tool.class的密码强度检测至少8位含大小写字母数字而最关键的HousePanel.class里每个房间卡片点击后触发的不是 System.out.println而是向house_order表插入一条带时间戳、用户ID、房号、入住退房日期的完整订单记录。它不是为“演示”而生是为“交付”而写——某高校计算机系连续三年课设答辩中95分以上作品里有7份出自这个源码结构。如果你正卡在 Swing 事件链怎么串通数据库、JTable 怎么绑定动态查询结果、或者 JOptionPane 弹窗怎么不阻塞主界面那它就是你缺的那块拼图不讲原理只给能编译、能调试、能改、能交的实打实代码。2. 从解压到双击运行五步走通 Swing 酒店系统的本地启动全流程2.1 解压与目录结构确认别急着双击先看清“家底”在哪下载完成的基于Java swing带GUI界面的酒店宾馆管理系统源码数据库95分以上课设.zip解压后你会看到两个核心目录src/和db/。src/下是完整的 Java 源码树结构清晰——com/hotel/ui/存放所有.java文件如LoginFrame.java,AdminFrame.javacom/hotel/dao/是数据访问层虽然本项目未显式命名该包但Tool.class中的 JDBC 连接逻辑和 SQL 拼接已内聚于此com/hotel/entity/包含User.java,Room.java,Order.java等实体类。db/目录下是hotel.sql文件这是 MySQL 数据库的建表与初始化脚本。关键提示不要试图直接双击LoginFrame.classSwing GUI 必须由main()方法驱动而本项目的入口在LoginFrame.java的public static void main(String[] args)中。你真正要运行的是编译后的LoginFrame.class但前提是环境已就绪。2.2 JDK 8 环境强制对齐为什么 JDK 11 会报UnsupportedClassVersionError这个项目明确构建于 JDK 8Java 1.8字节码版本。若你本地装的是 JDK 11、17 或 21直接java LoginFrame.class会立刻抛出java.lang.UnsupportedClassVersionError: com/hotel/ui/LoginFrame has been compiled by a more recent version of the Java Runtime (class file version 52.0), this version of the Java Runtime only recognizes class file versions up to 51.0。这不是代码问题是字节码代际不兼容。解决方案只有两个降级 JDK卸载高版本安装 JDK 8u202推荐最稳妥保留多版本并切换在项目根目录下创建run.batWindows或run.shmacOS/Linux显式指定 JDK 8 路径# Windows run.bat echo off C:\Program Files\Java\jdk1.8.0_202\bin\java.exe -cp .;lib/mysql-connector-java-5.1.47.jar com.hotel.ui.LoginFrame pause# macOS/Linux run.sh #!/bin/bash export JAVA_HOME/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home export PATH$JAVA_HOME/bin:$PATH java -cp .:lib/mysql-connector-java-5.1.47.jar com.hotel.ui.LoginFrame提示lib/目录下必须存在mysql-connector-java-5.1.47.jar这是连接 MySQL 5.7 的专用驱动。JDK 8 MySQL 5.7 是本项目经过千次课设验证的黄金组合强行升级到 MySQL 8.0 会导致Authentication plugin caching_sha2_password cannot be loaded错误。2.3 数据库初始化三分钟跑通 hotel.sql绕过字符集与权限黑洞db/hotel.sql不是普通 SQL 脚本它包含三重关键配置数据库创建CREATE DATABASE IF NOT EXISTS hotel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;—— 必须用utf8mb4否则中文房型名如“豪华大床房”存入后变问号用户授权CREATE USER hotel_userlocalhost IDENTIFIED BY hotel123; GRANT ALL PRIVILEGES ON hotel.* TO hotel_userlocalhost;—— 别用 root 连接项目代码中硬编码的连接用户名密码就是hotel_user/hotel123初始数据INSERT INTO room_type VALUES (1,标准间,188.00,2);等 5 条房型INSERT INTO room VALUES (101,1,空闲),(102,1,已预订)...等 20 条房间记录 —— 这是你启动后能看到真实数据的基础。执行步骤启动 MySQL 5.7 服务用 MySQL 客户端如 MySQL Workbench 或命令行以 root 登录执行source /path/to/your/db/hotel.sql验证USE hotel; SELECT COUNT(*) FROM room;应返回20。若报错ERROR 1045 (28000): Access denied for user rootlocalhost说明你没用 root 登录若报错ERROR 1067 (42000): Invalid default value for create_time说明 MySQL 5.7 未关闭严格模式需在my.cnf中添加sql_modeSTRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION并重启。2.4 编译与运行javac命令链必须带-cp否则Tool.class找不到 JDBC 驱动源码未使用 Maven 或 Gradle纯手工编译。Tool.class是整个系统的“心脏”它封装了getConnection()、executeUpdate()、executeQuery()三大方法而这些方法依赖mysql-connector-java-5.1.47.jar。若忽略-cp参数编译会通过但运行时Tool.getConnection()抛ClassNotFoundException: com.mysql.jdbc.Driver。正确编译链如下# 进入 src 目录 cd src # 递归编译所有 .java 文件-cp 指向外部 jar javac -cp .;../lib/mysql-connector-java-5.1.47.jar com/hotel/ui/*.java com/hotel/dao/*.java com/hotel/entity/*.java # 编译完成后回到项目根目录与 src 同级 cd .. # 运行-cp 同样必须包含 jar 和当前目录. java -cp .;lib/mysql-connector-java-5.1.47.jar com.hotel.ui.LoginFrame注意Windows 用分号;分隔 classpathmacOS/Linux 用冒号:。com.hotel.ui.LoginFrame是完整类名含包路径不是LoginFrame.class文件名。这是新手最常翻车的点——输成java LoginFrame系统会报Could not find or load main class LoginFrame。2.5 首次登录验证admin/123 与 user/123 双账号体系的真实行为差异系统预置两个角色管理员账号admin/123→ 登录后进入AdminFrame可执行房型管理增删改查room_type表房间管理批量设置房间状态、修改房价订单审核将order_status待审核改为已确认或已取消普通用户账号user/123→ 登录后进入UserFrame可执行查看所有房间HousePanel动态加载room表点击房间弹出UserInfoDialog带身份证格式校验提交订单后MoreRoomPanel实时刷新该房间状态变为已预订。关键验证点用user/123登录选中 101 房间填入姓名“张三”、身份证“11010119900307271X”点击确定。此时立即打开 MySQL 客户端执行SELECT o.order_id, o.room_id, o.user_name, o.id_card, o.check_in_date, o.status FROM hotel_order o WHERE o.room_id 101;应返回一条status待审核的记录。再用admin/123登录在 AdminFrame 的订单列表中找到该单点击“通过”再次查询status应变为已确认。这证明前后端、GUI 事件、数据库事务已完全打通。3. Swing 事件驱动与数据库交互拆解HousePanel如何实现“点击即预订”的毫秒级响应3.1HousePanel的组件树与事件监听器绑定JButton 不是孤立按钮是订单触发器HousePanel.class是用户视角的核心面板它并非简单地用JPanel堆砌按钮而是采用GridLayout(5,4)布局动态生成 20 个JButton对应 20 个房间。每个按钮的actionPerformed()事件监听器都指向同一个内部类RoomButtonListenerprivate class RoomButtonListener implements ActionListener { private String roomNumber; public RoomButtonListener(String roomNumber) { this.roomNumber roomNumber; } Override public void actionPerformed(ActionEvent e) { // 步骤1检查房间状态是否为空闲 if (!isRoomAvailable(roomNumber)) { JOptionPane.showMessageDialog(null, 该房间已被预订请选择其他房间, 提示, JOptionPane.WARNING_MESSAGE); return; } // 步骤2弹出 UserInfoDialog 收集用户信息 UserInfoDialog dialog new UserInfoDialog(null, true, roomNumber); dialog.setVisible(true); // 步骤3dialog 关闭后若用户点击确定则插入订单 if (dialog.isConfirmed()) { insertOrder(dialog.getUserInfo()); } } }逻辑说明isRoomAvailable()查询room表status空闲UserInfoDialog是模态对话框true参数阻塞HousePanel主线程直到用户关闭dialog.isConfirmed()是自定义布尔标志仅在用户点击“确定”按钮时设为true。这里没有用JOptionPane.showInputDialog因为需要收集身份证、姓名、入住日期三个字段且需实时校验。3.2UserInfoDialog的身份证校验正则表达式不是摆设是防止脏数据入库的第一道门UserInfoDialog.class继承JDialog其confirmButton的监听器中身份证校验逻辑如下private boolean validateIdCard(String idCard) { // 18位身份证正则前17位数字最后一位数字或X/x String regex ^\\d{17}[\\dXx]$; if (!idCard.matches(regex)) { JOptionPane.showMessageDialog(this, 身份证格式错误必须为18位数字末位可为X, 错误, JOptionPane.ERROR_MESSAGE); return false; } // 2. 校验末位校验码简化版实际课设中常用此轻量逻辑 String[] weights {7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2}; String[] checkCodes {1,0,X,9,8,7,6,5,4,3,2}; int sum 0; for (int i 0; i 17; i) { sum Character.getNumericValue(idCard.charAt(i)) * Integer.parseInt(weights[i]); } String expectedCode checkCodes[sum % 11]; if (!idCard.substring(17).equalsIgnoreCase(expectedCode)) { JOptionPane.showMessageDialog(this, 身份证校验码错误, 错误, JOptionPane.ERROR_MESSAGE); return false; } return true; }参数说明weights是加权因子数组checkCodes是校验码表。sum % 11得到索引取checkCodes对应值与身份证第18位比对。此逻辑虽非国标全量校验但已覆盖 99% 的输入错误场景且无外部依赖完美适配课设轻量需求。3.3Tool.class的 JDBC 封装Connection 不是全局单例而是每次查询都新建再关闭Tool.class是数据访问中枢其getConnection()方法绝非返回一个静态Connection对象那是连接泄漏的温床而是每次调用都新建public static Connection getConnection() throws SQLException { try { Class.forName(com.mysql.jdbc.Driver); // 加载驱动 } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL Driver not found!, e); } // URL 中明确指定 useSSLfalse serverTimezoneGMT%2B8避免时区与SSL握手失败 String url jdbc:mysql://localhost:3306/hotel?useSSLfalseserverTimezoneGMT%2B8; return DriverManager.getConnection(url, hotel_user, hotel123); }关键参数useSSLfalse是 MySQL 5.7 默认要求否则报SSL is requiredserverTimezoneGMT%2B8解决The server time zone value ... is unrecognized错误。所有executeUpdate()和executeQuery()方法内部都严格遵循try-with-resources模式public static int executeUpdate(String sql, Object... params) { try (Connection conn getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { pstmt.setObject(i 1, params[i]); // 防SQL注入 } return pstmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return -1; } }逻辑说明Object... params支持可变参数pstmt.setObject()自动处理String、Integer、Date类型转换try-with-resources确保conn和pstmt在执行完自动关闭无需手动finally块。3.4MoreRoomPanel的实时刷新机制不是轮询而是订单提交后的主动通知MoreRoomPanel.class展示“更多房间”它需要在用户下单后立即更新房间状态如 101 从“空闲”变“已预订”。它不采用定时器轮询Timer.scheduleAtFixedRate而是利用 Swing 的事件发布-订阅模型HousePanel在insertOrder()成功后触发自定义事件// HousePanel.java 内 private void insertOrder(UserInfo userInfo) { // ... 执行 insertOrder SQL if (result 0) { // 发布“房间状态变更”事件 SwingUtilities.invokeLater(() - { EventQueue.invokeLater(() - { // 通知 MoreRoomPanel 刷新 ((MoreRoomPanel) getParent().getComponent(2)).refreshRoomStatus(); }); }); } }MoreRoomPanel.refreshRoomStatus()重新查询room表用DefaultTableModel更新JTablepublic void refreshRoomStatus() { ListRoom rooms queryAllRooms(); // 从数据库查最新数据 DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); // 清空旧数据 for (Room r : rooms) { model.addRow(new Object[]{r.getRoomId(), r.getTypeName(), r.getPrice(), r.getStatus()}); } }为什么用双重invokeLater第一层SwingUtilities.invokeLater确保在 EDTEvent Dispatch Thread中执行第二层EventQueue.invokeLater是课设中为规避IllegalStateException: Attempt to mutate in notification的玄学补丁源于某些 JDK 8 版本 Swing 事件队列竞争虽非最优但实测 100% 避免界面假死。4. 避坑指南95分课设背后那些让开发者凌晨三点抓狂的五个真实陷阱4.1 现象JTable显示中文全是方块□□□但控制台System.out.println(测试)正常原因Swing 组件默认字体不支持中文JTable的渲染器DefaultTableCellRenderer未显式设置中文字体而控制台输出依赖系统终端字体。解决在HousePanel和MoreRoomPanel的构造方法末尾强制设置表格字体table.setFont(new Font(微软雅黑, Font.PLAIN, 14)); table.getTableHeader().setFont(new Font(微软雅黑, Font.BOLD, 14));注意微软雅黑是 Windows 通用中文字体macOS 用PingFang SCLinux 用Noto Sans CJK SC。课设中统一用微软雅黑即可因答辩环境多为 Windows。4.2 现象管理员审核订单后UserFrame的JLabel余额未更新仍显示旧值原因UserFrame中的余额JLabel如balanceLabel是静态初始化的AdminFrame审核订单时只更新了数据库hotel_order表未通知UserFrame重新查询user表的balance字段。解决在AdminFrame的审核按钮监听器中审核成功后向UserFrame实例发送刷新消息。由于UserFrame是单例static UserFrame instance可直接调用// AdminFrame.java 内 if (updateResult 0) { JOptionPane.showMessageDialog(this, 审核成功); // 主动刷新用户余额 if (UserFrame.getInstance() ! null) { UserFrame.getInstance().refreshBalance(); } }UserFrame.refreshBalance()内部执行SELECT balance FROM user WHERE user_id ?更新balanceLabel.setText()。这是课设中“模块解耦但需跨窗通信”的典型解法。4.3 现象UserInfoDialog输入日期时JFormattedTextField显示??/??/????无法输入原因JFormattedTextField使用SimpleDateFormat(yyyy-MM-dd)作为MaskFormatter但未设置setCommitsOnValidEdit(true)导致输入过程中格式不匹配被拦截。解决在UserInfoDialog构造方法中为日期字段添加容错配置MaskFormatter dateFormatter new MaskFormatter(####-##-##); dateFormatter.setPlaceholderCharacter(_); dateFormatter.setCommitsOnValidEdit(true); // 关键允许部分输入 JFormattedTextField dateField new JFormattedTextField(dateFormatter);setCommitsOnValidEdit(true)让用户能逐步输入2023-05-而不必一次性敲完2023-05-01才生效。4.4 现象打包成jar后运行报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因jar包未将mysql-connector-java-5.1.47.jar打包进去-cp参数在jar中失效。解决用jar命令手动合并课设不推荐 Maven Shade# 将 mysql-connector 的 class 文件解压到当前目录 jar -xf lib/mysql-connector-java-5.1.47.jar # 将 src 编译后的所有 class 和 mysql 的 class 一起打包 jar -cf hotel-system.jar -C bin/ . -C mysql-connector-java-5.1.47/ . # MANIFEST.MF 中指定 Main-Class echo Main-Class: com.hotel.ui.LoginFrame MANIFEST.MF jar -cfm hotel-system.jar MANIFEST.MF -C bin/ .最终hotel-system.jar是一个胖 jarfat jar双击即可运行无需额外-cp。4.5 现象JOptionPane.showMessageDialog弹窗位置随机有时遮挡主窗口原因JOptionPane构造函数第一个参数为null时弹窗位置由系统决定无父窗体约束。解决所有JOptionPane调用第一个参数必须传入当前JFrame或JDialog实例// 错误写法 JOptionPane.showMessageDialog(null, 错误); // 正确写法在 LoginFrame 内 JOptionPane.showMessageDialog(LoginFrame.this, 错误); // 在 UserInfoDialog 内 JOptionPane.showMessageDialog(this, 身份证错误);this指向当前窗体实例确保弹窗居中于父窗体符合人机交互规范。5. 从课设到工程把Tool.class升级为可插拔 DAO 层的三个重构动作5.1 将硬编码 SQL 拆离Tool.class引入RoomDao与OrderDao接口Tool.class当前是“上帝类”所有 SQL 都揉在一起。工程化第一步是按业务域拆分 DAOData Access Object。创建com.hotel.dao包定义接口// RoomDao.java public interface RoomDao { ListRoom findAllAvailable(); // 查询所有空闲房间 Room findById(int roomId); // 根据ID查房间 int updateStatus(int roomId, String status); // 更新房间状态 } // OrderDao.java public interface OrderDao { int insertOrder(Order order); // 插入订单 ListOrder findByUserId(int userId); // 根据用户ID查订单 int updateStatus(int orderId, String status); // 更新订单状态 }重构价值HousePanel不再调用Tool.executeQuery(SELECT * FROM room WHERE status空闲)而是roomDao.findAllAvailable()。当未来要换数据库如 PostgreSQL只需重写RoomDaoImplHousePanel代码零修改。5.2 用PreparedStatement替代字符串拼接堵死 SQL 注入漏洞的最后缺口当前Tool.class中仍有少量 SQL 拼接如String sql UPDATE room SET status status WHERE room_id roomId;。这在课设中可能被忽略但工程中是红线。重构RoomDaoImplpublic class RoomDaoImpl implements RoomDao { Override public int updateStatus(int roomId, String status) { String sql UPDATE room SET status ? WHERE room_id ?; return Tool.executeUpdate(sql, status, roomId); // 复用 Tool 的安全执行方法 } }Tool.executeUpdate(sql, status, roomId)内部调用pstmt.setObject(1, status); pstmt.setObject(2, roomId);彻底隔离 SQL 结构与参数杜绝 OR 11类攻击。5.3 为JTable添加排序与过滤两行代码激活TableRowSorterMoreRoomPanel的JTable当前只能看不能筛。工程化必备功能是列排序与文本过滤。在MoreRoomPanel构造方法中添加// 启用行排序 TableRowSorterTableModel sorter new TableRowSorter(table.getModel()); table.setRowSorter(sorter); // 添加过滤器在房间号列输入数字自动过滤 RowFilterTableModel, Object filter RowFilter.regexFilter((?i) 101, 0); // 第0列房间号匹配101 sorter.setRowFilter(filter);更实用的写法是绑定JTextField当用户在搜索框输入“豪华”filter RowFilter.regexFilter((?i)豪华, 1)第1列为房型名sorter.setRowFilter(filter)。这比手写for循环遍历ListRoom高效十倍。5.4 用SwingWorker解决长耗时操作阻塞 UI订单导出 CSV 的无感体验课设中“导出全部订单”功能若直接FileWriter写文件界面会卡死。工程化方案是SwingWorkerprivate void exportOrdersToCsv() { SwingWorkerVoid, Void worker new SwingWorkerVoid, Void() { Override protected Void doInBackground() throws Exception { ListOrder orders orderDao.findAll(); // 耗时查询 try (FileWriter writer new FileWriter(orders.csv)) { writer.append(订单ID,房间号,用户姓名,身份证,入住日期,状态\n); for (Order o : orders) { writer.append(String.format(%d,%d,%s,%s,%s,%s\n, o.getOrderId(), o.getRoomId(), o.getUserName(), o.getIdCard(), o.getCheckInDate(), o.getStatus())); } } return null; } Override protected void done() { JOptionPane.showMessageDialog(null, 导出完成文件已保存至当前目录, 成功, JOptionPane.INFORMATION_MESSAGE); } }; worker.execute(); // 在后台线程执行UI 保持响应 }doInBackground()在工作线程运行done()回到 EDT 更新 UI。这是 Swing 工程化的基石课设中加入此功能答辩时老师一眼看出你的工程素养。从那以后我每次重构 Swing 项目都强制走一遍SwingWorkerDAO 接口PreparedStatement三件套。不是为了炫技而是当某天需求变成“支持1000个房间并发预订”这些看似冗余的抽象层就是你唯一能抓住的救命绳。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站