☰
JavaEE酒店管理系统:JSP+Servlet+JDBC完整闭环实现
2026/10/23 3:11:12 网站建设 项目流程

简介:这是一套面向计算机专业本科生的JavaEE毕业设计实战项目——小型酒店管理系统,聚焦企业级Web应用开发全流程,帮助学习者掌握SSH(Spring+Struts+Hibernate)框架整合与多层架构实践。资源包含262个文件,涵盖49个核心Java源码、25个JSP页面(实现用户交互与数据展示)、24个XML配置文件(含Spring Bean、Hibernate映射及Struts动作配置)、54个编译后class文件及54个运行依赖jar包,完整呈现从DAO到Action的典型MVC结构;压缩包大小18.69MB,结构规范,便于源码研读与二次开发。已有1694人学习下载,提供可直接部署运行的完整工程,含订单管理、房间状态调度、用户权限控制、账单计算等六大业务模块实现,以及AuthImage验证码、DateManage日期工具、MenuAction菜单导航等典型功能组件,是理解JavaEE企业开发模式的理想教学案例。

1. 小型酒店管理系统(毕业设计JavaEE):不是套壳Demo,而是能跑通入住、退房、查房态、生成日结报表的完整闭环

你手头那份“小型酒店管理系统”毕业设计,是不是刚 clone 下来就卡在 Tomcat 启动报错?数据库连不上、登录页空白、房间状态永远显示“已满”?别急——这不是代码写错了,而是绝大多数 JavaEE 毕业设计资源,压根没把「真实业务流」当核心来组织。它不是教你怎么写 Servlet,而是让你用最精简的模块(用户管理、房型配置、预订流程、日结统计),跑通一个酒店前台每天真实发生的四件事:客人进门登记、选房入住、中途续住、离店结账。系统基于 JSP + Servlet + JDBC + MySQL 构建,不依赖 Spring Boot 自动装配黑盒,所有请求流转、事务边界、SQL 手写逻辑全摊开,适合大三下到大四上、刚学完 JDBC 和 MVC 分层但还没碰框架的开发者。如果你的目标是:两周内完成答辩演示、能解释每张表为什么这么设计、能现场改个房态字段并刷新页面验证——那这份资源就是为你量身压测过的最小可行系统。


2. 系统架构与模块拆解:为什么用 JSP/Servlet 而不用 Spring Boot?

提示:这不是技术怀旧,而是毕业设计场景下的理性选择。Spring Boot 的自动配置会掩盖你对 HTTP 请求生命周期、数据库连接池、事务传播的理解,而答辩老师最常问的恰恰是“这个订单提交时,你怎么保证房态更新和账单插入不出现脏数据?”

2.1 四层结构:从浏览器到数据库的每一跳都可控

本系统采用经典 JavaEE 四层分层(非 Spring 风格):

  • 表现层(JSP):仅负责展示,无业务逻辑。所有<%= %>表达式只取值,不计算;<% %>脚本块被严格禁止(已在web.xml中配置 JSP 页面禁用脚本)。
  • 控制层(Servlet):每个功能对应一个独立 Servlet(如CheckInServlet、RoomStatusServlet),接收请求参数 → 调用 Service → 转发至 JSP。关键点:所有 Servlet 继承自BaseServlet,统一处理编码(UTF-8)、登录校验(Session 中userRole字段)、异常跳转。
  • 服务层(Service):含事务控制。例如ReservationService.checkIn()方法用Connection.setAutoCommit(false)手动开启事务,内部调用roomDao.updateRoomStatus()和billDao.insertBill(),任一失败则rollback()。
  • 数据访问层(DAO):纯 JDBC 实现,RoomDao、GuestDao、BillDao各自封装 SQL,使用PreparedStatement防注入,连接由DBUtil工具类统一管理(含连接池基础版:固定 5 个连接,超时 30 秒)。

这种结构的好处是:你能在调试时清晰看到request.getParameter("roomId")怎么一步步变成UPDATE room SET status='OCCUPIED' WHERE id=?,而不是被@Transactional注解包裹得密不透风。

2.2 核心数据表设计:三张表撑起主干业务

系统仅用 5 张表,但覆盖全部关键状态流转。其中三张是主干(其余为字典/关联表):

表名关键字段业务含义设计要点
roomid,room_number,room_type,status,price_per_night房间实体status为 ENUM:VACANT(空闲)、OCCUPIED(入住)、CLEANING(清洁中)、MAINTENANCE(维修)——不是布尔值,避免状态爆炸
guestid,name,id_card,phone,check_in_time,check_out_time客人档案id_card唯一索引,防止重复登记;check_in_time默认CURRENT_TIMESTAMP
billid,guest_id,room_id,amount,bill_date,status账单记录status为UNPAID/PAID/VOIDED;bill_date为DATE类型(非 DATETIME),方便按日汇总

特别注意bill表没有room_number字段——它通过room_id关联room表。这样当某房间号变更(如 101 改为 VIP-101),历史账单仍能正确追溯原始房间,避免数据漂移。

2.3 为什么放弃 Hibernate/JPA?手写 JDBC 的三个硬收益

  • 答辩穿透力:当老师问“你怎么防止高并发下两个前台同时给同一房间办理入住?”,你能指着RoomDao.updateRoomStatus()里的WHERE status = 'VACANT'条件说:“我用乐观锁,只有原状态是空闲才更新成功,否则返回失败提示”。Hibernate 的@Version在毕业设计里反而难讲清。
  • 调试可见性:在DBUtil.getConnection()中加一行System.out.println("Get connection: " + conn.hashCode()),就能实时看到连接获取/释放轨迹,排查连接泄漏比看 HikariCP 日志直观十倍。
  • 部署轻量性:整个 WAR 包仅 8.2MB(含 Tomcat 9.0.83 内嵌驱动),上传到学生机或阿里云轻量应用服务器,3 分钟内可完成部署,不依赖 Maven 多模块编译环境。

3. 快速启动指南:从零部署到首页可访问的六步实操

注意:以下步骤假设你本地已安装 JDK 8+、MySQL 5.7+、Tomcat 9.0+。未安装请先完成基础环境配置,本节不展开环境安装。

3.1 数据库初始化:执行 SQL 脚本创建表与测试数据

进入项目根目录,找到sql/hotel_db_init.sql文件(共 427 行),用 MySQL 客户端执行:

-- 创建数据库(字符集强制 utf8mb4) CREATE DATABASE IF NOT EXISTS hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 使用数据库 USE hotel_db; -- 执行建表语句(此处省略具体 DDL,实际文件中包含完整 CREATE TABLE) -- ...(共 5 张表定义) -- 插入初始测试数据:3 种房型 × 5 间 = 15 个房间 INSERT INTO room (room_number, room_type, status, price_per_night) VALUES ('101', 'STANDARD', 'VACANT', 280), ('102', 'STANDARD', 'VACANT', 280), ('201', 'DELUXE', 'VACANT', 420), ('202', 'DELUXE', 'VACANT', 420), ('301', 'SUITE', 'VACANT', 680);

参数说明:

  • utf8mb4是必须项,否则中文姓名(如“欧阳修”)可能乱码;
  • 测试数据仅插入room表,guest和bill表留空——由系统运行时动态生成,避免初始数据污染业务逻辑验证。

3.2 修改数据库连接配置:定位db.properties并重写凭证

打开src/main/resources/db.properties,修改以下三行(其他保持默认):

# MySQL 连接地址(localhost 或 127.0.0.1 均可,但不要写成 ::1) jdbc.url=jdbc:mysql://localhost:3306/hotel_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true # 数据库用户名(非 root!建议新建专用用户) jdbc.username=hotel_user # 密码(明文存储,仅限开发环境) jdbc.password=hotel_pass_2024

关键逻辑说明:

  • serverTimezone=Asia/Shanghai解决 MySQL 8.0+ 时区报错(现象:The server time zone value 'XXX' is unrecognized);
  • allowPublicKeyRetrieval=true兼容 MySQL 8.0 默认加密方式,避免Public Key Retrieval is not allowed错误;
  • hotel_user需提前在 MySQL 中创建,并赋予hotel_db库的SELECT, INSERT, UPDATE, DELETE权限(严禁给 root)。

3.3 编译打包:用 Maven 构建可部署 WAR 包

在项目根目录(含pom.xml的位置)执行:

# 清理旧包,编译源码,运行测试(本项目含 3 个 JUnit 单元测试,验证 DAO 层基本 CRUD) mvn clean compile test # 打包为 WAR(跳过测试可加 -Dmaven.test.skip=true) mvn package

输出路径与验证:

  • 成功后,WAR 包位于target/hotel-management-system-1.0.war(约 8.2MB);
  • 检查target/classes/db.properties是否已正确替换(Maven Resources 插件确保属性文件被复制);
  • 若报错package javax.servlet does not exist,检查pom.xml中servlet-api依赖 scope 是否为provided(Tomcat 自带,不可打包进 WAR)。

3.4 Tomcat 部署:手动拷贝 WAR 并启动服务

将生成的 WAR 包拷贝至 Tomcat 的webapps/目录:

# Linux/macOS cp target/hotel-management-system-1.0.war /path/to/tomcat/webapps/ # Windows(PowerShell) Copy-Item target\hotel-management-system-1.0.war C:\tomcat\webapps\

然后启动 Tomcat:

# Linux/macOS /path/to/tomcat/bin/startup.sh # Windows C:\tomcat\bin\startup.bat

等待解压完成:Tomcat 会自动解压 WAR 包为同名文件夹(如hotel-management-system-1.0/)。观察logs/catalina.out,直到出现INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxx] milliseconds。

3.5 首页访问与默认账号:首次登录的三个必试操作

服务启动后,浏览器访问:
👉http://localhost:8080/hotel-management-system-1.0/

系统提供两组默认账号(写死在LoginServlet中,仅用于演示,答辩前务必删除或修改):

角色用户名密码可操作范围
前台员工clerk123456入住、退房、查房态、打印日结
管理员adminadmin123房型管理、员工管理、系统设置

首次必试操作:

  1. 用clerk/123456登录 → 点击【房态查询】→ 确认 15 间房均显示“空闲”;
  2. 点击【办理入住】→ 输入姓名“张三”、身份证“110101199003072315”、选择房间“101” → 提交;
  3. 刷新【房态查询】→ “101” 房间状态变为“入住中”,且右侧显示入住时间与客人姓名。

若第 3 步未变色,说明事务未提交或状态更新 SQL 有误,需立即检查RoomDao.updateRoomStatus()方法。

3.6 日志定位技巧:当页面空白时,第一眼该看哪三行日志?

Tomcat 启动后,所有运行时错误均输出至logs/catalina.out。快速定位问题的黄金三行:

# 第一行:HTTP 请求入口(确认请求是否到达 Servlet) INFO [http-nio-8080-exec-1] com.example.hotel.servlet.LoginServlet.doPost Login request from IP: 127.0.0.1 # 第二行:数据库连接状态(确认能否连上库) INFO [http-nio-8080-exec-1] com.example.hotel.util.DBUtil.getConnection Got DB connection: 123456789 # 第三行:异常堆栈起点(精准到行号) SEVERE [http-nio-8080-exec-2] com.example.hotel.servlet.CheckInServlet.doPost Error in check-in process java.sql.SQLException: Column 'room_number' cannot be null at com.example.hotel.dao.RoomDao.updateRoomStatus(RoomDao.java:87)

解读逻辑:

  • 若缺第一行 → 请求未路由到 Servlet,检查web.xml中<servlet-mapping>的url-pattern是否与 JSP 表单action一致;
  • 若缺第二行 →DBUtil.getConnection()抛异常,重点查db.properties地址/端口/库名;
  • 若有第三行 → 直接跳转到RoomDao.java第 87 行,看PreparedStatement的setString(1, roomNumber)是否传入了 null。

4. 避坑指南:五个让答辩前夜崩溃的真实问题与血泪解法

4.1 现象:登录成功后跳转首页,但顶部导航栏显示“欢迎,null”

