在电商后台管理中,SPU和SKU是两个最基础却极其重要的概念。它们是商品体系的核心骨架,直接影响着库存管理、价格策略、搜索优化和用户体验。本文将深入解析这两个概念的区别、联系以及在实际业务中的应用,帮助你彻底理解电商后台的商品管理逻辑。

一、核心概念解析:SPU与SKU的定义

1. SPU(Standard Product Unit)- 标准产品单元

SPU是商品的”类别”或”系列”概念,代表一组具有相同属性和功能的产品集合。它描述的是一个产品的”身份”,而不是具体的某一个商品实例。

SPU的特征:

  • 标准化:SPU是标准化的产品概念,不包含颜色、尺寸等具体属性
  • 共享性:同一个SPU下的所有SKU共享大部分属性(如品牌、型号、核心功能)
  • 管理维度:SPU主要用于商品分类、品牌管理和销售数据分析

实际例子:

  • 苹果iPhone 15 Pro是一个SPU,它代表了2023年苹果发布的Pro系列手机这一类产品
  • 小米Redmi Note 13也是一个SPU,代表小米的中端手机系列
  • 优衣库的HEATTECH保暖内衣也是一个SPU,代表一个特定系列的产品

在电商后台,SPU通常包含以下信息:

  • 产品名称(如”iPhone 15 Pro”)
  • 品牌(Apple)
  • 核心卖点(A17 Pro芯片、钛金属边框)
  • 基本参数(屏幕尺寸6.1英寸、后置三摄)
  • 产品描述和营销文案
  • 主图和详情页模板

2. SKU(Stock Keeping Unit)- 库存单位

SKU是实际存储和销售的最小单位,是SPU的具体实例化。每个SKU对应一个具体的商品规格,是库存管理、订单处理和价格计算的最小单元。

SKU的特征:

  • 具体化:SKU必须包含所有影响库存和销售的属性组合
  • 唯一性:每个SKU都有唯一的编码,用于系统识别
  • 实操性:SKU直接对应仓库中的实际商品和订单中的具体商品

继续上面的例子:

  • iPhone 15 Pro(SPU)下的SKU包括:
    • iPhone 15 Pro 256GB 原色钛金属
    • iPhone 15 Pro 512GB 蓝色钛金属
    • iPhone 15 Pro 1TB 白色钛金属
  • 小米Redmi Note 13(SPU)下的SKU包括:
    • Redmi Note 13 8GB+128GB 子夜黑
    • Redmi Note 13 12GB+256GB 星沙白
  • 优衣库HEATTECH保暖内衣(SPU)下的SKU包括:
    • HEATTECH圆领长袖T恤 L 黑色
    • HEATTECH圆领长袖T恤 M 白色

每个SKU都有独立的:

  • SKU编码(如:APL-IP15P-256G-PT)
  • 库存数量
  • 销售价格
  • 规格属性(颜色、尺寸、容量等)
  • 条形码/二维码

二、SPU与SKU的核心区别对比

为了更清晰地理解两者的区别,我们从多个维度进行详细对比:

对比维度 SPU(标准产品单元) SKU(库存单位)
定义 一组相似产品的集合,代表产品类别 具体的商品规格,库存管理的最小单位
颗粒度 粗粒度,一个SPU包含多个SKU 细粒度,直接对应具体商品
属性 包含通用属性(品牌、型号、核心功能) 包含所有属性(颜色、尺寸、容量、版本等)
价格 通常展示参考价或价格区间 有明确的销售价格
库存 不直接管理库存,库存是SKU级别的 直接管理库存,是库存的最小单位
编码 可选,用于产品分类 必须,用于系统识别和仓库管理
应用场景 商品展示、搜索、品牌管理、数据分析 订单处理、库存管理、物流配送、财务核算
数量关系 一个SPU可以对应多个SKU(1:N) 一个SKU只能属于一个SPU(N:1)

详细区别举例说明

