news 2026/10/13 7:25:07

JavaEE小型酒店管理系统毕业设计:Servlet+JSP+JDBC实现入住换房退房闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaEE小型酒店管理系统毕业设计:Servlet+JSP+JDBC实现入住换房退房闭环

简介:这份资源是一套面向高校计算机专业毕业设计的小型酒店管理系统,基于JavaEE技术栈并采用Spring、Struts、Hibernate经典SSH框架组合实现,适合正在准备毕设或希望系统练习企业级Web开发的学习者。系统围绕真实酒店业务展开,涵盖用户与权限管理、房间类型与状态维护、预订入住退房流程、账单收费、经营报表统计及系统参数配置等模块,完整串联实体映射、Service业务层、Controller请求处理与JSP视图展示,并涉及事务管理、SQL注入与XSS防护等实践要点。压缩包共262个文件,约18.69MB,包含49个java源码、25个jsp页面、24个xml配置、54个jar依赖及54个class文件,另有properties、css、js与说明文档,结构清晰便于按层阅读。目前已有1692人学习下载,可作为理解SSH整合与酒店业务建模的完整参考案例。

1. 小型酒店管理系统:从入住登记到退房结算,一套能跑通的 JavaEE 最小闭环

很多同学做毕业设计时,一上来就想着把系统做“大”,结果做到一半发现连最核心的入住登记都跑不通。小型酒店管理系统这个题目的本质,是用 JavaEE 技术栈把酒店前台最日常的三件事——开房、换房、退房——串成一条完整的数据链路。它不需要高并发,不需要分布式,但要求你对 Servlet 请求流转、JDBC 事务控制、JSP 页面传参有扎实的掌握。适合正在准备毕业设计、想找一个业务闭环清晰、技术栈主流、代码量可控的 Java 方向学生。我见过太多人把时间花在 UI 美化上,最后数据库表都建错了,这篇笔记就按“能跑通”的标准来拆。

2. 技术选型与数据库设计:为什么用 Servlet+JSP+MySQL 而不是 Spring Boot

2.1 毕业设计场景下的选型逻辑

很多同学纠结要不要上 Spring Boot,我的建议是:如果你的 Java 基础一般,Servlet+JSP+JDBC 这套“老三层”反而更容易在答辩时讲清楚。原因很直接——Spring Boot 帮你封装了太多东西,答辩老师问一句“请求是怎么从浏览器到你的业务代码的”,你可能只能答“框架自动处理的”。而 Servlet 的doGet/doPost方法、web.xml或注解配置、request.getParameter()取值,每一步都是显式可见的,你能指着代码说清楚数据流向。

从工作量看,小型酒店管理系统的核心表不超过 6 张,业务逻辑集中在“房间状态流转”和“订单金额计算”两个点上。用 Servlet 写一个CheckInServlet,里面直接调 JDBC 完成插入和更新,代码量大约 80 行,逻辑一目了然。用 Spring Boot 的话,你要配application.yml、写 Entity、Mapper、Service、Controller,代码量翻三倍,但业务复杂度并没有增加。毕业设计的评分标准里,“技术栈先进性”只占一小部分,“功能完整性”和“代码规范性”才是大头。

当然,如果你已经比较熟悉 Spring Boot,用Spring Boot + MyBatis + Thymeleaf也完全可以,但要注意别把简单问题复杂化。我一般会建议:先保证核心业务跑通,再考虑加框架。下面给出的方案以 Servlet+JSP+JDBC 为主线,因为这套组合的“黑匣子”最少,出问题了你能自己排查。

2.2 数据库表设计:五张表撑起整个系统

小型酒店管理系统的数据库不需要花哨,五张表足够:room(房间)、room_type(房型)、guest(客人)、order(订单)、user(系统用户)。其中order是核心,它连接了客人、房间和时间段。

