电商小程序购物车与库存扣减:SKU 模型、合并规则与防超卖实践

电商类小程序的骨架其实很清楚:商品 → 购物车 → 订单 → 支付 → 履约。真正让开发者头疼的不是流程,而是三个具体问题:同一个商品有多种规格时购物车怎么存 、库存什么时候扣 、并发下单怎么不超卖 。

这篇文章按这三个问题展开,给出可落地的数据模型与关键代码。示例基于 Node.js + MySQL 8,思路可以直接迁移到其他技术栈。

一、SKU 模型:先把商品拆对

很多项目在这里埋下第一颗雷:把商品当成一个整体存,规格用字符串拼接,比如 "大杯/加冰/少糖"。后果是价格算不准、库存扣不准、退款算不清。

正确做法是三层模型:

复制代码
-- 商品(SPU):只描述"是什么"
CREATE TABLE spu (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  title         VARCHAR(120) NOT NULL,
  category_id   BIGINT NOT NULL,
  status        TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架',
  created_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 规格项(如"杯型""温度")
CREATE TABLE spec (
  id        BIGINT PRIMARY KEY AUTO_INCREMENT,
  spu_id    BIGINT NOT NULL,
  name      VARCHAR(40) NOT NULL,
  KEY idx_spu (spu_id)
);

-- 规格值(大杯/中杯、加冰/去冰)
CREATE TABLE spec_value (
  id        BIGINT PRIMARY KEY AUTO_INCREMENT,
  spec_id   BIGINT NOT NULL,
  value     VARCHAR(40) NOT NULL,
  sort      INT NOT NULL DEFAULT 0,
  KEY idx_spec (spec_id)
);

-- SKU:可售卖的最小单位,价格和库存都挂在这一层
CREATE TABLE sku (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  spu_id        BIGINT NOT NULL,
  spec_value_ids VARCHAR(200) NOT NULL COMMENT '规格值id,逗号分隔,如 3,7',
  code          VARCHAR(60) NOT NULL COMMENT '商家编码',
  price_cents   INT NOT NULL COMMENT '售价,分',
  stock         INT NOT NULL DEFAULT 0,
  lock_stock    INT NOT NULL DEFAULT 0 COMMENT '已锁定未付款',
  UNIQUE KEY uk_code (code),
  KEY idx_spu (spu_id)
);

为什么价格挂在 SKU 上 :同一款奶茶,大杯比中杯贵 3 元,加料再加 2 元。价格是 SKU 的属性,不是 SPU 的。SPU 层只放"展示价"(一般取最低 SKU 价),真正的成交价必须按 SKU 算。

为什么库存也要分 stock 和 lock_stock :下单未付款的库存要锁住,否则两个人同时下单会把同一件货卖两次。可售库存 = stock - lock_stock。

二、购物车:存 SKU 还是存"组合"

购物车的表结构很简单,坑在唯一键。

复制代码
CREATE TABLE cart_item (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id     BIGINT NOT NULL,
  sku_id      BIGINT NOT NULL,
  quantity    INT NOT NULL DEFAULT 1,
  selected    TINYINT NOT NULL DEFAULT 1,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_user_sku (user_id, sku_id),
  KEY idx_user (user_id)
);

uk_user_sku 是关键:同一个用户加同一个 SKU 两次,应该合并成一条并把数量累加,而不是插两条。这条唯一索引直接在数据库层面保证了这一点,应用层用 INSERT ... ON DUPLICATE KEY UPDATE 就能实现幂等加法:

复制代码
async function addToCart(userId, skuId, quantity) {
  if (quantity <= 0) throw new Error('数量必须大于 0');
  await db.query(
    `INSERT INTO cart_item (user_id, sku_id, quantity)
     VALUES (?, ?, ?)
     ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)`,
    [userId, skuId, quantity]
  );
}

这里有一个容易被忽略的细节 :如果商品后来下架了、SKU 被删了,购物车里那条记录怎么处理?常见做法是保留记录但不允许勾选,结算时提示"该商品已下架",而不是直接删掉用户的数据。用户会记得自己加过什么,突然消失会让他怀疑系统。

加料怎么处理 :有一种做法是把"加料"也做成 SKU,通过组合生成新的 SKU(比如"大杯 + 珍珠"是一个 SKU)。SKU 数量会指数级增长,几十个组合很快变成几百个,维护成本极高。

更实用的做法是分成两层:主商品 SKU + 加料选项(独立的 sku_addon 表),购物车里存 sku_id 和一个加料 JSON 字段。结算时价格 = SKU 价 + 加料价之和。

复制代码
ALTER TABLE cart_item
  ADD COLUMN addons JSON NULL COMMENT '加料:[{"id":1,"qty":1,"price_cents":200}]';

代价是"同一 SKU + 不同加料"会变成两条购物车记录,唯一键要改成 (user_id, sku_id, addons_hash)。这个取舍要按业务定:加料组合少于 10 种,用 SKU 组合更简单;组合多,用 JSON 更灵活。

三、库存扣减:三个时机,各有利弊

这是电商系统里争论最多的问题。三种主流做法:

|----------------------------|-----------|-----------------|-------------|
| 时机 | 优点 | 缺点 | 适用 |
| 下单即扣(减 stock ) | 实现简单,不会超卖 | 恶意下单可占库存,需靠超时释放 | 库存紧张、单价低的商品 |
| 下单锁库存(加 lock_stock ),支付后转扣 | 兼顾防超卖和体验 | 逻辑稍复杂,要处理超时释放 | 大多数零售电商 |
| 支付成功才扣 | 不会占库存 | 高并发下必然超卖 | 几乎不推荐 |

推荐第二种。核心是两条 SQL:

复制代码
-- 下单:尝试锁定库存,条件里带上可售库存判断
UPDATE sku
SET lock_stock = lock_stock + ?
WHERE id = ?
  AND stock - lock_stock >= ?;
-- 影响行数为 0 表示库存不足,直接回滚整个下单事务

-- 支付回调成功:锁定转实扣
UPDATE sku
SET stock = stock - ?,
    lock_stock = lock_stock - ?
WHERE id = ?;

第一条 SQL 是防超卖的关键。不要在应用层先 SELECT 查库存再 UPDATE ,两步之间有时间窗,并发下必然出错。把判断条件写进 WHERE,让数据库的行锁来保证原子性,这是最省事也最可靠的做法。

四、订单超时关闭与库存释放

锁了库存不付款,库存就被占着。必须有超时释放机制。

推荐做法是订单表带过期时间,用定时任务扫描:

复制代码
-- 订单表关键字段
-- status: 1待支付 2已支付 3已取消 4已完成
-- expire_at: 下单后 15 分钟

-- 每 30 秒扫描一次
UPDATE orders
SET status = 3, cancel_reason = 'TIMEOUT'
WHERE status = 1
  AND expire_at < NOW()
LIMIT 200;

取消成功后,在同一事务里把 lock_stock 减回去。

这里有个经典错误 :先查出一批过期订单,再逐条取消,中间如果有用户刚好付款成功,就会把已付款的订单改成已取消。正确做法是取消时加状态条件:

复制代码
UPDATE orders SET status = 3
WHERE id = ? AND status = 1;  -- 只有仍待支付才取消

影响行数为 0,说明状态已经变了,跳过这条,不要释放库存。

五、并发下还要注意的三件事

5.1 购物车结算时的价格漂移

用户加购时价格是 39,结算时商家改成了 45。要以结算时刻的价格为准,并且在提交订单页明确提示"价格已变动"。不要用加购时的价格成交,也不要不提示就直接涨价------前者亏钱,后者投诉。

5.2 重复提交订单

用户手抖点两次"提交订单",会生成两个订单、锁两次库存。解决方式是前端按钮置灰 + 后端幂等键:

复制代码
CREATE TABLE order_idempotent (
  idem_key  VARCHAR(64) PRIMARY KEY,
  order_no  VARCHAR(32) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

提交时先插入幂等键(主键冲突则返回已有订单号),再创建订单。幂等键可以用 userId + cartHash + 时间窗口 生成。

5.3 库存为负的排查

如果监控发现某个 SKU 的 stock 出现负数,说明有绕过 WHERE 条件扣减的路径。常见的漏点有两个:后台手工改库存的接口没做校验、退款回补库存时没做上限判断。这两处都要加上"不得小于 0"的约束,数据库层面也可以用 UNSIGNED 兜底。

六、库存与订单的对账口径

系统跑起来之后,运营迟早会问一个问题:这个月卖了多少件、还剩多少件,为什么对不上?

对不上通常有三个来源,理清楚就能自己排查:

一是锁定量没释放。 用户下单锁了库存、订单超时取消了,但释放逻辑失败重启了,lock_stock 就一直挂着。表现是"明明有货却显示售罄"。排查方式是查 lock_stock > 0 且对应订单已取消或已完成的 SKU。

二是手工改库存。 后台调整库存的接口如果直接写 stock,不写流水,事后就查不出是谁改的、为什么改。规范做法是后台改库存也必须写一条流水,注明操作人和原因。

三是退款回补。 退货入库和退款是两件事,退款成功不代表货已经回到仓库。库存应该在"确认收货入库"时才回补,而不是退款时立刻回补,否则会出现"账上有货、仓里没有"。

这三条对应三个校验任务:每日扫描异常锁定、每月核对库存流水与余额差、每周核对退货入库单与退款单。三张表跑一遍,差异基本都能定位。

七、小结

电商小程序的购物车和库存,难点不在功能多少,而在几个关键决策是否一次做对:

  • SKU 模型把价格和库存挂到最小可售单位,SPU 只做展示;- 购物车唯一键决定合并行为,加料用 JSON 还是 SKU 组合要按组合数量取舍;- 库存用"下单锁、支付扣",防超卖靠 WHERE 条件里的原子判断,不靠应用层查询;- 超时关闭必须带状态条件更新,避免与支付回调打架;- 重复提交用幂等键,价格漂移要提示。

青海青帝信息科技有限公司在本地电商类项目中,把这五条列为代码评审的必查项------它们出问题的概率不高,但一旦出问题,往往是在大促当天。

相关推荐
AI源码1 小时前
家乡介绍小程序
小程序·notepad++
2501_915921436 小时前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview
FX1317887KP_9 小时前
KET/PET/FCE备考攻略:口语训练的三大误区与科学训练路径
小程序·ket口语·pet口语·fce备考·剑桥英语
海上彼尚10 小时前
【微信小程序】xxx该网站的安全证书存在问题,可选择 “继续”在浏览器中访问。
安全·微信小程序·小程序
FX1317887KP_11 小时前
KPF口语考试题目类型全梳理:KET、PET、FCE各级别考什么、怎么练
小程序·ket口语·pet口语·fce备考·剑桥英语
Ai-_Man12 小时前
ChatGPT还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·javascript·人工智能·chatgpt·小程序·ecmascript
andrsted14 小时前
练题簿在线免费刷题:个人备考,从练新题到模拟自测怎么用?
学习·微信小程序·小程序·学习方法·刷题
李纲明15 小时前
WordPress 首页正常、内页全 404:固定链接与伪静态规则排查步骤(Apache / Nginx)
nginx·apache·wordpress
一木 之林1 天前
微信小程序开发全攻略·文章版|05 路由·交互·网络与音乐播放器实战
网络·微信小程序·小程序·交互