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

PHP 購物車實現:Session/Redis 存儲、SKU 庫存扣減、優惠券、異步下單

     編輯:PHP 相關知識

架構分層

CartController (API)
    ↓
CartService (業務邏輯:增刪改查、校驗、價格計算、優惠)
    ↓
CartStorageInterface
    ├── SessionCartStorage (遊客/低併發)
    ├── RedisCartStorage   (登錄用戶/高併發/多端同步)
    └── DatabaseCartStorage (持久化訂單前快照)
    ↓
ProductRepository (SKU 庫存、價格、屬性讀取)
CouponService (優惠券驗證、疊加規則)
EventDispatcher (CartUpdated → 異步更新推薦/庫存預扣)

核心數據結構

// Redis Hash: cart:user:{uid}
HSET cart:user:1001 sku:10001 '{"qty":2,"price":29900,"attrs":{"color":"紅","size":"L"},"selected":1}'
HSET cart:user:1001 sku:10002 '{"qty":1,"price":59900,"attrs":{"color":"藍"},"selected":1}'
EXPIRE cart:user:1001 2592000  # 30天

// Session 結構
$_SESSION['cart'] = [
    'items' => [
        'sku:10001' => ['qty'=>2, 'price'=>29900, 'attrs'=>..., 'selected'=>true],
    ],
    'updated_at' => 1694800000,
];

CartService 關鍵方法

<?php
class CartService {
    private CartStorageInterface $storage;
    private ProductRepository $products;
    private CouponService $coupons;

    public function add(int|string $skuId, int $qty = 1, array $attrs = []): array {
        $sku = $this->products->getSku($skuId);
        if (!$sku) throw new Exception('SKU 不存在');
        if ($sku['stock'] < $qty) throw new Exception('庫存不足');

        $cart = $this->storage->get();
        $key = "sku:$skuId";
        $existing = $cart['items'][$key] ?? null;
        $newQty = ($existing['qty'] ?? 0) + $qty;
        if ($sku['stock'] < $newQty) throw new Exception('總數量超過庫存');

        $cart['items'][$key] = [
            'qty' => $newQty,
            'price' => $sku['price'], // 單價快照
            'attrs' => $attrs ?: $sku['attrs'],
            'selected' => true,
        ];
        $cart['updated_at'] = time();
        $this->storage->save($cart);
        $this->dispatch('cart.updated', ['user_id' => $this->uid, 'sku_id' => $skuId, 'qty' => $newQty]);
        return $this->calculateTotals($cart);
    }

    public function updateQty(int|string $skuId, int $qty): array {
        if ($qty <= 0) return $this->remove($skuId);
        $sku = $this->products->getSku($skuId);
        if (!$sku || $sku['stock'] < $qty) throw new Exception('庫存不足');
        $cart = $this->storage->get();
        $key = "sku:$skuId";
        if (!isset($cart['items'][$key])) throw new Exception('商品不在購物車');
        $cart['items'][$key]['qty'] = $qty;
        $cart['updated_at'] = time();
        $this->storage->save($cart);
        return $this->calculateTotals($cart);
    }

    public function remove(int|string $skuId): array {
        $cart = $this->storage->get();
        unset($cart['items']["sku:$skuId"]);
        $cart['updated_at'] = time();
        $this->storage->save($cart);
        return $this->calculateTotals($cart);
    }

    public function select(int|string $skuId, bool $selected): array {
        $cart = $this->storage->get();
        $key = "sku:$skuId";
        if (isset($cart['items'][$key])) {
            $cart['items'][$key]['selected'] = $selected;
            $cart['updated_at'] = time();
            $this->storage->save($cart);
        }
        return $this->calculateTotals($cart);
    }

    public function applyCoupon(string $code): array {
        $cart = $this->storage->get();
        $coupon = $this->coupons->validate($code, $this->getSelectedItems($cart));
        if (!$coupon) throw new Exception('優惠券無效');
        $cart['coupon'] = ['code' => $code, 'discount' => $coupon->calculate($cart)];
        $this->storage->save($cart);
        return $this->calculateTotals($cart);
    }

    private function calculateTotals(array $cart): array {
        $subtotal = 0; $discount = 0; $count = 0;
        foreach ($cart['items'] as $item) {
            if (!$item['selected']) continue;
            $line = $item['price'] * $item['qty'];
            $subtotal += $line;
            $count += $item['qty'];
        }
        if (!empty($cart['coupon'])) $discount = $cart['coupon']['discount'];
        return [
            'items' => array_values($cart['items']),
            'subtotal' => $subtotal,
            'discount' => $discount,
            'total' => max(0, $subtotal - $discount),
            'count' => $count,
            'coupon' => $cart['coupon'] ?? null,
        ];
    }
}
?>

