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,
];
<?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,
];
}
}
?>
<?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']}");
}
}
}
// 後端推送 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 管道批量寫入、分布式鎖防併發、結算流程悲觀鎖扣減庫存、前端實時同步等生產級代碼骨架。實際落地還需加入:促銷規則引擎(滿減/折扣/贈品)、多倉庫分配策略、購物車合併(遊客轉登錄)、失效商品自動清理、大促預熱/限流/熔斷。