简介:这份资源是一套面向高校计算机专业毕业设计的小型酒店管理系统,基于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是否跳转到登录页。这六个点覆盖了核心业务链路,任何一个出问题都会影响演示效果。我一般会提前准备一份测试数据,包括两个房型、五个房间、三个客人,演示时按“入住→换房→退房”的顺序走一遍,基本能覆盖所有功能点。
做毕业设计最怕的不是功能少,而是功能跑不通。先把入住、换房、退房这条主线跑顺,再考虑加报表、加图表。希望帮到你。
本文还有配套的精品资源,点击获取