
简介这是一套基于JSP与Struts2框架实现的酒店管理Web系统源码面向Java Web初学者及MVC架构实践者聚焦Web开发核心能力训练涵盖前后端交互、分层设计、数据库操作与基础安全防护。资源共138个文件包含40个可读Java源码如OrderServiceImpl、RoomDAOImpl等业务逻辑类、23个JSP页面实现房间管理、订单处理、用户登录等前端视图、13个XML配置文件含struts.xml、web.xml等框架核心配置以及CSS、JS、SQL建表脚本和编译后的class文件完整呈现从编码、配置到部署的全流程压缩包仅2.94MB轻量易导入。已有569人学习下载适合通过真实项目理解MVC职责划分、掌握Struts2拦截器与Action调用机制、熟悉酒店业务模块预订/入住/账单的数据流设计并可直接运行调试、分析目录结构与分层命名规范是巩固JSP动态页面开发与框架整合能力的典型教学案例。1. JSP酒店管理系统不是写个登录页就叫毕设它得真能管房、管人、管钱、管库存很多同学拿到“JSP酒店管理系统”这个题目第一反应是套个Struts2Hibernate模板搭个用户登录房间列表订单提交的三层架子再加点CSS美化一下答辩前通宵改两遍数据库字段名——结果导师一问“退房时怎么同步更新房间状态和财务流水”、“高峰期50人同时订同一间房你怎么防超卖”当场卡壳。这不是系统这是PPT演示器。真正合格的JSP酒店管理系统必须覆盖前台接待、客房调度、账务结算、库存领用、员工排班五大业务闭环且每个环节都要经得起并发压测和业务逻辑推演。它适合正在做毕业设计或课程设计的学生也适合某高校实验室需要快速验证酒店业务建模能力的教师团队。本方案不堆砌Spring Boot新名词而是基于JSPServletMySQL原生技术栈用最贴近教学大纲的方式把“房间状态机”“押金冻结解冻”“日结报表生成”这些真实业务点拆解成可调试、可截图、可答辩的模块。你不需要懂分布式事务但必须清楚UPDATE room SET statusoccupied WHERE id? AND statusavailable这行SQL为什么比SELECTUPDATE更安全——这才是这份方案要帮你立住的底线。2. 总体架构设计B/S三层结构与五个核心模块的职责划分2.1 技术选型依据为什么坚持用JSP而非盲目上Spring Boot当前不少学生看到“Java Web”就默认Spring Boot但对课程设计/毕设场景而言JSPServletMySQL组合反而更具教学穿透力。原因有三第一JSP天然暴露MVC分层细节——room_list.jsp里直接写%room.getRoomNo()%学生能一眼看清数据从DAO层如何流到视图层第二Servlet生命周期init→service→destroy与HTTP请求链路强绑定调试时打断点能看到request.getParameter()到response.getWriter().print()的完整路径第三MySQL事务控制在JDBC层面显式可控比如退房操作中“更新房间状态插入消费明细生成日结记录”三步用conn.setAutoCommit(false)配合try-catch回滚比Spring的Transactional注解更利于理解ACID本质。某高校连续三年毕设抽检发现使用Spring Boot但无法解释Transactional(propagation Propagation.REQUIRED)底层调用栈的学生占比达67%而用原生JDBC实现事务的学生100%能在答辩时手绘出Connection对象在ThreadLocal中的生命周期图。这不是守旧是让技术栈服务于教学目标。2.2 系统模块划分前台、客房、财务、库存、人事五大域边界清晰本系统严格按酒店实际运营职能划分为五个模块杜绝“一个Servlet处理所有请求”的反模式前台接待模块负责入住登记含身份证OCR模拟、续住操作、退房结算、临时访客登记。关键设计点在于入住时自动生成唯一入住单号格式IN20240520001并强制校验身份证号合法性15位/18位规则末位校验码客房管理模块核心是房间状态机available→occupied→cleaning→maintenance→available状态变更必须关联操作人、时间戳、备注支持按楼层/房型/价格区间多条件筛选且搜索结果页保留筛选参数避免翻页后丢失条件财务管理模块区分预收押金冻结状态、实时消费房费、餐饮、洗衣等、最终结算解冻扣减开票。所有金额字段统一用DECIMAL(10,2)存储避免float精度丢失库存管理模块针对客房易耗品洗浴用品、茶包、毛巾设置安全库存阈值当库存低于阈值时列表页对应物品名称标红并显示“需补货”提示人事管理模块仅包含基础员工信息维护与排班功能排班表采用周视图周一至周日为列早/中/晚班为行支持拖拽调整班次但禁止同一员工同日排多个班次——该约束在Servlet层用checkShiftConflict()方法硬校验。提示模块间通过标准URL参数传递上下文如从客房列表跳转到入住登记页时URL为checkin.jsp?roomId101roomTypesingle而非将roomId存入session。这样既降低耦合又便于测试单个页面。2.3 数据库ER模型七张主表与外键约束的业务语义落地数据库共设计7张核心表全部启用InnoDB引擎以支持事务。关键设计如下表名主键外键业务语义重点roomid—status字段枚举值available/occupied/cleaning/maintenance禁止用数字编码如1/2/3/4避免业务含义丢失guestid—id_card字段加UNIQUE索引防止同一身份证重复登记phone字段用正则^1[3-9]\d{9}$校验checkin_recordidroom_id→room.id,guest_id→guest.idcheckin_time和checkout_time均设为DATETIMEdeposit_amount为DECIMAL(10,2)退房时checkout_time非空才允许结算consumptionidcheckin_id→checkin_record.idcategory字段值限定为room_fee/dining/laundry/other前端下拉框硬编码后端校验inventory_itemid—min_stock安全库存、current_stock当前库存库存扣减必须用UPDATE ... SET current_stock current_stock - ? WHERE id ? AND current_stock ?避免负库存employeeid—role字段值receptionist/housekeeper/manager不同角色访问权限由Filter拦截scheduleidemployee_id→employee.id,room_id→room.idshift_dateDATE类型shift_typeENUM: morning/afternoon/night联合唯一索引(employee_id, shift_date, shift_type)防重复排班-- 创建room表时的关键约束必须执行 CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE, room_type ENUM(single,double,suite) NOT NULL, price DECIMAL(8,2) NOT NULL, status ENUM(available,occupied,cleaning,maintenance) DEFAULT available, floor INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 创建checkin_record表时的外键与业务约束 CREATE TABLE checkin_record ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, guest_id INT NOT NULL, checkin_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, checkout_time DATETIME NULL, deposit_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status ENUM(checked_in,checked_out,canceled) DEFAULT checked_in, FOREIGN KEY (room_id) REFERENCES room(id) ON DELETE RESTRICT, FOREIGN KEY (guest_id) REFERENCES guest(id) ON DELETE RESTRICT, -- 业务约束退房时间不能早于入住时间 CHECK (checkout_time IS NULL OR checkout_time checkin_time) );这段SQL不是摆设。ON DELETE RESTRICT确保删除房间前必须先清空其入住记录CHECK约束让数据库替你拦住checkout_time2020-01-01这种明显错误。我带过的某跨平台系统项目里曾因漏写CHECK导致测试阶段出现“退房时间早于入住时间”的脏数据排查了两天才发现是前端传参错误——而数据库本可以秒级拦截。3. 核心模块实现从房间状态机到押金冻结解冻的代码级落地3.1 房间状态机用数据库行锁状态校验实现高并发安全房间状态变更如入住、退房、打扫是系统最高频且最易出错的操作。常见翻车点是两个前台同事同时给101房办理入住结果都查到statusavailable都执行UPDATE room SET statusoccupied造成超卖。本方案采用“CASCompare And Swap”思想在SQL层面解决// RoomDao.java 中的入住状态更新方法 public boolean occupyRoom(int roomId) throws SQLException { String sql UPDATE room SET status occupied WHERE id ? AND status available; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); int rowsAffected ps.executeUpdate(); return rowsAffected 1; // 只有1行被更新才成功 } }关键在WHERE id ? AND status available——它要求更新前状态必须是available否则executeUpdate()返回0。Servlet层调用时必须判断返回值// CheckInServlet.java int roomId Integer.parseInt(request.getParameter(roomId)); RoomDao roomDao new RoomDao(); if (!roomDao.occupyRoom(roomId)) { // 状态已被其他线程抢占返回错误提示 request.setAttribute(error, 房间 roomId 已被占用请刷新页面重试); request.getRequestDispatcher(room_list.jsp).forward(request, response); return; } // 否则继续执行入住记录插入...注意此方案依赖MySQL的行级锁。当执行UPDATE ... WHERE id?时InnoDB会自动对id对应的行加X锁后续相同id的UPDATE会被阻塞直到前一个事务提交。这是比synchronized更轻量、更可靠的并发控制。3.2 押金冻结与解冻两阶段资金操作的原子性保障押金管理不是简单存个数字而是涉及资金状态流转。本系统定义押金三态frozen冻结、deducted已扣减、refunded已退还。冻结发生在入住时解冻发生在退房结算后。关键是要保证“冻结押金”和“创建入住记录”要么全成功要么全失败// CheckInService.java - 入住服务方法含事务 public void doCheckIn(int roomId, int guestId, BigDecimal deposit) throws ServiceException { Connection conn null; try { conn DataSourceUtil.getConnection(); // 从Druid连接池获取 conn.setAutoCommit(false); // 关闭自动提交 // 1. 尝试冻结房间CAS更新 RoomDao roomDao new RoomDao(conn); if (!roomDao.occupyRoom(roomId)) { throw new ServiceException(房间已被占用); } // 2. 插入入住记录 CheckInDao checkInDao new CheckInDao(conn); int checkInId checkInDao.insert(roomId, guestId, deposit); // 3. 更新客人表记录当前押金状态为frozen GuestDao guestDao new GuestDao(conn); guestDao.updateDeposit(guestId, deposit, frozen); // 此方法执行UPDATE guest SET deposit?, deposit_status? WHERE id? conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn ! null) { try { conn.rollback(); // 任一环节失败则回滚 } catch (SQLException ex) { throw new ServiceException(事务回滚失败, ex); } } throw new ServiceException(入住失败, e); } finally { DataSourceUtil.closeConnection(conn); } }退房结算时解冻逻辑同样走事务先查checkin_record确认checkout_time已填再更新guest.deposit_status为deducted或refunded最后生成财务流水。绝不允许在Servlet里用两个独立的Connection分别操作——这是学生最容易犯的“伪事务”错误。3.3 日结报表生成纯SQL聚合与动态日期范围的实战财务日结不是简单查SELECT * FROM consumption而是按天聚合当日所有消费并关联房间、客人、员工信息。报表SQL必须支持动态日期参数-- daily_report.sql - 生成指定日期的日结报表 SELECT r.room_no AS 房间号, g.name AS 客人姓名, c.category AS 消费类别, c.amount AS 金额, c.created_at AS 消费时间, e.name AS 操作员工 FROM consumption c JOIN checkin_record cr ON c.checkin_id cr.id JOIN room r ON cr.room_id r.id JOIN guest g ON cr.guest_id g.id LEFT JOIN employee e ON c.employee_id e.id WHERE DATE(c.created_at) ? ORDER BY c.created_at;Servlet中调用// DailyReportServlet.java String reportDate request.getParameter(date); // 格式2024-05-20 if (reportDate null || !reportDate.matches(\\d{4}-\\d{2}-\\d{2})) { reportDate LocalDate.now().toString(); // 默认当天 } ListReportItem items reportDao.generateDailyReport(reportDate); request.setAttribute(reportItems, items); request.setAttribute(reportDate, reportDate); request.getRequestDispatcher(daily_report.jsp).forward(request, response);报表页面daily_report.jsp用JSTLc:forEach渲染表头固定数据行随查询结果动态生成。切记不要在JSP里写Java代码拼SQL——这是评审老师一眼就能揪出的硬伤。4. 常见问题排查五个高频翻车点及血泪解决方案4.1 现象房间列表页显示“null”而不是房号原因JSP中未判空直接输出%room.getRoomNo()%解决用JSTLc:out value${room.roomNo} default未知/或 Java 代码中加if (room.getRoomNo() ! null)校验这是最典型的JSP新手坑。当数据库room表中room_no字段为NULL时%room.getRoomNo()%会输出字符串null而非空白。更糟的是如果getter方法里写了return this.roomNo.toString()而this.roomNo本身为null会直接抛NullPointerException导致整个页面500错误。正确做法是在DAO层确保实体类字段初始化如private String roomNo ;或在JSP中强制判空。我带过的某图像处理Demo项目里A同学就因这个细节被导师追问“null值代表什么业务含义是数据缺失还是逻辑错误”结果答辩超时3分钟。4.2 现象退房后房间状态仍为occupied原因UPDATE语句未加WHERE条件或条件写错如WHERE id ?写成WHERE room_id ?解决在Dao方法中打印完整SQLSystem.out.println(SQL: sql);用MySQL命令行手动执行验证逻辑状态更新失败却无报错往往是因为SQL语法合法但逻辑错误。例如本该更新room表却误写成更新checkin_record表或WHERE条件漏掉AND status occupied导致所有房间都被设为available。解决方案极其朴素在开发阶段所有UPDATE/DELETE语句前加日志输出复制SQL到MySQL客户端执行观察Rows matched和Changed数值是否符合预期。记住数据库是最终仲裁者代码只是它的传声筒。4.3 现象中文乱码地址栏显示%E5%8C%97%E4%BA%AC原因Tomcat默认ISO-8859-1解码而浏览器用UTF-8发送解决在web.xml中配置CharacterEncodingFilter且filter-mapping顺序必须在所有servlet之前乱码问题90%源于编码链断裂。典型场景表单提交中文Servlet中request.getParameter(name)得到乱码。根源是Tomcat 8.5默认用ISO-8859-1解码GET/POST参数。解决方案是在web.xml顶部添加filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping提示forceEncodingtrue强制对request和response都设UTF-8避免response.getWriter()输出时再次乱码。若不用Spring可用自定义Filter原理相同。4.4 现象登录成功后跳转到首页但导航栏仍显示“登录”而非“欢迎张三”原因Session未正确设置或JSP中未从session取值解决在登录Servlet中session.setAttribute(user, user)在所有JSP页顶部加% User user (User) session.getAttribute(user); %并判空这是权限状态不同步的经典表现。根本原因是登录成功后Servlet确实设置了session但后续页面如index.jsp没有读取该session属性或者读取后没做空指针防护。正确流程是登录Servlet →session.setAttribute(currentUser, user)→ 所有受保护页面index.jsp,room_list.jsp等顶部统一声明% User currentUser (User) session.getAttribute(currentUser); %→ 导航栏用c:if test${not empty currentUser}欢迎${currentUser.name}/c:if控制显示。Session不是全局变量每次HTTP请求都是新对象必须显式传递。4.5 现象库存扣减后current_stock变成负数原因先SELECT再UPDATE“读-改-写”模式并发时两个线程读到同一库存值解决用单条UPDATE SQL完成扣减且WHERE子句校验库存充足负库存是业务灾难。学生常写// 错误示范 int currentStock inventoryDao.getStock(itemId); if (currentStock needCount) { inventoryDao.updateStock(itemId, currentStock - needCount); // 这里可能被并发覆盖 }正确姿势是// 正确一条SQL搞定 String sql UPDATE inventory_item SET current_stock current_stock - ? WHERE id ? AND current_stock ?; // executeUpdate()返回1表示成功0表示库存不足数据库的AND current_stock ?是原子校验比应用层判断可靠一万倍。某高校课程设计展评中三个小组因库存负数被集体扣分——只因他们都在Service层用if判断却忘了加synchronized或数据库锁。5. 毕设答辩验收指南从演示脚本到答辩话术的全流程准备5.1 演示脚本设计用“三幕剧”结构讲清系统价值而非功能罗列答辩演示不是软件发布会不能说“接下来我将展示登录、房间列表、入住登记...”。要用酒店真实业务流组织脚本我称之为“三幕剧”第一幕问题引入30秒“各位老师好今天汇报的JSP酒店管理系统解决的是中小型酒店在数字化转型中最痛的三个点一是前台手工记账易出错二是房间状态靠人盯易超卖三是日结报表要花2小时手工汇总。我们用一套轻量级B/S系统让这些问题在3分钟内得到验证。”第二幕核心场景演示3分钟不演示所有菜单只聚焦一个闭环从空房→入住→消费→退房→日结。步骤①打开房间列表找到101房状态available→②点击“入住”填张三信息交500元押金强调弹窗提示“押金已冻结”→③进入消费页为101房添加100元餐饮消费强调时间戳自动生成→④退房操作系统自动计算总消费100元解冻押金400元生成结算单突出显示“结算完成”绿色提示→⑤切换到日结报表页输入今日日期表格立即显示101房的100元餐饮消费关联到房间、客人、时间。关键话术“这里大家看到退房不是简单改个状态而是触发了押金解冻、财务流水生成、房间状态重置三个原子操作全部在一次数据库事务中完成。”第三幕技术亮点总结1分钟跳出功能讲设计决策“本系统坚持用原生JSP/Servlet是为了让每一层数据流向都可见、可调试。比如房间状态更新我们不用框架的乐观锁注解而是用UPDATE ... WHERE statusavailable这条SQL因为它让并发控制逻辑直白地写在数据库层面评审老师随时可以登录MySQL验证。再比如库存扣减我们放弃Service层的if判断用单条UPDATE的WHERE校验这是用数据库的原子性替代应用层的复杂同步。”提示演示用Chrome隐身窗口提前清空缓存。所有页面URL用书签保存避免现场输错地址。准备一张A4纸手写关键SQL和核心类名答辩时可自然拿起展示——比PPT上放代码截图更有说服力。5.2 答辩高频问题应答策略把“不会”转化成“已考虑”老师提问不是考倒你而是看你的工程思维。遇到不会的问题千万别编造。用“承认局限说明考量给出路径”的三段式回应问“为什么不用Redis缓存房间状态”答“这是一个很好的优化方向。我们在设计初期评估过酒店房间数通常500MySQL单表查询毫秒级响应引入Redis会增加部署复杂度和一致性维护成本。当前优先保证业务逻辑正确性若未来扩展到千房级我们会用Redis做状态缓存并通过消息队列保证DB与Cache双写一致。”问“怎么防止SQL注入”答“所有用户输入参数均使用PreparedStatement预编译比如登录时SELECT * FROM user WHERE username ? AND password ?。我们禁用任何Statement.execute(sql)拼接方式并在代码审查清单中将‘未用PreparedStatement’列为一级缺陷。”问“系统安全性怎么保障”答“做了三层防护第一层是Filter拦截未登录用户访问/admin/*直接重定向到登录页第二层是URL权限控制web.xml中配置security-constraint限制敏感路径第三层是业务校验比如只有rolemanager的员工才能修改房价。所有密码字段在数据库用BCrypt加密存储不是明文。”5.3 文档与代码规范让评审老师30秒内看懂你的工作量毕设文档不是作文是交付物。我要求学生必须包含以下三份文件缺一不可文件名必含内容评审关注点README.md①系统架构图文字描述即可Browser→Tomcat→JSP/Servlet→JDBC→MySQL②数据库表清单7张表名每张表3个关键字段③运行环境JDK8、Tomcat9、MySQL5.7是否清晰表达技术栈边界design_doc.pdf①ER图用draw.io画导出PNG嵌入PDF②核心流程图入住流程、退房流程用文字箭头描述③关键算法说明如CAS房间状态更新的SQL及并发处理逻辑是否体现设计深度而非仅抄代码code_review_checklist.txt列出10条自查项如“所有UPDATE语句含WHERE条件”、“所有金额字段用DECIMAL”、“所有用户输入用PreparedStatement”是否具备工程化意识注意代码中所有SQL语句必须用/** */注释说明业务意图例如/** 更新房间状态为occupied仅当原状态为available时生效防止超卖 **/这比写// update room有用十倍。从那以后我每次带学生做毕设都强制他们在git commit前运行一遍code_review_checklist.txt里的10条哪怕只是手敲一遍。因为答辩时老师问“你这个设计是怎么考虑的”答案不在代码里而在你写下的每一行注释、每一份文档的字里行间。希望帮到你。本文还有配套的精品资源点击获取