在商城系统开发中,往往一个商品会存在多种销售规格如:颜色、内存等,而这些由销售规格组成的集合我们称之为商品的 SKU,商品的主体为 SPU。本文主要讲解商城项目开发中,商品销售规格及属性的逻辑理论和开发示例。

规格属性定义

我们先来看下传统意义上对商品规格与属性的定义:
商品规格:是指物件的体积、大小,型号是用来识别物品的编号。百度百科商品规格
商品属性:产品属性是指产品本身所固有的性质,是产品在不同领域差异性(不同于其他产品的性质)的集合。百度百科产品属性

运用到商城开发中的规格与属性:
规格是影响价格的,属性是不影响价格只展示的。本文统一命名为销售规格与商品属性。

销售规格:
商城开发 商品属性与规格 - 图1

销售规格在京东商家后台被叫做销售属性,在淘宝商家后台被叫做销售规格。

商品属性:
商城开发 商品属性与规格 - 图2
商城开发 商品属性与规格 - 图3

商品属性在客户端显示为规格与包装(京东) / 规格参数(淘宝天猫)。

商品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。

销售规格

销售规格其实就是会影响商品价格的元素。如下图为某型号手机在售卖时,用户可以选择不同的商品销售规格:
商城开发 商品属性与规格 - 图4

在了解销售规格之前需要先理解上面提到的商品SKU。

在商品创建编辑时,勾选对应的销售规格,生成 SKU 信息。如下图所示:在颜色区域,勾选了“酒红”和“红咖”,在尺码区域,勾选了 “165/84A”,那么将自动生成 “酒红/165/84A” 和 “红咖/165/84A” 2条 SKU 信息,以此类推。取消勾选后,与此属性值相关的 SKU 将自动去掉。
商城开发 商品属性与规格 - 图5
SKU 是指您的商品编号,对应到您每一款商品的每一款颜色和型号,在您编辑了商品的销售规格后即可生成,客户可以通过 SKU 直接搜索到您店铺的商品。商品 SKU 会显示在您的商品链接和商品介绍中。
每种组合出来的 SKU 可能会有不同的售价、运费与库存剩余情况。所以用户在购买时,不仅需要记录所购买的商品 ID,同时也需要记录购买的该商品的具体规格。

商品属性

商品属性是展示在客户端页面上的规格参数。如下图是某型号手机的商品属性:
商城开发 商品属性与规格 - 图6
商城开发 商品属性与规格 - 图7
不同类型的商品拥有不同的商品属性。如下图是京东商家后台中女装-棉服类目下需要填写的商品属性,但 * 带红色星号的信息为必填项:
商城开发 商品属性与规格 - 图8

规格属性绑定

商品独立管理

即商品独立管理使用规格与属性。
优点:基本没有。
缺点:这种比较不靠谱,因为会导致工作量过大。虽然可以通过“复制”功能来稍稍简化,但依然不会很理想。所以基本不会采用。

商品绑定

即商品独立绑定,需要使用规格与属性时调取。
这里我们习惯把规格、属性放在一个商品模型中去。比如有一个叫做手机的商品模型,下面包含了销售规格:内存(从列表中选择)、颜色(从列表中选择);商品属性:产地(手动输入)、操作系统(手动输入)等。
商城开发 商品属性与规格 - 图9
商城开发 商品属性与规格 - 图10
商城开发 商品属性与规格 - 图11
在商品创建编辑时,去选择要使用的商品模型,调取相对应的销售规格与商品属性。
当然,你也可以分开去创建销售规格与商品属性。比如:手机类销售规格、手机类商品属性,然后在商品创建编辑时,去调取对应规格与属性。
优点:灵活性,易于后期维护。
缺点:适合模型不多的系统。

类目绑定

类目绑定是在商品类目中进行绑定规格与属性,当用户在该类目下创建商品时,直接调出该类目中需要填写的规格与属性。
类目绑定同样可以使用上面商品绑定中提到的商品模型。但是更好的选择是每个类目下都进行规格与属性的设置。这里需要注意,并不是所有的类目都需要去设置规格与属性,我们只需在被允许添加商品的二级、三级类目下设置规格与属性即可。
当然,根据业务需求,你也可更灵活的去设置类目绑定。比如:三级分类共用自己的父级二级分类的规格与属性;创建商品模型,相似的类目共用同一个商品模型等。
优点:灵活性,易于后期维护。容易进行严格的管理,不易出错。
缺点:工作量大,不适合中小型项目。

品牌绑定

品牌绑定与类目绑定相似。同样是在创建编辑商品时,选择品牌,就调取该品牌中需要填写的规格与属性。
优点:灵活性,易于后期维护。容易进行严格的管理,不易出错。
缺点:工作量大,不适合中小型项目。

开发逻辑

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。
商城开发 商品属性与规格 - 图12
以下是各种情况下,后端的一些处理逻辑:

在下面演示的 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 商品销售规格表:

  1. CREATE TABLE `luck_product_specification` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `product_model_id` int(11) NOT NULL DEFAULT '0',
  4. `name` varchar(100) NOT NULL DEFAULT '',
  5. `sort` mediumint(8) NOT NULL DEFAULT '0',
  6. `status` tinyint(3) NOT NULL DEFAULT '1' COMMENT '1=Enabled 0=Disabled',
  7. PRIMARY KEY (`id`)
  8. ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COMMENT='商品销售规格';

luck_product_specification_option 商品销售规格选项表:

  1. CREATE TABLE `luck_product_specification_option` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `specification_id` int(11) NOT NULL DEFAULT '0',
  4. `value` varchar(100) NOT NULL DEFAULT '',
  5. `status` tinyint(3) NOT NULL DEFAULT '1' COMMENT '1=Enabled 0=Disabled',
  6. PRIMARY KEY (`id`)
  7. ) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COMMENT='商品销售规格选项';

luck_product 商品基础信息表:

  1. CREATE TABLE `luck_product` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `name` varchar(255) NOT NULL DEFAULT '' COMMENT '产品名',
  4. `category_id` int(11) NOT NULL DEFAULT '0',
  5. `brand_id` int(11) NOT NULL DEFAULT '0',
  6. `content` text COMMENT '内容',
  7. `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  8. `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  9. `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '0=Disabled 1=Enabled',
  10. PRIMARY KEY (`id`)
  11. ) ENGINE=InnoDB AUTO_INCREMENT=85 DEFAULT CHARSET=utf8mb4 COMMENT='商品基础信息 商品SPU';

luck_product_sku 商品SKU表:

  1. CREATE TABLE `luck_product_sku` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `product_id` int(11) NOT NULL DEFAULT '0',
  4. `sku` varchar(200) NOT NULL DEFAULT '',
  5. `image` varchar(255) NOT NULL DEFAULT '' COMMENT 'sku主图',
  6. `stock` mediumint(8) NOT NULL DEFAULT '0',
  7. `original_price` decimal(10,2) NOT NULL DEFAULT '0.00',
  8. `sale_price` decimal(10,2) NOT NULL DEFAULT '0.00',
  9. `default` tinyint(3) NOT NULL DEFAULT '0' COMMENT '1=spu 0=sku',
  10. `status` tinyint(3) NOT NULL DEFAULT '1' COMMENT '0=Disabled 1=Enabled',
  11. PRIMARY KEY (`id`)
  12. ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU';

luck_product_to_specification 商品关联销售规格表:

  1. CREATE TABLE `luck_product_to_specification` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `product_id` int(11) DEFAULT NULL,
  4. `sku` int(11) NOT NULL DEFAULT '0',
  5. `specification_id` int(11) NOT NULL DEFAULT '0',
  6. `specification_option_id` int(11) NOT NULL DEFAULT '0',
  7. PRIMARY KEY (`id`)
  8. ) ENGINE=InnoDB AUTO_INCREMENT=505 DEFAULT CHARSET=utf8mb4 COMMENT='商品关联销售规格';

数据使用

1,商品详情
思路:通过 SKU ID 查询 SPU 基础信息,通过 SPU 调取所有 SKU。
商城开发 商品属性与规格 - 图13
我们先来假设后台设置了一款手机存在以下几个SKU:
1)颜色:黑色,内存:16G
2)颜色:黑色,内存:64G
3)颜色:黑色,内存:128G
4)颜色:白色,内存:16G
对应的数据库存储结果如下,可参考上面的数据设计结构理解:
商品销售规格数据,product_specification 表:
商城开发 商品属性与规格 - 图14
商品销售规格选项数据,product_specification_option 表:
商城开发 商品属性与规格 - 图15
商品基础数据,product 表:
商城开发 商品属性与规格 - 图16
商品 SKU 数据,product_sku 表:
商城开发 商品属性与规格 - 图17
商品关联销售规格数据,product_to_specification 表:
商城开发 商品属性与规格 - 图18
上面的商品数据查询,主要难点在销售规格的数据组装上。所以,这里也提供下关于销售规格数据组装的示例代码:

示例代码使用 ThinkPHP5.1

  1. $sku = 1;
  2. // 当前 sku 信息
  3. $product_sku = Db::name('product_sku')->where('sku', $sku)->where('status', 1)->find();
  4. if (empty($product_sku)) abort(404);
  5. // 当前 sku 下的销售规格
  6. $current_specifications = Db::name('product_to_specification')->where('sku', $sku)->select();
  7. $current_specification_ids = array_column($current_specifications, 'specification_id');
  8. $current_specification_option_ids = array_column($current_specifications, 'specification_option_id');
  9. // 当前商品下的销售规格
  10. $product_to_specifications = Db::name('product_to_specification')->alias('product_to_specification')
  11. ->field('product_to_specification.*, product_specification.name as specification_name, product_specification_option.value as specification_option_value, product_sku.stock')
  12. ->leftJoin('product_specification', 'product_specification.id = product_to_specification.specification_id')
  13. ->leftJoin('product_specification_option', 'product_specification_option.id = product_to_specification.specification_option_id')
  14. ->leftJoin('product_sku', 'product_sku.sku = product_to_specification.sku')
  15. ->where('product_to_specification.product_id', $product_sku['product_id'])
  16. ->select();
  17. // 分配销售规格组
  18. $skus = [];
  19. foreach ($product_to_specifications as $key => $value) $skus[$value['sku']][] = $value;
  20. // 获取与当前销售规格有关联的sku
  21. // 获取与当前销售规格有关联的商品关联规格ID
  22. // 设置商品关联规格是否有效/可点击
  23. $have_product_skus = [];
  24. $have_product_to_specification_ids = [];
  25. foreach ($product_to_specifications as $key => $value) {
  26. if (count($current_specification_option_ids) > 1) {
  27. if (in_array($value['product_specification_option_id'], $current_specification_option_ids)) {
  28. $current_sku_specification_option_ids = array_column($skus[$value['sku']], 'product_specification_option_id');
  29. $the_same_date_count = array_intersect($current_sku_specification_option_ids, $current_specification_option_ids);
  30. if (count($the_same_date_count) >= count($current_specification_option_ids) - 1) {
  31. $have_product_skus[] = $value['sku'];
  32. $have_product_to_specification_ids[] = $value['id'];
  33. }
  34. }
  35. } else {
  36. $have_product_skus[] = $value['sku'];
  37. $have_product_to_specification_ids[] = $value['id'];
  38. }
  39. }
  40. foreach ($product_to_specifications as $key => $value) {
  41. $product_to_specifications[$key]['valid'] = 0;
  42. if (in_array($value['sku'], $have_product_skus)) {
  43. $product_to_specifications[$key]['valid'] = 1;
  44. }
  45. }
  46. // 组装数据
  47. $array = [];
  48. foreach ($product_to_specifications as $key => $value) {
  49. $array[$value['specification_id']]['specification_id'] = $value['specification_id'];
  50. $array[$value['specification_id']]['specification_name'] = $value['specification_name'];
  51. $array[$value['specification_id']]['options'][$value['specification_option_id']]['specification_option_id'] = $value['specification_option_id'];
  52. $array[$value['specification_id']]['options'][$value['specification_option_id']]['specification_option_value'] = $value['specification_option_value'];
  53. if ($value['valid'] == 1) {
  54. $array[$value['product_specification_id']]['options'][$value['product_specification_option_id']]['valid'] = $value['valid'];
  55. $array[$value['product_specification_id']]['options'][$value['product_specification_option_id']]['sku'] = $value['sku'];
  56. $array[$value['product_specification_id']]['options'][$value['product_specification_option_id']]['stock'] = $value['stock'];
  57. }
  58. if ($value['sku'] == $sku) $array[$value['specification_id']]['options'][$value['specification_option_id']]['selected'] = 1;
  59. }
  60. dd($array);

示例代码中的打印结果如下:

  1. array(2) {
  2. [1] => array(3) {
  3. ["specification_id"] => int(1)
  4. ["specification_name"] => string(6) "颜色"
  5. ["options"] => array(2) {
  6. [1] => array(5) {
  7. ["specification_option_id"] => int(1)
  8. ["specification_option_value"] => string(6) "黑色"
  9. ["valid"] => int(1)
  10. ["sku"] => int(3)
  11. ["stock"] => int(200)
  12. ["selected"] => int(1)
  13. }
  14. [2] => array(4) {
  15. ["specification_option_id"] => int(2)
  16. ["specification_option_value"] => string(6) "白色"
  17. }
  18. }
  19. }
  20. [2] => array(3) {
  21. ["specification_id"] => int(2)
  22. ["specification_name"] => string(6) "内存"
  23. ["options"] => array(3) {
  24. [4] => array(5) {
  25. ["specification_option_id"] => int(4)
  26. ["specification_option_value"] => string(3) "16G"
  27. ["valid"] => int(1)
  28. ["sku"] => int(4)
  29. ["stock"] => int(200)
  30. }
  31. [5] => array(5) {
  32. ["specification_option_id"] => int(5)
  33. ["specification_option_value"] => string(3) "64G"
  34. ["valid"] => int(1)
  35. ["sku"] => int(2)
  36. ["stock"] => int(200)
  37. ["selected"] => int(1)
  38. }
  39. [6] => array(5) {
  40. ["specification_option_id"] => int(6)
  41. ["specification_option_value"] => string(4) "128G"
  42. ["valid"] => int(1)
  43. ["sku"] => int(3)
  44. ["stock"] => int(200)
  45. }
  46. }
  47. }
  48. }

从打印结果来看,可用 selected,valid 字段控制选中与可点击的操作。
2,商品列表
思路:查询 SPU,关联 SPU 下的 SKU。商品链接默认为该 SPU 下的第一个 SKU。
商城开发 商品属性与规格 - 图19