購物車存放於 `HttpSession`,核心數據結構:`List
package com.shop.cart;
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; }
// 1. 添加/合併
public void addItem(Goods goods, int quantity) {
if (quantity <= 0) return;
Optional
- exist = items.stream()
.filter(i -> i.getGoods().getId().equals(goods.getId()))
.findFirst();
if (exist.isPresent()) {
exist.get().setQuantity(exist.get().getQuantity() + quantity);
} else {
items.add(new Item(goods, quantity));
}
}
// 2. 修改數量 (含移除邏輯)
public void updateQuantity(Long goodsId, int newQuantity) {
items.removeIf(item -> {
if (item.getGoods().getId().equals(goodsId)) {
if (newQuantity <= 0) return true; // 移除
item.setQuantity(newQuantity);
return false;
}
return false;
});
}
// 3. 刪除單個商品
public void removeItem(Long goodsId) {
items.removeIf(item -> item.getGoods().getId().equals(goodsId));
}
// 4. 清空購物車
public void clear() {
items.clear();
}
// 5. 計算總價
public BigDecimal getTotalPrice() {
return items.stream()
.map(Item::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 6. 總商品件數
public int getTotalQuantity() {
return items.stream().mapToInt(Item::getQuantity).sum();
}
// 內部類:購物項
public static 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));
}
}
}
@WebServlet("/cart/*")
public class CartController extends HttpServlet {
@Autowired GoodsService goodsService; // 查詢商品詳情
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
String action = req.getPathInfo(); // /add, /update, /remove, /clear, /checkout
CartManager cart = getCart(req);
try {
switch (action) {
case "/add": add(req, cart); break;
case "/update": update(req, cart); break;
case "/remove": remove(req, cart); break;
case "/clear": clear(cart); break;
case "/checkout": checkout(req, resp, cart); break;
default: resp.sendError(404);
}
} catch (Exception e) {
handleError(req, resp, e);
}
}
private void add(HttpServletRequest req, CartManager cart) {
Long goodsId = Long.valueOf(req.getParameter("goodsId"));
int qty = ParseUtils.parseInt(req.getParameter("quantity"), 1);
Goods goods = goodsService.findById(goodsId);
if (goods != null && goods.getStock() >= qty) {
cart.addItem(goods, qty);
}
redirectBack(req, resp);
}
private void update(HttpServletRequest req, CartManager cart) {
Long goodsId = Long.valueOf(req.getParameter("goodsId"));
int qty = ParseUtils.parseInt(req.getParameter("quantity"), 0);
// 庫存校驗
Goods goods = goodsService.findById(goodsId);
if (goods != null && qty > goods.getStock()) {
qty = goods.getStock(); // 自動調整至最大庫存
}
cart.updateQuantity(goodsId, qty);
redirectBack(req, resp);
}
private void remove(HttpServletRequest req, CartManager cart) {
Long goodsId = Long.valueOf(req.getParameter("goodsId"));
cart.removeItem(goodsId);
redirectBack(req, resp);
}
private void clear(CartManager cart) {
cart.clear();
}
private void checkout(HttpServletRequest req, HttpServletResponse resp, CartManager cart) {
if (cart.getItems().isEmpty()) {
req.setAttribute("error", "購物車為空");
forward("/cart/view.jsp", req, resp);
return;
}
// 1. 庫存二次校驗 (防超賣)
for (Item item : cart.getItems()) {
Goods goods = goodsService.findById(item.getGoods().getId());
if (goods == null || goods.getStock() < item.getQuantity()) {
req.setAttribute("error", "商品 [" + item.getGoods().getName() + "] 庫存不足");
forward("/cart/view.jsp", req, resp);
return;
}
}
// 2. 創建訂單 (事務邊界在 Service)
OrderDTO orderDTO = buildOrderDTO(req, cart);
OrderVO order = orderService.createOrder(orderDTO); // 扣減庫存、寫入訂單/明細、清空購物車
// 3. 跳轉支付/成功頁
resp.sendRedirect(req.getContextPath() + "/order/success?orderNo=" + order.getOrderNo());
}
private CartManager getCart(HttpServletRequest req) {
HttpSession session = req.getSession(true);
CartManager cart = (CartManager) session.getAttribute("cart");
if (cart == null) {
cart = new CartManager();
session.setAttribute("cart", cart);
}
return cart;
}
}
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService {
@Autowired OrderDao orderDao;
@Autowired OrderItemDao itemDao;
@Autowired GoodsDao goodsDao;
public OrderVO createOrder(OrderDTO dto) {
// 1. 生成訂單號
String orderNo = OrderNoGenerator.generate(); // 如:YYYYMMDDHHMMSS + 隨機數
// 2. 鎖定庫存並扣減 (悲觀鎖 SELECT ... FOR UPDATE)
List itemPos = new ArrayList<>();
BigDecimal total = BigDecimal.ZERO;
for (OrderItemDTO itemDTO : dto.getItems()) {
GoodsPO goods = goodsDao.lockById(itemDTO.getGoodsId()); // SELECT * FROM goods WHERE id=? FOR UPDATE
if (goods.getStock() < itemDTO.getQuantity()) {
throw new BizException("商品 " + goods.getName() + " 庫存不足");
}
goods.setStock(goods.getStock() - itemDTO.getQuantity());
goodsDao.updateStock(goods); // 更新庫存
OrderItemPO item = new OrderItemPO();
item.setGoodsId(goods.getId());
item.setGoodsName(goods.getName());
item.setPrice(goods.getPrice()); // 鎖定下單價格
item.setQuantity(itemDTO.getQuantity());
item.setSubtotal(goods.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity())));
itemPos.add(item);
total = total.add(item.getSubtotal());
}
// 3. 寫入訂單主表
OrderPO order = new OrderPO();
order.setOrderNo(orderNo);
order.setUserId(dto.getUserId());
order.setTotalAmount(total);
order.setStatus(OrderStatus.PENDING_PAYMENT);
order.setReceiverName(dto.getReceiverName());
order.setReceiverPhone(dto.getReceiverPhone());
order.setReceiverAddress(dto.getReceiverAddress());
order.setCreateTime(new Date());
orderDao.insert(order);
// 4. 批量寫入明細
for (OrderItemPO item : itemPos) {
item.setOrderId(order.getId());
}
itemDao.batchInsert(itemPos);
// 5. 清空購物車 (若購物車在資料庫,這裡刪除;Session 由 Controller 清除)
cartDao.clearByUserId(dto.getUserId());
return convertToVO(order, itemPos);
}
}
<c:forEach items="${cart.items}" var="item" varStatus="st">
<tr>
<td>${item.goods.name}</td>
<td>¥${item.goods.price}</td>
<td>
<form action="${pageContext.request.contextPath}/cart/update" method="post">
<input type="hidden" name="goodsId" value="${item.goods.id}"/>
<input type="number" name="quantity" value="${item.quantity}" min="1"
max="${item.goods.stock}" class="qty-input"/>
<button type="submit">更新</button>
</form>
</td>
<td>¥${item.subtotal}</td>
<td>
<a href="${pageContext.request.contextPath}/cart/remove?goodsId=${item.goods.id}"
onclick="return confirm('確定移除?')">刪除</a>
</td>
</tr>
</c:forEach>
<tr>
<td colspan="3">總計</td>
<td><strong>¥${cart.totalPrice}</strong></td>
<td><a href="${pageContext.request.contextPath}/cart/checkout" class="btn-checkout">去結算</a></td>
</tr>
<script>
$('.qty-input').on('change', function() {
const $form = $(this).closest('form');
$.post($form.attr('action'), $form.serialize(), function(data) {
location.reload(); // 簡單粗暴;生產環境局部刷新總價、小計
});
});
</script>
購物車從「加入」到「結算生成訂單」涉及會話管理、領域建模、庫存一致性、事務控制、前端交互等全棧技能。`CartManager` 作為領域服務封裝核心邏輯,Controller 薄調度,Service 重業務與事務,DAO 純持久化,配合悲觀鎖/樂觀鎖解決併發超賣,幂等設計保證支付安全,是電商核心流程的經典實踐範本。