简介:这份JSP酒店管理系统面向Java Web初学者、课程设计与毕业设计开发者,基于JSP与Struts2构建,围绕房间管理、预订处理、入住退房、客户信息与账单计算等业务展开,可用于理解MVC分层、Action-Service-DAO协作以及数据库表结构设计。压缩包为rar格式,共138个文件、约2.94MB,其中40个java源码、42个class、23个jsp页面、13个xml配置,另含sql脚本、css与js等资源,便于对照阅读后端实现、前端页面和部署配置。已有569人学习下载,说明其在JSP与Struts2入门实践中具有一定参考价值。读者可据此梳理用户登录、订单查询、分页展示与房间状态维护等请求流转,参考JSP页面、Struts2配置及拦截器用法完成环境搭建,并借助SQL脚本理解实体关系与数据一致性;对需要完成酒店管理类课程设计、毕业设计或夯实Java Web基础的学习者较为适用。
1. 从一张前台交接班表说起:JSP酒店管理系统要接住的三件事
“JSP 酒店管理系统”在毕设选题里出现频率不低,真到动手却容易卡在业务链上。前台早上交接班,先核对房态:哪些房间已退未清、押金还剩多少、昨晚换房有没有留下差价。这套 JSP 酒店管理系统要接住的就是入住登记、房态变更、账务结算三件事,让前台不靠纸质表也能把班交清楚。适合两类人:做 JSP 酒店管理毕设或课设的学生,需要一套能讲清业务边界、能跑通链路的方案;以及小体量住宿场景的开发者,想用 JSP + Servlet + MySQL 快速搭一个前台可用的管理后台。下面从需求拆解起,把选型、库表、核心模块和验收逐层铺开。
2. 需求拆解与技术选型:为什么 JSP + Servlet + MySQL 这套老组合还没退休
2.1 三类用户与四个核心用例
酒店管理系统的用户角色不宜铺得太大。按前台实际班次拆,通常只保留三类人:前台接待、客房主管、系统管理员。前台接待负责入住、换房、退房和收银;客房主管只关心房态和清扫进度;系统管理员维护房间资料、价格策略和操作员账号。角色一多,权限矩阵就会膨胀,JSP 页面里的判断也会变得零碎,最后答辩时连自己都说不清哪个角色能点哪个按钮。
四个核心用例足够撑起一套可演示的系统:入住登记、换房处理、退房结算、房态查询。入住登记要同时写入住单、占用房间、收押金;换房处理要释放原房、占用新房、记录差价;退房结算要生成账单、退还押金、把房态改成待清扫;房态查询要按楼层和状态筛选,让前台一眼看到可售房。把四个用例的输入输出画清楚,后面的 Servlet 和 JSP 页面才有落点。
提示:需求阶段不要急着画页面。先把“谁在什么状态下能做哪个动作”写成一张状态迁移表,后面写 Filter 和事务时直接照着表来。
2.2 技术栈选型与版本约束
这套方案用 JSP + Servlet + JavaBean + JDBC + MySQL,容器选 Tomcat。JSP 负责展示,Servlet 收口请求和业务编排,JavaBean 承载数据,JDBC 做持久化。看上去是“老组合”,但对酒店管理这种以表单、列表、状态流转为主的系统,反而省掉了前端构建链和额外学习成本。毕设场景里,评审老师更关心业务是否闭环、事务是否可靠,而不是用了多新的脚手架。
版本上,JDK 用 8 或 11,Servlet 用 4.0 对应 Tomcat 9,MySQL 用 5.7 或 8.0。数据库连接不要在每个 Servlet 里 DriverManager.getConnection,直接上 Druid 或 HikariCP 连接池,初始化参数写在 druid.properties 里。JSP 页面统一放 WEB-INF/jsp 下,避免被直接访问;静态资源放 webapp/static,Filter 放行。字符编码用 UTF-8,JSP 页面、数据库连接串、Tomcat 的 URIEncoding 三处保持一致,后面第五 chapter 会专门说这个坑。
| 层次 | 技术选择 | 责任边界 |
|---|---|---|
| 展示层 | JSP + JSTL | 只做数据渲染和表单提交,不写业务判断 |
| 控制层 | Servlet | 接收参数、校验、调用 Service、转发页面 |
| 业务层 | JavaBean / Service 类 | 事务编排、房态校验、账单计算 |
| 持久层 | JDBC + 连接池 | SQL 执行、结果集映射 |
| 数据库 | MySQL | 房间、入住单、账单、操作员四类主表 |
2.3 数据库表设计:从房间状态到账单流水
库表设计决定后面代码好不好写。房间表 room 存房号、楼层、房型、门市价、当前状态;入住单表 stay_order 存入住人、证件号、入住时间、预计离店、押金、状态;账单表 bill 存订单号、房费、其他消费、押金抵扣、实收、结算时间;操作员表 operator 存账号、密码哈希、角色。房态不要用中文直接存,用 tinyint 或 char(1) 编码:0 可售、1 已入住、2 待清扫、3 维修。编码短,索引快,JSP 里用 JSTL 做映射也方便。
-- 房间表:房态用编码,避免中文状态参与查询和索引 CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE, floor_no INT NOT NULL, room_type VARCHAR(20) NOT NULL, rack_rate DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0可售 1已入住 2待清扫 3维修', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入住单:一个订单对应一间房,换房时旧单关单、新单续开 CREATE TABLE stay_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, room_id BIGINT NOT NULL, guest_name VARCHAR(50) NOT NULL, id_card VARCHAR(30) NOT NULL, checkin_time DATETIME NOT NULL, expect_leave DATETIME NOT NULL, deposit DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1在住 2已换房 3已退房', KEY idx_room_status (room_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 账单:退房时生成,金额字段全部用 DECIMAL,不用 FLOAT CREATE TABLE bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, room_fee DECIMAL(10,2) NOT NULL, other_fee DECIMAL(10,2) NOT NULL DEFAULT 0, deposit_deduct DECIMAL(10,2) NOT NULL DEFAULT 0, real_pay DECIMAL(10,2) NOT NULL, settle_time DATETIME NOT NULL, UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;上面三张表是主链,房间表上的 version 字段是给并发更新留的后悔药。入住和退房都要先UPDATE room SET status=?, version=version+1 WHERE id=? AND version=?,影响行数为 0 就说明房态被别人改过,Servlet 要回滚并提示前台刷新。账单表把房费、其他消费、押金抵扣拆开存,答辩时能直接展示“押金不是收入,抵扣和退还要分开记”这个财务常识。
3. 总体架构设计:三层结构、四个模块与会话状态管理
3.1 分层结构:JSP 只做展示,Servlet 收口业务
JSP 酒店管理系统最容易写歪的地方,是把 JDBC 代码和业务判断直接塞进 JSP 脚本段。页面一多,复制粘贴的 Connection、PreparedStatement 和 if-else 就会把项目拖成黑匣子。我一般按三层收口:JSP 只拿 request 里的 list 和 map 做渲染;Servlet 做参数校验、调用 Service、设置转发地址;Service 里开事务、调 DAO、算金额。每层只做一件事,后面加“钟点房”或“团队入住”时才有地方下手。
包结构按com.hotel.web、com.hotel.service、com.hotel.dao、com.hotel.entity分。Servlet 不直接 new DAO,通过一个简单的工厂或构造注入拿 Service 实例。JSP 里禁止出现<%@ page import="java.sql.*" %>,只保留 JSTL 的c:forEach、fmt:formatDate。这样答辩时打开一个 JSP 文件,看到的是干净的 HTML 和 EL 表达式,而不是一屏 Java 代码。
3.2 模块划分:前台接待、客房管理、账务结算、后台维护
模块按操作对象拆,不按页面拆。前台接待模块管入住、换房、退房、在住查询;客房管理模块管房态看板、清扫任务、维修锁定;账务结算模块管押金流水、账单查询、日结汇总;后台维护模块管房间资料、房型价格、操作员账号。四个模块共享 room、stay_order、bill 三类主数据,但每个模块只暴露自己的 Servlet 路径,比如/front/checkin、/room/status、/bill/settle、/admin/room。
房态看板是模块之间的连接点。前台办理入住后,看板上的房间从绿色变成红色;客房主管点“清扫完成”,房间从待清扫变回可售;维修锁定则直接跳过清扫状态。看板不做复杂查询,按楼层分组查 room 表,JSP 用两层c:forEach渲染楼层和房间卡片。房间卡片上只放房号、房型、状态和两个操作按钮,避免前台在忙的时候点错。
3.3 会话与权限:用 Filter 拦住越权请求
登录成功后把 operator 对象放进 HttpSession,Filter 统一拦截/front/*、/room/*、/bill/*、/admin/*。没登录跳登录页,角色不匹配返回 403 页面。Filter 里不要写业务判断,只做“有没有登录、角色够不够”两件事。静态资源、登录页、登录接口放行,否则 CSS 和 JS 会被拦掉,页面样式全丢。
// 权限过滤器:只做登录态和角色白名单,不碰业务 public class AuthFilter implements Filter { private static final Map<String, String> ROLE_MAP = new HashMap<>(); static { ROLE_MAP.put("/admin/", "ADMIN"); ROLE_MAP.put("/front/", "RECEPTION"); ROLE_MAP.put("/room/", "HOUSEKEEPING"); ROLE_MAP.put("/bill/", "RECEPTION"); } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); String ctx = request.getContextPath(); String path = uri.substring(ctx.length()); // 放行登录页、登录接口和静态资源 if (path.startsWith("/login") || path.startsWith("/static/")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); Object operator = session == null ? null : session.getAttribute("operator"); if (operator == null) { response.sendRedirect(ctx + "/login.jsp"); return; } // 按路径前缀比对角色,角色不够直接 403 for (Map.Entry<String, String> entry : ROLE_MAP.entrySet()) { if (path.startsWith(entry.getKey())) { String role = (String) session.getAttribute("role"); if (!entry.getValue().equals(role)) { response.sendError(403, "无权访问"); return; } } } chain.doFilter(req, resp); } }web.xml 里把 Filter 映射到/*,顺序放在编码过滤器之后。编码过滤器先执行,保证请求参数按 UTF-8 解析;权限过滤器后执行,避免未登录请求把中文参数解析成乱码再跳转。两个 Filter 都不要在doFilter里吞异常,否则前台点了按钮没反应,控制台也没有堆栈,排查起来很痛苦。
4. 核心模块落地:入住登记、换房与退房结算的代码实现
4.1 入住登记:事务里同时写订单和改房态
入住登记要在一个事务里做三件事:校验房间是否可售、写 stay_order、把 room.status 改成已入住。三步必须同成同败,否则会出现“订单写了但房态没改”或者“房态占了但没有入住单”的脏数据。Servlet 里先做参数非空和日期合法性校验,再调 Service。Service 用Connection.setAutoCommit(false)开事务,捕获异常回滚,finally 关连接。
// 入住登记:同一事务里写订单 + 占房,房态用乐观锁防并发 public String checkin(CheckinDTO dto) throws SQLException { Connection conn = null; try { conn = DataSourceUtil.getConnection(); conn.setAutoCommit(false); // 1. 查房态和版本号,确认可售 Room room = roomDao.findForUpdate(conn, dto.getRoomId()); if (room == null || room.getStatus() != 0) { throw new BizException("房间不可售,请刷新房态"); } // 2. 写入住单 String orderNo = "ST" + System.currentTimeMillis(); stayOrderDao.insert(conn, orderNo, dto, room); // 3. 改房态,带版本号,影响行数为 0 说明被抢改 int rows = roomDao.updateStatus(conn, dto.getRoomId(), 1, room.getVersion()); if (rows == 0) { throw new BizException("房态已被其他操作修改,请重试"); } conn.commit(); return orderNo; } catch (Exception e) { if (conn != null) conn.rollback(); throw new BizException("入住失败:" + e.getMessage(), e); } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }findForUpdate里用SELECT ... FOR UPDATE锁住房间行,和后面的乐观锁版本号一起用,听起来有点重复,但酒店前台经常两个人同时点同一间房,行锁能挡住大部分并发,版本号则兜住“查询和更新之间被改”的窗口。押金字段在入住单里存正数,退房时按实际消费抵扣,抵扣不完的退现金,抵扣超出的补收。不要在一个字段里用正负号同时表示收和退,后面日结对账会很难受。
4.2 换房处理:差价与房态回滚
换房不是简单改 room_id。旧房要释放成待清扫,新房要占用,旧入住单关单,新入住单续开,差价记在账单备注里。如果新房在写入过程中失败,旧房必须回滚回已入住,否则前台会把客人晾在走廊。Service 里按“锁旧房、锁新房、关旧单、开新单、改两个房态”的顺序执行,任何一步失败都整体回滚。
// 换房:旧房释放、新房占用、旧单关单、新单续开,全部在一个事务里 public void changeRoom(Long oldOrderId, Long newRoomId, BigDecimal priceDiff) throws SQLException { Connection conn = null; try { conn = DataSourceUtil.getConnection(); conn.setAutoCommit(false); StayOrder oldOrder = stayOrderDao.findActive(conn, oldOrderId); if (oldOrder == null) throw new BizException("原入住单不存在或已退房"); Room oldRoom = roomDao.findForUpdate(conn, oldOrder.getRoomId()); Room newRoom = roomDao.findForUpdate(conn, newRoomId); if (newRoom.getStatus() != 0) throw new BizException("新房不可售"); // 旧房改成待清扫,新房改成已入住 roomDao.updateStatus(conn, oldRoom.getId(), 2, oldRoom.getVersion()); roomDao.updateStatus(conn, newRoomId, 1, newRoom.getVersion()); // 旧单关单,新单继承入住人和押金 stayOrderDao.close(conn, oldOrderId, 2); stayOrderDao.insertFromChange(conn, oldOrder, newRoom, priceDiff); conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw new BizException("换房失败:" + e.getMessage(), e); } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }差价这里只做记录,不在换房时立即收钱。很多课设方案会在换房时直接改账单,结果客人退房时发现金额对不上。我的习惯是换房单上写price_diff备注,退房结算时把新旧房型差价合并进房费,账单只生成一次,前台解释起来也清楚。
4.3 退房结算:账单生成与押金退还
退房结算先查在住单,按实际离店时间和门市价算房费,加上其他消费,减去押金抵扣,生成 bill 记录,再把订单改成已退房、房间改成待清扫。房费计算按“过夜数 × 门市价 + 钟点费”拆,日期用ChronoUnit.DAYS.between算,不要用Date.getTime()相减再除 86400000,夏令时和跨月会算错。押金抵扣不能超过押金本身,超出部分走补收。
// 退房结算:算房费、写账单、退押金、改房态,四步同事务 public Bill checkout(Long orderId, BigDecimal otherFee) throws SQLException { Connection conn = null; try { conn = DataSourceUtil.getConnection(); conn.setAutoCommit(false); StayOrder order = stayOrderDao.findActive(conn, orderId); if (order == null) throw new BizException("订单不存在或已退房"); long nights = Math.max(1, ChronoUnit.DAYS.between( order.getCheckinTime().toLocalDate(), LocalDate.now())); BigDecimal roomFee = order.getRackRate().multiply(BigDecimal.valueOf(nights)); BigDecimal total = roomFee.add(otherFee); BigDecimal depositDeduct = total.min(order.getDeposit()); BigDecimal realPay = total.subtract(depositDeduct); Bill bill = billDao.insert(conn, order.getOrderNo(), roomFee, otherFee, depositDeduct, realPay); stayOrderDao.updateStatus(conn, orderId, 3); roomDao.updateStatus(conn, order.getRoomId(), 2, order.getRoomVersion()); conn.commit(); return bill; } catch (Exception e) { if (conn != null) conn.rollback(); throw new BizException("退房失败:" + e.getMessage(), e); } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }账单生成后不要直接删入住单,订单状态改成已退房即可。日结报表要按 settle_time 汇总,删了单就查不到历史。押金退还如果走现金,账单上记“押金抵扣”和“实收”,不要把退现金记成负数收入;如果走线上,另加一张退款流水表,别把支付渠道和账单混在一张表里。
5. 避坑与排查:JSP 酒店管理系统里最容易翻车的五个点
5.1 中文乱码与日期格式
现象:前台输入“张三”,列表里显示“å¼ ä¸‰”;日期提交2024-06-01后台解析报错。原因通常是三处编码不一致:JSP 页面 pageEncoding 是 UTF-8,Tomcat 的 URIEncoding 没配,数据库连接串没写 characterEncoding。解决方式是把 JSP 头部、EncodingFilter、JDBC URL 统一成 UTF-8,日期字段在 Servlet 里用LocalDate.parse显式指定yyyy-MM-dd,不要依赖默认 Locale。
5.2 连接池耗尽与事务未提交
现象:系统跑一会儿就卡住,控制台报wait millis 60000, active 20。原因多半是 Service 里开了事务,异常分支忘了 rollback,或者 Connection 没在 finally 关。解决方式是把连接获取和关闭收进模板方法,事务统一在 Service 层开,DAO 只接收 Connection 参数,不自己拿连接。连接池初始大小按前台并发 5 到 10 配,最大连接不要超过数据库 max_connections 的 70%。
5.3 房态并发更新丢失
现象:两个前台同时给同一间房办入住,两单都成功,房态只保留最后一条。原因是没有行锁或版本号,UPDATE room SET status=1 WHERE id=?谁后执行谁覆盖。解决方式是入住和退房都带version条件更新,影响行数为 0 就抛业务异常并提示刷新。实在要硬扛并发,就在查询时加FOR UPDATE,把房间行锁到事务结束。
5.4 JSP 里写业务逻辑导致的维护噩梦
现象:改一个押金规则,要在五个 JSP 里找if (deposit > 0)。原因是早期图快,把 Java 代码写进脚本段。解决方式是把计算收进 Service,JSP 只拿结果。已经写乱的,先抽一个BillService.calc(),把 JSP 里的金额判断逐个替换成 EL 表达式,再删脚本段。不要一边加新功能一边重构,先把主链路抽干净。
5.5 权限过滤器漏配静态资源
现象:登录页样式全丢,控制台一堆 302。原因是 Filter 映射到/*,把/static/css和/login.jsp也拦了。解决方式是在 Filter 里显式放行/static/、/login、/captcha,并且把放行判断放在登录态判断之前。另外,sendRedirect之后要return,否则请求会继续往下走,偶尔出现“登录页和业务页同时输出”的怪现象。
6. 验收与答辩:把系统跑通、把数据说清、把边界讲明
6.1 验收清单:从入住到退房的完整链路
验收不要只看页面能不能打开,按一条完整链路走:登录前台账号,查可售房,办入住收押金,看房态变红,做一次换房,看旧房变待清扫、新房变已入住,再退房结算,看账单金额和押金抵扣是否对得上,最后用客房主管账号把房间点成可售。每一步都核对数据库里的 room.status、stay_order.status、bill 金额。下面这张表可以直接当验收记录用。
| 步骤 | 操作 | 预期结果 | 检查点 |
|---|---|---|---|
| 1 | 前台登录 | 进入房态看板 | 未登录访问 /front/checkin 跳登录页 |
| 2 | 查可售房 | 只显示 status=0 的房间 | 楼层筛选正确 |
| 3 | 办入住 | 生成 ST 开头订单,房态变 1 | stay_order 和 room 同事务提交 |
| 4 | 换房 | 旧房变 2,新房变 1,旧单关单 | 差价备注写入新单 |
| 5 | 退房 | 生成 bill,房态变 2 | 房费=过夜数×门市价,押金抵扣不超押金 |
| 6 | 客房清扫 | 房态从 2 变 0 | 只有客房主管角色可操作 |
6.2 答辩技巧:用一张房态流转图讲清设计
答辩时不要从“我用了 JSP”开始讲,先画房态流转:可售 0 → 已入住 1 → 待清扫 2 → 可售 0,维修 3 可以随时插入和退出。把入住、换房、退房三个动作标在箭头上,再说每个动作对应哪个 Servlet、哪个事务方法。老师问“并发怎么办”,就讲 version 乐观锁和FOR UPDATE;问“押金怎么退”,就讲账单里 deposit_deduct 和 real_pay 分开存;问“为什么不用前端框架”,就讲系统以表单和列表为主,JSP + Servlet 能把事务和权限收在服务端,减少联调面。
我一般会让学生准备一段三分钟演示:先造一间可售房,办入住,故意用两个浏览器同时点同一间房,展示第二个请求被版本号挡住;再换房、退房,打开账单表对金额。这个演示比念 PPT 管用,老师能看到边界处理,而不是只看增删改查。从那以后我每次带项目验收,都强制走一遍“并发点同一间房”的流程,漏了这一步,后面补事务要花双倍时间。希望帮到你。
本文还有配套的精品资源,点击获取