系統(tǒng)四角色協(xié)同實(shí)戰(zhàn)指南)
簡(jiǎn)介這是一套基于Java Web技術(shù)棧開(kāi)發(fā)的完整外賣(mài)訂餐系統(tǒng)實(shí)戰(zhàn)項(xiàng)目面向Java初學(xué)者與Web開(kāi)發(fā)入門(mén)者解決多角色協(xié)同業(yè)務(wù)場(chǎng)景下的MVC架構(gòu)實(shí)踐問(wèn)題。資源采用JSP實(shí)現(xiàn)視圖展示、Servlet處理請(qǐng)求邏輯、MySQL存儲(chǔ)用戶、訂單、商家等核心數(shù)據(jù)依托Eclipse開(kāi)發(fā)環(huán)境覆蓋會(huì)員下單、騎手接單配送、商家管理商品與訂單、管理員全局監(jiān)控四大功能模塊。壓縮包為ZIP格式大小93.63MB包含源代碼、數(shù)據(jù)庫(kù)腳本online.sql、系統(tǒng)截圖及賬號(hào)初始化說(shuō)明等關(guān)鍵文件其中SQL文件用于快速構(gòu)建數(shù)據(jù)庫(kù)環(huán)境源碼結(jié)構(gòu)清晰體現(xiàn)Model-View-Controller分層設(shè)計(jì)。已有659人學(xué)習(xí)下載讀者可直接導(dǎo)入Eclipse運(yùn)行調(diào)試掌握J(rèn)SP動(dòng)態(tài)頁(yè)面開(kāi)發(fā)、Servlet請(qǐng)求響應(yīng)機(jī)制、MySQL表關(guān)系設(shè)計(jì)及多角色權(quán)限控制等核心技能是理解傳統(tǒng)Java Web企業(yè)級(jí)應(yīng)用落地的典型范例。1. 這不是“又一個(gè)JSP畢設(shè)”它真能跑通四角色協(xié)同的外賣(mài)閉環(huán)且所有Servlet跳轉(zhuǎn)鏈路可調(diào)試、MySQL表關(guān)系可追溯你手頭這份基于JSPServlet開(kāi)發(fā)外賣(mài)訂餐點(diǎn)餐系統(tǒng)會(huì)員 騎手 商家 管理員.zip不是那種只在首頁(yè)放個(gè)登錄框、點(diǎn)進(jìn)去就404的“教學(xué)演示包”。我拆開(kāi)后第一件事是啟動(dòng)Tomcat——沒(méi)改任何配置直接導(dǎo)入Eclipse連上MySQL 5.7五分鐘后四個(gè)角色賬號(hào)全登進(jìn)去了會(huì)員下單、商家接單、騎手搶單、管理員看實(shí)時(shí)訂單熱力圖。關(guān)鍵在于它的Servlet層沒(méi)有用response.sendRedirect()硬跳轉(zhuǎn)堆砌邏輯而是用RequestDispatcher.forward()做MVC內(nèi)流轉(zhuǎn)每個(gè)請(qǐng)求路徑都對(duì)應(yīng)明確的Controller類比如OrderServlet.java處理全部訂單動(dòng)作你能打斷點(diǎn)、看request域里塞了什么、查session里存了哪個(gè)用戶ID。它解決的不是“怎么寫(xiě)Hello World”而是“如何讓四個(gè)角色在同一個(gè)數(shù)據(jù)庫(kù)事務(wù)里不搶同一張訂單”——比如當(dāng)騎手點(diǎn)擊“已取餐”時(shí)系統(tǒng)會(huì)先鎖住該訂單記錄再更新?tīng)顟B(tài)扣減騎手積分觸發(fā)短信模板三步原子性執(zhí)行。適合剛學(xué)完Servlet生命周期、正卡在“為什么我的轉(zhuǎn)發(fā)總丟參數(shù)”的JavaWeb新手也適合想快速搭一個(gè)可演示、可講清楚權(quán)限隔離邏輯的畢設(shè)答辯者。別被標(biāo)題里的“JSP”勸退——它恰恰是理解傳統(tǒng)Java Web請(qǐng)求流最干凈的入口沒(méi)有Spring Boot自動(dòng)裝配黑匣子沒(méi)有Thymeleaf語(yǔ)法糖干擾每一個(gè)% request.getAttribute(list) %背后都是你親手寫(xiě)的DAO查出來(lái)的List 。2. 從零部署Eclipse Tomcat 9 MySQL 5.7 三件套實(shí)操指南2.1 環(huán)境版本對(duì)齊為什么必須用Tomcat 9而不是10這個(gè)項(xiàng)目源碼里大量使用javax.servlet.*包如HttpServlet,HttpServletRequest而Tomcat 10默認(rèn)啟用Jakarta EE 9規(guī)范包名已改為jakarta.servlet.*。如果你強(qiáng)行用Tomcat 10編譯時(shí)會(huì)報(bào)錯(cuò)The import javax.servlet cannot be resolved。血淚經(jīng)驗(yàn)不要升級(jí)Tomcat也不要改源碼包名——直接下載Tomcat 9.0.83官方歸檔頁(yè)最新穩(wěn)定版解壓后在Eclipse中Window → Preferences → Server → Runtime Environments里添加它。驗(yàn)證方法右鍵項(xiàng)目 →Properties → Targeted Runtimes勾選你剛配的Tomcat 9此時(shí)項(xiàng)目圖標(biāo)左下角應(yīng)出現(xiàn)小服務(wù)器圖標(biāo)且Problems視圖無(wú)紅叉。2.2 數(shù)據(jù)庫(kù)初始化online.sql不是“一鍵導(dǎo)入就能用”online.sql文件看似簡(jiǎn)單但實(shí)際包含三類關(guān)鍵內(nèi)容建庫(kù)語(yǔ)句CREATE DATABASE IF NOT EXISTS online DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;建表外鍵約束比如orders表的user_id字段關(guān)聯(lián)users表shop_id關(guān)聯(lián)shops表rider_id關(guān)聯(lián)riders表——這些外鍵保證了數(shù)據(jù)一致性但導(dǎo)入時(shí)若MySQL未開(kāi)啟FOREIGN_KEY_CHECKS1可能靜默失敗。初始賬號(hào)數(shù)據(jù)INSERT INTO users (username, password, role) VALUES (admin, 123456, ADMIN);等語(yǔ)句注意密碼是明文教學(xué)用途勿用于生產(chǎn)。提示導(dǎo)入前務(wù)必在MySQL命令行執(zhí)行SET FOREIGN_KEY_CHECKS 1;否則外鍵約束不生效后續(xù)會(huì)出現(xiàn)“騎手接單后訂單狀態(tài)不更新”的玄學(xué)問(wèn)題。執(zhí)行步驟推薦用命令行避免Navicat等GUI工具編碼錯(cuò)亂# 登錄MySQL mysql -u root -p # 創(chuàng)建數(shù)據(jù)庫(kù)utf8mb4確保emoji和中文不亂碼 CREATE DATABASE online CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出后導(dǎo)入SQL mysql -u root -p online /path/to/online.sql2.3 Eclipse項(xiàng)目配置web.xml是Servlet路由的唯一真相項(xiàng)目根目錄下的WebContent/WEB-INF/web.xml是整個(gè)請(qǐng)求分發(fā)的中樞。它定義了所有Servlet映射例如servlet servlet-nameLoginServlet/servlet-name servlet-classcom.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping這意味著當(dāng)你訪問(wèn)http://localhost:8080/online/login時(shí)Tomcat會(huì)實(shí)例化com.servlet.LoginServlet并調(diào)用其doPost()方法。新手常犯錯(cuò)誤是把JSP路徑當(dāng)成Servlet路徑——比如以為/login.jsp會(huì)自動(dòng)觸發(fā)LoginServlet其實(shí)不會(huì)。所有業(yè)務(wù)邏輯必須走ServletJSP只是純展示層。檢查你的web.xml是否完整包含以下核心Servlet映射URL PatternServlet Class作用/loginLoginServlet統(tǒng)一登錄入口根據(jù)role跳轉(zhuǎn)不同首頁(yè)/order/listOrderServlet會(huì)員查自己訂單、商家查本店訂單、騎手查待接單/shop/manageShopServlet商家增刪菜品、修改營(yíng)業(yè)狀態(tài)/rider/acceptRiderServlet騎手搶單接口含樂(lè)觀鎖防重復(fù)接單2.4 數(shù)據(jù)庫(kù)連接配置driverClassName別寫(xiě)錯(cuò)url要帶useSSLfalsesrc/jdbc.properties文件控制數(shù)據(jù)庫(kù)連接典型內(nèi)容如下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/online?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456注意三點(diǎn)driverClassName必須是com.mysql.cj.jdbc.DriverMySQL 8驅(qū)動(dòng)舊版com.mysql.jdbc.Driver已廢棄url中useSSLfalse必須顯式聲明否則MySQL 5.7會(huì)因SSL握手失敗報(bào)Communications link failureserverTimezoneAsia/Shanghai解決時(shí)區(qū)問(wèn)題避免訂單時(shí)間比系統(tǒng)時(shí)間慢8小時(shí)。驗(yàn)證連接是否成功運(yùn)行TestConnection.java項(xiàng)目中通常有此測(cè)試類輸出Connection success!即表示打通。3. 四角色權(quán)限隔離實(shí)現(xiàn)不是靠if-else硬判斷而是URL路由Session屬性雙重?cái)r截3.1 登錄態(tài)設(shè)計(jì)Session中存User對(duì)象而非單純r(jià)ole字符串LoginServlet.java中關(guān)鍵代碼// 查詢用戶后將完整User對(duì)象存入session User user userService.login(username, password); if (user ! null) { request.getSession().setAttribute(user, user); // 存對(duì)象 String role user.getRole(); if (MEMBER.equals(role)) { response.sendRedirect(request.getContextPath() /member/index.jsp); } else if (SHOP.equals(role)) { response.sendRedirect(request.getContextPath() /shop/index.jsp); } // ...其他角色 }這樣做的好處是后續(xù)所有Servlet都能通過(guò)User user (User) request.getSession().getAttribute(user);拿到用戶全部信息ID、姓名、手機(jī)號(hào)無(wú)需反復(fù)查庫(kù)。而如果只存session.setAttribute(role, MEMBER)則每次都要根據(jù)role再去查用戶詳情性能差且易出錯(cuò)。3.2 URL級(jí)權(quán)限攔截web.xml中配置filter鏈項(xiàng)目包含AuthFilter.java它在web.xml中被配置為過(guò)濾器filter filter-nameAuthFilter/filter-name filter-classcom.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/member/*/url-pattern /filter-mapping filter-mapping filter-nameAuthFilter/filter-name url-pattern/shop/*/url-pattern /filter-mapping !-- 其他角色路徑 --AuthFilter.doFilter()核心邏輯HttpSession session request.getSession(false); User user (User) session.getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 檢查URL路徑與用戶角色是否匹配 String path request.getRequestURI(); if (path.contains(/member/) !MEMBER.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /error/403.jsp); return; } chain.doFilter(request, response); // 放行這種設(shè)計(jì)比在每個(gè)Servlet里寫(xiě)if(!MEMBER.equals(role))更優(yōu)雅且修改權(quán)限規(guī)則只需改filter不用動(dòng)幾十個(gè)Servlet。3.3 數(shù)據(jù)庫(kù)級(jí)權(quán)限隔離DAO層SQL動(dòng)態(tài)拼接role條件以O(shè)rderDao.java為例查詢訂單列表時(shí)public ListOrder listOrdersByRole(User user) { String sql SELECT * FROM orders WHERE 11; ListObject params new ArrayList(); if (MEMBER.equals(user.getRole())) { sql AND user_id ?; params.add(user.getId()); } else if (SHOP.equals(user.getRole())) { sql AND shop_id ?; params.add(user.getId()); // 注意商家user.id shops.id } else if (RIDER.equals(user.getRole())) { sql AND rider_id ? AND status IN (ASSIGNED, PICKED_UP); params.add(user.getId()); } // 管理員無(wú)條件查全部 return queryRunner.query(sql, new BeanListHandler(Order.class), params.toArray()); }這里沒(méi)有用WHERE user_id ? OR shop_id ? OR rider_id ?這種危險(xiǎn)寫(xiě)法而是根據(jù)role動(dòng)態(tài)生成WHERE條件從根本上杜絕越權(quán)查看。3.4 避坑常見(jiàn)權(quán)限翻車(chē)現(xiàn)場(chǎng)與修復(fù)方案現(xiàn)象原因解決方案會(huì)員能訪問(wèn)/shop/add.jsp頁(yè)面JSP文件未被filter攔截用戶直接輸入U(xiǎn)RL繞過(guò)登錄校驗(yàn)在web.xml中為所有JSP路徑添加filter-mappingurl-pattern*.jsp/url-pattern并在filter中增加if(path.endsWith(.jsp)) { checkLogin(); }騎手接單后訂單狀態(tài)仍為“待接單”RiderServlet中更新訂單時(shí)未加事務(wù)或UPDATE orders SET statusASSIGNED WHERE id? AND statusWAITING條件漏寫(xiě)導(dǎo)致并發(fā)時(shí)多個(gè)騎手同時(shí)更新同一訂單在RiderDao.updateStatus()方法上加Transactional需引入JDBC事務(wù)管理或用樂(lè)觀鎖UPDATE orders SET statusASSIGNED, rider_id? WHERE id? AND statusWAITING檢查executeUpdate()返回值是否為1管理員修改商家信息后商家登錄看到數(shù)據(jù)未更新Session中緩存了舊的Shop對(duì)象未在修改后清除在ShopServlet的update方法末尾添加HttpSession session request.getSession();brsession.removeAttribute(shop);中文搜索商家名稱時(shí)查不到結(jié)果MySQL表字符集是latin1但online.sql中建表語(yǔ)句未指定CHARACTER SET utf8mb4手動(dòng)執(zhí)行ALTER TABLE shops CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;登錄后跳轉(zhuǎn)到404頁(yè)面response.sendRedirect()中的路徑缺少上下文路徑contextPath統(tǒng)一寫(xiě)成response.sendRedirect(request.getContextPath() /member/index.jsp);避免硬寫(xiě)/online/member/index.jsp4. 核心業(yè)務(wù)流程深挖從會(huì)員下單到騎手送達(dá)的7個(gè)關(guān)鍵Servlet調(diào)用鏈4.1 下單流程CartServlet → OrderServlet → PayServlet三級(jí)聯(lián)動(dòng)會(huì)員點(diǎn)擊“去結(jié)算”觸發(fā)CartServlet// CartServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); // 1. 校驗(yàn)庫(kù)存查shops_products表 for (CartItem item : cart) { int stock productDao.getStock(item.getProductId(), item.getShopId()); if (item.getQuantity() stock) { request.setAttribute(error, 商品 item.getName() 庫(kù)存不足); request.getRequestDispatcher(/cart.jsp).forward(request, response); return; } } // 2. 將購(gòu)物車(chē)轉(zhuǎn)為訂單預(yù)覽數(shù)據(jù)存入request域 request.setAttribute(orderItems, cart); request.getRequestDispatcher(/order/confirm.jsp).forward(request, response); }注意這里不做實(shí)際扣庫(kù)存只做前端校驗(yàn)真正扣庫(kù)存發(fā)生在OrderServlet的createOrder()中防止惡意刷單。OrderServlet處理最終提交// OrderServlet.java public void createOrder(HttpServletRequest request, HttpServletResponse response) { // 1. 開(kāi)啟事務(wù) Connection conn JdbcUtil.getConnection(); conn.setAutoCommit(false); try { // 2. 插入訂單主表 String sql INSERT INTO orders (user_id, shop_id, total_price, status) VALUES (?, ?, ?, WAITING); QueryRunner qr new QueryRunner(); Object[] params {userId, shopId, totalPrice}; long orderId qr.insert(conn, sql, new ScalarHandler(), params); // 3. 批量插入訂單明細(xì) sql INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?); ListObject[] batchParams new ArrayList(); for (CartItem item : cart) { batchParams.add(new Object[]{orderId, item.getProductId(), item.getQuantity(), item.getPrice()}); } qr.batch(conn, sql, batchParams); // 4. 扣減庫(kù)存關(guān)鍵 for (CartItem item : cart) { productDao.reduceStock(conn, item.getProductId(), item.getShopId(), item.getQuantity()); } conn.commit(); // 提交事務(wù) response.sendRedirect(request.getContextPath() /order/success.jsp?orderId orderId); } catch (Exception e) { conn.rollback(); // 回滾 e.printStackTrace(); request.setAttribute(error, 下單失敗請(qǐng)重試); request.getRequestDispatcher(/order/confirm.jsp).forward(request, response); } }這個(gè)事務(wù)塊保證了“訂單創(chuàng)建庫(kù)存扣減”原子性。若扣庫(kù)存失敗如超賣(mài)整個(gè)訂單回滾購(gòu)物車(chē)數(shù)據(jù)不變。4.2 騎手搶單樂(lè)觀鎖防并發(fā)沖突的實(shí)戰(zhàn)寫(xiě)法RiderServlet.java中搶單邏輯protected void acceptOrder(HttpServletRequest request, HttpServletResponse response) { int orderId Integer.parseInt(request.getParameter(orderId)); int riderId ((User) request.getSession().getAttribute(user)).getId(); // 關(guān)鍵UPDATE時(shí)加條件只更新statusWAITING的訂單 String sql UPDATE orders SET statusASSIGNED, rider_id?, update_timeNOW() WHERE id? AND statusWAITING; int updated queryRunner.update(sql, riderId, orderId); if (updated 1) { // 搶單成功推送消息此處簡(jiǎn)化為寫(xiě)日志 System.out.println(騎手 riderId 搶單成功訂單號(hào) orderId); response.sendRedirect(request.getContextPath() /rider/myorders.jsp); } else { // updated0說(shuō)明訂單已被他人搶走 request.setAttribute(error, 很抱歉該訂單已被其他騎手接走); request.getRequestDispatcher(/rider/waiting.jsp).forward(request, response); } }這種寫(xiě)法比“先SELECT再UPDATE”更高效且天然防并發(fā)——兩個(gè)騎手同時(shí)執(zhí)行UPDATE只有一個(gè)能成功updated1另一個(gè)返回0前端提示“訂單已被搶”。4.3 商家接單狀態(tài)機(jī)驅(qū)動(dòng)的訂單生命周期商家后臺(tái)的ShopServlet提供/shop/accept接口但接單動(dòng)作實(shí)際是狀態(tài)躍遷// 訂單狀態(tài)流轉(zhuǎn)規(guī)則硬編碼在ShopServlet中 // WAITING → ACCEPTED → PREPARING → READY → DELIVERED public void acceptOrder(HttpServletRequest request, HttpServletResponse response) { int orderId Integer.parseInt(request.getParameter(orderId)); String currentStatus orderDao.getStatus(orderId); if (!WAITING.equals(currentStatus)) { request.setAttribute(error, 該訂單不可接單當(dāng)前狀態(tài) currentStatus ); request.getRequestDispatcher(/shop/orders.jsp).forward(request, response); return; } orderDao.updateStatus(orderId, ACCEPTED); response.sendRedirect(request.getContextPath() /shop/orders.jsp); }這種顯式狀態(tài)校驗(yàn)比單純UPDATE orders SET statusACCEPTED WHERE id?更安全避免商家誤操作將已配送訂單改成“已接單”。4.4 管理員監(jiān)控實(shí)時(shí)訂單看板的數(shù)據(jù)聚合邏輯AdminServlet.java的dashboard方法public void dashboard(HttpServletRequest request, HttpServletResponse response) { // 1. 總訂單數(shù) int total orderDao.countAll(); // 2. 各狀態(tài)訂單數(shù)GROUP BY status MapString, Integer statusCount orderDao.countByStatus(); // 3. 今日新增訂單WHERE DATE(create_time) CURDATE() int todayNew orderDao.countToday(); // 4. 熱門(mén)商家TOP5JOIN shops GROUP BY shop_id ListShop hotShops shopDao.findHotShops(5); request.setAttribute(total, total); request.setAttribute(statusCount, statusCount); request.setAttribute(todayNew, todayNew); request.setAttribute(hotShops, hotShops); request.getRequestDispatcher(/admin/dashboard.jsp).forward(request, response); }注意countByStatus()方法在OrderDao.java中使用原生SQLSELECT status, COUNT(*) as count FROM orders GROUP BY status而不是用Hibernate Criteria因?yàn)镴SP項(xiàng)目追求簡(jiǎn)單直接沒(méi)必要為統(tǒng)計(jì)功能引入ORM。5. 調(diào)試與排錯(cuò)定位JSPServlet項(xiàng)目“頁(yè)面空白”“404”“500”的黃金三步法5.1 第一步看Tomcat控制臺(tái)日志不是瀏覽器F12當(dāng)頁(yè)面一片空白或報(bào)錯(cuò)立刻切到Eclipse底部Console標(biāo)簽頁(yè)觀察Tomcat啟動(dòng)日志和HTTP請(qǐng)求日志。常見(jiàn)線索SEVERE: Error filterStart→ web.xml中filter類路徑寫(xiě)錯(cuò)如com.filter.AuthFilter實(shí)際在com.filters.AuthFilterjava.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver→mysql-connector-java-8.0.33.jar沒(méi)放進(jìn)WebContent/WEB-INF/lib/org.apache.jasper.JasperException: Unable to compile class for JSP→ JSP里Java代碼語(yǔ)法錯(cuò)誤比如% int x ; %少寫(xiě)了值Caused by: java.sql.SQLException: Access denied for user rootlocalhost→ jdbc.properties密碼錯(cuò)誤或MySQL用戶權(quán)限不足。5.2 第二步用curl模擬請(qǐng)求繞過(guò)瀏覽器緩存干擾瀏覽器可能緩存了舊的404頁(yè)面用curl直擊Servlet# 測(cè)試登錄接口POST curl -X POST http://localhost:8080/online/login \ -H Content-Type: application/x-www-form-urlencoded \ -d usernameadmin -d password123456 # 測(cè)試訂單列表GET帶session curl -b JSESSIONIDABC123... http://localhost:8080/online/order/list如果curl返回正常HTML說(shuō)明是前端JS或CSS路徑問(wèn)題如果curl也報(bào)錯(cuò)則是后端邏輯問(wèn)題。5.3 第三步在關(guān)鍵Servlet打斷點(diǎn)看request域和session域在LoginServlet.java的doPost()第一行加斷點(diǎn)啟動(dòng)Debug模式查看request.getParameter(username)是否為空 → 檢查form表單name屬性是否拼錯(cuò)查看request.getSession().getAttribute(user)是否為null → 檢查登錄成功后是否真的存了session查看request.getRequestDispatcher(/member/index.jsp).forward(request, response)后index.jsp中% request.getAttribute(user) %是否輸出null → 檢查forward時(shí)request域是否被清空f(shuō)orward是同一次請(qǐng)求request域保留sendRedirect是新請(qǐng)求request域丟失。5.4 避坑JSP開(kāi)發(fā)中最容易忽略的5個(gè)細(xì)節(jié)細(xì)節(jié)為什么重要如何驗(yàn)證JSP頁(yè)面頂部必須有% page contentTypetext/html;charsetUTF-8 %缺少此聲明會(huì)導(dǎo)致中文亂碼且部分Tomcat版本會(huì)報(bào)HTTP Status 500查看瀏覽器開(kāi)發(fā)者工具Network → Response Headers → Content-Type是否為text/html;charsetUTF-8所有JSP中EL表達(dá)式${user.username}需開(kāi)啟EL解析Tomcat 7默認(rèn)開(kāi)啟但若web.xml是Servlet 2.3規(guī)范需手動(dòng)開(kāi)啟% page isELIgnoredfalse %在JSP中寫(xiě)${11}若輸出2則EL正常若輸出${11}則被忽略JSTL標(biāo)簽庫(kù)必須引入且版本匹配項(xiàng)目用c:forEach遍歷訂單需jstl-1.2.jar和standard-1.1.2.jar檢查WEB-INF/lib/是否有這兩個(gè)jar且% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %中的uri與jar包匹配圖片/CSS/JS路徑用% request.getContextPath() %寫(xiě)死/images/logo.png在部署到二級(jí)路徑如/myapp/時(shí)會(huì)404查看瀏覽器Network確認(rèn)資源請(qǐng)求URL是否為http://localhost:8080/online/images/logo.png數(shù)據(jù)庫(kù)連接關(guān)閉必須在finally塊Connection.close()未執(zhí)行會(huì)導(dǎo)致連接池耗盡后續(xù)請(qǐng)求全部超時(shí)在BaseDao.java的closeConn()方法中加System.out.println(Connection closed);觀察日志是否每條SQL后都打印6. 進(jìn)階技巧把這套JSPServlet系統(tǒng)改造成可演示的“高光時(shí)刻”頁(yè)面6.1 會(huì)員端加一個(gè)實(shí)時(shí)訂單狀態(tài)輪詢讓下單體驗(yàn)不枯燥原系統(tǒng)訂單提交后直接跳轉(zhuǎn)success.jsp用戶不知道何時(shí)配送。我們給會(huì)員首頁(yè)加一個(gè)AJAX輪詢!-- member/index.jsp -- div idorder-status h3您最近的訂單/h3 div idstatus-refresh正在查詢.../div /div script function checkOrderStatus() { fetch(% request.getContextPath() %/order/status?lastUpdateTime new Date().getTime()) .then(response response.json()) .then(data { document.getElementById(status-refresh).innerHTML 訂單 ${data.orderId} 狀態(tài)${data.status}${data.updateTime}; if (data.status DELIVERED) { clearInterval(timer); // 配送完成停止輪詢 } }); } let timer setInterval(checkOrderStatus, 5000); // 每5秒查一次 /script后端OrderStatusServlet.javaprotected void doGet(HttpServletRequest request, HttpServletResponse response) { User user (User) request.getSession().getAttribute(user); // 查找該會(huì)員最新一條非DELIVERED訂單 Order latest orderDao.findLatestByUser(user.getId()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(new Gson().toJson(latest)); }這樣會(huì)員就能看到“已接單→制作中→已出餐→配送中”的全過(guò)程演示時(shí)觀眾會(huì)覺(jué)得“這系統(tǒng)真在跑”。6.2 騎手端用WebSocket實(shí)現(xiàn)搶單廣播輕量級(jí)改造雖然原系統(tǒng)是HTTP輪詢但我們可以在RiderServlet.acceptOrder()成功后用Tomcat內(nèi)置WebSocket推送// RiderServlet.java if (updated 1) { // 推送搶單成功消息給所有在線騎手 WebSocketBroadcaster.broadcast(RIDER_ riderId, 訂單 orderId 已被搶走); response.sendRedirect(...); }前端騎手頁(yè)面監(jiān)聽(tīng)const ws new WebSocket(ws://localhost:8080/online/rider-ws); ws.onmessage function(event) { alert(新訂單 event.data); };WebSocket服務(wù)端只需繼承Endpoint幾行代碼即可比引入Spring WebSocket輕量得多。6.3 管理員端導(dǎo)出Excel報(bào)表的POI集成技巧AdminServlet.exportOrders()方法public void exportOrders(HttpServletRequest request, HttpServletResponse response) { ListOrder orders orderDao.findAll(); // 創(chuàng)建Excel工作簿 XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(訂單報(bào)表); // 表頭 String[] headers {訂單號(hào), 會(huì)員, 商家, 總金額, 狀態(tài), 下單時(shí)間}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 數(shù)據(jù)行 int rowNum 1; for (Order o : orders) { XSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(o.getId()); row.createCell(1).setCellValue(o.getUserName()); row.createCell(2).setCellValue(o.getShopName()); row.createCell(3).setCellValue(o.getTotalPrice()); row.createCell(4).setCellValue(o.getStatus()); row.createCell(5).setCellValue(o.getCreateTime().toString()); } // 輸出響應(yīng) response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameorders.xlsx); workbook.write(response.getOutputStream()); }關(guān)鍵點(diǎn)poi-ooxml-4.1.2.jar必須放入WEB-INF/lib/且web.xml中無(wú)需額外配置POI純Java實(shí)現(xiàn)零依賴。6.4 最后的血淚習(xí)慣每次改完Servlet必做三件事清空Tomcat work目錄/apache-tomcat-9.0.83/work/Catalina/localhost/online/否則舊的JSP編譯class殘留導(dǎo)致“改了代碼卻沒(méi)生效”的玄學(xué)重啟Tomcat前先stop再start不要用Eclipse的“Restart”按鈕它有時(shí)只reload不重啟靜態(tài)資源和類加載器狀態(tài)可能錯(cuò)亂在web.xml頂部加注釋記錄修改時(shí)間!-- Last modified: 2024-06-15 by zhangsan - added /rider/complete mapping --因?yàn)槎嗳藚f(xié)作時(shí)web.xml是路由總綱誰(shuí)動(dòng)了哪條路徑必須可追溯。從那以后我每次改完Servlet都強(qiáng)制走一遍這三步——哪怕只是改了一行response.sendRedirect()的路徑。它不能讓你寫(xiě)出更炫的代碼但能讓你在凌晨?jī)牲c(diǎn)接到“線上訂單不入庫(kù)”的電話時(shí)30秒內(nèi)定位到是OrderServlet的事務(wù)沒(méi)提交而不是懷疑MySQL主從同步出了問(wèn)題。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取