-- 房型表:定义标准间、大床房等 CREATE TABLE room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, bed_count INT DEFAULT 1 ); -- 房间表:每个物理房间 CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_number VARCHAR(10) UNIQUE NOT NULL, type_id INT, status TINYINT DEFAULT 0, -- 0空闲 1已入住 2维修 FOREIGN KEY (type_id) REFERENCES room_type(id) ); -- 客人表 CREATE TABLE guest ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) UNIQUE NOT NULL, phone VARCHAR(15) ); -- 订单表:核心业务表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, guest_id INT, room_id INT, check_in_time DATETIME, check_out_time DATETIME, deposit DECIMAL(10,2), total_amount DECIMAL(10,2), status TINYINT DEFAULT 0, -- 0在住 1已退房 2已取消 FOREIGN KEY (guest_id) REFERENCES guest(id), FOREIGN KEY (room_id) REFERENCES room(id) );

这里有几个参数需要特别注意。room.status用TINYINT而不是BOOLEAN,因为后面可能扩展“维修”“预留”等状态。orders表名用复数是因为order是 SQL 关键字,直接写order会导致建表语句报错,这是血泪经验。check_in_time和check_out_time都用DATETIME,不要用DATE,因为酒店按小时计费时DATE会丢失时间精度。deposit字段是押金,退房时如果房间有损坏需要扣款,这个字段就是用来做结算依据的。

2.3 项目目录结构与依赖

用 Maven 管理依赖,pom.xml里只需要三个核心包:javax.servlet-api、mysql-connector-java、jstl。不要引入连接池框架,毕业设计的数据量用DriverManager直接获取连接就够了,引入连接池反而增加配置复杂度。

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

目录结构按标准 Web 项目来:src/main/java下分dao、service、servlet三个包,src/main/webapp下放 JSP 页面和WEB-INF/web.xml。dao包只负责 SQL 执行,service包负责业务逻辑(比如计算总金额),servlet包负责接收请求和跳转。这种分层不是为了“显得专业”,而是为了你调试时能快速定位问题——页面报错看 servlet,数据不对看 service,SQL 异常看 dao。

3. 核心业务实现:入住登记、换房与退房结算的代码落地

3.1 入住登记:一个 Servlet 完成三张表的写入

入住登记是整个系统最复杂的操作,因为它要同时写入guest、orders两张表,并更新room表的状态。这三步必须在一个数据库事务里完成,否则会出现“客人信息写入了但房间没锁上”的脏数据。

// CheckInServlet.java 核心代码 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String name = request.getParameter("name"); String idCard = request.getParameter("idCard"); String phone = request.getParameter("phone"); int roomId = Integer.parseInt(request.getParameter("roomId")); double deposit = Double.parseDouble(request.getParameter("deposit")); Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入或查询客人信息 int guestId = GuestDao.insertOrGet(conn, name, idCard, phone); // 2. 创建订单 OrderDao.createOrder(conn, guestId, roomId, deposit); // 3. 更新房间状态为已入住 RoomDao.updateStatus(conn, roomId, 1); conn.commit(); // 三步都成功才提交 response.sendRedirect("roomList.jsp"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); request.setAttribute("error", "入住登记失败:" + e.getMessage()); request.getRequestDispatcher("checkIn.jsp").forward(request, response); } finally { DBUtil.close(conn); } }

这段代码的关键在conn.setAutoCommit(false)和conn.commit()的配对使用。很多同学写 JDBC 时习惯每条 SQL 自动提交,结果入住操作执行到一半报错,客人信息已经写进去了,但房间状态还是空闲,第二天另一个客人来开同一间房,系统就翻车了。GuestDao.insertOrGet方法里要先按id_card查询,如果客人已存在就返回已有 ID,不存在才插入,这样回头客再次入住时不会产生重复的客人记录。

参数说明:deposit押金金额由前端输入,一般设为房费的一半或固定 200 元。roomId从房间列表页面传递过来,必须是当前状态为“空闲”的房间。如果前端传了一个已入住的房间 ID,RoomDao.updateStatus应该检查受影响行数,如果返回 0 说明状态不对,要主动抛异常触发回滚。

3.2 换房操作:两个 UPDATE 的顺序不能反

