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

PHP 購物車完整實現:從 Session 存儲到訂單結算的工程化指南

     編輯:PHP 相關知識

架構概覽:四頁面協作流程

  1. 商品展示頁:讀取數據庫渲染商品卡片,「加入購物車」鏈接攜帶 id 跳轉
  2. 加入處理頁 (addgouwuche.php):驗證登錄、庫存、去重 → 寫入 Session → 重定向購物車頁
  3. 購物車頁 (gouwu1.php):讀取 Session → 查詢商品詳情 → 渲染列表 → 支持數量修改、移除、清空、小計/總計 → 「去結算」跳轉
  4. 結算頁 (gouwu2.php):收貨信息表單 → JS 驗證 → 提交訂單保存 → 彈窗顯示訂單詳情

核心數據結構:Session 雙陣列設計

$_SESSION['producelist'] = "商品ID1@商品ID2@商品ID3@";  // 以 @ 分隔,尾部保留 @ 便於 explode
$_SESSION['quatity']     = "數量1@數量2@數量3@";        // 一一對應
// 示例:用戶加入 id=10 數量2、id=20 數量1
// producelist = "10@20@"
// quatity     = "2@1@"

優缺點:實現簡單、無需資料表、適配無登錄購物。但:

  • 查詢 N 次數據庫(N=商品種類),易成 N+1 問題
  • Session 文件鎖導致併發性能瓶頸
  • 字符串拼接/分割脆弱,含特殊字符易出錯
  • 無法支援「已登錄用戶跨設備同步購物車」

關鍵代碼深度重構

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

2. 購物車展示:單次查詢 + 關聯映射

<?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;
// 渲染模板...
?>

3. 數量更新:AJAX 異步 + 乐觀鎖

<?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]);

4. 結算下單:事務保證一致性

<?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 通知結果

安全加固清單

  • SQL 注入:全鏈路預處理語句(PDO::prepare),拒絕字符串拼接。
  • XSS:輸出商品名稱、用戶輸入地址時 htmlspecialchars(),模板引擎自動轉義。
  • CSRF:表單隱藏 csrf_token,AJAX 請求頭攜帶,驗證中間件統一攔截。
  • 權限越界:結算時驗證收貨地址屬於當前用戶,防止 ID 遍歷攻擊。
  • 敏感信息:訂單號不使用遞增 ID,採用時間戳+隨機數;日誌脫敏(不記錄完整地址、手機號)。

從 Session 字符串拼接起步,到 Redis 持久化、定價引擎、異步下單,購物車系統的演進映射了電商業務增長的每一階段。初期以「能跑通、易維護」為目標,代碼結構清晰、職責單一、預留擴展接口(如將存儲層抽象為 CartStorageInterface),才能在業務爆發時平滑重構,而非推倒重來。

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