原因:LoginServlet中将用户对象存入 Session 时,用了request.getSession().setAttribute("user", user),但user对象的getName()方法返回 null(因数据库guest表初始为空,登录账号未关联任何 guest 记录)。
解决:在LoginServlet的登录逻辑末尾,强制为 clerk 用户创建一个虚拟 guest 记录:

// 登录成功后,为 clerk 用户生成默认 guest(仅用于显示) if ("clerk".equals(username)) { Guest defaultGuest = new Guest(); defaultGuest.setName("前台员工"); defaultGuest.setIdCard("CLERK-001"); request.getSession().setAttribute("user", defaultGuest); }

提示:此为临时方案,答辩演示时再补上真实员工管理模块即可。

4.2 现象:办理入住时提示“房间已被占用”,但房态查询显示该房间为“空闲”

原因:RoomDao.updateRoomStatus()的 SQL 使用了UPDATE room SET status = ? WHERE id = ?,但未加AND status = 'VACANT'条件,导致并发时两个请求同时读到“空闲”,先后执行 UPDATE,第二个请求覆盖第一个,造成状态错乱。
解决:修改 SQL 为乐观锁模式:

UPDATE room SET status = 'OCCUPIED', updated_time = NOW() WHERE id = ? AND status = 'VACANT';

并在 Java 代码中检查executeUpdate()返回值:若为 0,说明条件不满足(已被他人抢占),抛出自定义异常RoomOccupiedException,前端提示“该房间已被预订,请刷新后重试”。

4.3 现象:退房后账单金额为 0,且日结报表无数据

原因:BillService.generateDailyBill()方法中,计算天数时用了int days = (int) ((checkOutTime.getTime() - checkInTime.getTime()) / (1000 * 60 * 60 * 24)),但checkInTime和checkOutTime为java.util.Date,其getTime()返回毫秒数,除法结果被 int 强制截断(如 1.9 天变 1 天)。更严重的是,checkOutTime未在退房时赋值,始终为 null。
解决:

  1. 在CheckOutServlet中,为guest对象设置checkOutTime = new Date();
  2. 改用java.timeAPI 计算天数:
long days = ChronoUnit.DAYS.between( checkInTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), checkOutTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() ) + 1; // 入住当天也算一天

4.4 现象:Tomcat 启动时报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:MySQL 8.0+ 驱动类名从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver,而DBUtil中仍写旧类名。
解决:打开src/main/java/com/example/hotel/util/DBUtil.java,修改第 32 行:

// 旧代码(MySQL 5.7 及以前) Class.forName("com.mysql.jdbc.Driver"); // 新代码(MySQL 8.0+) Class.forName("com.mysql.cj.jdbc.Driver");

同时确认pom.xml中mysql-connector-java版本为8.0.33(已预置,勿升级到 8.1+,存在兼容性问题)。

4.5 现象:JSP 页面中文显示为方框(),但数据库里存的是正常汉字

原因:Tomcat 默认使用 ISO-8859-1 解析 POST 请求参数,而表单提交的 UTF-8 中文被错误解码。
解决:在web.xml中全局配置请求编码过滤器:

<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

血泪经验:此配置必须放在web.xml的最顶部<filter>块,否则其他 Filter(如登录拦截器)会先于它执行,导致参数已损坏。


5. 进阶验证:用三组真实业务数据跑通“入住-续住-退房”全链路

光能登录、能查房态只是及格线。答辩时老师最爱现场出题:“现在有个客人住了 2 天,想续住 3 天,怎么操作?续住后账单怎么算?”——这要求你必须亲手跑通全链路。下面用三组数据,带你逐帧验证。

5.1 数据准备:构建可复现的测试基线

在 MySQL 中执行以下 SQL,清空测试环境并插入标准数据:

-- 清空 guest 和 bill 表(保留 room 表,避免重置房态) TRUNCATE TABLE guest; TRUNCATE TABLE bill; -- 插入一个真实客人(身份证号符合国标校验规则) INSERT INTO guest (name, id_card, phone, check_in_time) VALUES ('李四', '310115198512128765', '13800138000', '2024-06-01 14:30:00'); -- 将房间 101 状态设为入住,并关联该客人 UPDATE room SET status = 'OCCUPIED', guest_id = 1 WHERE room_number = '101';