换房是毕业设计里容易被忽略但答辩时经常被问到的功能。逻辑不复杂:把旧房间状态改回空闲,把新房间状态改为已入住,同时更新订单里的room_id。但这里有一个顺序问题——必须先更新订单,再更新房间状态,否则如果中间报错,订单还指向旧房间,但旧房间已经变成空闲了,数据就乱了。

// ChangeRoomServlet.java 核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int orderId = Integer.parseInt(request.getParameter("orderId")); int newRoomId = Integer.parseInt(request.getParameter("newRoomId")); Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 查询原订单信息 Order order = OrderDao.findById(conn, orderId); int oldRoomId = order.getRoomId(); // 2. 检查新房间是否空闲 Room newRoom = RoomDao.findById(conn, newRoomId); if (newRoom.getStatus() != 0) { throw new RuntimeException("目标房间不是空闲状态"); } // 3. 先更新订单的房间ID OrderDao.updateRoom(conn, orderId, newRoomId); // 4. 再释放旧房间、占用新房间 RoomDao.updateStatus(conn, oldRoomId, 0); RoomDao.updateStatus(conn, newRoomId, 1); conn.commit(); response.sendRedirect("orderList.jsp"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute("error", "换房失败:" + e.getMessage()); request.getRequestDispatcher("changeRoom.jsp").forward(request, response); } finally { DBUtil.close(conn); } }

注意第 2 步的检查:如果新房间不是空闲状态,直接抛异常,后面的更新都不会执行。这个检查必须在事务内做,因为如果在事务外检查,检查完到更新之间可能有另一个请求把房间占了。虽然毕业设计通常只有你一个人操作,但养成这个习惯对以后做真实项目有好处。

3.3 退房结算:金额计算与押金扣除

退房结算涉及金额计算,是答辩时老师最容易追问的地方。核心公式是:总金额 = 房费 × 入住天数 + 其他消费 - 押金。入住天数按自然日计算,比如 1 号下午入住、3 号上午退房,算 2 天。

// CheckOutServlet.java 金额计算部分 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int orderId = Integer.parseInt(request.getParameter("orderId")); double otherCost = Double.parseDouble(request.getParameter("otherCost")); // 其他消费 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); Order order = OrderDao.findById(conn, orderId); Room room = RoomDao.findById(conn, order.getRoomId()); RoomType type = RoomTypeDao.findById(conn, room.getTypeId()); // 计算入住天数 long checkIn = order.getCheckInTime().getTime(); long checkOut = System.currentTimeMillis(); long days = (checkOut - checkIn) / (1000 * 60 * 60 * 24); if (days == 0) days = 1; // 不足一天按一天算 // 计算总金额 double roomFee = type.getPrice() * days; double totalAmount = roomFee + otherCost - order.getDeposit(); // 更新订单状态和金额 OrderDao.checkOut(conn, orderId, totalAmount); // 释放房间 RoomDao.updateStatus(conn, room.getId(), 0); conn.commit(); request.setAttribute("totalAmount", totalAmount); request.getRequestDispatcher("checkOutResult.jsp").forward(request, response); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } e.printStackTrace(); } finally { DBUtil.close(conn); } }

days的计算用毫秒差除以一天的毫秒数,注意整数除法会截断,所以days == 0时要手动设为 1。otherCost是退房时前台输入的额外消费,比如房间里的饮料、损坏赔偿等。totalAmount可能为负数,表示需要退给客人钱;也可能为正数,表示客人需要补差价。前端页面要根据正负值显示不同的提示文案。

4. 避坑与排查:毕业设计里最容易翻车的五个点

4.1 中文乱码:POST 和 GET 要分开处理

现象:入住登记时客人姓名显示为???或乱码。原因:POST 请求默认用 ISO-8859-1 解码,GET 请求在 Tomcat 8 以后默认用 UTF-8 但 URL 参数仍可能乱码。解决:在 Servlet 的doPost第一行加request.setCharacterEncoding("UTF-8"),response.setContentType("text/html;charset=UTF-8")。对于 GET 请求,需要在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。JSP 页面头部也要加<%@ page contentType="text/html;charset=UTF-8" %>。这三处缺一处都可能乱码。

