JavaWeb酒店预订系统:JSP+Servlet+JDBC全链路实战 📅 发布时间:2026/9/13 1:48:40 👁 浏览次数: 简介本资源是一套面向计算机专业本科生毕业设计与JavaWeb初学者的酒店预订系统实战项目聚焦B/S架构下的客房预约、订单管理与多角色协同业务场景有效解决毕设选题难、工程实践缺、调试环境复杂等痛点。压缩包共3个文件1个SQL建库脚本用于初始化MySQL数据1个TXT项目说明文档含部署步骤与注意事项1个ZIP源码包含完整JSP页面、Servlet逻辑及JDBC数据库连接代码整体大小3.52MB结构清晰、开箱即用。已有1846人学习下载项目经严格调试支持JDKEclipseTomcat环境一键运行。读者可直接获取可执行的全功能系统涵盖用户端的注册登录、房间查询、订单增删改查与历史记录以及管理员端的订单条件检索、明细管理与状态维护同时掌握JSPServletJDBC三层开发范式与MySQL数据库设计实践。1. 这不是又一个“登录注册增删改查”的JavaWeb Demo而是一套能跑通真实预订闭环的毕设级系统很多同学拿到毕设选题时第一反应是去GitHub搜“JavaWeb 酒店系统”结果点开全是只有用户管理、房间列表静态展示、订单状态硬编码为“已确认”的半成品——根本没法演示“用户选房→提交订单→管理员审核→状态变更→历史查询”这条完整业务链。这套基于JavaWeb的酒店预订系统不同它用原生JSPServletJDBC实现了从可用房间动态筛选、订单临时占位、状态机驱动未确认/已确认/已取消、到管理员条件检索与明细联动修改的全路径逻辑。数据库脚本create.sql直接建好带外键约束的5张表用户、房间、订单、订单明细、设施连MySQL字符集和引擎都按生产习惯设为utf8mb4InnoDB。适合正在写毕设开题报告、需要答辩时能现场演示“用户下单后管理员后台实时看到待审核订单并一键确认”的计算机专业本科生也适合想补足JavaWeb工程化细节比如如何用request.getParameterValues()处理多房间选择、怎样在Servlet里做事务回滚防重复提交的转行学习者。2. 为什么用JSPServletJDBC而不是SpringBoot——看清技术选型背后的教学意图与调试边界2.1 教学场景下原生JavaWeb的不可替代性当前主流Java课程仍以JSPServlet为Web层基石原因很实际SpringBoot的自动配置会掩盖HTTP请求生命周期、Servlet容器启动流程、JDBC连接池初始化等关键机制。而本系统所有核心逻辑都暴露在可调试的.java文件中——比如OrderServlet.java里doPost()方法中你能清晰看到connection.setAutoCommit(false)开启事务、PreparedStatement批量插入订单主表与明细表、if (rows 0) connection.commit()成功提交、else { connection.rollback(); }失败回滚的完整链条。这种显式控制对理解ACID特性至关重要。对比SpringBoot项目中Transactional注解背后黑盒化的代理逻辑这里每行代码都在教你怎么手动管理数据库一致性。提示不要试图把本系统的web.xml直接迁移到SpringBoot环境。servlet和servlet-mapping的XML声明方式与SpringMVC的RequestMapping本质不同强行混合会导致URL路由失效。2.2 数据库设计如何支撑真实预订业务系统数据库共5张表关键设计点直指预订场景痛点表名核心字段设计意图调试验证点roomroom_id,room_type,price,status(enum: available,booked)status字段非布尔值而是枚举预留未来扩展如under_maintenance执行UPDATE room SET statusbooked WHERE room_id101后前端“可用房间查询”应实时过滤掉该房号order_masterorder_id,user_id,order_status(enum),create_time,confirm_timeorder_status含pending未确认、confirmed、cancelled三态支撑管理员审核流程在OrderListServlet中检查WHERE order_statuspending的SQL是否准确返回待处理订单order_detaildetail_id,order_id,room_id,check_in_date,check_out_date,daysdays字段由Java计算后存入避免每次查询都算日期差修改OrderDetailServlet.java中calculateDays()方法传入2024-06-01和2024-06-03应返回2create.sql脚本已预置测试数据3类房型标准间/豪华间/套房、5个用户、10间房。执行时注意MySQL版本兼容性——若用8.0需确认sql_mode未启用STRICT_TRANS_TABLES否则INSERT INTO user VALUES (1,admin,123456,admin)可能因密码长度不足被拒绝本系统密码未加盐仅作演示。2.3 开发环境配置的关键参数与常见报错定位EclipseTomcat组合下最常遇到的三个问题及解决路径2.3.1 Tomcat启动后页面404检查部署路径与上下文根确保项目Properties → Targeted Runtimes已勾选Tomcat版本且WebContent目录被正确识别为Web Root。若访问http://localhost:8080/hotel/login.jsp报404先验证# 进入Tomcat webapps目录确认hotel文件夹存在且结构正确 ls $CATALINA_HOME/webapps/hotel/ # 应输出WEB-INF/ css/ images/ js/ login.jsp ...若hotel文件夹缺失说明Eclipse未自动发布。右键项目 → Run As → Run on Server勾选“Always use this server configuration”。2.3.2 JDBC连接失败驱动加载与URL格式校验DBUtil.java中连接字符串必须与本地MySQL匹配// 正确写法MySQL 5.7 private static final String URL jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8; // 错误写法缺少时区参数导致datetime字段乱码 // private static final String URL jdbc:mysql://localhost:3306/hotel_db;驱动jar包需放入WEB-INF/lib/而非Eclipse Build Path——后者只影响编译运行时Tomcat只认lib目录。若报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver检查mysql-connector-java-8.0.28.jar是否在WEB-INF/lib/下。2.3.3 中文乱码三处必须同步设置数据库层面建库时指定字符集CREATE DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC连接URL如上characterEncodingutf8参数JSP页面每页顶部添加% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%3. 用户端核心流程实现从房间查询到订单生成的完整代码链路3.1 可用房间动态查询SQL拼接与日期冲突检测逻辑用户进入首页点击“可用房间查询”时AvailableRoomServlet.java接收check_in_date和check_out_date参数执行以下关键步骤3.1.1 构建排除已预订房间的SQL// 获取用户提交的入住/离店日期 String checkIn request.getParameter(check_in_date); String checkOut request.getParameter(check_out_date); // 查询在该时间段内已被预订的房间ID String sqlExcluded SELECT DISTINCT r.room_id FROM room r JOIN order_detail od ON r.room_id od.room_id JOIN order_master om ON od.order_id om.order_id WHERE om.order_status confirmed AND ? od.check_out_date AND ? od.check_in_date; PreparedStatement pstmtExcluded conn.prepareStatement(sqlExcluded); pstmtExcluded.setString(1, checkIn); // 占位符1入住日 pstmtExcluded.setString(2, checkOut); // 占位符2离店日 ResultSet rsExcluded pstmtExcluded.executeQuery(); // 收集被占用的room_id SetInteger occupiedIds new HashSet(); while (rsExcluded.next()) { occupiedIds.add(rsExcluded.getInt(room_id)); } // 查询所有可用房间排除occupiedIds String sqlAvailable SELECT * FROM room WHERE status available; PreparedStatement pstmtAvailable conn.prepareStatement(sqlAvailable); ResultSet rsAvailable pstmtAvailable.executeQuery(); // 过滤掉occupiedIds中的房间 ListRoom availableRooms new ArrayList(); while (rsAvailable.next()) { int roomId rsAvailable.getInt(room_id); if (!occupiedIds.contains(roomId)) { Room room new Room(); room.setRoomId(roomId); room.setRoomType(rsAvailable.getString(room_type)); room.setPrice(rsAvailable.getDouble(price)); availableRooms.add(room); } }注意此处用Java内存过滤而非单条SQL是因为MySQL不支持在WHERE子句中直接使用子查询排除动态日期范围易产生笛卡尔积。实际项目中可优化为LEFT JOIN IS NULL但本毕设更强调逻辑可读性。3.1.2 前端房间列表渲染与选择交互available_room.jsp通过JSTL遍历availableRooms集合c:forEach items${availableRooms} varroom div classroom-card h3${room.roomType}/h3 p价格¥${room.price}/晚/p form actionOrderServlet methodpost input typehidden nameroom_id value${room.roomId} input typehidden namecheck_in_date value${param.check_in_date} input typehidden namecheck_out_date value${param.check_out_date} button typesubmit立即预订/button /form /div /c:forEach关键点每个房间卡片绑定独立表单room_id随提交传递避免用户一次选多个房间时的ID混淆。3.2 订单创建事务控制防止超卖与数据不一致OrderServlet.java的doPost()方法是整个系统事务安全的核心3.2.1 两阶段提交与状态校验protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Connection conn null; PreparedStatement pstmtMaster null; PreparedStatement pstmtDetail null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关键关闭自动提交 // 1. 插入订单主表初始状态pending String sqlMaster INSERT INTO order_master (user_id, order_status, create_time) VALUES (?, pending, NOW()); pstmtMaster conn.prepareStatement(sqlMaster, Statement.RETURN_GENERATED_KEYS); pstmtMaster.setInt(1, userId); // 从session获取当前用户ID pstmtMaster.executeUpdate(); // 2. 获取生成的order_id ResultSet rs pstmtMaster.getGeneratedKeys(); rs.next(); int orderId rs.getInt(1); // 3. 插入订单明细关联room_id与日期 String sqlDetail INSERT INTO order_detail (order_id, room_id, check_in_date, check_out_date, days) VALUES (?, ?, ?, ?, ?); pstmtDetail conn.prepareStatement(sqlDetail); pstmtDetail.setInt(1, orderId); pstmtDetail.setInt(2, roomId); pstmtDetail.setString(3, checkIn); pstmtDetail.setString(4, checkOut); pstmtDetail.setInt(5, days); // days由Java计算DateUtils.daysBetween(checkIn, checkOut) pstmtDetail.executeUpdate(); // 4. 更新房间状态为booked关键防超卖 String sqlUpdateRoom UPDATE room SET status booked WHERE room_id ?; PreparedStatement pstmtRoom conn.prepareStatement(sqlUpdateRoom); pstmtRoom.setInt(1, roomId); int rows pstmtRoom.executeUpdate(); if (rows 0) { throw new RuntimeException(房间已被预订请刷新重试); // 状态更新失败即说明并发冲突 } conn.commit(); // 全部成功才提交 request.setAttribute(message, 预订成功请等待管理员确认); } catch (Exception e) { try { if (conn ! null) conn.rollback(); // 任一环节失败则回滚 } catch (SQLException ex) { ex.printStackTrace(); } request.setAttribute(error, 预订失败 e.getMessage()); } finally { DBUtil.close(conn, pstmtMaster, pstmtDetail); } request.getRequestDispatcher(result.jsp).forward(request, response); }提示days计算必须在Java层完成避免MySQLDATEDIFF()函数在不同版本行为差异DateUtils.daysBetween()方法需自行实现核心逻辑为ChronoUnit.DAYS.between(LocalDate.parse(checkIn), LocalDate.parse(checkOut))。3.3 用户订单状态机从pending到confirmed的流转驱动用户提交订单后状态始终为pending直到管理员操作。此时OrderListServlet.java中管理员查询逻辑需严格区分状态// 管理员查看待处理订单仅pending String sql SELECT om.order_id, u.username, om.create_time, om.order_status FROM order_master om JOIN user u ON om.user_id u.user_id WHERE om.order_status pending ORDER BY om.create_time DESC;而用户端查询“未确认订单”时SQL必须限定同一用户// 用户只能查自己的pending订单 String sql SELECT om.order_id, r.room_type, od.check_in_date, od.check_out_date FROM order_master om JOIN order_detail od ON om.order_id od.order_id JOIN room r ON od.room_id r.room_id WHERE om.user_id ? AND om.order_status pending;这种基于角色状态的双重过滤是避免越权访问的基础防线。4. 管理员后台实战条件检索、明细联动与状态变更的精准控制4.1 多条件组合查询动态SQL构建与空值安全管理员在admin_order_list.jsp中可输入订单号、用户名、日期范围进行检索。AdminOrderServlet.java需动态拼接WHERE子句StringBuilder sql new StringBuilder(SELECT om.order_id, u.username, om.create_time, om.order_status ); sql.append(FROM order_master om JOIN user u ON om.user_id u.user_id WHERE 11); ListString params new ArrayList(); if (!StringUtils.isEmpty(orderId)) { sql.append( AND om.order_id ?); params.add(orderId); } if (!StringUtils.isEmpty(username)) { sql.append( AND u.username LIKE ?); params.add(% username %); } if (!StringUtils.isEmpty(startDate)) { sql.append( AND om.create_time ?); params.add(startDate 00:00:00); } if (!StringUtils.isEmpty(endDate)) { sql.append( AND om.create_time ?); params.add(endDate 23:59:59); } PreparedStatement pstmt conn.prepareStatement(sql.toString()); for (int i 0; i params.size(); i) { pstmt.setString(i 1, params.get(i)); }注意日期范围查询必须补全时分秒00:00:00/23:59:59否则2024-06-01会漏掉当天0点后的订单。StringUtils.isEmpty()来自Apache Commons Lang若项目未引入可用str null || str.trim().length() 0替代。4.2 订单明细联动查询一对多关系的高效加载点击订单号查看明细时OrderDetailServlet.java需一次性加载主表明细房间信息// 一步查出订单主表、用户、房间类型、价格 String sql SELECT om.order_id, u.username, r.room_type, r.price, od.check_in_date, od.check_out_date, od.days FROM order_master om JOIN user u ON om.user_id u.user_id JOIN order_detail od ON om.order_id od.order_id JOIN room r ON od.room_id r.room_id WHERE om.order_id ?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, orderId); ResultSet rs pstmt.executeQuery(); ListOrderDetail details new ArrayList(); while (rs.next()) { OrderDetail detail new OrderDetail(); detail.setOrderId(rs.getString(order_id)); detail.setUsername(rs.getString(username)); detail.setRoomType(rs.getString(room_type)); detail.setPrice(rs.getDouble(price)); detail.setCheckInDate(rs.getString(check_in_date)); detail.setCheckOutDate(rs.getString(check_out_date)); detail.setDays(rs.getInt(days)); details.add(detail); }此SQL利用JOIN一次性获取所有字段避免N1查询即先查主表再循环查明细。若订单含多个房间结果集会有多行details列表自然承载一对多关系。4.3 状态变更原子操作确认/取消订单的幂等性保障管理员点击“确认订单”时ConfirmOrderServlet.java执行String sql UPDATE order_master SET order_status confirmed, confirm_time NOW() WHERE order_id ? AND order_status pending; // 关键WHERE限定原状态 PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, orderId); int rows pstmt.executeUpdate(); if (rows 0) { request.setAttribute(error, 订单状态已变更无法重复确认); }WHERE order_status pending确保只有处于待确认状态的订单才能被更新防止重复点击导致状态错乱。同理“取消订单”操作需检查当前状态是否为pending或confirmed并更新为cancelled。5. 毕设答辩高频问题应对与源码级调试技巧5.1 答辩必问如何证明系统解决了“超卖”问题不要只说“用了事务”要指向具体代码位置证据1OrderServlet.java第42行conn.setAutoCommit(false)开启事务证据2第68行UPDATE room SET status booked在事务内执行证据3第75行if (rows 0) throw new RuntimeException(...)捕获更新失败即房间已被他人抢订验证方法用两个浏览器同时打开available_room.jsp选同一房间提交第二个提交必然报“房间已被预订”5.2 调试技巧快速定位JSP页面中文乱码根源当login.jsp中输入中文用户名显示为??时按顺序检查页面编码声明确认% page contentTypetext/html; charsetUTF-8%存在且无拼写错误Tomcat配置打开conf/server.xml在Connector标签中添加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /数据库连接检查DBUtil.java中JDBC URL是否含characterEncodingutf8表单提交方法确保login.jsp中form methodpostGET方式需额外设置request.setCharacterEncoding(UTF-8)5.3 功能增强实操为订单增加总价计算字段若答辩老师问“如何显示订单总金额”可现场修改在order_master表新增total_amount DECIMAL(10,2)字段修改OrderServlet.java中插入主表的SQLString sqlMaster INSERT INTO order_master (user_id, order_status, create_time, total_amount) VALUES (?, pending, NOW(), ?); pstmtMaster.setDouble(2, totalPrice); // totalPrice roomPrice * days在admin_order_list.jsp中添加td${order.totalAmount}/td列此改动仅需3处代码1分钟内可完成体现快速迭代能力。提示total_amount必须在Java层计算后存入而非数据库视图计算——因为房间价格可能变动历史订单需锁定当时价格。系统运行时观察Tomcat/logs/catalina.out中的SQL打印若开启log4j比盲目猜错更高效。真正吃透这套源码你写的毕设不再是“能跑就行”而是每一行都经得起追问的工程实践。本文还有配套的精品资源点击获取