拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java酒店管理系统核心设计:对象建模、JDBC事务与并发控制

简介Java期末大作业酒店管理系统面向需要完成课程设计、期末实训或毕业项目的Java学习者提供可直接运行的源代码与配套设计报告。系统聚焦客房预订、入住登记、退房处理、账单结算等核心业务覆盖Swing/JavaFX界面开发、JDBC数据库增删查改、多线程同步、MVC分层与工厂、单例等设计模式可帮助读者把课堂上分散的Java知识串成完整项目。设计报告同步说明需求分析、数据库表结构、关键代码与测试结果截图与源码对应能降低理解门槛。压缩包共29个文件包含11个java源文件、1份设计报告doc文档、若干jpg/png界面图片以及12条资料辅助url链接整体仅9.7MB目录简洁便于对照源码、报告与截图进行学习。目前已吸引1640人浏览下载是期末答辩、实训总结与代码复现的实用参考。1. 一个被低估的 Java 期末作业拆开能当面试素材酒店管理系统是 Java 期末大作业里最常见的题目之一但越常见的题越能看出工程习惯。这次拆的是个 JavaFX 版酒店管理系统源码包Hotel、Room、Guest、Main 四个核心类外加一份知识点覆盖很全的设计报告把对象建模、JDBC、GUI 交互和并发场景串在了一起。对赶作业的学生来说它最大的价值不是“能跑”而是能讲清每个类为什么存在、状态为什么要流转对工作几年的人来说把 room 状态迁移和 JDBC 事务对照着看也能当一份速查的 java 面试八股文案例。下面不贴全部源码只挑关键设计思路方便你拿到包后顺着路线复现。2. 酒店管理系统的对象模型Hotel、Room、Guest 的类设计与多态2.1 从需求里提炼类谁该是实体谁该是聚合写代码之前先对着题目做名词提取。“客房”“客户”“订单记录”是最明显的三个实体对应源码包里的 Room.java、Guest.javaHotel.java 并不对应某张表而是聚合房间列表、对外提供入住退房操作的服务入口。很多学生把订单字段直接塞进 Guest导致客户对象越来越臃肿这属于把对象当结构体用。常见做法是先把三张表的关系画成 ER 图Room 一对多 Reservation、Guest 一对多 Reservation再反推 Java 类。ER 图画明白了类和类之间的引用关系基本就定下来了。答辩时老师爱问“你这几个类怎么分出来的”直接讲 ER 图和职责分离比念代码有用。设计报告里最好也补一张简化 ER 图哪怕用表格描述关系都比纯文字强。尤其当你的 ER 图里出现“房间—客户”多对多关系时中间表 Reservation 的存在会让报告看起来有明显的数据建模意识。2.2 房间类的继承体系模板方法与抽象方法搭配多数没参加过软件综合实践的同学第一版会写一个 Room 类里面放 type 字段然后 switch 处理价格。这种做法能跑但加一个房型就要改 switch而且不同房型的计价规则越堆越多。更合适的做法是用继承Room 是抽象基类StandardRoom 和 SuiteRoom 分别覆写计价或多态行为。先看基类public abstract class Room { protected String roomId; // 房号如 A101 protected String type; // 房型名称例如标准间/套房 protected double price; // 基准价格元/晚 protected int status; // 0空闲 1预订 2入住 3维修 public Room(String roomId, String type, double price) { this.roomId roomId; this.type type; this.price price; this.status 0; } // 模板方法子类不需要重写整个流程只改计价规则 public double calcPrice(int days) { double base price * days; if (days 3) { base * 0.9; // 连住三晚以上九折 } return base; } // 抽象方法强制子类给出分类描述用于界面展示 public abstract String getRoomCategory(); // 状态迁移必须走这里不要直接 setStatus public synchronized boolean changeStatus(int from, int to) { if (status ! from) { return false; } status to; return true; } }设计上有两点考虑。第一calcPrice 做成模板方法“连住打折”这个规则集中在唯一入口子类想加周末价时只需覆写并调用super.calcPrice(days)主流程不会散落各处。第二changeStatus(from, to) 用 synchronized 保护状态变化必须是“从合法旧状态转到新状态”内存层面先挡住非法流转。后面做多线程模拟时你会看到这个方法的真正用处。public class StandardRoom extends Room { public StandardRoom(String roomId) { super(roomId, 标准间, 280); } Override public String getRoomCategory() { return 标准间/双床; } } public class SuiteRoom extends Room { public SuiteRoom(String roomId) { super(roomId, 套房, 680); } Override public double calcPrice(int days) { if (isWeekend()) { return super.calcPrice(days) * 1.2; // 周末上浮 20% } return super.calcPrice(days); } private boolean isWeekend() { DayOfWeek day LocalDate.now().getDayOfWeek(); return day DayOfWeek.SATURDAY || day DayOfWeek.SUNDAY; } Override public String getRoomCategory() { return 豪华套房/大床; } }主程序遍历ListRoom时完全不用关心具体房型room.calcPrice(days)自动走进正确的实现。这就是多态在业务代码里的典型落点Java 基础面试问“多态解决了什么问题”可以直接拿这个场景回答消除类型分支判断让新增房型只改类、不改流程。更关键的是Test 阶段你可以分别对两个子类断言不同的计价结果不用 mock 任何东西。2.3 Guest 与 Hotel 的聚合职责Guest 类承担客户信息载体字段与 guest 表一一对应姓名、手机号、证件号外加当前房间号、入住时间等临时状态。Hotel 持有ListRoom提供 findRoom、checkIn、checkOut 等方法。这样设计报告里的类图层次很清楚Room 和 Guest 是实体类Hotel 是门面类Main 只负责组装JavaFX 的 Controller 只处理界面事件。类名关键字段主要方法说明RoomroomId, type, price, statuscalcPrice, changeStatus每间房一条记录Guestname, phone, idCard, roomIdgetters/setters对应 guest 表HotelListRoomfindRoom, checkIn, checkOut业务聚合入口Main无start, main启动 JavaFX 容器很多人觉得实体类写 getter/setter 啰嗦但课程设计阶段保留它们有三个实际好处第一JDBC 的 ResultSet 到对象映射可以直接调用第二JavaFX 的 TableView 在多列绑定时反射更稳定第三报告里写“封装性”时有现成代码可截图。真正的问题不在 getter/setter而在于让外部代码绕过 changeStatus 直接改状态字段所以 status 字段一定不要提供公开 setter。3. 状态机驱动的入住退房逻辑与跨天计费实现3.1 房间状态机四个状态三条合法迁移边酒店业务的典型状态有四个0 空闲、1 预订、2 入住、3 维修。合法迁移并不是所有状态之间都可达需要先列一张状态邻接表原状态目标状态触发动作0 空闲1 预订创建预订单0 空闲2 入住直接开房1 预订2 入住到店核销2 入住0 空闲退房结账2 入住3 维修房间故障很多课程设计会写成“点按钮就把房间状态改掉”完全不做状态校验。真正的业务系统里入住、退房、报修是三个入口入口之间应该共用同一套状态迁移规则否则会出现“房间已入住却还能被预订”这种脏数据。实现上不需要引入 State 模式changeStatus(from, to) 这种轻量校验已经够用字节码级别加锁两个线程并发修改同一个 room 对象时只有一个能成功非法迁移路径直接返回 false由调用方决定抛业务异常还是弹窗提示。3.2 入住状态校验、客户登记、SQL 写库三件事入住操作的代码已经能映射到真实需求public boolean checkIn(Hotel hotel, Guest guest, String roomId) { Room room hotel.findRoom(roomId); if (room null) { throw new BusinessException(房间不存在 roomId); } // 空闲(0) - 入住(2) if (!room.changeStatus(0, 2)) { throw new BusinessException(房间当前不可入住状态码 room.getStatus()); } guest.bindRoom(roomId); DbUtil.updateRoomStatus(roomId, 2); // 同步到数据库 return true; }changeStatus(0, 2) 是核心闸门第一个参数是期望的当前状态第二个参数是目标状态。如果房间已经被订走或入住调用方会收到 BusinessException界面层统一捕获后弹窗而不是返回一个容易被人忽略的 false。注意这里先改内存状态、再写数据库因此内存状态变更和 SQL 更新必须成对出现否则程序退出重进后状态会回退。用 BusinessException 而不是返回错误码是为了让 JavaFX 的 Controller 只写一个 try-catch 就能覆盖所有业务失败场景。3.3 退房结算与跨天计费的坑退房结算是系统中除入住之外最容易被扣分的地方难点几乎都在“入住天数怎么算”。粗暴的(now - checkIn) / 86400000有个明显问题当晚 8 点入住、次日上午 10 点退房毫秒差不足 24 小时算出 0 天这显然是错的。常见做法是“按中午 12 点作为新一天的分界”代码里用偏移量进位private int calcDays(Date checkIn, Date now) { long ms now.getTime() - checkIn.getTime(); // 加 12 小时再整除 24 小时超过中午 12 点即算多一天 long nights (ms 12 * 3600_000L) / 86_400_000L; return (int) Math.max(1, nights); }12 * 3600_000L是 12 小时的毫秒数等于把时间轴右移半天再整除一天本质是“超过中午 12 点就算多一天”。取Math.max(1, nights)保证哪怕入住和退房在同一分钟内也至少收一天房费。这个边界逻辑放进代码里老师问“客人呆不到一天怎么收钱”就有明确答案。接下来是结算主体public double settle(Guest guest, Room room, double extraFee) { int days calcDays(guest.getCheckInTime(), new Date()); double roomFee room.calcPrice(days); double total roomFee Math.max(0, extraFee); if (!room.changeStatus(2, 0)) { // 入住 - 空闲 throw new BusinessException(房间状态异常无法办理退房); } guest.unbindRoom(); System.out.printf([退房] 房号%s 天数%d 房费%.2f 杂费%.2f 合计%.2f%n, room.getRoomId(), days, roomFee, extraFee, total); return total; }这个结算逻辑里藏着两个容易被忽略的约束杂费必须是非负数否则客户可以通过负数杂费反向退款退房动作发生在金额计算完成后、返回结果前保证计费抛异常时房间状态不被破坏。真实项目里这里的房费写入、杂费写入、状态变更要放进同一个数据库事务第 4 章专门处理这个边界。4. JDBC 层落地建表、连接参数与事务边界4.1 先建表Java 对象和关系表一一对应写 JDBC 之前先把库建出来。下面的 SQL 适用于 MySQL 8.x 环境CREATE DATABASE hotel_db DEFAULT CHARSET utf8mb4; USE hotel_db; CREATE TABLE room ( room_id VARCHAR(10) PRIMARY KEY, type VARCHAR(20) NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 ); CREATE TABLE guest ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18) UNIQUE NOT NULL ); CREATE TABLE reservation ( id INT AUTO_INCREMENT PRIMARY KEY, guest_id INT NOT NULL, room_id VARCHAR(10) NOT NULL, check_in DATETIME, check_out DATETIME, days INT, total_fee DECIMAL(10,2), status TINYINT DEFAULT 0, CONSTRAINT fk_resv_guest FOREIGN KEY (guest_id) REFERENCES guest(id), CONSTRAINT fk_resv_room FOREIGN KEY (room_id) REFERENCES room(room_id) );status 用 TINYINT 而不是 VARCHAR是为了和 Java 里的 int 状态码直接映射省去字符串解析。外键约束兜底reservation 里不能出现 room 表不存在的房号。设计报告里画 ER 图时这三张表就是最核心的实体表把 id_card 设计成 UNIQUE是防止同一个证件号在不同姓名下重复登记这类约束放在数据库层面比在 Java 代码里做 if 判断可靠得多。4.2 连接参数与驱动乱码、时区、SSL 三个坑Java 工程里连 MySQL 最常用的驱动是 mysql-connector-j这里给一个简化版连接工具类import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/hotel_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8; private static Connection conn; public static synchronized Connection getConnection() throws SQLException { if (conn null || conn.isClosed()) { conn DriverManager.getConnection(URL, root, your_password); } return conn; } }getConnection 用 synchronized 修饰避免多线程同时创建连接开发阶段单连接够用生产环境要换成连接池。URL 里的参数分别解决三个具体问题useSSLfalse避免本地 MySQL 8 默认开启 SSL 后建连告警serverTimezoneAsia/Shanghai防止日期读写相差 8 小时characterEncodingutf8保证中文房名不会变成问号。下表可以直接写进设计报告参数作用踩坑提示useSSLfalse关闭 SSL 连接本地开发不加会看到大量 SSL 警告serverTimezone指定服务器时区不加可能报 CST 时区转换错误characterEncodingutf8设置字符编码乱码先查这一项allowPublicKeyRetrievaltrueMySQL 8 缓存 sha2 密码报 Public Key Retrieval 时追加4.3 PreparedStatement 与退房事务写 SQL 最容易出现字符串拼接注入。课程设计里不一定有人攻击你但答辩老师会问“查询安全性怎么保证”。标准回答就是用 PreparedStatementpublic static void updateRoomStatus(String roomId, int status) throws SQLException { String sql UPDATE room SET status ? WHERE room_id ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, status); ps.setString(2, roomId); ps.executeUpdate(); } }?占位符把参数和 SQL 模板分开setInt、setString 负责安全转义既避免注入风险也避免每次拼接字符串产生额外对象。退房结算涉及两步写操作更新 room.status 为 0、插入 reservation 记录。两步必须打包成一个事务否则中途失败会出现“房间已退但账单丢失”Connection conn DbUtil.getConnection(); conn.setAutoCommit(false); try { updateRoomStatus(roomId, 0); insertReservation(guestId, roomId, days, total); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); }事务口诀先关自动提交再执行多条更新最后 commit任何一个 SQL 失败就走 rollback。rollback 之后要恢复自动提交否则这个连接后续执行的每条 SQL 还留在未提交状态。这个细节在 java 面试的数据库隔离级别题里经常以各种形式出现对照这段代码理解比死记“原子性”定义深刻得多。5. JavaFX 界面接入与并发入住验证三个排错细节5.1 用 TableView 刷新房间状态Main 启动后通常用 TableView 展示房间列表列绑定房间号、房型、价格、状态字段。查询动作放到后台线程时回填数据必须回到 UI 线程Platform.runLater(() - tableView.setItems(rooms));JavaFX 的 UI 线程不是线程安全的后台线程直接操作 TableView 会抛Not on FX application thread。凡是看到这个异常基本都是忘了包这层。如果你在 Controller 里维护了一个ObservableListRoomsetItems 一次全量替换即可课程设计阶段不需要做增量更新。5.2 并发入住验证场景想验证第 2 章 changeStatus 的 synchronized 有没有生效不需要多客户端。写完 checkIn 后写一个临时测试类两个线程同时办理同一间房ExecutorService pool Executors.newFixedThreadPool(2); for (int i 0; i 2; i) { pool.execute(() - { try { service.checkIn(hotel, guest, A101); } catch (BusinessException e) { System.out.println(其中一个线程被拒绝: e.getMessage()); } }); } pool.shutdown();预期结果是一个线程成功另一个抛业务异常。如果没有 synchronized两个线程可能同时通过状态校验导致同一房间被登记给两位客人这是酒店系统最不能容忍的数据冲突。5.3 容易被扣分的三个收尾事项第一mysql-connector-j 的 jar 包要放进项目的 lib 目录并加入构建路径否则运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。第二用 try-with-resources 管理连接时如果连接来自 DbUtil 单例不要在 finally 里 close否则后续调用会拿到已关闭的 Connection。第三答辩前做一个“需求点到实现方法”的映射表逐项勾选预订、入住、退房、结账、查询空房。检查表里每一项都能对应到具体类和具体方法代码是完整的演示时就不会被突然打断后找不到入口。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门