Redis 存儲實現(支持分布式鎖防併發)

<?php
class RedisCartStorage implements CartStorageInterface {
    private Redis $redis;
    private string $prefix = 'cart:user:';
    private int $ttl = 2592000;

    public function __construct(Redis $redis) { $this->redis = $redis; }

    public function get(int $uid): array {
        $data = $this->redis->hGetAll($this->prefix . $uid);
        $items = [];
        foreach ($data as $k => $v) {
            $items[$k] = json_decode($v, true);
        }
        return ['items' => $items, 'coupon' => null, 'updated_at' => time()];
    }

    public function save(int $uid, array $cart): bool {
        $key = $this->prefix . $uid;
        $pipe = $this->redis->multi(Redis::PIPELINE);
        $pipe->del($key);
        foreach ($cart['items'] as $k => $v) {
            $pipe->hSet($key, $k, json_encode($v));
        }
        if (!empty($cart['coupon'])) {
            $pipe->hSet($key, 'coupon', json_encode($cart['coupon']));
        }
        $pipe->expire($key, $this->ttl);
        $pipe->exec();
        return true;
    }

    /** 分布式鎖保護關鍵操作(如結算前檢查庫存) */
    public function withLock(int $uid, callable $callback) {
        $lockKey = "lock:cart:$uid";
        $lockVal = uniqid('', true);
        $acquired = $this->redis->set($lockKey, $lockVal, ['nx', 'ex' => 10]);
        if (!$acquired) throw new Exception('系統繁忙,請稍後重試');
        try {
            return $callback();
        } finally {
            // Lua 腳本安全釋放鎖
            $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            $this->redis->eval($script, 1, $lockKey, $lockVal);
        }
    }
}
?>

結算下單流程(防超賣)

// OrderService::createFromCart()
public function createFromCart(int $uid): Order {
    $cart = $this->cartService->getSelected();
    if (empty($cart['items'])) throw new Exception('購物車為空');

    // 1. 獲取分布式鎖(按 SKU 粒度或整單)
    $locks = [];
    foreach ($cart['items'] as $item) {
        $locks[] = $this->redis->set("lock:stock:{$item['sku_id']}", $uid, ['nx', 'ex' => 10]);
    }
    if (in_array(false, $locks, true)) throw new Exception('並發過高,請重試');

    try {
        // 2. 再次校驗庫存(悲觀鎖/樂觀鎖)
        foreach ($cart['items'] as $item) {
            $sku = $this->productRepo->getSkuForUpdate($item['sku_id']); // SELECT ... FOR UPDATE
            if ($sku['stock'] < $item['qty']) throw new Exception("{$sku['name']} 庫存不足");
        }

        // 3. 扣減庫存(原子操作)
        foreach ($cart['items'] as $item) {
            $this->productRepo->decrStock($item['sku_id'], $item['qty']); // UPDATE ... SET stock=stock-?
        }

        // 4. 創建訂單、訂單項、記錄快照價格
        $order = $this->orderRepo->create([...]);
        foreach ($cart['items'] as $item) {
            $this->orderItemRepo->create(['order_id'=>$order->id, ...]);
        }

        // 5. 清空購物車選中項
        $this->cartService->clearSelected();

        // 6. 發送異步事件:庫存變更通知、推薦更新、發票、物流預處理
        $this->eventBus->dispatch('order.created', ['order_id' => $order->id]);

        return $order;
    } finally {
        // 釋放鎖
        foreach ($cart['items'] as $item) {
            $this->redis->del("lock:stock:{$item['sku_id']}");
        }
    }
}

前端狀態同步(WebSocket / SSE / 輪詢)

// 後端推送 CartUpdated 事件到用戶專屬頻道
// 前端訂閱
const es = new EventSource(`/api/cart/stream?token=${userToken}`);
es.onmessage = e => {
    const data = JSON.parse(e.data);
    if (data.type === 'cart.updated') {
        // 更新 Vue/React/Pinia/Redux 狀態
        cartStore.updateItem(data.sku_id, data.qty);
        cartStore.recalcTotals();
    }
};

總結

購物車系統核心在於「狀態存儲抽象、庫存一致性保護、價格快照、優惠計算、異步解耦」。本文提供了 Service + Storage 介面分層、Redis 管道批量寫入、分布式鎖防併發、結算流程悲觀鎖扣減庫存、前端實時同步等生產級代碼骨架。實際落地還需加入:促銷規則引擎(滿減/折扣/贈品)、多倉庫分配策略、購物車合併(遊客轉登錄)、失效商品自動清理、大促預熱/限流/熔斷。

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