在商城系统开发中,往往一个商品会存在多种销售规格如:颜色、内存等,而这些由销售规格组成的集合我们称之为商品的 SKU,商品的主体为 SPU。本文主要讲解商城项目开发中,商品销售规格及属性的逻辑理论和开发示例。
规格属性定义
我们先来看下传统意义上对商品规格与属性的定义:
商品规格:是指物件的体积、大小,型号是用来识别物品的编号。百度百科商品规格。
商品属性:产品属性是指产品本身所固有的性质,是产品在不同领域差异性(不同于其他产品的性质)的集合。百度百科产品属性。
运用到商城开发中的规格与属性:
规格是影响价格的,属性是不影响价格只展示的。本文统一命名为销售规格与商品属性。
销售规格:
销售规格在京东商家后台被叫做销售属性,在淘宝商家后台被叫做销售规格。
商品属性:
商品属性在客户端显示为规格与包装(京东) / 规格参数(淘宝天猫)。
商品SKU
1)SKU(或称商品SKU)指的是商品子实体。
2)商品和商品 SKU 是主次关系,一个商品包含若干个商品 SKU 子实体,商品 SKU 从属于商品。
3)SKU 不是编码,每个 SKU 包含一个唯一编码,即SKU Code,用于管理。
4)产品本身也有一个编码,即 Product Code,但不作为直接库存管理使用。有时为了方便管理,会通过产品的 Product Code 作为前缀生成 SKU Code。
SPU与SKU的区别
SPU 为商品的“款”,SKU 为商品的“件”。
SPU = Standard Product Unit (标准化产品单元),SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
例如:iPhone11 就是 SPU。
SKU = stock keeping unit(库存量单位)
SKU 即库存进出计量的单位,可以是以件、盒、托盘等为单位。
SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
例如:黑色 256G iPhone11 就是 SKU。
销售规格
销售规格其实就是会影响商品价格的元素。如下图为某型号手机在售卖时,用户可以选择不同的商品销售规格:
在了解销售规格之前需要先理解上面提到的商品SKU。
在商品创建编辑时,勾选对应的销售规格,生成 SKU 信息。如下图所示:在颜色区域,勾选了“酒红”和“红咖”,在尺码区域,勾选了 “165/84A”,那么将自动生成 “酒红/165/84A” 和 “红咖/165/84A” 2条 SKU 信息,以此类推。取消勾选后,与此属性值相关的 SKU 将自动去掉。
SKU 是指您的商品编号,对应到您每一款商品的每一款颜色和型号,在您编辑了商品的销售规格后即可生成,客户可以通过 SKU 直接搜索到您店铺的商品。商品 SKU 会显示在您的商品链接和商品介绍中。
每种组合出来的 SKU 可能会有不同的售价、运费与库存剩余情况。所以用户在购买时,不仅需要记录所购买的商品 ID,同时也需要记录购买的该商品的具体规格。
商品属性
商品属性是展示在客户端页面上的规格参数。如下图是某型号手机的商品属性:
不同类型的商品拥有不同的商品属性。如下图是京东商家后台中女装-棉服类目下需要填写的商品属性,但 * 带红色星号的信息为必填项:
规格属性绑定
商品独立管理
即商品独立管理使用规格与属性。
优点:基本没有。
缺点:这种比较不靠谱,因为会导致工作量过大。虽然可以通过“复制”功能来稍稍简化,但依然不会很理想。所以基本不会采用。
商品绑定
即商品独立绑定,需要使用规格与属性时调取。
这里我们习惯把规格、属性放在一个商品模型中去。比如有一个叫做手机的商品模型,下面包含了销售规格:内存(从列表中选择)、颜色(从列表中选择);商品属性:产地(手动输入)、操作系统(手动输入)等。
在商品创建编辑时,去选择要使用的商品模型,调取相对应的销售规格与商品属性。
当然,你也可以分开去创建销售规格与商品属性。比如:手机类销售规格、手机类商品属性,然后在商品创建编辑时,去调取对应规格与属性。
优点:灵活性,易于后期维护。
缺点:适合模型不多的系统。
类目绑定
类目绑定是在商品类目中进行绑定规格与属性,当用户在该类目下创建商品时,直接调出该类目中需要填写的规格与属性。
类目绑定同样可以使用上面商品绑定中提到的商品模型。但是更好的选择是每个类目下都进行规格与属性的设置。这里需要注意,并不是所有的类目都需要去设置规格与属性,我们只需在被允许添加商品的二级、三级类目下设置规格与属性即可。
当然,根据业务需求,你也可更灵活的去设置类目绑定。比如:三级分类共用自己的父级二级分类的规格与属性;创建商品模型,相似的类目共用同一个商品模型等。
优点:灵活性,易于后期维护。容易进行严格的管理,不易出错。
缺点:工作量大,不适合中小型项目。
品牌绑定
品牌绑定与类目绑定相似。同样是在创建编辑商品时,选择品牌,就调取该品牌中需要填写的规格与属性。
优点:灵活性,易于后期维护。容易进行严格的管理,不易出错。
缺点:工作量大,不适合中小型项目。
开发逻辑
1,首先,我们要确定商品以 SPU 展示还是 SKU?
京东由于采取类似于海外电商亚马逊的模式,储存时会给每个 SKU 赋予唯一编码,并且每个 SKU 会以 SKU 形成一个链接,而 SPU 是系统后台对不同属性、规格但又同属同一系的子产品进行统一管理的编码。
淘宝天猫的逻辑,与京东不同,每一款产品都拥有一个独立的 SPU,即商品 ID,SKU 都是附着在 SPU 下。
详情可参考下这篇文章:电商商品应以 SPU 还是 SKU 展示?
本文以 SKU 展示。
2,设置 单SKU 和 多SKU?
大部分商品都存在销售规格,但也有少部分商品并不需要销售规格。那么对于这类商品我们应该如何处理呢?
因为我们是以 SKU 展示商品,所以在操作这种没有销售规格的商品时,需要把它当做一个没有销售规格的 SKU 存储在 product_sku 表中。
参考京东的处理逻辑,不存在销售规格的商品保存为一个 SKU,该 SKU 中除了没有销售规格对应的 ID 外,其余信息均一致。当后期需要对这个商品添加销售规格时,原有的 SKU 自动下架,并根据添加的销售规格生成对应的 SKU。
以下是各种情况下,后端的一些处理逻辑:
在下面演示的 product_sku 表中,default 字段用来区分 SKU 和 商品基础信息保存的SKU。
1)添加商品时,商品存在销售规格:基础信息存储于 product 表;生成的 SKU 存储于 product_sku 表。
2)修改商品时,商品存在销售规格:前端提交 product id、sku id、新增加的销售规格生成的新的 SKU 信息。
3)添加商品时,商品不存在销售规格:基础信息存储于 product 表,并在 product_sku 表中存储一条不存在销售规格 ID 的 SKU 信息。
4)修改商品时,商品存在销售规格,清空销售规格:之前存储的 SKU 下架,并存储一条不存在销售规格 ID 的 SKU 信息。
5)修改商品时,商品不存在销售规格,新增了销售规格:之前的 SKU 下架,并存储新的 SKU 信息。
数据设计
数据库使用 MySQL。数据结构设计仅供参考。
luck_product_specification 商品销售规格表:
CREATE TABLE `luck_product_specification` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_model_id` int(11) NOT NULL DEFAULT '0',
`name` varchar(100) NOT NULL DEFAULT '',
`sort` mediumint(8) NOT NULL DEFAULT '0',
`status` tinyint(3) NOT NULL DEFAULT '1' COMMENT '1=Enabled 0=Disabled',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COMMENT='商品销售规格';
luck_product_specification_option 商品销售规格选项表:
CREATE TABLE `luck_product_specification_option` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`specification_id` int(11) NOT NULL DEFAULT '0',
`value` varchar(100) NOT NULL DEFAULT '',
`status` tinyint(3) NOT NULL DEFAULT '1' COMMENT '1=Enabled 0=Disabled',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COMMENT='商品销售规格选项';
luck_product 商品基础信息表:
CREATE TABLE `luck_product` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '产品名',
`category_id` int(11) NOT NULL DEFAULT '0',
`brand_id` int(11) NOT NULL DEFAULT '0',
`content` text COMMENT '内容',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '0=Disabled 1=Enabled',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=85 DEFAULT CHARSET=utf8mb4 COMMENT='商品基础信息 商品SPU';
luck_product_sku 商品SKU表:
CREATE TABLE `luck_product_sku` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL DEFAULT '0',
`sku` varchar(200) NOT NULL DEFAULT '',
`image` varchar(255) NOT NULL DEFAULT '' COMMENT 'sku主图',
`stock` mediumint(8) NOT NULL DEFAULT '0',
`original_price` decimal(10,2) NOT NULL DEFAULT '0.00',
`sale_price` decimal(10,2) NOT NULL DEFAULT '0.00',
`default` tinyint(3) NOT NULL DEFAULT '0' COMMENT '1=spu 0=sku',
`status` tinyint(3) NOT NULL DEFAULT '1' COMMENT '0=Disabled 1=Enabled',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU';
luck_product_to_specification 商品关联销售规格表:
CREATE TABLE `luck_product_to_specification` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) DEFAULT NULL,
`sku` int(11) NOT NULL DEFAULT '0',
`specification_id` int(11) NOT NULL DEFAULT '0',
`specification_option_id` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=505 DEFAULT CHARSET=utf8mb4 COMMENT='商品关联销售规格';
数据使用
1,商品详情
思路:通过 SKU ID 查询 SPU 基础信息,通过 SPU 调取所有 SKU。
我们先来假设后台设置了一款手机存在以下几个SKU:
1)颜色:黑色,内存:16G
2)颜色:黑色,内存:64G
3)颜色:黑色,内存:128G
4)颜色:白色,内存:16G
对应的数据库存储结果如下,可参考上面的数据设计结构理解:
商品销售规格数据,product_specification 表:
商品销售规格选项数据,product_specification_option 表:
商品基础数据,product 表:
商品 SKU 数据,product_sku 表:
商品关联销售规格数据,product_to_specification 表:
上面的商品数据查询,主要难点在销售规格的数据组装上。所以,这里也提供下关于销售规格数据组装的示例代码:
示例代码使用 ThinkPHP5.1
$sku = 1;
// 当前 sku 信息
$product_sku = Db::name('product_sku')->where('sku', $sku)->where('status', 1)->find();
if (empty($product_sku)) abort(404);
// 当前 sku 下的销售规格
$current_specifications = Db::name('product_to_specification')->where('sku', $sku)->select();
$current_specification_ids = array_column($current_specifications, 'specification_id');
$current_specification_option_ids = array_column($current_specifications, 'specification_option_id');
// 当前商品下的销售规格
$product_to_specifications = Db::name('product_to_specification')->alias('product_to_specification')
->field('product_to_specification.*, product_specification.name as specification_name, product_specification_option.value as specification_option_value, product_sku.stock')
->leftJoin('product_specification', 'product_specification.id = product_to_specification.specification_id')
->leftJoin('product_specification_option', 'product_specification_option.id = product_to_specification.specification_option_id')
->leftJoin('product_sku', 'product_sku.sku = product_to_specification.sku')
->where('product_to_specification.product_id', $product_sku['product_id'])
->select();
// 分配销售规格组
$skus = [];
foreach ($product_to_specifications as $key => $value) $skus[$value['sku']][] = $value;
// 获取与当前销售规格有关联的sku
// 获取与当前销售规格有关联的商品关联规格ID
// 设置商品关联规格是否有效/可点击
$have_product_skus = [];
$have_product_to_specification_ids = [];
foreach ($product_to_specifications as $key => $value) {
if (count($current_specification_option_ids) > 1) {
if (in_array($value['product_specification_option_id'], $current_specification_option_ids)) {
$current_sku_specification_option_ids = array_column($skus[$value['sku']], 'product_specification_option_id');
$the_same_date_count = array_intersect($current_sku_specification_option_ids, $current_specification_option_ids);
if (count($the_same_date_count) >= count($current_specification_option_ids) - 1) {
$have_product_skus[] = $value['sku'];
$have_product_to_specification_ids[] = $value['id'];
}
}
} else {
$have_product_skus[] = $value['sku'];
$have_product_to_specification_ids[] = $value['id'];
}
}
foreach ($product_to_specifications as $key => $value) {
$product_to_specifications[$key]['valid'] = 0;
if (in_array($value['sku'], $have_product_skus)) {
$product_to_specifications[$key]['valid'] = 1;
}
}
// 组装数据
$array = [];
foreach ($product_to_specifications as $key => $value) {
$array[$value['specification_id']]['specification_id'] = $value['specification_id'];
$array[$value['specification_id']]['specification_name'] = $value['specification_name'];
$array[$value['specification_id']]['options'][$value['specification_option_id']]['specification_option_id'] = $value['specification_option_id'];
$array[$value['specification_id']]['options'][$value['specification_option_id']]['specification_option_value'] = $value['specification_option_value'];
if ($value['valid'] == 1) {
$array[$value['product_specification_id']]['options'][$value['product_specification_option_id']]['valid'] = $value['valid'];
$array[$value['product_specification_id']]['options'][$value['product_specification_option_id']]['sku'] = $value['sku'];
$array[$value['product_specification_id']]['options'][$value['product_specification_option_id']]['stock'] = $value['stock'];
}
if ($value['sku'] == $sku) $array[$value['specification_id']]['options'][$value['specification_option_id']]['selected'] = 1;
}
dd($array);
示例代码中的打印结果如下:
array(2) {
[1] => array(3) {
["specification_id"] => int(1)
["specification_name"] => string(6) "颜色"
["options"] => array(2) {
[1] => array(5) {
["specification_option_id"] => int(1)
["specification_option_value"] => string(6) "黑色"
["valid"] => int(1)
["sku"] => int(3)
["stock"] => int(200)
["selected"] => int(1)
}
[2] => array(4) {
["specification_option_id"] => int(2)
["specification_option_value"] => string(6) "白色"
}
}
}
[2] => array(3) {
["specification_id"] => int(2)
["specification_name"] => string(6) "内存"
["options"] => array(3) {
[4] => array(5) {
["specification_option_id"] => int(4)
["specification_option_value"] => string(3) "16G"
["valid"] => int(1)
["sku"] => int(4)
["stock"] => int(200)
}
[5] => array(5) {
["specification_option_id"] => int(5)
["specification_option_value"] => string(3) "64G"
["valid"] => int(1)
["sku"] => int(2)
["stock"] => int(200)
["selected"] => int(1)
}
[6] => array(5) {
["specification_option_id"] => int(6)
["specification_option_value"] => string(4) "128G"
["valid"] => int(1)
["sku"] => int(3)
["stock"] => int(200)
}
}
}
}
从打印结果来看,可用 selected,valid 字段控制选中与可点击的操作。
2,商品列表
思路:查询 SPU,关联 SPU 下的 SKU。商品链接默认为该 SPU 下的第一个 SKU。