场景:某电商平台销售运动鞋

  • SPU:耐克Air Jordan 1 Mid运动鞋

    • 包含信息:品牌Nike、系列Air Jordan、款式Mid、材质皮革、适用场景休闲运动
    • 不包含:具体颜色、尺码
  • SKU

    • AJ1-Mid-42-黑白红(42码,黑白红配色)
    • AJ1-Mid-43-黑白红(43码,黑白红配色)
    • AJ1-Mid-42-芝加哥(42码,芝加哥配色)
    • AJ1-Mid-43-芝加哥(43码,芝加哥配色)

关键区别点:

  1. 库存管理:仓库里实际存放的是SKU级别的商品,每个SKU有独立的库存位置
  2. 价格策略:不同颜色可能价格相同,但不同尺码可能价格不同(如大码加价)
  3. 销售分析:分析”黑白红”配色的销量时,需要聚合该颜色下所有尺码的SKU数据
  4. 用户搜索:用户搜索”Air Jordan 1 Mid”时,系统展示SPU;用户筛选”42码”时,系统过滤SKU

三、SPU与SKU在电商后台的实际应用

1. 商品发布流程中的应用

标准流程:

1. 创建SPU(定义产品基本信息)
   ↓
2. 添加SKU(定义具体规格)
   ↓
3. 设置SKU属性(颜色、尺寸等)
   ↓
4. 为每个SKU设置价格和库存
   ↓
5. 关联SKU与SPU的图片和描述

实际案例:发布一款连衣裙

步骤1:创建SPU

{
  "spu_code": "DR2024SS001",
  "name": "法式碎花连衣裙",
  "brand": "LilyRose",
  "category": "女装/连衣裙",
  "material": "雪纺",
  "season": "春夏",
  "style": "法式复古",
  "main_image": "dr2024ss001_main.jpg",
  "description": "采用优质雪纺面料,法式复古设计,适合日常和约会穿着..."
}

步骤2:创建SKU

// SKU1
{
  "sku_code": "DR2024SS001-S-M",
  "spu_code": "DR2024SS001",
  "attributes": {
    "color": "碎花蓝",
    "size": "S"
  },
  "price": 299.00,
  "stock": 50,
  "images": ["dr2024ss001_blue_s_1.jpg", "dr2024ss001_blue_s_2.jpg"]
}

// SKU2
{
  "sku_code": "DR2024SS001-M-M",
  "spu_code": "DR2024SS001",
  "attributes": {
    "color": "碎花蓝",
    "size": "M"
  },
  "price": 299.00,
  "stock": 80,
  "images": ["dr2024ss001_blue_m_1.jpg", "dr2024ss001_blue_m_2.jpg"]
}

// SKU3
{
  "sku_code": "DR2024SS001-S-L",
  "spu_code": "DR2024SS001",
  "attributes": {
    "color": "碎花白",
    "size": "S"
  },
  "price": 299.00,
  "stock": 30,
  "images": ["dr2024ss001_white_s_1.jpg", "dr2024ss001_white_s_2.jpg"]
}

2. 库存管理中的应用

SPU级别库存(虚拟):

  • 显示总库存:所有SKU库存之和 = 50+80+30 = 160件
  • 用于前端展示”库存充足”提示

SKU级别库存(实际):

  • 精确管理每个规格的库存
  • 支持库存预警(如SKU库存低于20件时提醒补货)
  • 支持多仓库管理(SKU在不同仓库的库存分布)

库存操作示例:

-- 查询SPU总库存
SELECT SUM(stock) FROM sku WHERE spu_code = 'DR2024SS001';

-- 查询特定SKU库存
SELECT stock FROM sku WHERE sku_code = 'DR2024SS001-S-M';

-- 扣减库存(订单创建时)
UPDATE sku SET stock = stock - 1 WHERE sku_code = 'DR2024SS001-S-M' AND stock >= 1;

3. 价格管理中的应用

SPU价格策略:

  • 参考价:299元(展示在商品列表页)
  • 价格区间:299-399元(当不同SKU价格不同时)
  • 促销活动:SPU级别的满减、优惠券