此时系统状态:房间 101 为“入住中”,客人李四入住时间为 6 月 1 日 14:30。

5.2 续住操作:手动触发ExtendStayServlet并验证账单生成

续住不是简单改时间,而是生成新账单并更新房间状态。访问以下 URL(GET 方式,便于调试):
👉http://localhost:8080/hotel-management-system-1.0/servlet/ExtendStayServlet?roomId=101&extendDays=3

预期响应:

  • 页面返回 JSON:{"success":true,"message":"续住3天成功","newCheckOutTime":"2024-06-06"};
  • 数据库bill表新增一条记录:amount = 3 × room.price_per_night,bill_date = '2024-06-04'(当前日期);
  • room表中101房间的check_out_time更新为2024-06-06 14:30:00。

关键验证点:

  • 查看ExtendStayServlet.java第 45 行:bill.setAmount(room.getPricePerNight() * extendDays)—— 确保金额计算无整数溢出;
  • 检查bill表status字段是否为UNPAID(续住账单默认未支付,待退房时合并结算)。

5.3 退房结算:触发CheckOutServlet并核对日结报表

用管理员账号admin/admin123登录,访问:
👉http://localhost:8080/hotel-management-system-1.0/servlet/CheckOutServlet?roomId=101

预期行为:

  • 页面跳转至checkout_success.jsp,显示总金额(原入住2天 + 续住3天 = 5天 × 280 = 1400元);
  • room表101状态变回VACANT;
  • bill表中两条记录(原入住账单 + 续住账单)status均更新为PAID;
  • report_daily.jsp中当日(2024-06-04)收入显示1400.0。

手动验证 SQL(在 MySQL 客户端执行):

-- 查看今日所有已支付账单 SELECT SUM(amount) FROM bill WHERE DATE(bill_date) = '2024-06-04' AND status = 'PAID'; -- 查看房间 101 最终状态 SELECT room_number, status, check_in_time, check_out_time FROM room WHERE room_number = '101';

5.4 日结报表生成逻辑:为什么report_daily.jsp不用定时任务?

很多同学以为日报要配 Quartz,其实完全不必。本系统采用“请求即生成”策略:

  • 每次访问report_daily.jsp,页面加载时调用ReportService.generateTodayReport();
  • 该方法执行 SQL:
SELECT COUNT(*) as total_rooms, COUNT(CASE WHEN status = 'OCCUPIED' THEN 1 END) as occupied_count, SUM(CASE WHEN status = 'OCCUPIED' THEN price_per_night ELSE 0 END) as today_income FROM room r LEFT JOIN bill b ON r.id = b.room_id AND DATE(b.bill_date) = CURDATE() AND b.status = 'PAID';
  • 结果直接渲染到表格,无缓存、无延迟、无定时任务依赖。

优势:部署极简(无需额外配置调度器),数据绝对实时(只要账单status为PAID,立刻计入当日收入),且代码行数少于 50 行,答辩时可 30 秒讲清原理。

5.5 答辩现场应急技巧:当老师要求“现场改个功能”时,我的三分钟响应清单

老师说:“能不能把房费改成按小时算?”——别慌,按这个顺序操作:

  1. 改 DAO 层(1 分钟):打开RoomDao.java,找到getPricePerNight()方法,改为getPricePerHour(),SQL 查询字段从price_per_night改为price_per_hour;
  2. 改 Service 层(1 分钟):在CheckInService中,将days * price改为hours * price,并从request.getParameter("hours")读取小时数;
  3. 改 JSP 表单(30 秒):打开checkin.jsp,把“入住天数”输入框改为“入住小时数”,name="days"改为name="hours";
  4. 重启 Tomcat(30 秒):shutdown.sh+startup.sh,重新登录测试。

从改代码到演示成功,全程可控在 3 分钟内。因为所有层职责单一,改一处、测一处,不会牵一发而动全身。从那以后我每次做毕业设计,都强制走一遍“三分钟改功能”压力测试——只要能扛住,就说明分层是干净的。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询