SKU设计
好的,我们来更详细地拆解SKU(Stock Keeping Unit)设计,特别是如何优雅地处理不同商品品类(如服装和电子产品)拥有完全不同属性集的问题。
SKU设计的核心原理:变体与属性分离
核心思想:
不要尝试在商品主表 (products
) 中为所有可能的属性(如颜色、尺码、内存、CPU)都创建列。这会导致表结构极其庞大且难以维护,因为大部分列对特定商品来说都是空白的。
相反,我们采取**“通用属性池 + 灵活组合”**的策略:
- 通用属性池: 创建独立的属性类型 (
attributes
) 和属性值 (attribute_values
) 表,它们存储了系统中所有商品可能用到的所有属性的类型和具体值。 - 商品变体(SKU): 针对每个商品,其具体的、可购买的最小单位(SKU)是根据其所拥有的一组特定属性值来定义的。
- 灵活组合: 通过一个中间关联表 (
product_variant_attributes
),将每个SKU与它所包含的那些特定属性值关联起来。这样,一个服装SKU只关联“颜色”和“尺码”的属性值,而一个手机SKU则关联“存储容量”、“颜色”和“运行内存”的属性值。
核心表结构深入理解
我们再次回顾并深入理解之前提供的关键表结构:
-
products
(商品主表):- 存储商品通用信息,这些信息不随SKU变化。例如:商品名称、描述、品牌、主图、商品分类、自由属性 (
custom_attributes
JSONB)。 custom_attributes
(JSONB类型) 是一个非常重要的字段,它用于存储那些不作为SKU维度的额外、灵活的属性。比如:- 服装:材质、适用季节、领型、袖长。
- 电子产品:屏幕尺寸、摄像头像素、操作系统版本、接口类型。
- 特点: 这些属性对同一商品的所有SKU都一样,或者说它们只是描述性的,不参与SKU的组合。
- 存储商品通用信息,这些信息不随SKU变化。例如:商品名称、描述、品牌、主图、商品分类、自由属性 (
-
product_variants
(商品变体/SKU表):- 存储每个具体可购买的最小单位的信息。
- 每个
product_variant
记录代表一个唯一的SKU,它有自己的sku
编码、price
、stock
、甚至自己专属的image_url
。 - 重点:
product_variants
表本身不直接存储属性值。它只是一个“占位符”,表示一个SKU存在。
-
attributes
(属性类型表):- 定义属性的“种类”或“维度”。这是一个全局性的列表,包含了电商平台中所有商品可能用到的所有属性类型。
- 例如:
id=1, name='颜色', type='color'
- 例如:
id=2, name='尺码', type='text'
- 例如:
id=3, name='存储容量', type='text'
- 例如:
id=4, name='运行内存', type='text'
- 例如:
id=5, name='CPU型号', type='text'
-
attribute_values
(属性值表):- 存储每个属性类型下具体可选的值。这也是一个全局性的列表。
- 例如:
id=101, attribute_id=1 (颜色), value='#FF0000', display_name='红色'
id=102, attribute_id=1 (颜色), value='#0000FF', display_name='蓝色'
id=201, attribute_id=2 (尺码), value='S', display_name='小码'
id=202, attribute_id=2 (尺码), value='M', display_name='中码'
id=301, attribute_id=3 (存储容量), value='128GB', display_name='128GB'
id=302, attribute_id=3 (存储容量), value='256GB', display_name='256GB'
-
product_variant_attributes
(商品变体-属性值关联表):- 这是核心的连接点。它将
product_variants
和attribute_values
关联起来。 - 原理: 每个SKU由一组特定的属性值组合而成。这个表就记录了“SKU X 包含了 属性值 A 和 属性值 B”。
- 例如:
product_variant_id=SKU_ID_FOR_RED_S_TSHIRT, attribute_value_id=101 (红色)
product_variant_id=SKU_ID_FOR_RED_S_TSHIRT, attribute_value_id=201 (小码)
product_variant_id=SKU_ID_FOR_256GB_BLACK_PHONE, attribute_value_id=302 (256GB)
product_variant_id=SKU_ID_FOR_256GB_BLACK_PHONE, attribute_value_id=103 (黑色, 假设ID为103)
product_variant_id=SKU_ID_FOR_256GB_BLACK_PHONE, attribute_value_id=401 (8GB, 假设ID为401)
- 这是核心的连接点。它将
如何应对不同商品的属性差异?
这就是这套设计的精妙之处:
1. 统一的属性管理 (后端视角):
无论是服装还是电子产品,它们共用 attributes
和 attribute_values
这两个“属性池”。当需要添加一个新的属性(比如“洗涤方式”或者“电池容量”),只需要在这两个表中添加新的条目即可。
2. SKU级别的属性定义 (后端视角):
当创建一个具体商品(例如一件T恤或一部手机)时,你并不是为 product
定义它有哪些属性,而是为它的每个SKU定义它由哪些属性值组成。
-
服装类商品示例:T恤
products
表:id: 1, name: '时尚圆领T恤', description: '舒适纯棉T恤', category_id: 1 (服装), custom_attributes: {"材质": "纯棉", "领型": "圆领"}
attributes
表中需要存在:颜色
,尺码
attribute_values
表中需要存在:颜色
:红色
,蓝色
尺码
:S
,M
,L
product_variants
表 (部分SKU):id: 101, product_id: 1, sku: 'TSHIRT-RED-S', price: 99.00, stock: 50
id: 102, product_id: 1, sku: 'TSHIRT-RED-M', price: 109.00, stock: 30
id: 103, product_id: 1, sku: 'TSHIRT-BLUE-S', price: 99.00, stock: 40
product_variant_attributes
表 (关联这些SKU和属性值):product_variant_id: 101, attribute_value_id: (红色ID)
product_variant_id: 101, attribute_value_id: (S码ID)
product_variant_id: 102, attribute_value_id: (红色ID)
product_variant_id: 102, attribute_value_id: (M码ID)
product_variant_id: 103, attribute_value_id: (蓝色ID)
product_variant_id: 103, attribute_value_id: (S码ID)
- ...
- 结果: 对于这件T恤,它只有“颜色”和“尺码”作为可变属性。
-
电子产品示例:智能手机
products
表:id: 2, name: 'Pro Max 智能手机', description: '极致性能,超强续航', category_id: 2 (电子产品), custom_attributes: {"屏幕尺寸": "6.7英寸", "电池容量": "4000mAh", "充电接口": "Type-C"}
attributes
表中需要存在:颜色
,存储容量
,运行内存
,CPU型号
attribute_values
表中需要存在:颜色
:黑色
,白色
,金色
存储容量
:128GB
,256GB
,512GB
运行内存
:8GB
,12GB
CPU型号
:Snapdragon 8 Gen 2
,Apple A16 Bionic
product_variants
表 (部分SKU):id: 201, product_id: 2, sku: 'PHONE-BLK-128GB-8GB', price: 5999.00, stock: 20
id: 202, product_id: 2, sku: 'PHONE-WHT-256GB-12GB', price: 7999.00, stock: 15
- ...
product_variant_attributes
表 (关联这些SKU和属性值):product_variant_id: 201, attribute_value_id: (黑色ID)
product_variant_id: 201, attribute_value_id: (128GB ID)
product_variant_id: 201, attribute_value_id: (8GB ID)
product_variant_id: 202, attribute_value_id: (白色ID)
product_variant_id: 202, attribute_value_id: (256GB ID)
product_variant_id: 202, attribute_value_id: (12GB ID)
- ...
- 结果: 对于这款手机,它有“颜色”、“存储容量”和“运行内存”作为可变属性。
关键点:
后端在查询某个商品的详情时,会执行复杂的JOIN查询,从 products
表开始,通过 product_variants
表找到所有SKU,再通过 product_variant_attributes
和 attribute_values
表,聚合出这个商品所有可用变体以及所有涉及到的属性类型和属性值。
后端API如何返回这些信息
API响应中的 available_attributes
字段是后端根据 product.variants
和 product_variant_attributes
动态计算生成的。
例如,对于上面的T恤:
后端会扫描所有T恤的SKU,发现它们共同涉及到了 颜色
和 尺码
这两种属性,以及它们对应的 红色
, 蓝色
, S
, M
, L
这些具体值。然后将其组织成 available_attributes
返回给前端。
对于手机:
后端会扫描所有手机的SKU,发现它们共同涉及到了 颜色
, 存储容量
, 运行内存
这三种属性,以及它们对应的 黑色
, 白色
, 金色
, 128GB
, 256GB
, 512GB
, 8GB
, 12GB
这些具体值。然后将其组织成 available_attributes
返回给前端。
前端如何渲染
前端接收到这种聚合数据后,会:
- 渲染
product.name
,product.description
等通用信息。 - 渲染
product.custom_attributes
(JSONB) 作为商品详情的额外描述。 例如,对于T恤,显示“材质:纯棉”;对于手机,显示“屏幕尺寸:6.7英寸”。这些只是文本展示,不会有选择器。 - 遍历
product.available_attributes
数组,动态生成属性选择器。- 如果
attribute.name
是“颜色”,就渲染颜色块。 - 如果
attribute.name
是“尺码”、“存储容量”等,就渲染按钮或下拉框。 - 这样,前端无需知道商品具体是什么品类,只需要根据后端提供的
available_attributes
结构来渲染对应的属性选择UI。
- 如果
总结
这种设计模式的优点:
- 极度灵活: 可以轻松添加新的属性类型,无需修改数据库表结构。
- 高度可扩展: 任何商品都可以拥有任意数量和类型的变体属性。
- 数据一致性: 属性值统一管理,避免重复和不一致。
- 前端通用性: 前端代码可以通用化,只需根据后端API返回的数据动态渲染,无需为不同商品品类写不同的UI逻辑。
通过这种“属性池 + SKU组合”的设计,就能完美应对拥有不同尺码、不同颜色、不同自由属性(以及各种其他可变属性)的商品的复杂前后端设计需求。