SKU价格策略:

  • 基础价格:每个SKU独立定价
  • 动态调价:根据库存、成本、竞品调整单个SKU价格
  • 促销价格:特定SKU的限时折扣

实际案例:

SPU:iPhone 15 Pro
- 参考价:7999元起

SKU价格:
- 128GB 黑色:7999元
- 256GB 黑色:8999元
- 512GB 黑色:10999元
- 1TB 黑色:12999元
- 128GB 白色:7999元
- 256GB 白色:8999元

4. 前端展示中的应用

商品列表页:

  • 展示SPU信息(主图、名称、参考价)
  • 用户点击进入详情页

商品详情页:

  • 展示SPU的通用描述和规格
  • 通过SKU选择器让用户选择具体规格
  • 根据用户选择实时更新价格、库存和图片

SKU选择器实现(前端代码示例):

// SPU数据
const spu = {
  code: 'DR2024SS001',
  name: '法式碎花连衣裙',
  basePrice: 299,
  colors: ['碎花蓝', '碎花白'],
  sizes: ['S', 'M', 'L']
};

// SKU数据
const skus = [
  { code: 'DR2024SS001-S-M', color: '碎花蓝', size: 'S', price: 299, stock: 50 },
  { code: 'DR2024SS001-M-M', color: '碎花蓝', size: 'M', price: 299, stock: 80 },
  { code: 'DR2024SS001-L-M', color: '碎花蓝', size: 'L', price: 329, stock: 40 },
  { code: 'DR2024SS001-S-L', color: '碎花白', size: 'S', price: 299, stock: 30 },
  { code: 'DR2024SS001-M-L', color: '碎花白', size: 'M', price: 299, stock: 60 }
];

// SKU选择器逻辑
class SKUSelector {
  constructor(spu, skus) {
    this.spu = spu;
    this.skus = skus;
    this.selectedColor = null;
    this.selectedSize = null;
  }

  // 获取可选的颜色列表
  getAvailableColors() {
    return this.spu.colors.map(color => {
      const hasStock = this.skus.some(sku => 
        sku.color === color && sku.stock > 0
      );
      return { color, available: hasStock };
    });
  }

  // 获取指定颜色下的可选尺码
  getAvailableSizes(color) {
    return this.spu.sizes.map(size => {
      const sku = this.skus.find(s => s.color === color && s.size === size);
      return { 
        size, 
        available: sku ? sku.stock > 0 : false,
        price: sku ? sku.price : null
      };
    });
  }

  // 选择规格
  select(color, size) {
    const sku = this.skus.find(s => 
      s.color === color && s.size === size
    );
    
    if (!sku) {
      return { success: false, message: '该规格不存在' };
    }
    
    if (sku.stock <= 0) {
      return { success: false, message: '该规格已售罄' };
    }

    this.selectedColor = color;
    this.selectedSize = size;
    
    return {
      success: true,
      sku: sku.code,
      price: sku.price,
      stock: sku.stock
    };
  }

  // 获取当前选中状态
  getCurrentSelection() {
    if (!this.selectedColor || !this.selectedSize) {
      return null;
    }
    
    const sku = this.skus.find(s => 
      s.color === this.selectedColor && s.size === this.selectedSize
    );
    
    return sku ? {
      color: this.selectedColor,
      size: this.selectedSize,
      price: sku.price,
      stock: sku.stock,
      skuCode: sku.code
    } : null;
  }
}

// 使用示例
const selector = new SKUSelector(spu, skus);

// 用户点击颜色"碎花蓝"
console.log('可选尺码:', selector.getAvailableSizes('碎花蓝'));
// 输出: [{size: 'S', available: true, price: 299}, {size: 'M', available: true, price: 299}, {size: 'L', available: true, price: 329}]

// 用户选择"碎花蓝" + "M"
const result = selector.select('碎花蓝', 'M');
console.log(result);
// 输出: {success: true, sku: 'DR2024SS001-M-M', price: 299, stock: 80}

5. 搜索与筛选中的应用

