☰
JSP酒店管理系统课设指南:从表设计到部署避坑
2026/10/8 9:40:54 网站建设 项目流程

简介:这是一套基于JSP+MySQL的酒店管理系统WEB项目,采用B/S模式开发,适合正在学习Java Web、JSP网页编程,或需要完成课程设计、毕业设计的学生参考。项目内置完整的源代码和数据库脚本,前台客房展示、预订管理与后台维护等模块覆盖常用业务场景,能帮助读者理解从数据库设计、Java后台逻辑到JSP页面展示的完整开发流程。资源共186个文件,rar压缩包约1.28MB,主体为96个JSP页面,另有SQL脚本、Java类、JAR包、CSS样式、JS脚本以及JPG、GIF图片素材;其中SQL脚本可用于初始化数据库,图片素材用于网站界面展示,同时保留class、bak等工程文件,基本满足导入开发工具运行和二次修改的需要。目前已有381人学习/下载。对希望快速搭建酒店管理Demo、梳理Java Web项目开发思路的初学者来说,这套项目可直接导入MyEclipse/Eclipse配合Tomcat运行,省去从零搭建环境和准备数据的时间。

1. JSP酒店管理系统:课程设计里的Java Web全栈最小闭环

“JSP酒店管理系统”这个题目,几乎每年都会出现在软件工程课程设计和毕业设计的选题清单上。乍看是学生项目,实际上它把Java Web开发的主线串全了:JSP负责页面展示,Servlet处理请求,JavaBean封装数据,MySQL负责持久化,MyEclipse承担编辑、部署和调试,软件工程设计文档全程兜底。反直觉的是,很多觉得“JSP已经过时”的同学,恰恰是因为没吃透这套请求-响应模型,后面学Spring MVC时又回头补课。这篇笔记给你一条能直接照做的落地路线——从数据库表设计、连接池配置到核心功能代码和部署避坑,照着走完,你拿到的不仅是一个能跑的系统,还是一套能应付答辩的完整交付物,适合正在做课程设计或毕设、想快速出成果的读者。

2. 技术选型:JSP+Servlet+MySQL这套组合为什么还值得选

2.1 先分清业务边界:酒店管理系统和CRM系统不是一回事

很多人在设计表结构时会顺手把客户管理做得很重,甚至塞进客户回访、跟进记录这类模块,理由是“酒店也要维护客户关系”。这里有必要区分一下:酒店管理系统的核心是房间状态和预订周期,系统的每一次操作几乎都围绕“某间房在某段时间内属于谁”展开;而客户关系管理系统的核心是销售漏斗和交互历史。你做的是前者,把房态表、预订表、入住单这三张表理清楚,系统就立住了一半。客户表只需要存身份证号、手机号、姓名这些登记必需的字段,不要把它扩展成会员营销系统。

技术栈选型上,课程设计和毕业设计场景里,JSP+Servlet+JavaBean依然是性价比最高的组合。原因很实在:第一,评审老师要看的是你对HTTP协议、Session、JDBC这些底层机制的理解,框架会把它们藏起来;第二,这套技术栈和MyEclipse的集成度最高,新建Web Project、配置Tomcat、部署调试都是图形化操作,新手不容易在环境上耗尽耐心;第三,后面学Spring MVC时你会发现,DispatcherServlet、@RequestMapping这些概念的前身就是Servlet映射和doGet/doPost,现在打的基础不浪费。

2.2 网络编程在这套系统里的真实位置

标题里带“网络编程”,但酒店管理系统通常不需要用Socket自己写通信协议。这里的网络编程体现在三个层面:一是浏览器与Tomcat之间的HTTP请求-响应,你通过Servlet的doPost接收表单、通过response重定向页面,这是应用层编程;二是JDBC驱动通过TCP/IP与MySQL服务器通信,连接池管理的就是这些网络连接的生命周期;三是你在部署时需要配置Tomcat的Connector端口、MySQL的3306端口,这属于网络服务配置。常见做法是建议你在课设文档里画一张三层的网络拓扑图,标清楚客户端、Tomcat、MySQL之间的协议和端口,面试或答辩时能讲明白,比闷头写一个Socket聊天室有用得多。

如果你确实想在项目里体现一点Socket编程,可以加一个“客房状态广播”的选做功能:管理员办理入住后,通过Socket向大厅展示屏推送一条消息。但要注意,这个功能在架构上是可选的,不要因为它破坏了JSP页面同步刷新的主流程。

2.3 MyEclipse下的三层架构目录划分

