ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP酒店管理系统课设指南:从表设计到部署避坑

JSP酒店管理系统课设指南:从表设计到部署避坑 简介这是一套基于JSPMySQL的酒店管理系统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. 技术选型JSPServletMySQL这套组合为什么还值得选2.1 先分清业务边界酒店管理系统和CRM系统不是一回事很多人在设计表结构时会顺手把客户管理做得很重甚至塞进客户回访、跟进记录这类模块理由是“酒店也要维护客户关系”。这里有必要区分一下酒店管理系统的核心是房间状态和预订周期系统的每一次操作几乎都围绕“某间房在某段时间内属于谁”展开而客户关系管理系统的核心是销售漏斗和交互历史。你做的是前者把房态表、预订表、入住单这三张表理清楚系统就立住了一半。客户表只需要存身份证号、手机号、姓名这些登记必需的字段不要把它扩展成会员营销系统。技术栈选型上课程设计和毕业设计场景里JSPServletJavaBean依然是性价比最高的组合。原因很实在第一评审老师要看的是你对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/ # JavaBeanAdmin、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.10.20.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状态联动时不需要拼接SQLreservation表没有直接放客户姓名而是用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 namejdbc/hotel authContainer typejavax.sql.DataSource maxActive20 maxIdle8 maxWait10000 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password123456/ /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?error1); 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?error2); } } } catch (Exception e) { e.printStackTrace(); response.sendRedirect(login.jsp?error3); } } }逻辑说明先做空值校验再拿连接、预编译SQL、填充参数、执行查询。PreparedStatement在数据库端把SQL语句的骨架先编译好参数通过占位符传递输入值中的单引号、分号会被当作普通字符处理比如用户输入 OR 11时不会改变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 status1 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被改回0B再订这间房就出现了时间重叠。所以预订前还要查交叉日期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 port8080改成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.DriverMySQL 8.x并在连接URL里加上serverTimezoneAsia/ShanghaiuseSSLfalse避坑章节上文提到过。如果你用的是MySQL 5.x驱动URL里加上useUnicodetruecharacterEncodingutf8即可不需要serverTimezone。5.3 中文乱码四个入口必须全部统一UTF-8现象页面上中文全是问号或者MySQL表里存的中文显示乱码。原因JSP页面编码、请求编码、响应编码、数据库连接编码四层中有一层不是UTF-8就会出乱码。最常见的是JSP文件头里的pageEncoding没写或者request没有setCharacterEncoding。解决从上到下过一遍。JSP文件第一行写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%Servlet在读取参数前执行request.setCharacterEncoding(UTF-8)Tomcat的server.xml里给Connector加URIEncodingUTF-8MySQL连接URL上带characterEncodingutf8建库时用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-resourcesJava 7及以上版本支持它保证代码块结束后自动关闭实现AutoCloseable的资源。以本章的登录Servlet代码为例Connection、PreparedStatement、ResultSet都写在try的小括号里退出时自动close。如果你还在用老式写法检查finally里是否每个资源都判空关闭。连接池健康检查可以给context.xml加一条validationQuerySELECT 1getConnection时自动剔除坏连接。这个坑是课设验收现场翻车率最高的记住连接池数量不是越大越好而是用完必须还。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验证脚本可重复执行再检查每张表的行数增长是否合理最后专门造数据测试边界——退房当天能不能预订同一间房、取消的订单是否释放房间状态。现在做任何课设或小项目都先把数据库脚本和状态机的流转图画清楚再写代码这个习惯救了我很多次。希望帮到你。本文还有配套的精品资源点击获取
返回列表