SPU在搜索中的作用:

  • 用户搜索”连衣裙”时,系统返回SPU列表
  • 搜索结果去重(同一个SPU只展示一次)
  • 模糊匹配SPU名称、品牌、关键词

SKU在筛选中的作用:

  • 用户筛选”碎花蓝”时,系统过滤SKU属性
  • 筛选”价格200-300元”时,按SKU价格过滤
  • 筛选”有货”时,按SKU库存过滤

数据库查询示例:

-- 搜索SPU
SELECT * FROM spu 
WHERE name LIKE '%连衣裙%' OR brand LIKE '%连衣裙%';

-- 筛选SKU(带SPU信息)
SELECT DISTINCT spu.*, sku.price, sku.stock 
FROM spu 
JOIN sku ON spu.code = sku.spu_code
WHERE sku.color = '碎花蓝' 
  AND sku.price BETWEEN 200 AND 300
  AND sku.stock > 0;

四、实际应用中的常见问题与解决方案

问题1:如何处理SKU爆炸问题?

问题描述:当产品属性组合过多时,SKU数量会呈指数级增长。例如:

  • 10种颜色 × 10种尺码 × 5种材质 = 500个SKU
  • 实际可能只有部分组合有库存或销售

解决方案:

方案A:动态SKU生成

# 只为有库存的组合生成SKU
def generate_skus(base_product, combinations):
    skus = []
    for combo in combinations:
        # 检查该组合是否有实际库存或销售记录
        if combo['stock'] > 0 or combo['sales'] > 0:
            sku = {
                'code': f"{base_product['code']}-{combo['color']}-{combo['size']}",
                'attributes': combo,
                'price': base_product['base_price'] + combo.get('price_adjust', 0),
                'stock': combo['stock']
            }
            skus.append(sku)
    return skus

# 实际组合(只有部分有库存)
combinations = [
    {'color': '红色', 'size': 'M', 'stock': 50},
    {'color': '红色', 'size': 'L', 'stock': 0},  # 无库存,不生成SKU
    {'color': '蓝色', 'size': 'M', 'stock': 30}
]

方案B:SKU模板化

{
  "spu_code": "DR2024SS001",
  "name": "法式碎花连衣裙",
  "sku_template": {
    "colors": ["碎花蓝", "碎花白"],
    "sizes": ["S", "M", "L"],
    "price_by_size": {
      "S": 299,
      "M": 299,
      "L": 329
    },
    "stock_by_color_size": {
      "碎花蓝": {"S": 50, "M": 80, "L": 40},
      "碎花白": {"S": 30, "M": 60, "L": 0}
    }
  }
}

问题2:SKU属性如何标准化?

问题描述:不同运营人员可能用不同方式描述同一属性,导致数据混乱。例如颜色:”红色” vs “红” vs “红色系”。

解决方案:

建立属性值字典

-- 属性值字典表
CREATE TABLE attribute_dict (
  id INT PRIMARY KEY,
  attribute_name VARCHAR(50), -- 'color', 'size'
  standard_value VARCHAR(50), -- 标准值
  alias VARCHAR(200), -- 别名,JSON格式
  is_active BOOLEAN DEFAULT TRUE
);

-- 插入标准值
INSERT INTO attribute_dict VALUES
(1, 'color', '红色', '["红", "红色系", "red", "RED"]', TRUE),
(2, 'color', '蓝色', '["蓝", "蓝色系", "blue", "BLUE"]', TRUE),
(3, 'size', 'M', '["中", "中码", "Medium"]', TRUE);

-- 创建SKU时标准化
CREATE FUNCTION normalize_attribute(attr_name VARCHAR(50), attr_value VARCHAR(50))
RETURNS VARCHAR(50)
BEGIN
  DECLARE standard_val VARCHAR(50);
  
  SELECT standard_value INTO standard_val
  FROM attribute_dict
  WHERE attribute_name = attr_name 
    AND (attr_value = standard_value OR JSON_CONTAINS(alias, JSON_QUOTE(attr_value)))
  LIMIT 1;
  
  RETURN IFNULL(standard_val, attr_value);
