ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java Swing + MySQL 酒店管理系统开发:从表结构、事务到状态机实战

2026/9/15 3:56:44 拓冰建站 浏览量
Java Swing + MySQL 酒店管理系统开发:从表结构、事务到状态机实战 简介采用Java Swing和MySQL开发的图形化酒店管理系统是一套功能完整的课设级项目适合作为Java课程设计或毕业设计参考。系统区分管理员与普通用户登录涵盖客房管理、用户管理、订单管理、客房服务、历史记录和管理员权限等模块综合运用多对多查询、JFreeChart统计图表、MySQL触发器自动保存退房记录、CSV导出等功能并模仿QQ登录根据账号显示不同头像退出时可最小化到托盘内容完整、交互细节丰富还涉及房态查看、房间设备配图等实用细节。源码包共415个文件解压后约33.24MB以75个Java源文件、138个class字节码、173张png图片、11个jar依赖库以及SQL脚本、XML配置、MD文档为主分类清晰便于按模块阅读核心代码与界面资源。已有122人学习下载适合练习Swing界面布局、MySQL多表查询、触发器应用及文件导出订单多对多展示、用户头像联动、历史记录自动迁移等具体实现可直接用于扩展自己的项目整体代码结构规范注释清晰适合二次开发。1. 基于 Java Swing MySQL 的图形化酒店管理系统核心是先理顺表与状态酒店管理系统在课程设计和接单项目里出现频率很高但大多数变体都能追溯到同一个核心矛盾Swing 的事件模型不是为数据库查询设计的而 MySQL 的事务边界又必须在业务层显式控制。hotel-management 这类项目如果只把每个按钮绑定一条 SQL开发很快但一旦遇到“取消房间后又要换房”“前台交接时盘点应收”这些场景代码就会变成一团 if。这套系统要做的表面上是把房间、客户、订单填进界面上实际是把状态流转和结算流水对齐。下面从数据库建模开始按 Java Swing MySQL 的实际开发顺序把图形化酒店管理系统的代码骨架、参数设置和排错路径完整过一遍。适合已有 Java 基础、正在做课程设计或者想自己维护一套桌面前台管理系统的开发人员。拿到源码工程时文档里最关键的不是类图而是初始化 SQL 与连接串这两项写明白了项目换机器也能直接跑。2. 酒店管理系统的 MySQL 表结构设计与 JDBC 连接层2.1 房间表、客户表、预订表如何划分hotel-management 最常用的做法是拆成四类房间、客户、预订/入住单、消费流水。许多课程设计会把消费流水并进订单一列看似省事但结算时一旦出现加床、点餐、赔偿物品就要额外再造表。推荐的简化表结构如下CREATE TABLE rooms ( room_id INT AUTO_INCREMENT PRIMARY KEY, room_no VARCHAR(10) NOT NULL, room_type VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL, status VARCHAR(20) DEFAULT AVAILABLE ); CREATE TABLE guests ( guest_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20), id_card VARCHAR(30) ); CREATE TABLE reservations ( reservation_id INT AUTO_INCREMENT PRIMARY KEY, guest_id INT NOT NULL, room_id INT NOT NULL, check_in_date DATE, check_out_date DATE, total_amount DECIMAL(10,2), pay_status VARCHAR(20) DEFAULT UNPAID, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_res_guest FOREIGN KEY (guest_id) REFERENCES guests(guest_id), CONSTRAINT fk_res_room FOREIGN KEY (room_id) REFERENCES rooms(room_id) );这里有几个设计判断值得说清楚。price 直接放在 rooms 表里原因是这个系统按“房间类型”定价换房时按新房间价格重算如果要做周末调价或淡旺季价格应该抽一张 room_price 表。status 字段推荐用大写英文状态值比如 AVAILABLE、OCCUPIED不要直接存中文运行时再用枚举或字典映射成中文否则不同机器的字符集差异很容易让查询条件失效。外键在桌面管理系统里经常被省略但 reservations 表里的 guest_id、room_id 一旦连回主表就能避免删除客户或房间时留下孤儿订单。MySQL 的 InnoDB 引擎默认启用外键约束MyISAM 不支持建库时确认引擎是 InnoDB。日期字段尽量用 DATE 而不是 VARCHAR否则后面 DATEDIFF、DATE_FORMAT 这类报表统计全部要 CAST反而多出错。2.2 JDBC 连接 MySQL 的推荐连接串数据库建好以后Java 程序与 MySQL 的通道就是 JDBC。常见做法是把连接参数集中到一个 DbUtil 类后续 DAO 层所有类都从这里拿连接。连接串通常这样写public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/hotel_db ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接串里的参数按顺序讲useSSLfalse 是本地开发和课程设计里最常见的参数能跳过 SSL 握手时的证书提示降低连接延迟serverTimezoneAsia/Shanghai 解决 MySQL Connector/J 8.x 报出的时区异常characterEncodingutf8 与建库字符集保持一致避免存中文变成问号。如果装的是 MySQL 8.0驱动类名从 com.mysql.jdbc.Driver 换成了 com.mysql.cj.jdbc.Driver最好把驱动 jar 升级到 8.0 以上否则容易遇到通信链路异常。把这些参数做成一张表日常直接照抄参数取值作用useSSLfalse跳过本地 SSL 握手降低连接耗时serverTimezoneAsia/Shanghai统一 JVM 与 MySQL 时区避免 CST 歧义characterEncodingutf8连接层字符集必须与库表字符集一致rewriteBatchedStatementstrue批量 executeBatch 时拼接 SQL提高插入性能connectTimeout3000连接超时毫秒数桌面端让报错更快弹出如果装的是 MySQL 免安装版初始化 data 目录时默认没有指定字符集登录后先用SHOW VARIABLES LIKE character_set_server; 查看服务端字符集再在 my.ini 中设置 character-set-serverutf8mb4。用 Navicat 或 MySQL Workbench 建库时同样注意选择 utf8mb4 和 utf8mb4_unicode_ci。utf8mb4 比 utf8 多出的部分用来存 emoji 和其他扩展字符酒店客户姓名、备注字段里经常出现这类内容。2.3 不要在 Swing 事件线程里反复开关连接刚开始写 hotel-management 时最常见的写法是每个按钮的 actionPerformed 里都 new 一个 Connection用完整关。这在数据量小的演示系统里能跑但连续点击查询就会看到窗口卡顿。卡顿的根因不是 JDBC 慢而是 Swing 的事件分发线程被数据库等待占用整个界面没法重绘。我一般会把数据库操作拆到独立 DAO并在 UI 层用 SwingWorker 异步加载。更大的桌面系统也不直接用 DriverManager而是用 HikariCP 连接池替换方法放在最后一章。这里先记住一个原则Connection 没必要每次新建但也不能做成全局静态共享同一个连接同一时刻只能被一个线程执行 SQL多窗口同时操作时就交给连接池来协调。另外一个容易误用的点PreparedStatement 应该作为局部变量用后即关。如果把 Statement 放在成员变量里多次执行时连接状态混乱而且无法安全关闭。每个操作都要在最外层 finally 里关闭 ResultSet、PreparedStatement、Connection或者直接用 try-with-resources。3. 从预订到退房hotel-management 的房间状态机与事务控制3.1 用枚举和状态转换表约束房态酒店房间的状态不能靠肉眼判断。一个标准流程中房间在 AVAILABLE可售、RESERVED预留、OCCUPIED在住、CLEANING待清洁之间流转。如果直接用 String 赋值代码里就会到处出现if (status.equals(在住))拼错一个字查询结果就变成空。相对稳妥的方案是定义枚举public enum RoomStatus { AVAILABLE(可售), RESERVED(已预定), OCCUPIED(已入住), CLEANING(清洁中); private final String desc; RoomStatus(String desc) { this.desc desc; } public String getDesc() { return desc; } }枚举类既界定了合法状态集合又承担了中文显示。界面上直接调用 getDesc() 显示数据库里通过 name() 存入大写字符串。合法的房态跳转建议直接用下面这张表定义业务规则动作前置状态后置状态额外条件预订AVAILABLERESERVED客户信息完整可收押金入住RESERVEDOCCUPIED已核对身份证房费已收或挂账换房OCCUPIEDOCCUPIED新房间必须 AVAILABLE旧房间转 CLEANING退房OCCUPIEDCLEANING结清消费退还押金清洁完成CLEANINGAVAILABLE保洁确认把状态转移写成一张表好处是后续加“保洁员批量清洁”功能时只改枚举的转换关系不影响预订和结算逻辑。3.2 预订房间的乐观锁与事务提交直接执行 INSERT INTO reservations 再 UPDATE rooms 的问题在于如果第二步失败订房记录留下了但房间状态没变化前台会看到一张预定单挂在已经可售的房间上。二者必须同生共死。事务写法如下public void createReservation(Reservation r) throws SQLException { Connection conn DbUtil.getConnection(); try { conn.setAutoCommit(false); String lockSql UPDATE rooms SET statusRESERVED WHERE room_id? AND statusAVAILABLE; try (PreparedStatement ps conn.prepareStatement(lockSql)) { ps.setInt(1, r.getRoomId()); if (ps.executeUpdate() 0) { throw new SQLException(房间已被其他订单占用请重新选择); } } String insertSql INSERT INTO reservations (guest_id, room_id, check_in_date, check_out_date, total_amount) VALUES (?, ?, ?, ?, ?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, r.getGuestId()); ps.setInt(2, r.getRoomId()); ps.setDate(3, new java.sql.Date(r.getCheckInDate().getTime())); ps.setDate(4, new java.sql.Date(r.getCheckOutDate().getTime())); ps.setBigDecimal(5, r.getTotalAmount()); ps.executeUpdate(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }代码里的关键在UPDATE ... WHERE room_id? AND statusAVAILABLE。这相当于把“房间是否可用”的检查下沉到数据库行锁上同一时刻多个窗口同时点预订只有一个 UPDATE 能影响一行另一个 executeUpdate 返回 0 后直接抛异常。这种写法没有引入额外中间件课程设计、单机部署和多人同时操作同一个 MySQL 的场景都够用。提示不要先用 SELECT 查房态再 UPDATE因为两个连接之间有间隙并发时依然会超卖。try-with-resources 会帮我们关闭两个 PreparedStatement最后的 finally 只负责恢复自动提交和关掉 Connection事务回滚后连接可以复用。有人担心conn.setAutoCommit(true)放在 finally 里会不会覆盖回滚状态实际上 rollback 已经执行完这里只是把连接状态恢复成默认对业务无影响。3.3 退房时计算房费与更新状态退房的动作包含两部分算出应收金额然后修改订单支付状态和房间状态。金额最好不要拆成多段 Java 代码逐项求和直接用 SQL 计算可以减少代码量SELECT r.check_in_date, r.check_out_date, DATEDIFF(r.check_out_date, r.check_in_date) AS days, rm.price AS room_price FROM reservations r JOIN rooms rm ON r.room_id rm.room_id WHERE r.reservation_id ?;如果客户超期未退房而系统又把 check_out_date 设置为预计离店日DATEDIFF 会算出负数或偏小值。更稳妥的做法是用LEAST(CURRENT_DATE, r.check_out_date)把日期封顶SELECT DATEDIFF(LEAST(CURRENT_DATE, r.check_out_date), r.check_in_date) AS days, rm.price AS room_price FROM reservations r JOIN rooms rm ON r.room_id rm.room_id WHERE r.reservation_id ?;退房记录一般不做 DELETE因为酒店管理系统要保留历史流水。很多新手直接把记录删除最后报表和查账都缺数据。正确做法是把 pay_status 更新为 PAID房间状态再按流程更新到 CLEANING。换房时也只插入一条换房流水原房间转清洁新房间从 AVAILABLE 转 OCCUPIED这样账目才能追溯。4. 图形界面实现JTable 实时房态、字段校验与月度报表4.1 用 AbstractTableModel 让 JTable 与房态联动Swing 中显示房间数据最直接的方式是 JTable但把二维数组塞进 DefaultTableModel 的坏处是新增列、改类型都要重新构建数组。为酒店管理系统的房间列表定制 TableModel 是更好的做法public class RoomTableModel extends AbstractTableModel { private final String[] columns {房号, 类型, 价格, 状态}; private ListRoom rooms new ArrayList(); public void setRooms(ListRoom rooms) { this.rooms rooms; fireTableDataChanged(); } Override public int getRowCount() { return rooms.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Room room rooms.get(rowIndex); switch (columnIndex) { case 0: return room.getRoomNo(); case 1: return room.getRoomType(); case 2: return room.getPrice(); case 3: return RoomStatus.valueOf(room.getStatus()).getDesc(); default: return ; } } }把状态转中文的职责放在 getValueAt是因为表格数据只用于显示不需要把中文写回数据库。刷新时调用 model.setRooms(dao.findAll())JTable 会自动触发重绘。前台搜索时只要在下拉框、文本框的监听器里重新传条件执行同一个方法即可不需要为每种筛选单独写 Model。4.2 SwingWorker 加载数据避免界面冻住如果查询数据时按钮“按不动”说明数据库查询执行在了 EDT 上。正确做法是用 SwingWorker 在后台线程查数据库public void refreshRoomList() { new SwingWorkerListRoom, Void() { Override protected ListRoom doInBackground() { return roomDao.findAll(); } Override protected void done() { try { model.setRooms(get()); } catch (Exception e) { JOptionPane.showMessageDialog(frame, 加载房态失败 e.getMessage()); } } }.execute(); }SwingWorker 的 done 方法会在事件分发线程回调因此可以安全地刷新 JTable。普通 Thread 不能直接更新 UISwing 组件不是线程安全的。对于 hotel-management 这类小型系统SwingWorker 覆盖查询和报表生成已经足够连接池优化解决的是每次 new Connection 的开销和这个异步刷新是两件独立的事。4.3 表单字段校验规则与日期校验代码前台办理入住时最容易出错的字段是手机号和日期。在提交按钮的监听器里先做校验private boolean validateCheckInForm() { if (txtGuestName.getText() null || txtGuestName.getText().trim().isEmpty()) { JOptionPane.showMessageDialog(this, 姓名不能为空); txtGuestName.requestFocus(); return false; } if (!txtPhone.getText().trim().matches(^1[3-9]\\d{9}$)) { JOptionPane.showMessageDialog(this, 手机号格式不正确); txtPhone.requestFocus(); return false; } java.util.Date in dateCheckIn.getDate(); java.util.Date out dateCheckOut.getDate(); if (in null || out null || out.before(in)) { JOptionPane.showMessageDialog(this, 离店日期不能早于入住日期); return false; } return true; }校验顺序很重要先查空值再查格式最后查业务日期。如果一条 if 把条件用 连起来用户不知道光标该放在哪。手机号正则只针对大陆手机号如果系统面向游客可以放宽为 6 至 20 位字母数字数据库层仍然用 VARCHAR(30) 存储不要把所有校验责任交给正则。4.4 月度营收报表的 SQL 与展示报表是 Java 基础面试里常问的聚合查询场景。假设前台的结账动作只把 pay_status 更新为 PAID统计月度收入时使用SELECT DATE_FORMAT(check_in_date, %Y-%m) AS month, COUNT(*) AS order_count, SUM(total_amount) AS revenue FROM reservations WHERE pay_status PAID GROUP BY month ORDER BY month DESC;这个查询有两个细节DATE_FORMAT 的格式符大小写有讲究%Y 是四位年份%m 是两位月份写成小写 %y 就变成两位年份WHERE 里必须过滤 pay_status否则会把未收款订单算进收入。把查询结果填入 JTable 时同样用自定义 TableModel列改成“月份 / 订单数 / 收入”。数据量上来以后给 reservations 表加一个(pay_status, check_in_date)联合索引这个查询几乎不会有压力。5. 部署与维护hotel-management 的乱码排查、mysql 备份与连接池替换5.1 中文乱码的排查顺序hotel-management 换到新机器上运行最容易出现的是中文变问号。通常按三层排查库表字符集、连接 URL 字符集、JVM 的 file.encoding。先执行SHOW CREATE DATABASE hotel_db;如果结果显示 latin1就执行ALTER DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再重新导入数据。表本身是 latin1 的需要ALTER TABLE guests CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。连接 URL 按第二章写完整。JVM 层面IDE 里给运行配置加上-Dfile.encodingUTF-8打包成 jar 后用java -Dfile.encodingUTF-8 -jar hotel-management.jar启动。这三个点按顺序检查基本能定位十有八九的乱码。5.2 常用 mysql 备份命令与 HikariCP 连接池替换单机系统的备份用 mysqldump 就够命令如下mysqldump -uroot -p --single-transaction --default-character-setutf8mb4 hotel_db hotel_backup.sql--single-transaction 只适用于 InnoDB它不锁表能在前台操作不中断的情况下导出一致性快照MyISAM 表需要换成 --lock-all-tables。还原时先创建同名数据库再执行mysql -uroot -p hotel_db hotel_backup.sql。Windows 上可以把命令写成 mysql_backup.bat交给计划任务定时执行文件名里带日期方便保留历史版本。连接使用久了之后如果每次操作都新建连接并发高时会明显变慢。把 DbUtil 改成 HikariCP 连接池版本HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(root); config.setPassword(root); config.setMaximumPoolSize(5); config.setMinimumIdle(1); config.setConnectionTimeout(3000); HikariDataSource ds new HikariDataSource(config);桌面管理系统的 maximumPoolSize 设成 5 就够因为 SwingWorker 与事件线程并发的数据库操作很少超过两三个minimumIdle 设为 1避免空闲时也占用完整连接connectionTimeout 三秒拿不到连接时快速报错不让前台一直转圈。替换后原来调用DbUtil.getConnection()的地方不需要改。最后给一条收尾技巧所有 DAO 方法统一用 try-with-resources 处理 Connection、PreparedStatement、ResultSet 三个对象发生异常时连接会自动归还到连接池酒店管理系统长期维护起来省心很多。本文还有配套的精品资源点击获取