4.2 数据库连接泄漏:finally块里必须关闭

现象:系统运行一段时间后报Too many connections。原因:每次请求都DriverManager.getConnection()但没关闭,MySQL 默认最大连接数 151,毕业设计演示时可能连续操作几十次就满了。解决:把Connection、PreparedStatement、ResultSet的关闭都放在finally块里,按ResultSet → PreparedStatement → Connection的顺序关闭。如果嫌麻烦,写一个DBUtil.close(conn, ps, rs)静态方法统一处理。

4.3 房间状态不同步:事务没回滚

现象:入住操作报错后,客人信息写入了但房间还是空闲。原因:没有用事务,或者用了事务但 catch 块里没调rollback()。解决:确保setAutoCommit(false)后,所有数据库操作都在try块内,catch块里第一件事就是conn.rollback()。另外注意,rollback()本身也可能抛异常,要单独 try-catch,不要让它覆盖了原始异常信息。

4.4 JSP 页面取不到值:作用域搞混了

现象:Servlet 里request.setAttribute("roomList", list),JSP 里用${roomList}取不到。原因:用了response.sendRedirect()跳转,重定向会产生新的 request,原来的 attribute 丢失了。解决:需要传数据时用request.getRequestDispatcher("page.jsp").forward(request, response),转发是同一个 request。如果必须重定向,把数据放到 session 里,用完再session.removeAttribute()。

4.5 日期格式转换异常:前端传的格式和后端解析不一致

现象:退房时SimpleDateFormat.parse()报ParseException。原因:前端<input type="date">传的是2024-01-15格式,后端用yyyy-MM-dd HH:mm:ss解析就会失败。解决:统一前后端日期格式,要么都用yyyy-MM-dd,要么前端传时间戳。我一般会在 Servlet 里写一个parseDate(String)工具方法,先尝试yyyy-MM-dd HH:mm:ss,失败再尝试yyyy-MM-dd,都失败才抛异常。

5. 进阶技巧:用过滤器统一编码和登录校验

5.1 编码过滤器:一次配置,全站生效

与其在每个 Servlet 里写request.setCharacterEncoding("UTF-8"),不如写一个CharacterEncodingFilter,在web.xml里配置成拦截所有请求。这样新增 Servlet 时不用再操心编码问题。

// CharacterEncodingFilter.java public class CharacterEncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response); // 放行 } }

