JavaEE小型酒店管理系统毕业设计:房态、预订与退房结算 简介面向JavaEE课程设计与毕业设计场景的小型酒店管理系统源码包适合具备Java Web基础、希望完整实践SSHSpringStrutsHibernate分层开发的学生与初级开发者可借此理解酒店业务从请求处理、事务管理到数据库映射的落地方式。压缩包共262个文件、约18.69MB以49个java源码、25个jsp页面、24个xml配置、54个class及54个jar依赖为主另含properties、样式脚本和说明文档覆盖源码、依赖与页面资源。目前已有1695人学习下载。系统涵盖用户权限、房间状态与价格、预订入住退房、账单收费、经营报表及参数设置等模块Controller调用Service、Hibernate完成实体映射JSP结合EL/JSTL展示数据读者可据此梳理SSH项目的包结构、配置方式与事务边界并参考业务表设计和测试类组织形成可复用的毕业设计实现思路与排错参照。1. 小型酒店管理系统的需求切口为什么先做房态而不是先做订房小型酒店管理系统毕业设计最容易翻车的地方不是技术栈不够新而是功能铺得太大。很多同学一上来就想做在线预订、会员积分、报表大屏结果做到答辩前一周前台最核心的“看房态、办入住、退房结算”还没跑通。我在带 JavaEE 毕业设计时一般会建议先把需求收窄系统服务对象是小型酒店的前台和店长日客房量在几十间以内核心诉求是房态准确、入住可追溯、账务能对上。功能范围控制在房态管理、预订、入住退房、账务流水和权限五个模块技术栈用 JavaEE 常见的 Spring MVC MyBatis JSP MySQL部署在 Tomcat 上一台普通笔记本就能演示。这套方案适合需要交毕业设计或课程设计的学生也适合刚接手小型酒店类项目的开发者做参考。先把房态这个“地基”做稳后面的预订和入住才有意义。2. 技术选型与总体架构JavaEE 分层落地和四个模块的职责边界2.1 技术栈取舍Spring MVC MyBatis JSP 的毕业设计合理性小型酒店管理系统毕业设计通常只有 8 到 12 周一个人或两三个人完成技术选型的首要目标是“能跑通、能讲清、能演示”。JavaEE 体系里Spring MVC MyBatis JSP MySQL Tomcat 是比较稳的组合请求从 Controller 进入Service 管业务和事务Mapper 管 SQLJSP 负责页面渲染答辩时打开浏览器就能演示不依赖额外的前端构建环境。微服务、前后端分离不是不能做但时间会耗在接口联调、跨域和部署上最后业务逻辑反而讲不深。我一般会把版本统一锁死避免“在我电脑上能跑”的玄学问题。!-- pom.xml 关键依赖版本按实验室或学校环境统一锁定 -- dependencies !-- Spring MVC处理 Web 请求和视图跳转 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.39/version /dependency !-- MyBatis 与 Spring 整合负责 SQL 映射 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency !-- MySQL 驱动版本与本地数据库保持一致 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Servlet API 由 Tomcat 提供scope 设为 provided 避免冲突 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies这段配置的重点不在版本号而在依赖边界。javax.servlet-api用provided表示编译时需要、运行时由 Tomcat 容器提供打包成 war 后不会和容器自带的 Servlet 包打架。MySQL 驱动版本要和本机数据库匹配否则会出现连接失败但报错信息很模糊的情况。打包方式在pom.xml里设为war把 Tomcat 作为外部容器答辩时换一台机器也能部署。数据库连接信息不要写死在 Java 代码里放到jdbc.properties演示前只改一个文件就能切换环境。2.2 三层架构与包结构从 controller 到 mapper 的调用链小型系统不需要过度分层但 Controller、Service、Mapper 三层必须清晰。Controller 只做参数接收、简单校验和视图跳转不写业务判断Service 承担业务规则和事务边界比如“预订占房”必须在一个事务里完成房态更新和订单写入Mapper 只负责 SQL不写“如果状态等于几就怎样”的逻辑。包结构按职责划分避免所有类堆在一个包下。src/main/java/com/example/hotel ├── controller # 接收页面请求返回 JSP 视图或 JSON ├── service # 业务规则、事务边界、跨模块编排 │ └── impl ├── mapper # MyBatis 接口只声明数据访问方法 ├── entity # 与数据库表对应的持久化对象 ├── dto # 页面入参和出参避免实体直接暴露给页面 └── common # 统一异常、拦截器、工具类 src/main/resources ├── mapper # MyBatis XML写 SQL 和结果映射 ├── spring # Spring 容器与 MVC 配置 └── db # schema.sql、data.sql 初始化脚本以一个“查询房态列表”的请求为例调用链是RoomController.list()→RoomService.listRooms()→RoomMapper.selectRoomList()→room表。Controller 不直接调 Mapper否则事务和权限校验会散落在各处。DTO 和 Entity 分开的好处是页面字段可以裁剪比如入住登记页面不需要把数据库里的version字段暴露出去。统一异常放在common里Service 抛出业务异常Controller 统一捕获后跳转到错误页避免每个方法都写一遍 try-catch。这套结构不复杂但答辩时能清楚回答“请求怎么走、事务在哪一层、SQL 在哪定义”。2.3 模块边界房态、预订、入住退房、账务与权限的拆分模块拆分的原则是“一个模块只对自己核心表负责”。房态模块管房间档案和状态变更预订模块管预订单入住退房模块管在住记录账务模块管流水权限模块管用户和角色。跨模块操作通过 Service 调用完成不直接跨层访问对方的 Mapper。下面这张表可以作为毕业设计开题时的模块说明。模块核心职责关键表典型入口房态模块房间档案、可售查询、维修锁房、状态变更room、room_status_log/room/list、/room/status预订模块预订单创建、取消、到店确认booking/booking/create入住退房入住登记、换房、退房、在住查询checkin/checkin/save、/checkout/settle账务模块押金、消费、退款、结算流水account_flow/account/flow权限模块登录、角色、菜单、操作日志sys_user、sys_role、operation_log/login、/user/list模块边界清楚之后后面加功能不容易乱。比如“换房”看起来是房态模块的事实际涉及入住记录和两个房间的状态应该放在入住退房模块的 Service 里由它调用房态模块的更新方法。账务模块不直接改房态退房时由入住退房模块先结算、再释放房间。权限模块用拦截器做后端校验不能只在 JSP 里隐藏按钮。小型酒店管理系统毕业设计能把这几条边界讲清楚架构部分基本就稳了。3. 数据库与核心表设计房态、订单、入住记录怎么落表3.1 房间与房态表把“可售/在住/维修”拆成状态机房态表最容易犯的错是只在一个room表里放一个status字段然后所有业务都直接改它。这样短期能跑但一旦出现“前台显示空房、客房说有客人”的情况就查不出是谁改的。通常的做法是保留房间主表的状态字段作为当前快照同时增加一张状态变更流水表每次变更都记一条。状态值不要用魔法数字散落在代码里统一约定0 可售、1 在住、2 维修、3 预订。CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(16) NOT NULL COMMENT 房间号如 301, room_type VARCHAR(32) NOT NULL COMMENT 房型如大床房、标间, price DECIMAL(10,2) NOT NULL COMMENT 门市价金额用 DECIMAL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可售 1在住 2维修 3预订, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_room_no (room_no) ) COMMENT房间档案与当前房态快照; CREATE TABLE room_status_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, from_status TINYINT NOT NULL, to_status TINYINT NOT NULL, biz_type VARCHAR(32) NOT NULL COMMENT BOOKING/CHECKIN/CHECKOUT/REPAIR, biz_id BIGINT DEFAULT NULL COMMENT 关联业务主键, operator_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_room_time (room_id, created_at) ) COMMENT房态变更流水用于对账和排查;room.status是当前快照room_status_log是黑匣子。每次房态变更Service 里先更新room再插入一条日志两个动作放在同一个事务里。version字段用于乐观锁防止两个前台同时把同一间房改成不同状态。biz_type和biz_id记录这次变更来自哪张业务单比如入住登记对应CHECKIN和checkin.id退房对应CHECKOUT。答辩时如果被问“怎么保证房态可追溯”把这张流水表拿出来比口头解释更有说服力。3.2 预订与入住表从预订单到在住记录的字段与关联预订单和在住记录要分开。预订单表示“客人计划来住”在住记录表示“客人已经入住”。两者用booking_id关联散客没有预订单时该字段可以为空。预订表需要保留入住日期和离店日期方便查询某一天的可售房入住表需要保留实际入住时间、押金和在住状态。金额字段统一用DECIMAL(10,2)不要用FLOAT或DOUBLE否则退房结算时容易出现几分钱的误差。CREATE TABLE booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_no VARCHAR(32) NOT NULL COMMENT 预订单号业务唯一, room_id BIGINT NOT NULL, guest_name VARCHAR(64) NOT NULL, guest_phone VARCHAR(32) NOT NULL, check_in_date DATE NOT NULL COMMENT 计划入住日期, check_out_date DATE NOT NULL COMMENT 计划离店日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待入住 1已入住 2已取消 3已退房, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_booking_no (booking_no), KEY idx_room_date (room_id, check_in_date, check_out_date) ) COMMENT预订单; CREATE TABLE checkin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_id BIGINT DEFAULT NULL COMMENT 散客入住时为空, room_id BIGINT NOT NULL, guest_name VARCHAR(64) NOT NULL, guest_id_no VARCHAR(32) DEFAULT NULL COMMENT 证件号演示数据脱敏, check_in_time DATETIME NOT NULL, check_out_time DATETIME DEFAULT NULL COMMENT 为空表示仍在住, deposit DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 押金, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在住 1已退房, KEY idx_room_status (room_id, status) ) COMMENT入住记录;预订表的状态流转要固定创建后为 0办理入住后改成 1取消改成 2退房改成 3。入住表的check_out_time为空表示在住这个判断比额外加一个字段更直接。idx_room_date索引用于按房间和日期区间查询但要注意日期区间重叠的 SQL 写法常见条件是check_in_date 离店日期 AND check_out_date 入住日期不要写成简单的BETWEEN否则边界日期会算错。这些小地方在答辩时很容易被追问。3.3 账务流水与操作日志毕业设计里最容易被忽略的两张表很多毕业设计只做“退房时弹一个金额”不落账务流水最后金额对不上就查不出原因。账务表按流水记录不做“余额字段反复更新”每条记录表示一次资金动作收押金、记消费、退款、结算。金额用正负号区分收入支出汇总时直接SUM简单可靠。操作日志表记录关键动作比如谁改了房态、谁删了订单答辩时能回答“这个操作是谁做的”。CREATE TABLE account_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, checkin_id BIGINT NOT NULL, flow_type VARCHAR(32) NOT NULL COMMENT DEPOSIT/CONSUME/REFUND/SETTLE, amount DECIMAL(10,2) NOT NULL COMMENT 正数收入负数支出, remark VARCHAR(255) DEFAULT NULL, operator_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_checkin (checkin_id) ) COMMENT账务流水; CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, module VARCHAR(32) NOT NULL COMMENT ROOM/BOOKING/CHECKIN/ACCOUNT, action VARCHAR(32) NOT NULL COMMENT CREATE/UPDATE/DELETE/SETTLE, target_id BIGINT DEFAULT NULL, detail VARCHAR(512) DEFAULT NULL COMMENT 变更前后摘要, ip VARCHAR(64) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at) ) COMMENT关键操作日志;账务流水不要只存一个“最终金额”要把押金、消费、退款分开记。退房时先汇总流水再写一条SETTLE结算记录最后更新入住状态和房态。操作日志不需要记所有查询只记新增、修改、删除和结算这类关键动作。日志内容不要存完整证件号演示数据也要脱敏detail里存“房态从 3 改为 1”这种摘要就够。这两张表在论文里可能只占一小节但它们是系统能不能自圆其说的关键。4. 关键业务流程实现从预订到退房的代码落点4.1 房态查询与预订占房库存扣减与并发校验预订占房的核心不是“插一条订单”而是“先确认房间在目标日期可用再同时改房态和写订单”。小型系统不需要复杂的库存表但必须防止两个请求同时把同一间房订出去。我一般用“状态条件更新 乐观锁版本号”做第一道防线再用数据库唯一约束做第二道防线。下面这段 Service 代码把占房和写订单放在同一个事务里任何一步失败都回滚。Service public class BookingService { Autowired private RoomMapper roomMapper; Autowired private BookingMapper bookingMapper; Transactional(rollbackFor Exception.class) public Long createBooking(BookingDTO dto) { // 1. 带版本号的条件更新只有房态仍为“可售”且版本一致才占房成功 int updated roomMapper.updateStatusForBooking( dto.getRoomId(), dto.getVersion(), 0, 3); if (updated 0) { throw new BizException(房间已被占用请刷新房态后重试); } // 2. 写入预订单booking_no 由日期、房间号和序列生成 Booking booking new Booking(); booking.setBookingNo(generateNo(dto)); booking.setRoomId(dto.getRoomId()); booking.setGuestName(dto.getGuestName()); booking.setGuestPhone(dto.getGuestPhone()); booking.setCheckInDate(dto.getCheckInDate()); booking.setCheckOutDate(dto.getCheckOutDate()); booking.setStatus(0); booking.setTotalAmount(dto.getTotalAmount()); bookingMapper.insert(booking); return booking.getId(); } }updateStatusForBooking对应的 Mapper SQL 必须把status和version都放进WHEREUPDATE room SET status #{toStatus}, version version 1 WHERE id #{roomId} AND status #{fromStatus} AND version #{version}。参数含义分别是房间主键、页面带来的版本号、期望的原状态和目标状态。返回 0 表示有人先改了房态当前请求不能继续。事务注解不能省否则房态改了、订单没写就会出现“房间被占但没有订单”的脏数据。预订成功后页面要刷新房态不能只提示“预订成功”就完事。4.2 入住登记与换房入住记录写入和房态联动入住登记有两种来源预订转入住和散客直接入住。预订转入住时先把预订单状态从“待入住”改成“已入住”用条件更新防止同一个预订单被重复办理然后再插入入住记录、更新房态、写操作日志。散客入住没有预订单直接插入入住记录并把房态改成在住。换房则是在入住记录上改room_id同时释放原房间、占用新房间三个动作必须在同一个事务里。Service public class CheckinService { Autowired private CheckinMapper checkinMapper; Autowired private RoomMapper roomMapper; Autowired private OperationLogMapper logMapper; Transactional(rollbackFor Exception.class) public Long checkIn(CheckinDTO dto) { // 预订转入住条件更新预订单状态返回 0 说明已被处理过 if (dto.getBookingId() ! null) { int rows checkinMapper.markBookingCheckedIn(dto.getBookingId()); if (rows 0) { throw new BizException(预订单状态已变更请刷新后重试); } } Checkin checkin new Checkin(); checkin.setBookingId(dto.getBookingId()); checkin.setRoomId(dto.getRoomId()); checkin.setGuestName(dto.getGuestName()); checkin.setGuestIdNo(dto.getGuestIdNo()); checkin.setCheckInTime(new Date()); checkin.setDeposit(dto.getDeposit()); checkin.setStatus(0); checkinMapper.insert(checkin); // 房态从可售或预订改为在住 roomMapper.updateStatus(dto.getRoomId(), 1); logMapper.insert(new OperationLog(dto.getOperatorId(), CHECKIN, 入住, checkin.getId())); return checkin.getId(); } }markBookingCheckedIn的 SQL 要带状态条件例如UPDATE booking SET status 1 WHERE id #{id} AND status 0。返回 0 时不要继续插入入住记录否则会出现同一预订单两条在住记录。updateStatus也应该带原状态条件避免把维修中的房间直接改成在住。换房方法可以单独写一个changeRoom(checkinId, newRoomId)事务里先校验新房间可售再更新入住记录最后把原房间改回可售、新房间改为在住。操作日志要记清楚“从哪个房间换到哪个房间”答辩演示时这条日志很好用。4.3 退房结算与账务流水金额计算和事务边界退房结算是最容易出金额问题的地方。正确顺序是锁定入住记录、汇总账务流水、计算应退或应补金额、写结算流水、更新入住状态、释放房态。查询入住记录时用SELECT ... FOR UPDATE锁行防止两个收银员同时点退房。金额统一用BigDecimal不要用double做加减。下面代码做了简化实际项目里还要区分押金退还、消费补收和优惠减免。Service public class CheckoutService { Autowired private CheckinMapper checkinMapper; Autowired private AccountFlowMapper accountFlowMapper; Autowired private RoomMapper roomMapper; Transactional(rollbackFor Exception.class) public BigDecimal checkout(Long checkinId, Long operatorId) { // 行锁防止并发退房同一入住记录只能结算一次 Checkin checkin checkinMapper.selectByIdForUpdate(checkinId); if (checkin null || checkin.getStatus() ! 0) { throw new BizException(入住记录不存在或已退房); } // 汇总流水押金为正消费为负退款为负结算前得到余额 BigDecimal total accountFlowMapper.sumAmountByCheckinId(checkinId); BigDecimal payable total.max(BigDecimal.ZERO); // 写结算流水正数表示客户补款负数表示酒店退款 accountFlowMapper.insert(new AccountFlow( checkinId, SETTLE, payable, operatorId)); checkinMapper.updateCheckout(checkinId, new Date()); roomMapper.updateStatus(checkin.getRoomId(), 0); return payable; } }selectByIdForUpdate对应的 SQL 是SELECT * FROM checkin WHERE id #{id} FOR UPDATE它依赖数据库行锁所以整个方法必须在事务里执行。sumAmountByCheckinId对account_flow.amount求和正负号约定要在数据库注释和代码里保持一致。payable.max(BigDecimal.ZERO)只是示意真实逻辑要判断余额为正时收补款、为负时退押金。退房成功后房态改回可售但不要直接删入住记录历史数据要保留。金额计算、流水写入、房态释放三步在同一个事务里任何一步抛异常都整体回滚这是退房不翻车的基本要求。5. 避坑与常见问题房态不一致、重复预订和金额对不上怎么排查5.1 房态不一致多入口更新和异常未回滚现象是前台页面显示某间房“可售”客房阿姨却说里面有人或者客人已经退房系统里还是“在住”。原因通常有三个房态更新散落在多个 Controller 里有的地方改了room.status但没写日志入住和退房不在同一个事务里订单写失败但房态已经改了维修锁房和预订占房相互覆盖后一次更新没有校验原状态。解决方式是把房态变更收敛到房态模块的 Service所有入口统一调用更新时带原状态条件失败就抛异常回滚。再补一个每日对账 SQL查room.status 1但没有在住记录的房以及有在住记录但房态不是 1 的房。发现不一致时以入住记录为准人工修复并补一条room_status_log不要直接改数据库了事。5.2 重复预订先查后插在并发下会超卖现象是同一间房、同一个日期段出现两张有效预订单前台只能给其中一位客人换房。原因很典型代码先SELECT查可售再INSERT写订单两个请求同时查到“可售”然后都插入成功。单机演示不容易复现答辩时一紧张反而可能点出来。解决分两层第一层用状态条件更新或乐观锁占房失败就提示刷新第二层在数据库加唯一约束或幂等键比如用room_id check_in_date check_out_date 有效状态做唯一索引或者在预订单表加request_id防重复提交。对于小型系统状态条件更新加唯一约束已经够用。测试时用两个浏览器同时提交同一间房看是否只有一个成功这个场景答辩前一定要跑。5.3 金额对不上浮点数、优惠顺序和押金抵扣现象是退房金额和预订单金额差几毛甚至几块或者押金退了两次。原因通常是金额用了double多次加减后出现精度误差优惠、押金、消费的抵扣顺序不固定不同入口算出不同结果退房接口没有幂等重复点击写了多条结算流水。解决方式很明确数据库金额字段用DECIMAL(10,2)Java 用BigDecimal并且统一setScale(2, RoundingMode.HALF_UP)把计算顺序写死在 Service 里先汇总消费、再抵扣押金、最后算补退退房时用入住记录行锁和状态条件更新已经退房的记录不允许再次结算。账务流水只增不改出现差异时查流水比查余额字段靠谱得多。5.4 权限越权与日志缺失按钮隐藏不等于后端校验现象是普通前台账号能通过直接输入 URL 打开订单删除页面或者答辩老师问“这条房态是谁改的”时答不上来。原因是在 JSP 里用if隐藏了按钮但后端接口没有校验角色操作日志只记了登录没有记关键业务动作。解决方式是在 Spring MVC 拦截器里统一校验登录和角色Controller 方法上再加一层权限注解或手动判断关键操作统一调用日志服务记录用户、模块、动作、目标 ID 和变更摘要。权限表不用做得太复杂用户、角色、角色菜单三张表就够。答辩时演示一个“越权访问被拦截”的场景比只展示成功登录更加分。5.5 演示环境翻车硬编码路径和缺失的初始化脚本现象是答辩现场数据库连不上、页面 404、房态列表空白。原因往往是数据库连接写死在代码里、建表语句只在自己电脑上执行过、Tomcat 版本和编译版本不一致。解决方式是提供schema.sql和data.sql演示前先跑一遍初始化数据库配置外置到jdbc.properties换环境只改配置打包用mvn clean package生成 war提前在答辩用的机器上部署一次。我一般还会准备一个“演示数据最小集”至少 6 间房、2 条预订单、1 条在住记录保证从登录到退房全流程能走通。别等到现场才连数据库这是血泪经验。6. 验收与答辩按这套清单跑一遍演示不翻车验收不要只看“页面能不能打开”要按业务闭环走。我一般会让同学按“登录 → 房态列表 → 创建预订 → 预订转入住 → 换房 → 退房结算 → 查看账务流水和操作日志”的顺序演示一遍再补两个异常场景同一间房重复预订、已退房记录再次退房。下面这张清单可以直接拿去对。验收项通过标准常见失败点登录与权限不同角色菜单不同越权 URL 被拦截只隐藏按钮后端没校验房态查询可售、在住、维修、预订状态显示正确状态值前后端不一致创建预订占房成功写订单失败提示刷新先查后插导致超卖预订转入住预订单状态只变一次入住记录唯一重复点击生成两条记录换房原房间释放新房间占用日志可查只改入住表不改房态退房结算金额与流水一致房态释放浮点数误差、重复结算日志与初始化关键操作有记录空库能一键初始化只在自己电脑上能跑答辩时老师常问三个问题为什么用乐观锁、事务边界在哪、金额为什么用BigDecimal。回答时不要背概念直接指着代码说“占房更新带 version返回 0 就回滚”“退房方法整个在Transactional里行锁防重复结算”“金额字段是 DECIMALJava 用 BigDecimal 并统一保留两位”。演示前把数据库初始化脚本跑一遍浏览器清缓存Tomcat 日志开着出现异常先看控制台第一行报错不要急着改代码。从那以后我每次带毕业设计验收都强制让同学按这条闭环走一遍再故意制造一次重复预订和一次重复退房能扛住这两个场景系统基本就稳了。希望帮到你。本文还有配套的精品资源点击获取