
簡介本資源是一套面向計算機專業本科生畢業設計與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比盲目猜錯更高效。真正吃透這套源碼你寫的畢設不再是“能跑就行”而是每一行都經得起追問的工程實踐。本文還有配套的精品資源點擊獲取