id 跳轉$_SESSION['producelist'] = "商品ID1@商品ID2@商品ID3@"; // 以 @ 分隔,尾部保留 @ 便於 explode
$_SESSION['quatity'] = "數量1@數量2@數量3@"; // 一一對應
// 示例:用戶加入 id=10 數量2、id=20 數量1
// producelist = "10@20@"
// quatity = "2@1@"
優缺點:實現簡單、無需資料表、適配無登錄購物。但:
<?php
session_start();
require 'conn.php'; // PDO 連接
// 1. 登錄攔截器(建議抽離為中間件/基類)
if (empty($_SESSION['user_id'])) {
exit(json_encode(['code'=>401, 'msg'=>'請先登錄', 'redirect'=>'/login.php']));
}
// 2. 參數驗證
$id = (int)($_GET['id'] ?? 0);
$qty = max(1, (int)($_POST['qty'] ?? 1));
if ($id <= 0) exit(json_encode(['code'=>400, 'msg'=>'商品ID無效']));
// 3. 庫存檢查(行級鎖防止超賣)
$stmt = $pdo->prepare("SELECT id, name, price, stock FROM products WHERE id = ? FOR UPDATE");
$stmt->execute([$id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) exit(json_encode(['code'=>404, 'msg'=>'商品不存在']));
if ($product['stock'] < $qty) exit(json_encode(['code'=>409, 'msg'=>'庫存不足', 'stock'=>$product['stock']]));
// 4. Session 數組化存儲
$cart = $_SESSION['cart'] ?? []; // [['id'=>10, 'qty'=>2], ['id'=>20, 'qty'=>1]]
$found = false;
foreach ($cart as &$item) {
if ($item['id'] === $id) {
$item['qty'] += $qty;
$found = true;
break;
}
}
if (!$found) $cart[] = ['id' => $id, 'qty' => $qty];
$_SESSION['cart'] = $cart;
exit(json_encode(['code'=>200, 'msg'=>'加入成功', 'count'=>count($cart)]));
<?php
session_start();
require 'conn.php';
$cart = $_SESSION['cart'] ?? [];
if (empty($cart)) {
// 顯示空購物車模板
exit;
}
// 提取所有商品 ID
$ids = array_column($cart, 'id');
$placeholders = implode(',', array_fill(0, count($ids), '?'));
// 單次查詢獲取所有商品詳情
$stmt = $pdo->prepare("SELECT id, name, price, image, stock FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$products = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); // 鍵為 id,值為整行
// 合併購物車數量與商品信息
$items = [];
$total = 0;
foreach ($cart as $cartItem) {
$id = $cartItem['id'];
if (!isset($products[$id])) continue; // 商品已下架
$p = $products[$id];
$qty = min($cartItem['qty'], $p['stock']); // 再次校驗庫存
$subtotal = $p['price'] * $qty;
$total += $subtotal;
$items[] = [
'id' => $id,
'name' => $p['name'],
'price' => $p['price'],
'image' => $p['image'],
'qty' => $qty,
'stock' => $p['stock'],
'subtotal' => $subtotal,
];
}
$_SESSION['cart_total'] = $total;
// 渲染模板...
?>
<?php // update_cart.php
session_start();
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? 0);
$qty = max(1, (int)($_POST['qty'] ?? 1));
$cart = $_SESSION['cart'] ?? [];
foreach ($cart as &$item) {
if ($item['id'] === $id) {
$item['qty'] = $qty;
break;
}
}
$_SESSION['cart'] = $cart;
// 可選:即時查詢庫存回傳實際可用數量
echo json_encode(['code'=>200, 'qty'=>$qty]);
<?php // checkout.php
session_start();
require 'conn.php';
$userId = $_SESSION['user_id'] ?? 0;
$cart = $_SESSION['cart'] ?? [];
$addressId = (int)($_POST['address_id'] ?? 0);
// 驗證地址屬於當前用戶...
try {
$pdo->beginTransaction();
// 1. 鎖定商品行,扣減庫存
$ids = array_column($cart, 'id');
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, price, stock FROM products WHERE id IN ($placeholders) FOR UPDATE");
$stmt->execute($ids);
$products = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$orderItems = [];
$total = 0;
foreach ($cart as $item) {
$p = $products[$item['id']] ?? null;
if (!$p || $p['stock'] < $item['qty']) {
throw new Exception("商品 {$item['id']} 庫存不足");
}
$subtotal = $p['price'] * $item['qty'];
$total += $subtotal;
$orderItems[] = [
'product_id' => $item['id'],
'price' => $p['price'],
'qty' => $item['qty'],
'subtotal' => $subtotal,
];
// 扣減庫存
$pdo->prepare("UPDATE products SET stock = stock - ? WHERE id = ?")->execute([$item['qty'], $item['id']]);
}
// 2. 創建訂單主表
$orderNo = date('YmdHis') . mt_rand(1000, 9999);
$pdo->prepare("INSERT INTO orders (order_no, user_id, total_amount, address_id, status) VALUES (?, ?, ?, ?, 'pending')")
->execute([$orderNo, $userId, $total, $addressId]);
$orderId = $pdo->lastInsertId();
// 3. 批量寫入訂單明細
$stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, price, qty, subtotal) VALUES (?, ?, ?, ?, ?)");
foreach ($orderItems as $oi) {
$stmt->execute([$orderId, $oi['product_id'], $oi['price'], $oi['qty'], $oi['subtotal']]);
}
$pdo->commit();
unset($_SESSION['cart']); // 清空購物車
echo json_encode(['code'=>200, 'order_no'=>$orderNo, 'order_id'=>$orderId]);
} catch (Exception $e) {
$pdo->rollBack();
error_log("下單失敗: " . $e->getMessage());
echo json_encode(['code'=>500, 'msg'=>'下單失敗,請重試']);
}
| 痛點 | 優化方案 | 技術選型 |
|---|---|---|
| Session 文件鎖併發差 | Redis Hash 存儲購物車 | predis/phpredis + Lua 腳本原子操作 |
| 頻繁查詢商品詳情 | 本地緩存 + 異步失效 | APCu / Redis + 訂閱商品更新事件 |
| 超賣風險 | 樂觀鎖(version 字段)或 分布式鎖 | Redis SETNX / RedLock / 數據庫行鎖 FOR UPDATE |
| 購物車持久化/跨端同步 | 登錄後合併 Session → DB | 資料表 cart_items (user_id, product_id, qty, updated_at) |
| 促銷/優惠券/滿減計算 | 領域模型封裝定價引擎 | 策略模式 + 規則引擎 |
| 高併發下單 | 消息佇列削峰填谷 | RabbitMQ / Kafka + 異步下單 + 輪詢/WebSocket 通知結果 |
htmlspecialchars(),模板引擎自動轉義。csrf_token,AJAX 請求頭攜帶,驗證中間件統一攔截。從 Session 字符串拼接起步,到 Redis 持久化、定價引擎、異步下單,購物車系統的演進映射了電商業務增長的每一階段。初期以「能跑通、易維護」為目標,代碼結構清晰、職責單一、預留擴展接口(如將存儲層抽象為 CartStorageInterface),才能在業務爆發時平滑重構,而非推倒重來。