網上購物的核心流程:用戶瀏覽商品 → 加入購物車 → 查看/修改/刪除購物項 → 確認結算 → 填寫收貨信息 → 生成訂單 → 支付 → 訂單管理。購物車作為臨時存儲,需支撐:
| 決策點 | 方案 | 理由 |
|---|---|---|
| 存儲位置 | HttpSession | 用戶級隔離、生命週期與會話綁定、無需資料庫開銷、支援集群 Session 複製 |
| 容器類型 | ArrayList<Item> | 有序、隨機訪問、動態擴容、迭代性能優於 LinkedList |
| 項結構 | Item(Goods goods, int quantity) | 組合模式,封裝商品信息與購買屬性,單一職責 |
package com.shop.domain;
import java.io.Serializable;
import java.math.BigDecimal;
public class Goods implements Serializable {
private String goodsId; // 主鍵
private String goodsName; // 商品名稱
private BigDecimal price; // 單價 (金額必用 BigDecimal)
private int stock; // 庫存
private String imageUrl; // 圖片
private String description; // 描述
// 標準 Getter/Setter、equals/hashCode (按 goodsId)、toString
// 兩個分頁查詢輔助方法 (略)
}
package com.shop.cart;
import com.shop.domain.Goods;
import java.io.Serializable;
import java.math.BigDecimal;
public class Item implements Serializable {
private Goods goods;
private int quantity;
public Item(Goods goods, int quantity) {
this.goods = goods;
this.quantity = quantity;
}
public Goods getGoods() { return goods; }
public void setGoods(Goods goods) { this.goods = goods; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
// 衍生屬性:小計
public BigDecimal getSubtotal() {
return goods.getPrice().multiply(BigDecimal.valueOf(quantity));
}
// equals/hashCode 基於 goodsId,便於 List.contains/remove 判斷
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Item)) return false;
Item item = (Item) o;
return goods != null && goods.getGoodsId().equals(item.goods.getGoodsId());
}
@Override public int hashCode() { return goods != null ? goods.getGoodsId().hashCode() : 0; }
}
package com.shop.cart;
import com.shop.domain.Goods;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class CartManager implements Serializable {
private List- items = new ArrayList<>();
public List
- getItems() { return items; }
public void setItems(List
- items) { this.items = items; }
// 核心:添加商品 (自動合併同款)
public void addItem(Goods goods, int quantity) {
if (goods == null || quantity <= 0) return;
// 庫存校驗 (可選,視業務嚴格度)
// if (quantity > goods.getStock()) quantity = goods.getStock();
Optional
- existing = items.stream()
.filter(item -> item.equals(new Item(goods, 0))) // 利用 equals 比較 goodsId
.findFirst();
if (existing.isPresent()) {
existing.get().setQuantity(existing.get().getQuantity() + quantity);
} else {
items.add(new Item(goods, quantity));
}
}
// 便利方法:獲取總金額
public BigDecimal getTotalPrice() {
return items.stream()
.map(Item::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 便利方法:總件數
public int getTotalQuantity() {
return items.stream().mapToInt(Item::getQuantity).sum();
}
// 是否為空
public boolean isEmpty() { return items.isEmpty(); }
}
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="https://java.sun.com/jsp/jstl/fmt" %>
<div class="goods-grid">
<c:forEach items="${goodsPage.records}" var="goods">
<div class="goods-card">
<img src="${goods.imageUrl}" alt="${goods.goodsName}">
<h3>${goods.goodsName}</h3>
<p class="price">¥<fmt:formatNumber value="${goods.price}" type="currency" currencySymbol=""/></p>
<p class="stock">庫存:${goods.stock}</p>
<form action="${pageContext.request.contextPath}/cart/add" method="post">
<input type="hidden" name="goodsId" value="${goods.goodsId}"/>
<input type="number" name="quantity" value="1" min="1" max="${goods.stock}" class="qty">
<button type="submit" class="btn-add-cart">加入購物車</button>
</form>
</div>
</c:forEach>
</div>
<!-- 分頁導航 -->
<c:url value="/goods/list" var="pageUrl">
<c:param name="page" value="${goodsPage.pageNum - 1}"/>
</c:url>
<a href="${pageUrl}">上一頁</a> ...
@WebServlet("/cart/add")
public class AddToCartController extends HttpServlet {
@Autowired GoodsService goodsService;
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String goodsId = req.getParameter("goodsId");
int qty = Integer.parseInt(req.getParameter("quantity"));
// 1. 獲取/創建購物車
HttpSession session = req.getSession(true);
CartManager cart = (CartManager) session.getAttribute("cart");
if (cart == null) {
cart = new CartManager();
session.setAttribute("cart", cart);
}
// 2. 查詢商品 (含庫存、價格快照)
Goods goods = goodsService.findById(goodsId);
if (goods == null) {
resp.sendRedirect(req.getHeader("Referer") + "?error=商品不存在");
return;
}
// 3. 添加至購物車
cart.addItem(goods, qty);
// 4. 重定向回來源頁 (PRG 模式防重複提交)
String referer = req.getHeader("Referer");
resp.sendRedirect(referer != null ? referer : req.getContextPath() + "/goods/list");
}
}
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="https://java.sun.com/jsp/jstl/fmt" %>
<table class="cart-table">
<thead>
<tr><th>商品</th><th>單價</th><th>數量</th><th>小計</th><th>操作</th></tr>
</thead>
<tbody>
<c:forEach items="${cart.items}" var="item" varStatus="st">
<tr>
<td>
<img src="${item.goods.imageUrl}" width="50">
${item.goods.goodsName}
</td>
<td>¥<fmt:formatNumber value="${item.goods.price}" type="number" minFractionDigits="2"/></td>
<td>${item.quantity}</td>
<td>¥<fmt:formatNumber value="${item.subtotal}" type="number" minFractionDigits="2"/></td>
<td>
<a href="${pageContext.request.contextPath}/cart/remove?goodsId=${item.goods.goodsId}">移除</a>
</td>
</tr>
</c:forEach>
</tbody>
<tfoot>
<tr>
<td colspan="3">總計</td>
<td><strong>¥<fmt:formatNumber value="${cart.totalPrice}" type="number" minFractionDigits="2"/></strong></td>
<td><a href="${pageContext.request.contextPath}/cart/checkout">去結算</a></td>
</tr>
</tfoot>
</table>
JSP 購物車核心在於:**Session 作用域存儲 `CartManager`,`ArrayList