您现在的位置: 365建站网 > 365文章 > PHP 购物车怎么实现?Session 与数据库方案、价格计算与防超卖

PHP 购物车怎么实现?Session 与数据库方案、价格计算与防超卖

文章来源:365jz.com 点击数:13 更新时间:2026-04-08 21:42

购物车是电商系统里最“麻雀虽小五脏俱全”的模块:涉及会话状态、价格计算、库存校验、并发下单。本文用原生 PHP + MySQL 走一遍完整实现,重点讲清哪些数据放 Session、哪些必须放数据库,以及金额计算为什么不能用 float。

PHP 购物车 session 与数据库实现

一、三种存储方案的取舍

  • Session 存:未登录游客用。实现最简单,缺点是换设备/清 Cookie 就丢、无法跨端同步、并发下 Session 文件会互相阻塞。
  • Cookie 存:把商品 id 与数量写进 Cookie。省服务器资源,但客户端可篡改(必须服务端重算价格),且有 4KB 大小与 URL 编码限制。
  • 数据库存(购物车表):登录后必须用它。以 user_id 为主键的一部分,天然支持多端同步与“加购后三天再回来还在”。

成熟做法是混合方案:游客阶段用 Session,登录后一次性把 Session 购物车合并进数据库(merge 后清空 Session)。

二、表结构

CREATE TABLE cart (
  id        BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id   BIGINT UNSIGNED NOT NULL,
  sku_id    BIGINT UNSIGNED NOT NULL,
  qty       INT UNSIGNED    NOT NULL DEFAULT 1,
  added_at  INT UNSIGNED    NOT NULL,
  UNIQUE KEY uk_user_sku (user_id, sku_id),
  KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE sku (
  id        BIGINT UNSIGNED PRIMARY KEY,
  goods_id  BIGINT UNSIGNED NOT NULL,
  price     INT UNSIGNED    NOT NULL,      -- 单位:分
  stock     INT UNSIGNED    NOT NULL,
  status    TINYINT         NOT NULL DEFAULT 1
) ENGINE=InnoDB;

两个设计要点:价格字段用 INT 存“分”,绝不用 FLOAT/DOUBLE;购物车只存 sku_id + qty,不冗余存价格,展示时实时关联取价(否则改价后金额与商品详情页不一致,客诉就是这么来的)。

三、加购与合并

$stmt = $pdo->prepare("INSERT INTO cart (user_id, sku_id, qty, added_at)
                       VALUES (?,?,?,?)
                       ON DUPLICATE KEY UPDATE qty = qty + VALUES(qty)");
$stmt->execute([$uid, $skuId, $qty, time()]);

ON DUPLICATE KEY UPDATE 靠 uk_user_sku 唯一索引实现“存在则累加”,比先 SELECT 再判断少一次往返,也不会有并发覆盖问题。

游客登录后合并:

$pdo->beginTransaction();
try {
  foreach ($_SESSION['cart'] as $skuId => $qty) {
      $pdo->prepare("INSERT INTO cart(user_id,sku_id,qty,added_at) VALUES(?,?,?,?)
                     ON DUPLICATE KEY UPDATE qty=GREATEST(qty, VALUES(qty))")
          ->execute([$uid, $skuId, $qty, time()]);
  }
  unset($_SESSION['cart']);
  $pdo->commit();
} catch (Throwable $e) { $pdo->rollBack(); throw $e; }

合并策略要提前定:取较大值(GREATEST)还是相加,两者都会让用户困惑,一般用“取较大值”更稳。

四、金额计算:只用整数

PHP 里 0.1 + 0.7 === 0.7999999999999999,浮点误差在金额场景不可接受。规范做法:

  • 数据库、Session、接口传输都用分为单位的整数;
  • 展示层再 sprintf('%.2f', $cents / 100);
  • 涉及百分比折扣时先乘后除,避免中间小数:$pay = intdiv($price * $qty * $discount, 100);
  • 需要高精度则用 bcmath 扩展(bcadd/bcmul,指定 scale=2),composer require brick/money 是更工程化的选择。

另外一条铁律:总价必须由服务端计算。前端提交的只有 sku_id + qty,价格、优惠券、运费全部服务端重算。

五、库存校验与下单

购物车页面只做“提示”,真正扣减放在下单事务里:

$pdo->beginTransaction();
$rows = $pdo->prepare("UPDATE sku SET stock = stock - ? WHERE id = ? AND stock >= ? AND status = 1");
foreach ($items as $it) {
    $rows->execute([$it['qty'], $it['sku_id'], $it['qty']]);
    if ($rows->rowCount() === 0) { $pdo->rollBack(); throw new RuntimeException("SKU {$it['sku_id']} 库存不足"); }
}
// 写 order / order_item ...
$pdo->commit();

逐条判断 rowCount() 是防超卖的核心。若购物车含多个 SKU,任一失败要整体回滚,绝不允许“部分扣减”。

六、体验与安全细节

  • 失效商品处理:查询时 LEFT JOIN sku 并过滤 status=1,把下架/无货的单独分组显示为“失效商品”,不要静默消失。
  • 数量上限:单 SKU 限购(如 999 或活动限购 2 件)必须在服务端校验,前端输入框限制只是体验。
  • CSRF:加购、改数量、删除都是写操作,POST + 表单 token 校验。GET 方式加购会被爬虫预取,凭空生成购物车数据。
  • Session 并发阻塞:PHP 默认 session 文件锁会让同一用户的并发请求串行。列表页里有 AJAX 时,用完及时 session_write_close()。
  • 价格快照:订单表要存成交时的单价,购物车则实时取价,两者不能混用同一份数据。

七、常见问题 FAQ

问:Session 购物车在集群下怎么办?
答:Session 共享用 Redis(session.save_handler=redis)或粘性会话;更好的方案是尽早引导登录、把购物车落到数据库。

问:怎么实现“加入购物车后 30 分钟提醒”?
答:added_at 字段 + 定时任务扫描,或延迟队列(Redis zset 按时间戳排序,每分钟取到期项)。不要依赖客户端轮询。

问:多规格商品如何防重?
答:以 sku_id(规格维度)而非 goods_id 作为唯一键,这正是上面表结构设计的原因。

一句话收尾:购物车的技术难点不在“存”,而在“算价的权威性与并发下的库存一致性”。价格整数化、库存原子扣减、写操作 POST+CSRF,这三条守住就不会出大事故。

生产内容,同时打通搜索引擎(SEO)与AI搜索引擎(GEO)流量入口的软件! 点击进入365建站器 »