購物車是電商系統核心模組。本文提供一個基於 Session 識別用戶 + MySQL 持久化資料 的完整購物車類別,支援加入、修改、刪除、清空、計算總價、列表顯示等完整功能,並附帶資料表結構與使用範例。
CREATE TABLE `cart` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`session` CHAR(32) NOT NULL COMMENT '購物車 Session ID',
`product` VARCHAR(50) NOT NULL COMMENT '商品 ID 或 SKU',
`quantity` INT(11) NOT NULL DEFAULT 1 COMMENT '數量',
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_session_product` (`session`, `product`),
KEY `idx_session` (`session`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `products` (
`id` VARCHAR(50) NOT NULL PRIMARY KEY,
`name` VARCHAR(100) NOT NULL,
`price` DECIMAL(10,2) NOT NULL,
`description` TEXT,
`image` VARCHAR(255)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
class Cart {
private $db;
private $table = 'cart';
private $sessionId;
public function __construct(PDO $db) {
$this->db = $db;
$this->initSession();
}
// 初始化 Session ID (Cookie 儲存,有效期 30 天)
private function initSession() {
if (isset($_COOKIE['cart_sid'])) {
$this->sessionId = $_COOKIE['cart_sid'];
} else {
$this->sessionId = md5(uniqid(rand(), true));
setcookie('cart_sid', $this->sessionId, time() + 2592000, '/'); // 30 天
}
}
// 取得 Session ID (供前端顯示/除錯)
public function getSessionId() {
return $this->sessionId;
}
// 內部:檢查商品是否已在購物車
private function checkItem($product) {
$stmt = $this->db->prepare("SELECT quantity FROM {$this->table} WHERE session = ? AND product = ?");
$stmt->execute([$this->sessionId, $product]);
return $stmt->fetchColumn(); // 返回數量或 false
}
// 加入商品 / 增加數量
public function addItem($product, $quantity = 1) {
$qty = $this->checkItem($product);
if ($qty === false) {
$stmt = $this->db->prepare("INSERT INTO {$this->table} (session, product, quantity) VALUES (?, ?, ?)");
$stmt->execute([$this->sessionId, $product, $quantity]);
} else {
$newQty = $qty + $quantity;
$stmt = $this->db->prepare("UPDATE {$this->table} SET quantity = ? WHERE session = ? AND product = ?");
$stmt->execute([$newQty, $this->sessionId, $product]);
}
return true;
}
// 移除商品
public function deleteItem($product) {
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE session = ? AND product = ?");
return $stmt->execute([$this->sessionId, $product]);
}
// 修改商品數量
public function modifyQuantity($product, $quantity) {
if ($quantity <= 0) return $this->deleteItem($product);
$stmt = $this->db->prepare("UPDATE {$this->table} SET quantity = ? WHERE session = ? AND product = ?");
return $stmt->execute([$quantity, $this->sessionId, $product]);
}
// 清空購物車
public function clearCart() {
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE session = ?");
return $stmt->execute([$this->sessionId]);
}
// 計算總金額
public function getTotal() {
$stmt = $this->db->prepare("
SELECT SUM(p.price * c.quantity) AS total
FROM {$this->table} c
JOIN products p ON c.product = p.id
WHERE c.session = ?
");
$stmt->execute([$this->sessionId]);
return (float)$stmt->fetchColumn();
}
// 取得購物車詳細內容 (含商品資訊)
public function getContents() {
$stmt = $this->db->prepare("
SELECT c.product, c.quantity, p.name, p.price, p.description, p.image,
(p.price * c.quantity) AS subtotal
FROM {$this->table} c
JOIN products p ON c.product = p.id
WHERE c.session = ?
ORDER BY c.id
");
$stmt->execute([$this->sessionId]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$total = 0;
foreach ($items as &$item) {
$item['price'] = (float)$item['price'];
$item['subtotal'] = (float)$item['subtotal'];
$total += $item['subtotal'];
}
return ['items' => $items, 'total' => $total, 'count' => count($items)];
}
// 取得商品種類數
public function getItemCount() {
$stmt = $this->db->prepare("SELECT COUNT(*) FROM {$this->table} WHERE session = ?");
$stmt->execute([$this->sessionId]);
return (int)$stmt->fetchColumn();
}
// 取得商品總數量
public function getTotalQuantity() {
$stmt = $this->db->prepare("SELECT SUM(quantity) FROM {$this->table} WHERE session = ?");
$stmt->execute([$this->sessionId]);
return (int)$stmt->fetchColumn();
}
}
?>
<?php
require_once 'db.php'; // PDO 連線
require_once 'Cart.php';
$cart = new Cart($pdo);
// 加入商品 (從 POST 取得)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['product_id'])) {
$productId = $_POST['product_id'];
$qty = max(1, (int)($_POST['quantity'] ?? 1));
try {
$cart->addItem($productId, $qty);
// AJAX 回應
echo json_encode(['success' => true, 'count' => $cart->getTotalQuantity()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
}
?>
<?php
$cartData = $cart->getContents();
?>
<table class="cart-table">
<thead>
<tr><th>商品</th><th>單價</th><th>數量</th><th>小計</th><th>操作</th></tr>
</thead>
<tbody>
<?php foreach ($cartData['items'] as $item): ?>
<tr data-product="<?php echo $item['product']; ?>">
<td><img src="<?php echo $item['image']; ?>" width="50"> <?php echo htmlspecialchars($item['name']); ?></td>
<td>¥<?php echo number_format($item['price'], 2); ?></td>
<td>
<input type="number" class="qty-input" value="<?php echo $item['quantity']; ?>" min="1" max="99">
</td>
<td>¥<?php echo number_format($item['subtotal'], 2); ?></td>
<td><button class="btn-remove">移除</button></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr><td colspan="3">總計</td><td>¥<?php echo number_format($cartData['total'], 2); ?></td><td></td></tr>
</tfoot>
</table>
| 特性 | 說明 |
|---|---|
| Session + DB 混合 | 未登入用戶靠 Cookie 識別,登入後可將 Session ID 關聯到用戶帳號,實現跨裝置同步。 |
| 唯一索引防重複 | uk_session_product 確保同一購物車不會有重複商品行。 |
| PDO 預處理 | 全程使用參數綁定,防止 SQL 注入。 |
| 擴充欄位 | 可在 cart 表加入 attrs (JSON) 儲存規格 (顏色/尺寸)、price_override 處理促銷價。 |
| 快取優化 | 高流量下可將購物車內容寫入 Redis (Key: cart:{sessionId}),降低 DB 壓力。 |