web.xml里配置<url-pattern>/*</url-pattern>拦截所有请求。注意chain.doFilter()必须调用,否则请求会被卡在过滤器里,页面一直转圈。

5.2 登录校验过滤器:保护后台页面

酒店管理系统的房间列表、订单管理这些页面不能让人直接输 URL 访问,需要登录后才能看。用过滤器检查 session 里有没有user对象,没有就跳转到登录页。

// AuthFilter.java public class AuthFilter implements Filter { @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(); // 登录页和登录接口放行 if (uri.contains("login.jsp") || uri.contains("LoginServlet")) { chain.doFilter(request, response); return; } // 检查登录状态 if (request.getSession().getAttribute("user") == null) { response.sendRedirect("login.jsp"); return; } chain.doFilter(request, response); } }

这个过滤器要放在编码过滤器之后执行,顺序在web.xml里按<filter-mapping>的声明顺序决定。登录成功后把用户对象存到session.setAttribute("user", user),退出时session.invalidate()。

5.3 用 JSTL 简化 JSP 页面

JSP 里写 Java 代码<% %>是毕业设计里常见的做法,但代码可读性差。用 JSTL 的<c:forEach>和<c:if>替代,页面会干净很多。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <c:forEach items="${roomList}" var="room"> <tr> <td>${room.roomNumber}</td> <td>${room.typeName}</td> <td> <c:choose> <c:when test="${room.status == 0}">空闲</c:when> <c:when test="${room.status == 1}">已入住</c:when> <c:otherwise>维修中</c:otherwise> </c:choose> </td> </tr> </c:forEach> </table>

<c:choose>相当于 Java 的switch,比连续写<c:if>更清晰。注意room.status是TINYINT类型,JSTL 里直接和数字比较即可,不用转字符串。

5.4 答辩演示前的检查清单

演示前按这个顺序过一遍:数据库连接是否正常(DBUtil.getConnection()不报错)、房间列表能否显示、入住登记后房间状态是否变为已入住、换房后两个房间状态是否正确、退房后金额计算是否符合预期、未登录时直接访问roomList.jsp是否跳转到登录页。这六个点覆盖了核心业务链路,任何一个出问题都会影响演示效果。我一般会提前准备一份测试数据,包括两个房型、五个房间、三个客人,演示时按“入住→换房→退房”的顺序走一遍,基本能覆盖所有功能点。

做毕业设计最怕的不是功能少,而是功能跑不通。先把入住、换房、退房这条主线跑顺,再考虑加报表、加图表。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 7:24:16

高效文献检索实操指南:从需求定位到全文获取的完整方法

不用迷信“会查文献”&#xff0c;能短时间定位到高质量文献并真正读进去&#xff0c;才是做研究的基本功。这篇内容不绕弯子&#xff0c;直接给你一套能落地的文献查找方法和操作步骤&#xff0c;从明确需求到筛选管理都有&#xff0c;自己踩过的坑和好用的技巧也一并写了。1.…

作者头像 李华
网站建设 2026/10/13 7:23:58

Django+Vue校园外卖平台全栈开发实战

1. 校园外卖的需求画像&#xff1a;三方角色与核心业务流程做校园外卖平台之前&#xff0c;我首先花了一周时间观察校园外卖的实际场景&#xff0c;而不是直接打开 IDE 写代码。校园外卖和美团、饿了么这类社会外卖最大的区别在于配送半径短、高峰期高度集中、用户群体相对封闭…

作者头像 李华
网站建设 2026/10/13 7:23:03

Canon EDSDK 相机二次开发实战:从设备枚举到自动化采集全攻略

简介&#xff1a;佳能EDSDK 3.6.1&#xff08;EOS Digital Solution Development Kit&#xff09;是一套面向C#、VB和C开发者的相机控制开发包&#xff0c;用于在Windows桌面应用中实现与佳能数码相机的通信与操控&#xff0c;可完成实时预览、拍摄录像、曝光参数调整、设备管理…

作者头像 李华
网站建设 2026/10/13 7:22:53

GitHub贡献图刷绿原理与autoCommit实战指南

简介&#xff1a;这是一款基于 TypeScript 开发的 VSCode 插件&#xff0c;面向希望美化 GitHub 贡献图、灵活管理提交记录的前端开发者与开源爱好者。它支持一键批量提交 commit&#xff0c;可自由指定过去几年乃至未来日期的提交&#xff0c;既能固定次数也能随机次数&#x…

作者头像 李华
网站建设 2026/10/13 7:22:31

SpringBoot整合Kafka实战:从基础配置到生产级架构

SpringBoot整合Kafka&#xff1a;构建高效消息驱动应用做后端开发的&#xff0c;这几年基本都绕不开Kafka。我自己从最早在项目里引入Kafka做日志异步采集&#xff0c;到后来负责电商订单状态同步、用户行为轨迹管道&#xff0c;再到帮团队搭建基于Kafka的Event-Driven架构&…

作者头像 李华
网站建设 2026/10/13 7:22:27

从CIDR到路由聚合:网络边界、前缀计算与排错实战

我最早对 CIDR 产生敬畏心&#xff0c;是在一次给分公司做路由汇总的深夜。公司刚接入了一个新办公区&#xff0c;路由表里本来静静躺着几条 /24 的明细路由。我贪省事&#xff0c;想着合并成大网段能少几行配置&#xff0c;就在出口设备上写了一条更大的汇总路由。结果第二天工…

作者头像 李华