END;

问题3:如何处理SPU与SKU的价格同步?

问题描述:当SPU级别的促销活动(如品牌日全场9折)时,如何快速更新所有SKU价格?

解决方案:

方案A:价格分离策略

-- SKU表只存储基础价格
CREATE TABLE sku (
  sku_code VARCHAR(50) PRIMARY KEY,
  spu_code VARCHAR(50),
  base_price DECIMAL(10,2),
  -- 其他字段
  INDEX idx_spu (spu_code)
);

-- 促销活动表
CREATE TABLE promotion (
  id INT PRIMARY KEY,
  target_type ENUM('SPU', 'SKU'),
  target_code VARCHAR(50),
  discount_type ENUM('PERCENT', 'FIXED'),
  discount_value DECIMAL(10,2),
  start_time DATETIME,
  end_time DATETIME
);

-- 查询最终价格(实时计算)
SELECT 
  sku.sku_code,
  sku.base_price,
  COALESCE(
    CASE 
      WHEN p.discount_type = 'PERCENT' THEN sku.base_price * (1 - p.discount_value/100)
      WHEN p.discount_type = 'FIXED' THEN sku.base_price - p.discount_value
      ELSE sku.base_price
    END,
    sku.base_price
  ) as final_price
FROM sku
LEFT JOIN promotion p ON (p.target_type = 'SPU' AND p.target_code = sku.spu_code)
  OR (p.target_type = 'SKU' AND p.target_code = sku.sku_code)
WHERE p.start_time <= NOW() AND p.end_time >= NOW();

方案B:价格缓存策略

class PriceManager:
    def __init__(self):
        self.price_cache = {}
        self.promotion_cache = {}
    
    def get_sku_price(self, sku_code, spu_code):
        # 检查缓存
        cache_key = f"{sku_code}:{spu_code}"
        if cache_key in self.price_cache:
            return self.price_cache[cache_key]
        
        # 获取基础价格
        base_price = self.get_base_price(sku_code, spu_code)
        
        # 获取促销信息
        promotions = self.get_active_promotions(spu_code, sku_code)
        
        # 计算最终价格
        final_price = base_price
        for promo in promotions:
            if promo['type'] == 'percent':
                final_price *= (1 - promo['value']/100)
            elif promo['type'] == 'fixed':
                final_price -= promo['value']
        
        # 缓存结果(5分钟)
        self.price_cache[cache_key] = final_price
        return final_price
    
    def invalidate_cache(self, spu_code):
        # 清除相关缓存
        keys_to_delete = [k for k in self.price_cache.keys() if spu_code in k]
        for key in keys_to_delete:
            del self.price_cache[key]

问题4:如何处理SPU与SKU的图片管理?

问题描述:SPU有主图和详情图,SKU有特定规格的图片(如不同颜色的实物图),如何组织这些图片关系?

解决方案:

图片分层管理

{
  "spu_code": "DR2024SS001",
  "images": {
    "common": {
      "main": "dr2024ss001_main.jpg",
      "detail": [
        "dr2024ss001_detail_1.jpg",
        "dr2024ss001_detail_2.jpg"
      ],
      "video": "dr2024ss001_video.mp4"
    },
    "by_color": {
      "碎花蓝": {
        "color_main": "dr2024ss001_blue_main.jpg",
        "color_detail": [
          "dr2024ss001_blue_detail_1.jpg",
          "dr2024ss001_blue_detail_2.jpg"
        ]
      },
      "碎花白": {
        "color_main": "dr2024ss001_white_main.jpg",
        "color_detail": [
          "dr2024ss001_white_detail_1.jpg"
        ]
      }
    }
  }
}

图片获取逻辑:

function getDisplayImages(spu, sku) {
  const images = [];
  
  // 1. 优先使用SKU特定图片
  if (sku && spu.images.by_color[sku.color]) {
    const colorImages = spu.images.by_color[sku.color];
    if (colorImages.color_main) images.push(colorImages.color_main);
    if (colorImages.color_detail) images.push(...colorImages.color_detail);
  }
  
  // 2. 没有SKU特定图片时使用SPU通用图片
  if (images.length === 0) {
    if (spu.images.common.main) images.push(spu.images.common.main);
    if (spu.images.common.detail) images.push(...spu.images.common.detail);
  }
  
  // 3. 添加视频
  if (spu.images.common.video) {
    images.push({ type: 'video', url: spu.images.common.video });
  }
  
  return images;
}

五、SPU与SKU设计的最佳实践

1. 编码规范设计

推荐编码规则:

SPU编码:品类码 + 品牌码 + 系列码 + 序列号
示例:DR-LS-2024-001(连衣裙-LilyRose-2024系列-001)

SKU编码:SPU编码 + 属性简码 + 序列号
示例:DR-LS-2024-001-BLUE-M(SPU-颜色-尺码)

代码实现:

class ProductCodeGenerator:
    def __init__(self):
        self.category_map = {
            'dress': 'DR',
            'shirt': 'SH',
            'pants': 'PT',
            'shoes': 'SHO'
        }
        self.brand_map = {
            'LilyRose': 'LS',
            'Nike': 'NK',
            'Apple': 'APL'
        }
    
    def generate_spu_code(self, category, brand, series, year):
        cat = self.category_map.get(category, 'XX')
        bra = self.brand_map.get(brand, 'XX')
        return f"{cat}-{bra}-{year}-{self._get_sequence('spu')}"
    
    def generate_sku_code(self, spu_code, attributes):
        attr_str = ""
        for key, value in sorted(attributes.items()):
            # 取前两个字母作为简码
            attr_str += f"-{value[:2].upper()}"
        return f"{spu_code}{attr_str}"
    
    def _get_sequence(self, type):
        # 从数据库获取序列号
        # 实际实现需要数据库操作
        return "001"

2. 数据库设计最佳实践

推荐表结构:

-- SPU表
CREATE TABLE spu (
  id INT PRIMARY KEY AUTO_INCREMENT,
  spu_code VARCHAR(50) UNIQUE NOT NULL,
  name VARCHAR(200) NOT NULL,
  brand VARCHAR(100),
  category_path VARCHAR(200), -- 分类路径,如"女装/连衣裙"
  base_price DECIMAL(10,2),
  description TEXT,
  main_image VARCHAR(500),
  status ENUM('draft', 'active', 'inactive') DEFAULT 'draft',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_brand (brand),
  INDEX idx_category (category_path),
  INDEX idx_status (status)
);

-- SKU表
CREATE TABLE sku (
  id INT PRIMARY KEY AUTO_INCREMENT,
  sku_code VARCHAR(50) UNIQUE NOT NULL,
  spu_code VARCHAR(50) NOT NULL,
  attributes JSON NOT NULL, -- {"color": "红色", "size": "M", "material": "棉"}
  price DECIMAL(10,2) NOT NULL,
  cost DECIMAL(10,2), -- 成本价
  stock INT DEFAULT 0,
  stock_warning INT DEFAULT 10, -- 库存预警阈值
  images JSON, -- SKU特定图片
  barcode VARCHAR(100), -- 条形码
  status ENUM('active', 'inactive', 'sold_out') DEFAULT 'active',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (spu_code) REFERENCES spu(spu_code),
  INDEX idx_spu (spu_code),
  INDEX idx_stock (stock),
  INDEX idx_price (price),
  INDEX idx_attributes (attributes(64)) -- 对JSON字段前64字符建索引
);

-- 属性字典表
CREATE TABLE attribute_dict (
  id INT PRIMARY KEY AUTO_INCREMENT,
  attribute_name VARCHAR(50) NOT NULL,
  standard_value VARCHAR(50) NOT NULL,
  alias JSON, -- 别名列表
  display_order INT DEFAULT 0,
  is_required BOOLEAN DEFAULT TRUE,
  UNIQUE KEY unique_attr (attribute_name, standard_value)
);

