商城系统开发(二):商品服务与SKU管理

商城系统搭完基础框架后,第一个核心模块就是商品服务。商品看着简单,但 SPU/SKU 模型、属性管理、库存管理这些东西做起来水还是挺深的。这篇记录下商品服务的设计过程。

SPU 与 SKU

先把两个概念理清楚:

  • SPU(Standard Product Unit):标准产品单元,是一个抽象概念。比如"iPhone 15 Pro"是一个 SPU。
  • SKU(Stock Keeping Unit):库存管理单元,是 SPU 的具体变体。比如"iPhone 15 Pro 黑色 256GB"是一个 SKU。

一个 SPU 对应多个 SKU。用户在商品详情页看到的是 SPU 信息,选择颜色和规格后确定了具体的 SKU,加购和下单都是基于 SKU。

数据库表设计

SPU 表

CREATE TABLE spu (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL COMMENT '商品名称',
    subtitle VARCHAR(500) COMMENT '副标题',
    category_id BIGINT NOT NULL COMMENT '分类ID',
    brand_id BIGINT COMMENT '品牌ID',
    main_image VARCHAR(500) COMMENT '主图URL',
    images TEXT COMMENT '轮播图URLs(JSON数组)',
    detail_html TEXT COMMENT '详情富文本',
    status TINYINT DEFAULT 0 COMMENT '0下架 1上架 2删除',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_category (category_id),
    INDEX idx_status (status)
);

SKU 表

CREATE TABLE sku (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    spu_id BIGINT NOT NULL,
    name VARCHAR(200) NOT NULL COMMENT 'SKU名称(自动生成)',
    price BIGINT NOT NULL COMMENT '价格(分)',
    stock INT NOT NULL DEFAULT 0 COMMENT '库存',
    specs JSON COMMENT '规格属性 {"颜色":"黑色","存储":"256GB"}',
    image VARCHAR(500) COMMENT 'SKU图片',
    status TINYINT DEFAULT 1 COMMENT '0禁用 1启用',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_spu (spu_id)
);

属性表

CREATE TABLE product_attribute (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    category_id BIGINT NOT NULL COMMENT '所属分类',
    name VARCHAR(50) NOT NULL COMMENT '属性名(如:颜色/尺码)',
    type TINYINT NOT NULL COMMENT '1规格属性 2销售属性',
    values_list VARCHAR(1000) COMMENT '可选值列表(JSON)',
    sort_order INT DEFAULT 0
);

商品属性设计

属性分两种:

规格属性(Sale Attribute):决定 SKU 的属性,比如颜色、尺码、存储容量。用户在详情页选择的就是规格属性,不同的规格组合对应不同的 SKU,价格和库存可能不同。

基本属性(Base Attribute):描述商品特征但不影响 SKU 的属性,比如品牌、产地、材质。所有 SKU 共享同样的基本属性。

我在 product_attribute 表里用 type 字段区分。规格属性关联到 SKU,基本属性关联到 SPU。

SKU 组合生成

创建商品时,运营人员选择规格属性的值,系统自动生成 SKU 组合。比如:

  • 颜色:黑色、白色
  • 存储:128GB、256GB

生成的 SKU 组合:

SKU 颜色 存储 价格 库存
1 黑色 128GB 待填 待填
2 黑色 256GB 待填 待填
3 白色 128GB 待填 待填
4 白色 256GB 待填 待填

组合生成算法就是一个笛卡尔积。我在后端用递归实现:

输入: [["黑色","白色"], ["128GB","256GB"]]
输出: [["黑色","128GB"], ["黑色","256GB"], ["白色","128GB"], ["白色","256GB"]]

生成后前端展示为表格,运营人员填写每个 SKU 的价格和库存。如果某个组合不需要(比如白色 128GB 不卖了),可以单独禁用。

价格和库存管理

价格

价格存在 SKU 上,单位是"分"(int64),避免浮点数精度问题。前端展示时再转成"元"。

后期如果要做促销价、会员价,加一张价格表关联到 SKU:

CREATE TABLE sku_price (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    sku_id BIGINT NOT NULL,
    price_type TINYINT COMMENT '1原价 2促销价 3会员价',
    price BIGINT NOT NULL,
    start_time DATETIME,
    end_time DATETIME
);

当前阶段先只做原价,促销后面再加。

库存

库存也在 SKU 上。库存操作(扣减、回退)是高频且需要保证原子性的操作,这块用了 Redis + Lua 脚本来做,后面订单模块再详细讲。

数据库里的 stock 字段作为最终一致的持久化存储,Redis 里的库存用于实时扣减。定期同步。

商品状态管理

SPU 有三个状态:

  • 下架(0):新创建的商品默认是下架状态,不在前台展示
  • 上架(1):审核通过后上架,用户可以看到和购买
  • 删除(2):逻辑删除,不物理删除数据

状态转换规则:

下架 ←→ 上架
下架  → 删除
上架  → 下架 → 删除

不能从上架状态直接删除,必须先下架。这是为了防止误操作——上架中的商品可能有人正在下单。

SKU 也有自己的启用/禁用状态。一个 SPU 上架时,至少要有一个启用的 SKU,否则上架失败。

商品详情接口设计

商品详情页需要聚合多种数据:

{
    "spu": {
        "id": 1001,
        "name": "iPhone 15 Pro",
        "subtitle": "强大的 A17 Pro 芯片",
        "main_image": "...",
        "images": ["...", "..."],
        "detail_html": "...",
        "base_attrs": [
            {"name": "品牌", "value": "Apple"},
            {"name": "产地", "value": "中国"}
        ]
    },
    "skus": [
        {
            "id": 2001,
            "specs": {"颜色": "黑色", "存储": "256GB"},
            "price": 899900,
            "stock": 150,
            "image": "..."
        },
        ...
    ],
    "spec_options": {
        "颜色": ["黑色", "白色", "蓝色"],
        "存储": ["128GB", "256GB", "512GB"]
    }
}

spec_options 用于前端渲染规格选择器。用户选择具体规格后,前端根据选择的值匹配到对应的 SKU,展示该 SKU 的价格和库存。

几个细节

  1. SKU 名称自动生成:SKU 名称 = SPU 名称 + 规格属性值,如"iPhone 15 Pro 黑色 256GB"。运营不需要手动填。

  2. 商品搜索:当前用 MySQL FULLTEXT 做简单搜索,后面数据量大了切 Elasticsearch。商品表里 name 和 subtitle 字段加了全文索引。

  3. 图片管理:每个 SPU 有一组轮播图,每个 SKU 也可以有自己的图片(用于展示不同颜色的实物图)。图片上传到 OSS,数据库只存 URL。

  4. 分类管理:商品分类用的是三级分类(大类→中类→小类),用 parent_id 实现树形结构。前端用级联选择器展示。

商品服务是商城的基础,后面的购物车、订单、搜索都依赖它。设计时尽量把模型做规范,后面扩展起来会轻松很多。