MyEclipse创建Web项目时默认的WebRoot目录,对应的是部署到Tomcat后的应用根路径。我一般会按下面的结构组织代码,让软件工程设计文档里的“分层架构图”可以直接对应到目录:

HotelSystem/ ├── src/ │ ├── com/hotel/entity/ # JavaBean:Admin、Room、Customer等 │ ├── com/hotel/dao/ # 数据访问:JDBC操作封装 │ ├── com/hotel/servlet/ # 控制器:接收请求并跳转 │ └── com/hotel/util/ # DBUtil连接池工具、MD5工具 ├── WebRoot/ │ ├── login.jsp # 登录页 │ ├── admin/ │ │ ├── index.jsp # 主框架页 │ │ ├── room_list.jsp # 客房列表 │ │ ├── reservation_add.jsp # 预订登记 │ │ └── ... # 其他功能页面 │ ├── css/ # 样式 │ └── WEB-INF/ │ ├── web.xml # Servlet映射、欢迎页面 │ └── lib/ # mysql-connector-java.jar └── sql/ └── init.sql # 建库建表脚本

把这个结构讲清楚后,软件工程文档里的模块划分、包结构图、部署视图就全都有了素材。一个容易翻车的点是lib目录:MySQL驱动Jar必须放在WebRoot/WEB-INF/lib下,而不是只加到Build Path里。只加Build Path,编译能通过,但部署到Tomcat运行时必然报ClassNotFoundException,这个坑我在第一次做课设时踩过,后面细说。

3. MySQL端先行:6张核心表的设计与JDBC连接池参数

3.1 先画ER图再写代码:表结构是这套系统的地基

动手写第一行JSP之前,我习惯先把数据库脚本写完。酒店管理系统最核心的六张表是:管理员表admin、房型表room_type、客房表room、客户表customer、预订表reservation和入住单表check_in。它们的关系很清楚:房型一对多客房,客户一对多预订,预订多对一客房。如果你用MySQL 8.x的官方压缩版做数据库,下载解压后要手动执行mysqld --initialize-insecure初始化数据目录,再注册Windows服务,直接用安装版跳过这一步也行。

设计字段时有三个细节容易被忽略。第一,金额字段用DECIMAL(10,2),不要用float或double,二进制浮点在做结算统计时会出现0.1+0.2=0.30000000000000004这种问题,答辩时被问到会很尴尬。第二,日期字段用DATE类型存入住日期和离店日期,不要用DATETIME带时分秒,否则“当天入住当天离店”的区间判断容易出错。第三,每张表都加一个create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,方便以后排查数据问题。

3.2 建库建表:字符集用utf8mb4而不是utf8

下面是init.sql的核心部分,你可以在MyEclipse里直接连MySQL执行,也可以用命令行SOURCE导入:

CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hotel_db; CREATE TABLE admin ( admin_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 'MD5加盐后的密文', real_name VARCHAR(20) ); CREATE TABLE room_type ( type_id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(20) NOT NULL, price DECIMAL(10,2) NOT NULL, bed_count TINYINT DEFAULT 1 ); CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE, type_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0空房 1入住 2维修', FOREIGN KEY (type_id) REFERENCES room_type(type_id) ); CREATE TABLE customer ( customer_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, phone VARCHAR(11) ); CREATE TABLE reservation ( reservation_id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, customer_id INT NOT NULL, arrive_date DATE NOT NULL, leave_date DATE NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待入住 1已入住 2已取消', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES room(room_id), FOREIGN KEY (customer_id) REFERENCES customer(customer_id) );

这里有几个设计上的说明。id_card加了UNIQUE约束,同一身份证不能重复登记,这是酒店实名登记的硬性要求;room表的status是TINYINT,用数字表示状态而不是字符串,是为了后面UPDATE状态联动时不需要拼接SQL;reservation表没有直接放客户姓名,而是用customer_id关联,这是为了满足第三范式的规范,答辩时评审老师会看这一点。

如果你在课程设计中途想改表结构,直接用ALTER TABLE加列或加约束是可以的,但要注意:如果表里已经有重复数据,加UNIQUE约束会报Duplicate entry错误。常见做法是先写一条查询把重复数据找出来清理掉,再执行修改结构的语句。这个“先清理后加约束”的顺序,比直接执行ALTER TABLE省心得多。

3.3 数据库连接池:参数怎么设,连接从哪里来

新建项目时我一般不在每个DAO里用DriverManager.getConnection,那样每次请求都新建物理连接,数据库压力大,也拿不出手。常见做法是在Tomcat里配置JNDI数据源,database连接池由Tomcat统一管理。在MyEclipse里,你可以在WebRoot/META-INF/context.xml里写:

