小型酒店管理系统毕设设计:Servlet+JSP实现状态机与事务闭环

发布时间:2026/10/23 14:05:04
小型酒店管理系统毕设设计:Servlet+JSP实现状态机与事务闭环
简介这是一套面向计算机专业本科生的JavaEE毕业设计实战项目聚焦小型酒店业务全流程管理帮助学习者系统掌握SSHSpringStrutsHibernate企业级开发技术栈。资源完整实现用户管理、房间状态监控、在线预订与入住退房、账单结算及经营报表等核心模块覆盖MVC分层架构、ORM映射、事务控制与Web交互等关键能力训练。压缩包共262个文件含49个Java源码如OrderFormAction、UserAction等典型控制器与服务类、25个JSP页面负责前端展示、24个XML配置文件Spring/Struts/Hibernate核心配置、54个class字节码及54个jar依赖库辅以图片、CSS、JS及properties配置资源整体18.69MB结构清晰便于逐层研读与调试。已有1694人学习下载提供可直接运行的完整工程包含实体类定义、Service业务逻辑、Action控制器及JSP视图链路是理解SSH整合原理与酒店系统业务建模的理想实践样本。1. 小型酒店管理系统为什么一个“只管20间房”的系统反而最容易在毕设答辩里翻车很多同学拿到“小型酒店管理系统”这个毕设题目时第一反应是松口气——不就是增删改查嘛Spring Boot搭个CRUD前端用Thymeleaf或Vue写几个表单三天就能跑起来。但去年我帮某高校三位同学做中期检查时发现80%的“能运行”系统在导师问出“客人退房时怎么确保房间状态、账单、库存比如毛巾/洗浴用品三者同步更新”这个问题后当场卡壳。不是代码写不出来而是从需求分析阶段就没想清楚“小型”二字的真实约束它不意味着功能缩水而是要求逻辑闭环更紧、边界更清晰、异常路径更显性。这套Java EE方案专为这类真实场景设计——它不堆砌微服务、不硬套分布式事务而是用JDBCServletJSP兼容Java EE 7规范打底把预订、入住、续住、退房、报表五大主流程拆成可验证的原子操作每个状态变更都带日志回溯和手动冲正入口。适合需要交完整文档、能现场演示、且答辩时经得起“如果……会怎样”连环追问的学生。2. 总体架构设计三层结构与四个核心模块的职责划分本系统采用经典Java EE分层架构但做了针对性精简去掉EJB容器依赖用Servlet直接调度业务逻辑层数据库层封装JDBC连接池与预编译语句模板。所有模块均通过接口定义契约便于后期替换如用MyBatis替代原生JDBC。整个系统部署为单WAR包Tomcat 8.5即可运行避免环境配置成为答辩拦路虎。2.1 模块划分与数据流向系统划分为四个高内聚模块非按技术栈切分而按酒店运营实体职责划分前台接待模块处理实时操作包括客房查询、预订登记、入住办理、续住操作、退房结算。所有操作必须支持“撤销”非数据库回滚而是生成反向业务单据。客房管理模块维护房间基础信息房号、类型、价格、状态、楼层分布图、维修记录。状态字段采用枚举值VACANT_CLEAN,OCCUPIED,DIRTY,MAINTENANCE禁止布尔型isAvailable。账务结算模块独立于前台操作支持按日结账、按订单结账、多支付方式现金/微信/支付宝模拟及发票打印。关键设计所有账单生成后不可修改金额仅允许“作废重开”保留原始单据痕迹。统计报表模块提供三种固定报表日入住率统计、月营收趋势、客户来源分析及一个自定义SQL查询入口仅限SELECT带白名单字段校验。提示模块间通信不通过全局变量或静态方法全部走RequestDispatcher.forward()或重定向携带参数保证请求链路可追踪。例如退房操作完成后跳转至账单页时URL中必须包含billId20240521001而非roomId301checkInDate2024-05-20。2.2 数据库设计6张表支撑全业务闭环共6张核心表严格遵循第三范式无冗余字段。关键设计点如下表名主要字段设计说明roomroom_id(PK),room_no,room_type,price_per_night,status,floorstatus为TINYINT映射枚举room_no唯一索引支持前缀查询如3F-guestguest_id(PK),id_card,name,phone,emailid_card加唯一索引杜绝重复登记email允许NULL因老年客人常不提供reservationres_id(PK),guest_id,room_id,check_in_date,check_out_date,status(RESERVED/CONFIRMED/CANCELLED)check_in_date与check_out_date组合索引加速房态查询check_in_recordrecord_id(PK),res_id,room_id,actual_check_in,deposit_amount,staff_id记录实际入住时间用于计算超时续住费用deposit_amount非空强制预收押金check_out_recordrecord_id(PK),record_id_from_checkin,actual_check_out,total_amount,paid_amount,payment_method,invoice_norecord_id_from_checkin外键关联入住记录确保退房必有入住system_loglog_id(PK),operator,operation_type,target_id,before_state,after_state,ip_address,create_time所有敏感操作入住/退房/修改房价必记日志before_state存JSON字符串如{status:OCCUPIED,deposit:200}-- 创建room表的关键DDL含注释 CREATE TABLE room ( room_id INT NOT NULL AUTO_INCREMENT COMMENT 主键, room_no VARCHAR(10) NOT NULL COMMENT 房号如301、VIP-01, room_type TINYINT NOT NULL COMMENT 房型1-标准间,2-大床房,3-套房, price_per_night DECIMAL(8,2) NOT NULL DEFAULT 0.00 COMMENT nightly价格, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-空闲清洁,2-已入住,3-待打扫,4-维修中, floor TINYINT NOT NULL COMMENT 所在楼层, PRIMARY KEY (room_id), UNIQUE KEY uk_room_no (room_no), KEY idx_floor_status (floor,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客房基础信息表;这段DDL不是照搬模板idx_floor_status复合索引直指高频查询场景——前台查“三楼还有哪些空房”uk_room_no确保房号不重复曾有学生因未加唯一索引导致录入301两次后续退房逻辑崩溃。status用TINYINT而非VARCHAR既节省空间又通过代码层枚举强约束避免数据库里出现vacant和VACANT混用。2.3 技术选型依据为什么坚持用ServletJSP而非Spring Boot这不是守旧而是对毕设场景的务实选择可追溯性优先Servlet生命周期init()→service()→destroy()与HTTP请求一一对应学生调试时能清晰看到“哪个方法处理了退房请求”而Spring Boot的自动装配会让初学者迷失在Controller、Service、Transactional的嵌套调用里。零配置部署WAR包丢进Tomcatwebapps即运行无需application.properties调优、无需理解spring.profiles.active。某同学曾因本地IDEA配置了H2内存库导出WAR到老师电脑上因没配MySQL驱动而报ClassNotFoundException耗掉答辩前两天。教学友好性JSP中c:forEach遍历房间列表、fmt:formatNumber格式化金额语法直观学生改一行HTML就能看到效果而Vue需理解组件、props、v-model绑定容易陷入“页面不刷新”的玄学问题。兼容性兜底Java EE 7规范被主流教材如《Java Web开发详解》覆盖老师熟悉Spring Boot 3.x要求JDK 17而部分高校机房仍为JDK 8。当然若你课程明确要求Spring Boot本方案可无缝迁移——业务逻辑层Service代码完全复用仅需将Servlet改为RestControllerJSP视图层替换为Thymeleaf模板数据库访问层从JDBC改为JdbcTemplate。迁移工作量约4小时我在附赠的《双版本对照手册》里已标出所有改动点。3. 核心业务实现入住与退房的原子性保障与状态机设计入住与退房是酒店系统最核心、也最容易出错的两个流程。很多毕设系统在此处用简单SQLUPDATE room SET statusOCCUPIED完事结果导致“客人已入住但账单未生成”或“退房成功房间状态却仍是OCCUPIED”。本方案采用显式状态机数据库事务业务日志三重保障。3.1 入住流程五步原子操作与异常熔断入住不是单次点击而是五个严格顺序执行的操作任一失败则整体回滚校验房态查询room表确认status VACANT_CLEAN且room_id未被其他预订占用关联reservation表查statusCONFIRMED且check_in_date today生成入住单插入check_in_recordactual_check_in取new Date()deposit_amount由前端传入强制≥200元更新房态UPDATE room SET statusOCCUPIED WHERE room_id?关闭预订UPDATE reservation SET statusCHECKED_IN WHERE res_id?记录日志插入system_logoperation_typeCHECK_INbefore_state存更新前的room行JSONafter_state存更新后的。// CheckInService.java 关键代码段 public boolean processCheckIn(int roomId, int guestId, BigDecimal deposit) { Connection conn null; PreparedStatement ps null; try { conn DataSourceUtil.getConnection(); // 自定义连接池获取 conn.setAutoCommit(false); // 开启事务 // 步骤1校验房态含子查询防并发 String checkSql SELECT status FROM room WHERE room_id ? AND status 1; ps conn.prepareStatement(checkSql); ps.setInt(1, roomId); ResultSet rs ps.executeQuery(); if (!rs.next()) { throw new BusinessException(房间不可用状态非空闲清洁或已被占用); } // 步骤2插入入住单 String insertRecordSql INSERT INTO check_in_record (res_id, room_id, actual_check_in, deposit_amount, staff_id) VALUES (?, ?, ?, ?, ?); ps conn.prepareStatement(insertRecordSql, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, getReservationIdByRoom(roomId)); // 伪代码实际查reservation表 ps.setInt(2, roomId); ps.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps.setBigDecimal(4, deposit); ps.setInt(5, getCurrentStaffId()); ps.executeUpdate(); // 步骤3 4更新房态 关闭预订省略具体SQL // ... conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { /* 忽略回滚异常 */ } } throw new BusinessException(入住失败 e.getMessage(), e); } finally { DataSourceUtil.closeQuietly(conn, ps, null); } }关键细节说明conn.setAutoCommit(false)是事务起点但不使用Transactional注解——毕设答辩时老师可能问“Spring的事务传播机制是什么”而原生JDBC事务能让你直答“就是JDBC API的setAutoCommit(false)”步骤1的AND status 1用数字而非字符串因room.status是TINYINT避免隐式类型转换getReservationIdByRoom()方法内部用SELECT res_id FROM reservation WHERE room_id ? AND status CONFIRMED确保入住的是已确认预订而非直接散客散客流程另起分支DataSourceUtil.closeQuietly()是自定义工具类确保Connection无论成功失败都释放防止连接池耗尽——这是学生最容易忽略的“隐形炸弹”。3.2 退房流程状态机驱动与冲正机制退房比入住更复杂因涉及金额计算、多支付方式、发票关联。本方案将退房抽象为状态机check_out_record.status字段控制流程状态值含义可触发操作0待结算计算费用、选择支付方式、生成账单1已结算打印发票、归档记录2已冲正仅允许查看不可再操作// CheckOutService.java 中的结算逻辑片段 public void settleCheckout(int recordId) { // 1. 查询入住记录获取入住时间 CheckInRecord inRecord checkInDao.findById(recordId); // 2. 计算应收费用按天数*房价 超时费超过12:00加收半天 long days ChronoUnit.DAYS.between(inRecord.getActualCheckIn().toLocalDate(), LocalDate.now()); BigDecimal baseFee inRecord.getRoomPrice().multiply(BigDecimal.valueOf(days)); // 3. 判断是否超时退房时间 当日12:00则加收半天房费 LocalTime checkoutTime LocalTime.now(); if (checkoutTime.isAfter(LocalTime.NOON)) { baseFee baseFee.add(inRecord.getRoomPrice().divide(BigDecimal.valueOf(2))); } // 4. 更新退房记录 CheckOutRecord outRecord new CheckOutRecord(); outRecord.setRecordIdFromCheckin(recordId); outRecord.setActualCheckOut(new Timestamp(System.currentTimeMillis())); outRecord.setTotalAmount(baseFee); outRecord.setStatus((byte)1); // 标记为已结算 checkOutDao.update(outRecord); // 5. 更新房间状态为DIRTY roomDao.updateStatus(roomId, (byte)3); }这里的设计哲学是把业务规则写死在代码里而不是藏在数据库触发器或存储过程中。因为规则易读if (checkoutTime.isAfter(LocalTime.NOON))比MySQL的TIME(NOW()) 12:00:00更直观规则可测JUnit可直接mockLocalTime.now()测试超时逻辑规则可审计所有费用计算步骤都在Java层日志可追溯每一步金额来源。3.3 避坑指南入住/退房流程中最容易翻车的5个点现象、原因、解决全是血泪经验总结现象多人同时预订同一房间最终两人入住成功原因校验房态时未加数据库行锁SELECT status FROM room WHERE room_id ?后另一线程也查到status1然后都执行UPDATE解决将校验SQL改为SELECT status FROM room WHERE room_id ? AND status 1 FOR UPDATE强制加写锁或在应用层用Redis分布式锁SETNX room:301 lock但毕设推荐前者更轻量。现象退房后房间状态变为OCCUPIED而非DIRTY原因UPDATE room SET statusDIRTY语句未执行因前面的账单计算抛异常但事务未回滚忘记conn.rollback()解决所有数据库操作必须包裹在try-catch-finally中catch块内强制rollback()finally块内close()连接本方案DataSourceUtil.closeQuietly()已内置此逻辑。现象客人用现金支付但系统显示“微信支付成功”原因前端传参payment_method2微信但后端未校验该值是否在合法枚举范围内直接拼SQL插入解决在Service层首行加入if (!PaymentMethod.isValid(code)) throw new BusinessException(非法支付方式)PaymentMethod为枚举类isValid()方法校验code是否为1/2/3。现象修改房价后历史账单金额被错误更新原因账单表check_out_record.total_amount未存快照而是每次查询时动态计算room.price * days解决total_amount字段必须在生成账单时写死不可动态计算room.price只影响未来入住不影响已结算账单。现象退房时系统卡死Tomcat线程池耗尽原因system_log表未建索引INSERT INTO system_log在高并发下锁表阻塞其他请求解决为system_log.create_time字段添加B-tree索引日志表单独建库或定期归档毕设可简化为每月清空加TRUNCATE TABLE system_log按钮。4. 前端交互与用户体验JSP模板中的防呆设计与状态反馈学生常把前端当“美化层”但酒店系统恰恰相反——好的交互能规避80%的业务错误。本方案JSP页面不追求炫酷专注三个原则状态可见、操作可逆、错误可读。4.1 房态可视化用CSS类名驱动状态感知房间列表页room/list.jsp不依赖JavaScript渲染状态而是由Servlet传入ListRoomJSP用JSTL循环输出c:forEach items${roomList} varroom div classroom-card ${room.statusCssClass} div classroom-no${room.roomNo}/div div classroom-type${room.typeName}/div div classroom-status${room.statusDesc}/div /div /c:forEach关键在room.statusCssClass——这是Room实体类的一个计算属性public class Room { private byte status; // 1,2,3,4 public String getStatusCssClass() { switch (status) { case 1: return status-vacant; case 2: return status-occupied; case 3: return status-dirty; case 4: return status-maintenance; default: return status-unknown; } } public String getStatusDesc() { switch (status) { case 1: return 空闲清洁; case 2: return 已入住; case 3: return 待打扫; case 4: return 维修中; default: return 状态异常; } } }这样做的好处前端零逻辑CSS类名由后端决定避免JS里写if(status1) addClass(vacant)降低出错概率状态强一致getStatusCssClass()与getStatusDesc()共用同一switch不可能出现“显示‘空闲清洁’但CSS却是红色背景”的矛盾扩展成本低新增状态只需改switch前后端无需联调。配套CSSstyle.css.room-card { border: 1px solid #ddd; padding: 10px; margin: 5px; } .status-vacant { background-color: #d4edda; border-left: 4px solid #28a745; } .status-occupied { background-color: #f8d7da; border-left: 4px solid #dc3545; } .status-dirty { background-color: #fff3cd; border-left: 4px solid #ffc107; } .status-maintenance { background-color: #cce5ff; border-left: 4px solid #007bff; }4.2 操作可逆所有关键操作提供“撤回”按钮在checkin/detail.jsp入住详情页底部固定位置放置c:if test${record.status 0} !-- 仅当入住单未结算时显示 -- button typebutton onclickconfirmRevoke(${record.recordId}) classbtn btn-warning btn-sm i classicon-undo/i 撤回入住 /button /c:ifconfirmRevoke()调用后台/revokeCheckIn?recordId123该接口执行查询check_in_record确认状态为0未结算UPDATE room SET statusVACANT_CLEAN WHERE room_id?UPDATE reservation SET statusCONFIRMED WHERE res_id?插入system_logoperation_typeREVOKE_CHECK_IN返回成功前端跳转回房间列表页。注意撤回操作不删除数据而是将状态置为可重用。这符合酒店管理规范——所有操作必须留痕不能物理删除。4.3 错误反馈用HTTP状态码自定义消息体替代alert弹窗当用户输入无效身份证号时Servlet不返回response.getWriter().print(身份证格式错误)而是// 在CheckInServlet中 if (!IdCardValidator.isValid(idCard)) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:400,\msg\:\身份证号码不合法请检查18位数字及X\}); return; }前端AJAX捕获$.post(/checkin, data, function(res) { // 成功处理 }).fail(function(xhr) { if (xhr.status 400) { var msg JSON.parse(xhr.responseText).msg; $(#error-msg).text(msg).show(); // 显示在表单上方 } });这种设计让错误可定位、可测试、可国际化msg可从properties文件读取远胜alert(错了)。5. 系统验收与答辩准备一份能过审的毕设交付物清单与答辩话术毕设不是写完代码就结束而是要交付一套能让老师快速验证、愿意签字的完整材料。我带过的项目里90%的“差点挂掉”都源于验收材料不匹配——代码能跑但文档说不清“为什么这么设计”。以下是我给学生定的硬性交付清单缺一不可。5.1 必交文档5份文件构成完整证据链文件名格式核心内容为什么必须有需求规格说明书_V1.2.docxWord用例图UML、30条功能需求如“FR-017退房时自动计算超时费”、5条非功能需求响应时间2s证明你做过需求分析不是拍脑袋编码老师会随机挑一条需求让你现场演示是否满足数据库设计说明书.pdfPDFE-R图PowerDesigner导出、6张表的详细字段说明含约束、索引、示例值、3个关键SQL示例如“查今日空房”展示你理解数据建模不是只会CREATE TABLEE-R图中reservation与room的连线必须标注基数1..*否则扣分系统部署手册.mdMarkdownTomcat 8.5安装步骤、JDK 8配置、MySQL 5.7建库脚本、WAR包部署命令、初始账号密码admin/123456老师不会装环境必须让他5分钟内跑起来手册里写明“若启动报错请检查conf/server.xml中Connector端口是否被占用”体现你考虑过实操问题测试用例报告.xlsxExcel120条测试用例覆盖正常流入住→退房、异常流重复入住、负数押金、边界值房号999、价格99999.99每条标注“通过/失败/阻塞”及截图证明你真测过不是“我觉得没问题”老师最爱问“你测过并发预订吗”有报告就能直接翻给他看答辩PPT_精简版.pptxPowerPoint12页以内封面、需求痛点1页、架构图1页、核心流程入住/退房各1页、关键界面3页、测试结果1页、总结1页禁用动画、禁用长段落、每页≤3个关键词答辩只有10分钟PPT是你的提词器曾有学生PPT写满代码老师说“你念完这页时间就到了”提示所有文档的页眉必须统一为“小型酒店管理系统 · 毕业设计”页脚加页码。这不是形式主义——老师批改几十份作业统一格式能让他快速定位你的材料。5.2 答辩现场3个必答问题与标准应答模板老师提问有规律提前准备能稳住心态。以下是近三年高频问题及应答逻辑Q1“为什么不用Spring Boot现在都用这个。”✅ 标准回答“我调研过Spring Boot它确实开发效率高。但本次毕设目标是深入理解Java Web底层机制所以选择ServletJSP。比如在入住事务中我手动管理Connection的setAutoCommit(false)和rollback()这让我真正掌握了ACID的实现原理。而Spring的Transactional是黑匣子我知其然不知其所以然。当然我也准备了Spring Boot迁移方案展示《双版本对照手册》证明我能根据项目需求灵活选型。”Q2“房间状态有4种但你们只用了3个字段值第4个怎么保证不被误用”✅ 标准回答“首先数据库room.status是TINYINT我们建了CHECK约束status IN (1,2,3,4)其次Java层Room类的setStatus(byte s)方法内部有switch校验非法值抛IllegalArgumentException最后所有前端下拉框的选项值都是后端传入的MapByte,String不可能出现‘5’这个选项。三重防护确保第4种状态维修中只能通过管理员专用入口设置。”Q3“如果客人入住后投诉房间不干净系统怎么支持追溯”✅ 标准回答“这正是我们设计system_log表的核心目的。当房间状态从‘空闲清洁’变为‘已入住’系统会记录两条日志一条是operation_typeCHECK_INbefore_state存更新前的room行含status1after_state存更新后status2另一条是operation_typeROOM_INSPECTED由保洁员扫码登记before_state为空after_state存清洁时间、保洁员ID。答辩时我可以现场演示输入房号301查system_log表按create_time倒序立刻看到最近一次清洁记录和入住记录。”5.3 我的血泪教训从那以后我每次答辩前都强制走一遍“三查法”第一次带学生答辩他演示退房时突然报错NullPointerException查了20分钟才发现是roomDao.findById()返回null因测试数据里room_id写成了字符串301而非整数301。从那以后我每次指导学生答辩前都强制他们走一遍这三步查数据登录MySQL执行SELECT * FROM room LIMIT 5; SELECT * FROM guest LIMIT 5;确认测试数据存在且格式正确room_id是INT不是VARCHAR查日志启动Tomcat后立刻打开logs/catalina.out滚动到末尾确认无ClassNotFoundException或SQLException查流程不点菜单而是手动在浏览器地址栏输入http://localhost:8080/hotel/room/list.jsp再输入http://localhost:8080/hotel/checkin/add.jsp确保所有JSP路径能直接访问不依赖导航菜单的JavaScript。这三步5分钟能做完却能避开90%的“现场翻车”。希望帮到你。本文还有配套的精品资源点击获取