1. 商品规格数据结构
商品中都有属性,不同商品,属性往往不同。
1.1 规格属性内容
在生活中,我们很容易发现,虽然商品不同,规格不同。但是统一分类的商品,比如都是手机,其规格是一致的,但是值不一样。也就是说,商品的规格参数应该是与分类绑定的。每一个分类都有统一的规格参数模版,但不同商品其参数值可能不同。
因此:
- 规格参数的名称(key)与值(value)应该分开保存
- 一个分类,对应一套规格参数模版,只有规格参数key,没有值;
- 一个分类对应多个商品,每个商品但规格值不同,每个商品对应一套规格值。
1.2 横表与竖表
值我们暂且不管,新增商品时,再来填写规格参数值即可。我们现思考规格参数模版(key)该如何设计。
- 规格数据首先要分组,组内有不同的规格参数
- 不同分类,其分组名称也不同
- 不同分组,组内属性也不同
这样就意味着:有多少分类,就有多少分组。
如果按照传统设计,我们会以规格参数作为数据库字段名。如品牌、型号都是字段,那么表的字段就会无限多。这样的表称为横表。
一条信息,描述所有数据。例如:
我们不这样做,我们一条信息,只描述一条规格属性,也就是把规格参数作为字段的值,而非字段本身。这样的设计称为竖表设计。
例如:
不过,规格和规格组也要单独保存,都采用竖表设计。所以我们有两张表:
如图:
1.3 SpecGroup表结构
规格参数分组表:tb_spec_group
CREATE TABLE `tb_spec_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
`name` varchar(50) NOT NULL COMMENT '规格组的名称',
PRIMARY KEY (`id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';
1.3.2 SpecParam规格参数
规格参数表:tb_spec_param
CREATE TABLE `tb_spec_param` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id',
`group_id` bigint(20) NOT NULL,
`name` varchar(255) NOT NULL COMMENT '参数名',
`numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',
`unit` varchar(255) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',
`generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',
`searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',
`segments` varchar(1000) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',
PRIMARY KEY (`id`),
KEY `key_group` (`group_id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';
通用属性
用一个布尔类型字段来标记是否为通用:
- generic来标记是否为通用属性:
true:代表通用属性
false:代表sku特有属性
搜索过滤
与搜索相关的有两个字段:
- searching:标记是否用作过滤
true:用于过滤搜索
false:不用于过滤
segments:某些数值类型的参数,在搜索时需要按区间划分,这里提前确定好划分区间
比如电池容量,0 ~ 2000mAh,2000mAh ~ 3000mAh,3000mAh ~ 4000mAh
数值类型
某些规格参数可能为数值类型,这样的数据才需要划分区间,我们有两个字段来描述:
- numberic:是否为数值类型
true:数值类型
false:不是数值类型
unit:参数的单位
1.4 从面向对象角度分析
我们现在要表示规格参数的key,不要值。
如果用java对象表示:首先可以知道其可以分成组和组内属性两部分。组和组内属性分别是两类事物。
对组进行描述:对组内对共同特征进行提取,可以采用java中的类(class Group)进行描述。将主体、基本信息、操作系统等看成类中的name字段,将每一个分组下对应的多个参数(主体下的型号、入网年份)看成类中的List params,同时,每个组也会有一个属于自己的id来标识自己。
对组内属性进行描述:上述对List params又是一个新的事物,同样也可采用类(class Param)的概念进行描述。同样有name(参数名)、id字段,同时还要又一个groupId来说明这些参数属于哪一个组分类下的,这样就做到了Group表和Param表相互关联。同时,Group表中也应添加categoryId来说明这些规格组是属于哪个商品分类下的。但是,再考虑,规格参数属性里面应该也要加入是否为数值类型的字段;若是,那还会衍生出一个新的字段Unit;由于有一些规格参数的字段将来要做搜索的,因此还应该有判断该参数是否为搜索字段;如果可以搜索,同时又是数值类型,那还会有一个segment字段(字符串类型)。到此为止,规格参数就描述清楚了。
2. 商品表结构分析
全品类电商网站,商品种类繁多。每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU和SKU。
2.1 SKU和SPU
- SPU:Standard Product Unit(标准产品单位),一组具有共同属性的商品集
- SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品
以图为例:
– 本页的华为Mate10就是一个商品集(SPU)
– 因为颜色、内存等不同,而细分出不同等Mate10,如亮黑色128G版。(SKU)
可以看出:
- SPU是一个抽象等商品集概念,为了方便后台等管理
- SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU
- 同一个SPU下的SKU有共有属性,也有特有属性
2.2 表结构
2.2.1 表结构分析
其实,SKU的特有属性是商品规格参数的一部分。规格参数我们已经提前设计好了,每一个分类都有自己的规格参数。因此我们在设计商品表的时候不要设计特有属性,这些特性设计到规格参数里了。因此规格参数表里的generic字段对特有属性还是通用属性进行了区分。
也就是说,我们没必要单独对SKU特有属性进行设计,它可以看做是规格参数中对一部分。这样规格参数中对属性可以标记成两部分:
- 所有sku共享对规格属性(称为通用属性),我们记录在spu表中
- 每个sku不同对规格属性(称为特有属性),我们记录在sku表中
2.2.2 SPU表
2.2.2.1 spu表结构
CREATE TABLE `tb_spu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题(商品名称)',
`sub_title` varchar(255) DEFAULT '' COMMENT '子标题',
`cid1` bigint(20) NOT NULL COMMENT '1级类目id',
`cid2` bigint(20) NOT NULL COMMENT '2级类目id',
`cid3` bigint(20) NOT NULL COMMENT '3级类目id',
`brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
`saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',
`create_time` datetime DEFAULT NULL COMMENT '添加时间',
`last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象的商品,比如 iphone8';
由于SPU表中某些字段的值非常大,比如商品描述。
因此为了不影响spu表的性能及查询速度,我们对表做了垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail
注:
- 垂直拆分:表字段过多,实现数据的解耦
- 水平拆分:表数据量多(一张表有1万条数据,水平拆分成两个表,每个表5千条数据)数据库查询时做排序较麻烦,不过现在有很多中间件帮我们实现分表 查询、排序等。用在 订单表中,订单数据量很大,一张表存不下,必然要做水平拆分。
2.2.2.2 spu_detail 表结构
CREATE TABLE `tb_spu_detail` (
`spu_id` bigint(20) NOT NULL,
`description` text COMMENT '商品描述信息',
`generic_spec` varchar(3000) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
`special_spec` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
`packing_list` varchar(1000) DEFAULT '' COMMENT '包装清单',
`after_service` varchar(1000) DEFAULT '' COMMENT '售后服务',
PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2.2.3 spu中对规格参数
前面讲过规格参数与商品分类绑定,一个分类下对所有SPU具有类似的规格参数。SPU下的SKU可能会有不同对规格参数,因此:
- SPU中保存通用对规格参数信息。
- SKU中保存特有规格参数。
2.2.2.3.1 generic_spec字段
generic_spec:保存通用规格参数信息的值,这里为了方便查询,使用了json格式。
json结构,其中都是键值对:
- key:对应的规格参数的spec_param的id
- value:对应规格参数的值
2.2.2.3.2 special_spec字段
由于SPU下的每一个sku,其值也都不一样,所以值会有很多,形成数组。
因此我们在SPU中,会把特有属性的所有值都记录下来,形成一个数组:
也是json结构:
- key:规格参数id
- value:spu属性的数组
问题:特有规格参数应该在sku中记录才对,为什么在spu中也要记录一份(聚合sku集合)?
- 因为我们有时候需要把所有规格参数都查询出来,而不是只查询1个sku的属性。比如,商品详情页展示可选的规格参数时:
刚好符号我们的结构,这样页面渲染就非常方便了。
2.2.3 SKU表
2.2.3.1 sku表结构
CREATE TABLE `tb_sku` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
`spu_id` bigint(20) NOT NULL COMMENT 'spu id',
`title` varchar(255) NOT NULL COMMENT 'sku特有标题',
`images` varchar(1000) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
`price` bigint(15) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
`indexes` varchar(100) COMMENT '特有规格属性在spu属性模板中的对应下标组合',
`own_spec` varchar(1000) COMMENT 'sku的特有规格参数,json格式',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
`create_time` datetime NOT NULL COMMENT '添加时间',
`last_update_time` datetime NOT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`),
KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的64GB的iphone 8';
还有一张stock表,代表库存;
因为库存字段写频率较高,而SKU的其它字段(颜色、价格)以读为主,因此我们将两张表分离,读写不会干扰(垂直拆分)。
2.2.3.2 stock库存表
CREATE TABLE `tb_stock` (
`sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id',
`seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存',
`seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量',
`stock` int(9) NOT NULL COMMENT '库存数量',
PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';
2.2.3.3 sku中的规格参数
2.2.3.3.1 indexes字段
在SPU表中,已经对特有规格参数及可选项进行了保存,结构如下:
{
"4": [
"香槟金",
"樱花粉",
"磨砂黑"
],
"12": [
"2GB",
"3GB"
],
"13": [
"16GB",
"32GB"
]
}
这些特有属性如果排列组合,会产生12个不同的SKU,而不同的SKU,其属性就是上面备选项中的一个。
比如:
- 红米4X,香槟金,2GB内存,16GB存储
- 红米4X,磨砂黑,2GB内存,32GB存储
你会发现,每一个属性值,对应于SPUoptions数组的一个选项,如果我们记录下角标,就是这样:
- 红米4X,0,0,0
- 红米4X,2,0,1
既然如此,我们就可以将不同角标串联起来,作为SPU下不同SKU的标示。这就是我们的indexes字段。
这个设计在商品详情页(页面渲染)会特别有用:当用户点击选中一个特有属性,你就能根据 角标快速定位到sku。
2.2.3.3.2 own_spec字段
看结构:
{
"4":"香槟金",
"12":"2GB",
"13":"16GB"
}
保存的是特有属性的键值对。
SPU中保存的是可选项,但不确定具体的值,而SKU中的保存的就是具体的值。