首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> JSP 程式設計 >> 關於JSP

JSP 購物車實戰進階:修改數量、刪除商品、生成訂單完整流程實現

     編輯:關於JSP

承接上文:購物車核心模型回顧

購物車存放於 `HttpSession`,核心數據結構:`List`,其中 `Item` 封裝 `Goods` (商品信息) 與 `quantity` (數量)。`CartManager` 作為無狀態業務組件,提供增刪改查操作。本文補全修改數量、刪除商品、清空購物車、生成訂單四大功能,並探討併發、事務、幂等性等工程化課題。

CartManager 完整實現

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));
        }
    }
}

Controller 處理各類請求

@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 層)

@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);
    }
}

前端頁面關鍵交互

購物車列表頁 (cart/view.jsp)

<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>

AJAX 異步更新數量 (提升體驗)

<script>
    $('.qty-input').on('change', function() {
        const $form = $(this).closest('form');
        $.post($form.attr('action'), $form.serialize(), function(data) {
            location.reload(); // 簡單粗暴;生產環境局部刷新總價、小計
        });
    });
</script>

工程化關鍵點總結

  1. Session 購物車 vs 資料庫購物車:匿名用戶用 Session,登入用戶合併至資料庫表 `cart_item`,支援多端同步。
  2. 幂等性:訂單號唯一索引、支付回調幂等處理、防重複提交 Token。
  3. 超賣防護:悲觀鎖 (`FOR UPDATE`) 或樂觀鎖 (`version` 字段) + 庫存扣減 SQL `UPDATE goods SET stock=stock-? WHERE id=? AND stock>=?`。
  4. 價格快照:訂單明細冗存 `price`、`goods_name`,防商品變價/下架影響歷史訂單。
  5. 事務邊界:Service 方法 `@Transactional`,Controller 不開啟事務。
  6. 異步解耦:下單後發送訂單創建事件 (MQ),通知服務、積分服務、庫存同步異步處理。
  7. 安全:CSRF Token 保護表單提交、XSS 轉義輸出、SQL 注入防護 (參數化)。

總結

購物車從「加入」到「結算生成訂單」涉及會話管理、領域建模、庫存一致性、事務控制、前端交互等全棧技能。`CartManager` 作為領域服務封裝核心邏輯,Controller 薄調度,Service 重業務與事務,DAO 純持久化,配合悲觀鎖/樂觀鎖解決併發超賣,幂等設計保證支付安全,是電商核心流程的經典實踐範本。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved