戰(zhàn):基于Session的內(nèi)存購物車實(shí)現(xiàn)與避坑指南)
簡介這是一套基于JavaWeb技術(shù)實(shí)現(xiàn)的簡易購物車系統(tǒng)完整源碼適合Java初學(xué)者及希望鞏固Web開發(fā)基礎(chǔ)的中級開發(fā)者。代碼圍繞Servlet與JSP、Session會話管理、JDBC數(shù)據(jù)庫交互、MVC設(shè)計模式、JSTL與EL表達(dá)式等核心知識點(diǎn)展開覆蓋商品展示、加入購物車、修改數(shù)量、刪除商品及訂單處理等典型業(yè)務(wù)流程并附有開發(fā)文檔與項(xiàng)目安裝說明能幫助讀者理清從頁面請求到數(shù)據(jù)持久化的完整鏈路。壓縮包共54個文件包含14個Java源文件、14個編譯后的class文件、6個JSP頁面、數(shù)據(jù)庫相關(guān)配置及properties、xml等輔助資源整體大小約1.58MB目錄結(jié)構(gòu)清晰適合直接導(dǎo)入Eclipse或IntelliJ IDEA配合Tomcat運(yùn)行學(xué)習(xí)。已有4125人瀏覽學(xué)習(xí)。通過研讀源碼和筆記可快速掌握J(rèn)avaWeb項(xiàng)目從環(huán)境搭建、數(shù)據(jù)庫設(shè)計到功能實(shí)現(xiàn)的常見思路對提升實(shí)際項(xiàng)目開發(fā)能力很有幫助。1. 什么是 javaWeb 簡易購物車它不是一張表而是會話里的一塊內(nèi)存很多人第一次接觸 javaWeb 購物車時會下意識地想把「購物車」設(shè)計成數(shù)據(jù)庫中的一張表加購就是 insert改數(shù)量就是 update刪除就是 delete。這個思路不算錯但對一個網(wǎng)頁端的簡易購物車來說它把問題想復(fù)雜了。真正的購物車在絕大多數(shù)教學(xué)項(xiàng)目和基礎(chǔ)實(shí)踐中只是一個放在 HttpSession 里的內(nèi)存結(jié)構(gòu)。用戶點(diǎn)一下「加入購物車」服務(wù)端往 Session 里塞一個 HashMap用戶清空購物車服務(wù)端把這個 Session 屬性刪掉。整個過程不碰數(shù)據(jù)庫也不落盤。這么設(shè)計的第一層原因是簡單不需要額外建表、不需要處理事務(wù)、不需要考慮未登錄用戶怎么關(guān)聯(lián)購物車數(shù)據(jù)一個會話對象全搞定了。第二層原因是貼合 HTTP 的無狀態(tài)模型——購物車本質(zhì)是「某一次瀏覽過程里的臨時意圖」Session 天生就是為這種臨時狀態(tài)設(shè)計的。給剛?cè)腴T JavaWeb 的人做一個可以復(fù)現(xiàn)、能看清請求流轉(zhuǎn)路徑的項(xiàng)目用 Session 存購物車比引入 Redis 或數(shù)據(jù)庫表都直觀得多。這篇筆記面向的讀者很明確正在做 JavaWeb 課設(shè)或入門練手的人、需要給現(xiàn)有管理系統(tǒng)補(bǔ)一個購物車模塊的人、被教科書里各種「MVC 三層架構(gòu)」繞暈的初學(xué)者。我會先用最樸素的方式講清楚購物車應(yīng)該長什么樣然后給出一個能直接跑起來的最小工程再把加購、改數(shù)量、刪商品這些操作的代碼逐段拆開最后列幾個我實(shí)際踩過的坑。這套方案代碼量不大但該有的請求、轉(zhuǎn)發(fā)、重定向、會話、JDBC 讀寫都齊了值得照著做一遍。2. 技術(shù)選型與職責(zé)拆解為什么 JSP Servlet JDBC 就夠2.1 購物車為什么是內(nèi)存結(jié)構(gòu)而不是數(shù)據(jù)庫表購物車?yán)锏臄?shù)據(jù)有一個鮮明特點(diǎn)生命周期極短的臨時性。用戶打開瀏覽器訪問一次購物網(wǎng)站往購物車?yán)锛恿藥准唐啡缓箨P(guān)閉頁面走人這條數(shù)據(jù)就不再有任何意義了。如果把它落進(jìn)數(shù)據(jù)庫你不僅要為每個匿名訪客建一張購物車明細(xì)表還要在會話過期時寫定時任務(wù)去清理孤兒數(shù)據(jù)否則表會越攢越臟。為一個簡易項(xiàng)目引入這種復(fù)雜度不劃算。常見的做法是用 HttpSession 的 setAttribute 方法存放一個 Map商品Id, 數(shù)量。同一個商品再次加購時只要數(shù)量加 1 而不是重新插入。這個 Map 的生命周期由容器管理默認(rèn)超時時間一般是 30 分鐘用戶關(guān)掉瀏覽器后 Session 也隨之失效。要展示購物車?yán)锏纳唐吩斍槊Q、單價、小計再通過商品 Id 去查一遍商品表即可。也就是說購物車只存 Id 和數(shù)量兩個字段商品的其他信息仍然以數(shù)據(jù)庫為準(zhǔn)。為什么不是 CookieCookie 也存在瀏覽器端看似也能存購物車但 Cookie 的容量限制只有 4KB 左右而一個購物車?yán)飫虞m幾十件商品的 Id 列表很容易撐爆這個上限。更重要的是 Cookie 里的數(shù)據(jù)是明文的用戶可以隨意篡改。Session 把數(shù)據(jù)留在服務(wù)端客戶端只拿一個看不見內(nèi)容物的會話 ID安全性和容量都更合適。簡易購物車用 Session是成本和安全性之間的平衡點(diǎn)不是沒考慮過其他方案。還有一個實(shí)操上的好處調(diào)試直觀。你用 Servlet 處理加購請求時直接打印 session.getAttribute(cart) 就能看到當(dāng)前購物車的內(nèi)容不用連數(shù)據(jù)庫執(zhí)行 select。對于還沒把調(diào)試工具鏈玩熟的初學(xué)者這個「黑匣子」能被直接看到內(nèi)部狀態(tài)排查問題的成本低得多。2.2 職責(zé)拆解把 JSP、Servlet、JDBC 各放對位置一個可以交差的簡易購物車項(xiàng)目至少要分出四層職責(zé)但每層都很薄。第一層是 JSP 頁面負(fù)責(zé)展示商品列表和購物車內(nèi)容第二層是 Servlet負(fù)責(zé)接收請求、調(diào)用業(yè)務(wù)方法、決定跳轉(zhuǎn)方向第三層是一個購物車相關(guān)的 JavaBeanCartItem 和 Cart封裝購物車的存儲結(jié)構(gòu)第四層是商品數(shù)據(jù)的 JDBC 訪問代碼負(fù)責(zé)把商品表里的記錄查出來。不少人會把 JDBC 代碼直接寫進(jìn) Servlet 的 doGet 方法里這種做法在小項(xiàng)目里能跑但會讓你后面改數(shù)據(jù)庫連接配置時到處找代碼。我一般會單獨(dú)建一個類哪怕這個類只有一個靜態(tài)方法也要和 Servlet 分開。同理JSP 頁面里不要出現(xiàn) Java 代碼片段。用 EL 表達(dá)式讀購物車數(shù)據(jù)用 JSTL 的 c:forEach 遍歷商品列表這兩樣?xùn)|西是 JavaWeb 入門階段必須順手學(xué)會的——它們能讓你把頁面代碼控制在二十行以內(nèi)。關(guān)于版本選擇Servlet 可以用 3.1 或 4.0JSP 用 2.3 以上JDK 用 8 或 11Tomcat 用 9.x。這些版本都是經(jīng)過多年驗(yàn)證的穩(wěn)定組合網(wǎng)上能找到大量兼容性說明。不要用 Servlet 5.0 以上的版本因?yàn)閺?5.0 開始采用了 Jakarta EE 命名空間與老教材里的 javax.servlet 包名不兼容會讓照著舊教程寫代碼的初學(xué)者在第一關(guān)就翻車。# 這里給一個最小目錄結(jié)構(gòu)之后照著建就不會亂 src/main/java ├── com.demo.entity # CartItem、Cart、Product ├── com.demo.dao # 商品表訪問 ├── com.demo.web # CartServlet、GoodsServlet src/main/webapp ├── WEB-INF/web.xml # Servlet 映射 ├── goodsList.jsp # 商品列表頁 └── cart.jsp # 購物車頁面建目錄的時候包名盡量不要用默認(rèn)包否則 Servlet 注解掃描和 web.xml 配置都可能出問題。用公司域名反寫或者干脆用 com.demo 這種形似域名反寫的結(jié)構(gòu)是最穩(wěn)妥的選擇。3. 最小可運(yùn)行工程目錄、依賴與數(shù)據(jù)庫初始化3.1 用 Maven 搭好骨架pom.xml 里的依賴清單我會用 Maven 來管理這個項(xiàng)目。原因不是它多高級而是它能幫你鎖定依賴版本避免手動往 WEB-INF/lib 里拷 jar 包時漏掉某一個、運(yùn)行期才報 ClassNotFoundException。對于一個入門項(xiàng)目pom.xml 只需包含 servlet-api、jsp-api 和 mysql-connector-java 三樣其中前兩個還要顯式聲明 provided scope因?yàn)?Tomcat 容器本身自帶這兩個庫打包時不需要重復(fù)打入。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies第二個依賴記得補(bǔ)上 jstl 和 taglibs因?yàn)轫撁嫜h(huán)要用 c:forEach沒有 JSTL 你只能退回 Java 代碼片段頁面會變得很難讀。版本號不用追新能穩(wěn)定工作的老版本反而更省心。mysql-connector 用 8.0.x 配合 MySQL 5.7 或 8.0 都能正常連接驅(qū)動類全名是 com.mysql.cj.jdbc.Driver這個寫法在 8.x 里已經(jīng)是標(biāo)配。dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencyMaven 配好后在項(xiàng)目根目錄執(zhí)行 mvn package確認(rèn)能打出 war 包。這一步通過說明依賴引入沒問題后續(xù)任何報錯都不太可能與缺 jar 有關(guān)了。3.2 商品表與初始化數(shù)據(jù)購物車得有東西可加購物車本身不存商品明細(xì)但頁面得能從商品表查出可加購的物品。我一般只建一張 goods 表包含 id、name、price、stock 四個字段。price 用 decimal(10,2) 而不是 float避免浮點(diǎn)精度在結(jié)算小計時出現(xiàn) 0.30000000000000004 這種尷尬。CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 10 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO goods (name, price, stock) VALUES (機(jī)械鍵盤, 199.00, 50); INSERT INTO goods (name, price, stock) VALUES (游戲鼠標(biāo), 89.00, 80); INSERT INTO goods (name, price, stock) VALUES (顯示器支架, 129.00, 30);字符集用 utf8mb4 而不是 utf8是一個血淚經(jīng)驗(yàn)utf8 在 MySQL 里實(shí)際是 utf8mb3存不了 emoji也存不了部分生僻漢字。你寫個「」進(jìn)商品名舊字符集直接報錯。建表時把字符集定了以后少一堆麻煩。數(shù)據(jù)庫連接信息按慣例寫在 src/main/resources 下的 jdbc.properties 里然后寫一個簡單的 DBUtil 類讀取配置、獲取連接。連接串里必須帶上 useUnicodetruecharacterEncodingutf8否則中文參數(shù)往數(shù)據(jù)庫里寫會亂碼。時區(qū)參數(shù) serverTimezoneAsia/Shanghai 在 MySQL 8.x 下是必須的不加會報時區(qū)錯誤。Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/shopdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; conn DriverManager.getConnection(url, root, 你的密碼);注意 Class.forName 在連接池或較新驅(qū)動里已經(jīng)不是必寫項(xiàng)但對初學(xué)者來說保留更穩(wěn)。不需要理解它背后的 SPI 機(jī)制知道這行代碼是觸發(fā)驅(qū)動類加載即可。4. 購物車核心邏輯的實(shí)現(xiàn)加入、改數(shù)量、刪除4.1 定義 CartItem 和 Cart會話里存什么購物車?yán)锩恳恍惺且粭l CartItem至少要有商品 id、商品名稱、單價、加購數(shù)量四個屬性再加一個 getSubtotal() 方法計算小計。商品名稱和單價是在加購時從數(shù)據(jù)庫查出來的這樣頁面展示購物車時不必再查一次數(shù)據(jù)庫性能好一些代碼也簡單。public class CartItem { private int id; // 商品 id private String name; // 商品名稱 private double price; // 單價 private int count; // 加購數(shù)量 public double getSubtotal() { return price * count; } // 構(gòu)造方法、getter/setter 省略 }Cart 類不直接操縱 Session它只負(fù)責(zé)持有一個 MapInteger, CartItem。加購時判斷 map 里有沒有同一個 id有就把 count 加 1沒有就 new 一個 CartItem 塞進(jìn)去。把業(yè)務(wù)放在這個類里Servlet 就只做參數(shù)解析和跳轉(zhuǎn)兩件事后期如果想換成 Redis 存儲或其他方式只需要替換 Cart 的實(shí)現(xiàn)控制層可以不動。public class Cart { private MapInteger, CartItem items new LinkedHashMap(); public void addToCart(CartItem item) { CartItem exist items.get(item.getId()); if (exist ! null) { exist.setCount(exist.getCount() 1); } else { items.put(item.getId(), item); } } public ListCartItem list() { return new ArrayList(items.values()); } // remove、clear 方法類似不再贅述 }用 LinkedHashMap 而不是 HashMap 是有意為之它保證遍歷順序和插入順序一致購物車頁面顯示的順序就和用戶加購的順序一致了。這個小細(xì)節(jié)很多人不注意但頁面效果差異明顯。4.2 加購請求的完整鏈路Servlet 與 JSP 之間的配合加購操作的前端只是一個鏈接或者一個按鈕點(diǎn)擊后攜帶商品 id 請求 CartServlet。Servlet 根據(jù) action 參數(shù)分發(fā)處理add 是加購update 是改數(shù)量delete 是刪一項(xiàng)clear 是清空。最后統(tǒng)一重定向回商品列表頁或購物車頁而不是轉(zhuǎn)發(fā)到 JSP。WebServlet(/cart) public class CartServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); HttpSession session req.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } if (add.equals(action)) { int id Integer.parseInt(req.getParameter(id)); Product p ProductDao.findById(id); CartItem item new CartItem(p.getId(), p.getName(), p.getPrice(), 1); cart.addToCart(item); } else if (delete.equals(action)) { cart.remove(Integer.parseInt(req.getParameter(id))); } resp.sendRedirect(req.getContextPath() /goods); } }這里要解釋為什么用重定向而不是轉(zhuǎn)發(fā)轉(zhuǎn)發(fā)是服務(wù)端內(nèi)部跳轉(zhuǎn)地址欄不變重定向是瀏覽器重新發(fā)起一次請求地址欄會變成目標(biāo)地址。如果加購后用轉(zhuǎn)發(fā)回到商品列表頁用戶按 F5 刷新時會再次提交上一次的加購請求商品數(shù)量會被反復(fù)疊加。重定向正是解決「刷新導(dǎo)致重復(fù)提交」的最樸素方案。ProductDao.findById 里的 JDBC 代碼老生常談建立連接、寫 SQL、填參數(shù)、執(zhí)行查詢、封裝結(jié)果集、關(guān)資源。唯一要強(qiáng)調(diào)的是 finally 里逐個關(guān)閉 ResultSet、Statement、Connection并且要按這個順序關(guān)否則連接不釋放多刷新幾次頁面就會出現(xiàn)連接超時。public static Product findById(int id) { String sql SELECT id, name, price, stock FROM goods WHERE id?; // PreparedStatement 預(yù)編譯防 SQL 注入 // 結(jié)果封裝成 Product 對象返回 }在頁面端遍歷購物車只需這樣一段 EL JSTLc:forEach items${sessionScope.cart.list()} varitem tr td${item.name}/td td${item.price}/td td a hrefcart?actionupdateid${item.id}count1/a ${item.count} a hrefcart?actionupdateid${item.id}count-1-/a /td td${item.subtotal}/td tda hrefcart?actiondeleteid${item.id}刪除/a/td /tr /c:forEach需要留意的是 sessionScope 的寫法只有加上它才能明確告訴 EL 去 Session 里取 cart而不是去請求域或頁面域里找。這個小細(xì)節(jié)如果漏掉頁面上的購物車一直是空的排查時容易一頭霧水。改數(shù)量操作不用單獨(dú)加一個文本框直接在現(xiàn)有數(shù)字旁邊放加減兩個鏈接通過 count 參數(shù)傳 ±1后端拿到后修改數(shù)量這樣就避免了表單提交要處理的空值問題。5. 五個高頻坑Session 丟失與亂碼排查記錄5.1 商品數(shù)量莫名其妙疊加刷新一次漲一次現(xiàn)象加入一件商品后點(diǎn)瀏覽器刷新購物車?yán)镞@件商品的數(shù)量每次 1無法通過刷新讓頁面停留在原狀。原因加購請求用的是 doGet瀏覽器地址欄的請求 URL 不會因?yàn)轫撁嫣D(zhuǎn)而改變刷新時瀏覽器原樣重發(fā)上一次請求。如果加購后是轉(zhuǎn)發(fā)到列表頁這個請求會被反復(fù)提交每次都在原數(shù)量上加 1。另外頁面里如果用了 這種方式直接提交 GET 請求刷新重放的幾率也高。解決把加購處理結(jié)束后的響應(yīng)改為重定向即 resp.sendRedirect() 返回列表頁。重定向會讓地址欄變成列表頁的 URL刷新時提交的是列表頁的查詢請求而不是加購請求。對于數(shù)量變化類操作更穩(wěn)妥的是改用 POST 提交并且處理完后仍然重定向雙保險。我習(xí)慣的規(guī)則是所有寫操作一律 POST 重定向讀操作才用 GET 轉(zhuǎn)發(fā)。5.2 加購時商品名變成 或亂碼現(xiàn)象使用 Tomcat 8 及以下版本運(yùn)行項(xiàng)目時請求參數(shù)里的中文商品名在購物車?yán)镲@示為一堆問號或亂碼。原因GET 請求的中文參數(shù)默認(rèn)按 ISO-8859-1 解碼超過這個字符集范圍的字符全部變成問號。Tomcat 8 及以上版本默認(rèn) URI 編碼是 UTF-8問題不大但很多教材配套的教學(xué)環(huán)境還在用 Tomcat 7。解決在請求到達(dá) Servlet 之前先指定請求編碼或者在 Tomcat 的 server.xml 的 Connector 配置里加上 URIEncodingUTF-8。代碼層面最省事的做法是寫一個 Filter在 doFilter 開頭設(shè)置 req.setCharacterEncoding(UTF-8)然后 chain.doFilter 放行。注意設(shè)置編碼動作必須發(fā)生在讀取任何參數(shù)之前否則已解碼的參數(shù)無法回頭修正。同時JSP 頁面頂部寫 pageEncoding“UTF-8”數(shù)據(jù)庫連接串里帶 characterEncodingutf8三層一起到位才算是徹底解決。public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }5.3 同一個商品加購后數(shù)量沒有 1反而出現(xiàn)兩行現(xiàn)象往購物車?yán)锛右患唐穬纱钨徫镘図撁娉霈F(xiàn)兩行同名的商品而不是一行數(shù)量為 2。原因CartItem 里的 equals 和 hashCode 沒有重寫導(dǎo)致 MapInteger, CartItem 的 key 是商品 idInteger 包裝類型時不存在重復(fù)問題但如果你把 Cart 實(shí)現(xiàn)里的 key 換成了 CartItem 對象且沒有重寫 equals那么每次 new 的 CartItem 都是不同對象永遠(yuǎn)不能命中已有的項(xiàng)。初學(xué)者最容易在這里寫錯把 key 類型和實(shí)際類型混在一起。解決嚴(yán)格保持 Map 的 key 類型為 Integer 商品 id判斷是否存在時用 id 作為查找依據(jù)而不是把 CartItem 本身當(dāng)作 key。如果你的設(shè)計里一定要用對象做 key那么必須重寫 equals 和 hashCode且只比較商品 id。經(jīng)驗(yàn)之談能用基本類型包裝類做 key 就不要用對象少寫代碼就是少出錯。5.4 頁面能打開但購物車數(shù)據(jù)顯示不完整點(diǎn)刪除報空指針現(xiàn)象列表頁能正常顯示商品但進(jìn)入購物車頁后部分字段空白點(diǎn)擊刪除某個商品時后臺拋 NullPointerException頁面白屏。原因這是典型的用戶換了個瀏覽器或者清了 Cookie 導(dǎo)致 Session 丟失。Session 是靠 Cookie 里的 JSESSIONID 維持的瀏覽器禁止 Cookie 后每次請求都是新的會話cart 屬性自然為 null。另一個常見情況是先直接打開了購物車頁還沒通過列表頁加購過任何商品此時 session 里根本沒有 cart 這個屬性。解決在購物車頁面的 Servlet 里做空值兜底取出 cart 為 null 時就 new 一個空的 Cart 放入 session而不是直接拿 null 去調(diào)方法。頁面端為了優(yōu)雅可以在購物車為空時提示「還沒有商品去逛逛」而不是渲染一個空表格。這段邏輯兩三行就能寫完但能擋住大部分線上白屏。Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); }5.5 修改數(shù)量時輸入了負(fù)數(shù)或 0購物車?yán)锍霈F(xiàn)離譜數(shù)據(jù)現(xiàn)象用戶通過加減鏈接操作數(shù)量理論上每次只傳 ±1但如果有人手工修改 URL 參數(shù)傳 count-999購物車?yán)锏臄?shù)量就變成了負(fù)數(shù)。原因后端缺少兜底校驗(yàn)?zāi)玫絽?shù)后直接運(yùn)算。簡易項(xiàng)目通常不面對惡意攻擊但一個「不小心手滑」的普通用戶也能制造同樣的臟數(shù)據(jù)。負(fù)數(shù)的購物車數(shù)量會導(dǎo)致購物車總計出現(xiàn)負(fù)數(shù)展示上非常難看計算結(jié)算金額時還會引發(fā)邏輯混亂。解決在后端更新數(shù)量之后、保存之前做一次最小值校驗(yàn)數(shù)量小于 1 時直接刪除這一項(xiàng)或者重置為 1。數(shù)量大于庫存時按庫存上限截斷。這套校驗(yàn)邏輯放在 Cart 類的 updateCount 方法里保證任何入口進(jìn)來的數(shù)據(jù)都會被兜住。類似的兜底同樣適用于商品 id 參數(shù)Integer.parseInt 前先做一次正則匹配或 try-catch避免非數(shù)字參數(shù)直接讓 Servlet 拋 NumberFormatException。6. 從「能跑」到「能交差」的進(jìn)階調(diào)整到此一個基于 Session 存儲、JSP Servlet 展示、JDBC 讀商品表的簡易購物車已經(jīng)完整跑通了。如果做完這個還覺得不過癮或者這是你的課程設(shè)計需要「加亮點(diǎn)」下面幾個方向是我會優(yōu)先考慮的按性價比從高到低排列。第一個值得改的是把商品詳情也納入購物車展示。當(dāng)前方案在加購時把商品名和單價存進(jìn)了 CartItem這有個隱患后臺改了商品價格購物車?yán)锏呐f價不會自動更新。進(jìn)階做法是購物車只存 id 和數(shù)量每次渲染購物車頁時用 id 批量查一次商品表詳情實(shí)時對齊。代價是每次打開購物車多一次查詢但換來的是價格數(shù)據(jù)的準(zhǔn)確性。這一步改動不大但能把「購物車是臨時視圖」這個理念貫徹到位。第二個方向是加上數(shù)量校驗(yàn)與庫存聯(lián)動。在 updateCount 方法里查一下當(dāng)前商品的庫存加購數(shù)量超過庫存時彈出提示不允許繼續(xù)添加。這樣即使沒人惡意攻擊也能擋住誤操作頁面體驗(yàn)感提升明顯。庫存扣減可以放在「結(jié)算」動作里做如果沒有做結(jié)算功能至少要做到下單時二次校驗(yàn)防止超賣——雖然簡易項(xiàng)目通常不需要真上鎖但提前把庫存校驗(yàn)邏輯埋好后續(xù)接支付寶或微信支付時會省很多事。第三個方向是引入 Cookie 持久化未登錄用戶的購物車。做法是用戶未登錄時把購物車編碼為 JSON 字符串寫入 Cookie登錄后將 Cookie 中的內(nèi)容合并進(jìn)賬戶的購物車數(shù)據(jù)。這個功能在真實(shí)電商里是標(biāo)配但實(shí)現(xiàn)起來涉及 JSON 序列化、Cookie 編解碼、合并策略量不小。我的建議是如果做的是課設(shè)這個方向?qū)儆诩臃猪?xiàng)但投入產(chǎn)出比一般把 Session 版本打磨到極致已經(jīng)足夠如果是在公司里做企業(yè)級項(xiàng)目這幾乎是硬需求。我在經(jīng)歷模擬項(xiàng)目X的時候一開始也想著把所有狀態(tài)都放進(jìn)數(shù)據(jù)庫后來發(fā)現(xiàn)會話過期時數(shù)據(jù)庫里堆滿了沒人認(rèn)領(lǐng)的臨時記錄清洗麻煩不說還拖慢查詢。把購物車放 Session 后又踩過刷新重復(fù)提交的坑被 QA 同學(xué)反復(fù)反饋「加一次變兩次」?!笇懖僮骱笠宦芍囟ㄏ颉惯@個習(xí)慣就是那次被磨出來的?;仡^來看做簡易購物車最有價值的收獲并不是學(xué)會了幾個標(biāo)簽和 API而是真正理解了 HTTP 無狀態(tài)和 Session 之間的關(guān)系。如果你照著上面的代碼跑通了建議你試著改一個功能來驗(yàn)證自己是否真理解把「加入購物車」改成「商品詳情頁內(nèi)選擇數(shù)量后加入」看看參數(shù)傳遞和數(shù)量疊加邏輯會出現(xiàn)哪些新問題。這個改動不復(fù)雜但能考驗(yàn)?zāi)銓φ麄€請求鏈路的掌握程度。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取