-- 促销价格表(分离价格策略)
CREATE TABLE promotion_price (
  id INT PRIMARY KEY AUTO_INCREMENT,
  target_type ENUM('SPU', 'SKU') NOT NULL,
  target_code VARCHAR(50) NOT NULL,
  promotion_type ENUM('percent', 'fixed', 'group') NOT NULL,
  promotion_value DECIMAL(10,2) NOT NULL,
  start_time DATETIME NOT NULL,
  end_time DATETIME NOT NULL,
  priority INT DEFAULT 0, -- 优先级,数字越大优先级越高
  INDEX idx_target (target_type, target_code),
  INDEX idx_time (start_time, end_time)
);

3. 前端展示优化策略

SPU列表页优化:

// 批量加载SPU和SKU数据,减少请求次数
async function loadSPUList(category, filters) {
  // 1. 查询SPU列表(分页)
  const spuList = await api.get('/spu/list', {
    params: { category, page: 1, size: 20 }
  });
  
  // 2. 批量查询这些SPU的SKU信息(聚合查询)
  const spuCodes = spuList.map(spu => spu.code);
  const skuInfo = await api.post('/sku/batch', { spuCodes, filters });
  
  // 3. 组装数据
  return spuList.map(spu => {
    const skus = skuInfo[spu.code] || [];
    const minPrice = Math.min(...skus.map(s => s.price));
    const hasStock = skus.some(s => s.stock > 0);
    
    return {
      ...spu,
      minPrice,
      hasStock,
      image: spu.main_image,
      // 只返回第一个SKU的图片作为展示
      previewImage: skus[0]?.images?.[0] || spu.main_image
    };
  });
}

详情页SKU选择器优化:

// 预加载所有SKU数据,避免多次请求
class SPUDetailManager {
  constructor(spuCode) {
    this.spuCode = spuCode;
    this.spu = null;
    this.skus = [];
    this.attributeMap = {}; // 属性映射,用于快速查找
  }
  
  async load() {
    // 一次性加载SPU和所有SKU
    const [spu, skus] = await Promise.all([
      api.get(`/spu/${this.spuCode}`),
      api.get(`/sku/list?spu=${this.spuCode}`)
    ]);
    
    this.spu = spu;
    this.skus = skus;
    this.buildAttributeMap();
  }
  
  buildAttributeMap() {
    // 构建属性组合到SKU的映射
    this.attributeMap = {};
    this.skus.forEach(sku => {
      const key = this.getAttributeKey(sku.attributes);
      this.attributeMap[key] = sku;
    });
  }
  
  getAttributeKey(attributes) {
    // 将属性对象转换为可排序的字符串键
    return Object.entries(attributes)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([k, v]) => `${k}:${v}`)
      .join('|');
  }
  
  // 快速查找SKU
  findSKU(attributes) {
    const key = this.getAttributeKey(attributes);
    return this.attributeMap[key];
  }
}

六、总结

SPU和SKU是电商后台商品体系的两大支柱,理解它们的区别和联系是高效管理电商后台的基础:

核心要点回顾:

  1. SPU是”类别”,SKU是”实例”:SPU定义产品是什么,SKU定义具体是什么规格
  2. 管理维度不同:SPU用于展示和分类,SKU用于库存和交易
  3. 1:N关系:一个SPU对应多个SKU,一个SKU只属于一个SPU
  4. 数据分离:价格、库存、图片等数据应根据使用场景合理分布在SPU和SKU层级

实际应用建议:

  • 设计阶段:先规划SPU结构,再细化SKU组合
  • 编码规范:建立清晰的SPU和SKU编码规则
  • 数据管理:保持SPU和SKU数据的一致性,建立数据校验机制
  • 性能优化:批量查询、缓存策略、索引优化
  • 用户体验:SKU选择器要清晰展示可选/不可选状态,避免用户困惑

掌握SPU和SKU的核心概念,不仅能帮助你更好地设计电商后台系统,还能在实际运营中做出更合理的商品策略和库存管理决策。