<Context> <Resource name="jdbc/hotel" auth="Container" type="javax.sql.DataSource" maxActive="20" maxIdle="8" maxWait="10000" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/hotel_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8" username="root" password="123456"/> </Context>

参数说明:maxActive是最大活跃连接数,课程设计场景20就够,不要追求大——MySQL服务端的max_connections默认只有151,你把连接池的maxActive设成200,并发一高反而会被MySQL拒绝连接;maxIdle是空闲连接上限,8到10比较合理;maxWait是拿不到连接时的最长等待毫秒数,10000意味着超过10秒就抛SQLException,页面不会一直转圈。driverClassName这里用的是com.mysql.cj.jdbc.Driver,这是MySQL 8.x系列驱动的类名;如果你还在用5.x的驱动,类名是com.mysql.jdbc.Driver,写错启动时会直接报ClassNotFoundException。

然后是DBUtil工具类,所有DAO都通过它取连接:

package com.hotel.util; import javax.naming.Context; import javax.naming.InitialContext; import javax.sql.DataSource; import java.sql.Connection; public class DBUtil { private static DataSource dataSource; static { try { Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:/comp/env"); dataSource = (DataSource) envCtx.lookup("jdbc/hotel"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws Exception { return dataSource.getConnection(); } }

注意静态代码块只执行一次,这是连接池复用的关键。之后在DAO里拿到的是池里的物理连接,close()不是真正断开MySQL连接,而是把连接归还给池子。如果代码里漏掉了close,连接就会被一直占用,攒到maxActive就卡死——这个坑在避坑章节里单独讲。

4. 核心功能实现:登录防注入、客房状态联动与预订事务

4.1 登录功能:PreparedStatement挡掉SQL注入

先做一个登录Servlet,同时把个人信息展示页面作为登录成功的落地页。这里不使用Statement拼SQL,而是用PreparedStatement的占位符机制。直接看代码:

package com.hotel.servlet; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; import java.sql.*; import com.hotel.util.DBUtil; import com.hotel.util.MD5Util; public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || username.isEmpty() || password == null || password.isEmpty()) { response.sendRedirect("login.jsp?error=1"); return; } String sql = "SELECT admin_id, real_name FROM admin WHERE username=? AND password=?"; String salt = "hotel_salt_2024"; // 固定的盐,实际项目建议每个用户随机盐 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, MD5Util.md5(salt + password)); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { HttpSession session = request.getSession(); session.setAttribute("adminName", rs.getString("real_name")); session.setAttribute("adminId", rs.getInt("admin_id")); response.sendRedirect("admin/index.jsp"); } else { response.sendRedirect("login.jsp?error=2"); } } } catch (Exception e) { e.printStackTrace(); response.sendRedirect("login.jsp?error=3"); } } }

逻辑说明:先做空值校验,再拿连接、预编译SQL、填充参数、执行查询。PreparedStatement在数据库端把SQL语句的骨架先编译好,参数通过占位符传递,输入值中的单引号、分号会被当作普通字符处理,比如用户输入' OR '1'='1时不会改变SQL的语义。密码在比较前先加盐再做MD5,这是对明文存储的补救——哪怕数据库脚本泄露,拿到的也不是可直接登录的明文。JSP页面的表单action指向这个Servlet,配好web.xml里的servlet-mapping后,登录链路就通了。

登录成功跳转到admin/index.jsp,这就是一个jsp个人信息展示页面,在页面顶部用<%=session.getAttribute("adminName")%>显示当前登录用户,顺带展示登录时间和最近一次登录IP。这个细节在答辩时很加分,因为它体现了会话状态的使用。

4.2 预订核心:事务处理让房间状态不打架

预订是整个系统最容易出状态错乱的地方。常见翻车现场是:预订表里多了一条记录,但room表的状态还是0,导致同一间房被二次预订。我一般把“插入预订记录+把房间状态改成1(已占用)”放在同一个事务里,保证要么同时成功,要么同时回滚。核心代码如下:

public boolean createReservation(int roomId, int customerId, Date arriveDate, Date leaveDate) { String sqlCheckRoom = "SELECT status FROM room WHERE room_id=? FOR UPDATE"; String sqlInsert = "INSERT INTO reservation(room_id, customer_id, arrive_date, leave_date) VALUES(?,?,?,?)"; String sqlUpdateRoom = "UPDATE room SET status=1 WHERE room_id=?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 检查房间状态 boolean roomAvailable; try (PreparedStatement ps = conn.prepareStatement(sqlCheckRoom)) { ps.setInt(1, roomId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) return false; roomAvailable = rs.getInt("status") == 0; } } if (!roomAvailable) { conn.rollback(); return false; } // 插入预订记录 try (PreparedStatement ps = conn.prepareStatement(sqlInsert)) { ps.setInt(1, roomId); ps.setInt(2, customerId); ps.setDate(3, arriveDate); ps.setDate(4, leaveDate); ps.executeUpdate(); } // 更新房间状态 try (PreparedStatement ps = conn.prepareStatement(sqlUpdateRoom)) { ps.setInt(1, roomId); ps.executeUpdate(); } conn.commit(); return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }

关键说明在“FOR UPDATE”这一行:它把room表的这一行锁住,直到事务提交或回滚才释放。两个管理员同时点击“预订同一间房”时,第二个事务会被阻塞,等第一个事务结束后读到的是已更新的status值,从而安全失败。这是处理并发预订的最朴素解法,比在Java代码里加synchronized靠谱得多——因为synchronized锁只在一个Tomcat实例内生效,而FOR UPDATE是数据库侧的锁,跨进程有效。事务结束后finally里close连接,把连接归还给连接池,这一步不能省。

4.3 日期交叉查重:一张表挡住“重复时间段”的脏数据

只锁住房间状态还不够。客人A订了1号到3号,客人B要订2号到5号,预订时room.status是0吗?不是。但如果A已经退房,status被改回0,B再订这间房,就出现了时间重叠。所以预订前还要查交叉日期:

SELECT COUNT(*) FROM reservation WHERE room_id = ? AND status IN (0, 1) AND NOT (leave_date <= ? OR arrive_date >= ?);

这条SQL的语义是:找出所有“尚未离店周期结束”的预订,排除掉那些离店日期早于等于新到日期、或到店日期晚于等于新离店日期的记录。只要结果大于0,说明时间区间有交叉,拒绝预订。三个问号分别对应room_id、arriveDate、leaveDate。在代码里,这个查询和上面的FOR UPDATE检查放在同一个事务里执行,顺序是先查交叉、再锁行、再插入、再改状态。

页面层还有一个小坑:预订表单提交后直接刷新页面,会触发浏览器重复提交同一个POST请求。常见做法是PRG模式——Servlet处理完预订后不直接转发到成功页,而是response.sendRedirect到另一个URL,所以上面登录和预订成功后的跳转都用了重定向。这样刷新时执行的是GET请求,不会重复插入预订记录。不要指望在JSP页面里加location.reload()解决问题,那只会让页面加载完后刷新一次,然后继续带出旧表单数据。

5. MyEclipse部署与调试避坑:五个必踩的坑和后悔药

5.1 Tomcat端口占用:8080被抢了,项目启动闪退

现象:在MyEclipse里启动Tomcat,控制台报Port 8080 required by Tomcat v8.0 Server is already in use,或者Tomcat启动后立刻闪退。

原因:8080端口被其他程序占用。最常见的是Oracle数据库、其他IDE的内置服务器,或者上一次Tomcat没完全退出。这个问题的玄学之处在于,MyEclipse有时显示停止,但后台java进程还在。

解决:Windows下先找到占端口的进程。运行netstat -ano | findstr 8080,记下最后一列的PID,再用taskkill /PID 进程号 /F杀掉。不想强杀的话,可以改Tomcat的server.xml,在Tomcat安装目录conf下找到<Connector port="8080"改成8081,然后在MyEclipse的Servers视图里双击Tomcat,把端口同步改掉。改完重启服务。

5.2 mysql驱动类名和时区:ClassNotFound和Server time zone错误

现象:部署后访问登录页,报ClassNotFoundException: com.mysql.jdbc.Driver,或者The server time zone value '�й���ʱ��' is unrecognized。

原因:类名报错是因为驱动Jar没放到WebRoot/WEB-INF/lib下,或者MySQL 8.x驱动用了旧类名。时区报错是因为MySQL 8.x连接必须显式指定serverTimezone,否则驱动无法判断服务器时区。

解决:第一,把mysql-connector-java的Jar复制到WebRoot/WEB-INF/lib目录,不要只放在Build Path里。第二,驱动类名统一用com.mysql.cj.jdbc.Driver(MySQL 8.x),并在连接URL里加上serverTimezone=Asia/Shanghai&useSSL=false,避坑章节上文提到过。如果你用的是MySQL 5.x驱动,URL里加上useUnicode=true&characterEncoding=utf8即可,不需要serverTimezone。

5.3 中文乱码:四个入口必须全部统一UTF-8

现象:页面上中文全是问号,或者MySQL表里存的中文显示乱码。

原因:JSP页面编码、请求编码、响应编码、数据库连接编码四层中有一层不是UTF-8,就会出乱码。最常见的是JSP文件头里的pageEncoding没写,或者request没有setCharacterEncoding。

解决:从上到下过一遍。JSP文件第一行写<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>;Servlet在读取参数前执行request.setCharacterEncoding("UTF-8");Tomcat的server.xml里给Connector加URIEncoding="UTF-8";MySQL连接URL上带characterEncoding=utf8,建库时用utf8mb4。四个入口一致后,乱码问题基本绝迹。

5.4 WebRoot部署路径与exe打包误区

现象:部署成功后访问http://localhost:8080/显示Tomcat首页,访问http://localhost:8080/HotelSystem/login.jsp却404,或者改了Java代码后页面不生效。

原因:MyEclipse部署到Tomcat时,Web Context Root和项目名不一致。或者项目根本没部署到Tomcat的webapps下,而是启动了Tomcat自带的ROOT。

解决:在项目上右键Properties,搜索Web Context Root,确认值是你想要的访问路径。导出部署包时用MyEclipse的Export→WAR file,导出的war包放到Tomcat的webapps目录下,启动Tomcat会自动解压。这里有个常见的思维误区:标题里带myeclipse和编辑,有同学想把整个Web项目用exe4j打包成exe文件。exe4j只能打包Java桌面程序的主类,Web项目是跑在Tomcat容器里的,正确做法是打war包+绿色版Tomcat一起发给别人,而不是打包成exe。别在这个问题上浪费一晚上,这是血泪经验。

5.5 连接池耗尽:页面卡死,后台报Connection is not available

现象:系统用一段时间后,页面越点越慢,最后报Connection is not available, request timed out,重启Tomcat又恢复正常,过一会儿又卡死。

原因:DAO里的Connection没有关闭,连接池的连接被借光。绝大多数情况是ResultSet、Statement、Connection的关闭顺序写错,或者有的分支return前忘了close。还有一种情况是事务里抛出异常后没有回滚,连接带着未提交事务被还回池里,状态一直是脏的。

解决:全部改用try-with-resources,Java 7及以上版本支持,它保证代码块结束后自动关闭实现AutoCloseable的资源。以本章的登录Servlet代码为例,Connection、PreparedStatement、ResultSet都写在try的小括号里,退出时自动close。如果你还在用老式写法,检查finally里是否每个资源都判空关闭。连接池健康检查可以给context.xml加一条validationQuery="SELECT 1",getConnection时自动剔除坏连接。这个坑是课设验收现场翻车率最高的,记住:连接池数量不是越大越好,而是用完必须还。

6. 进阶验证:给预订表加唯一业务键,压测并发不翻车

最后一公里是验证。很多课设能演示正常流程,但一开并发就现出原形。你可以在reservation表上加一列order_no,作为业务唯一键。它不参与查询条件,只做唯一性兜底。生成规则可以简单拼接时间戳加随机数,比如20240601153028 + 4位随机数。在插入预订记录之前,先尝试插入order_no,插入成功者才允许继续业务;插入失败说明有人在几百毫秒前抢占了同一间房。这是把“数据库唯一约束”当乐观锁用的手法,不比FOR UPDATE差,而且不需要长事务。

ALTER TABLE reservation ADD COLUMN order_no VARCHAR(30) NULL, ADD UNIQUE KEY uk_order_no (order_no);

执行这条语句前注意一个坑:如果reservation表里已经有重复数据,直接加UNIQUE约束会报错。你要先跑一条分组查询把重复order_no找出来,清掉或改成随机值,再执行ALTER TABLE。这也是mysql修改表结构时最常见的尴尬场景。验证方法上,可以打开两个浏览器窗口,同时对一个房源提交预订;也可以用JMeter模拟30个并发线程同时提交,最终计数结果应该只有一条成功记录。

我自己的习惯是:做完一个功能,先在干净的MySQL上重跑一遍init.sql验证脚本可重复执行;再检查每张表的行数增长是否合理;最后专门造数据测试边界——退房当天能不能预订同一间房、取消的订单是否释放房间状态。现在做任何课设或小项目,都先把数据库脚本和状态机的流转图画清楚,再写代码,这个习惯救了我很多次。希望帮到你。

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

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

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

立即咨询