掌握SKU和SPU关系及表设计(三) 一、掌握SKU和SPU关系 二、数据库表的设计

电商系统中涉及到商品时必然会遇到的几个概念,SPU、SKU、单品等。彻底搞懂和明白了这几个概念对设计商品表是十分必要的前提条件。

1.1、SPU:标准化产品单元

SPU = Standard Product Unit (标准化产品单元),SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

1.2、SKU:库存量单位

SKU=stock keeping unit(库存量单位) SKU即库存进出计量的单位(买家购买、商家进货、供应商备货、工厂生产都是依据SKU进行的)。SKU是物理上不可分割的最小存货单元。也就是说一款商品,可以根据SKU来确定具体的货物存量。如一件M码(四个尺码:S码、M码、L码、X码)的粉色(三种颜色:粉色、黄色、黑色)Zara女士风衣,其中M码、粉色就是一组SKU的组合。SKU在生成时, 会根据属性生成相应的笛卡尔积,根据一组SKU可以确定商品的库存情况,那么上面的Zara女士风衣一共有4 * 3 = 12个SKU组合。M码+粉色这两个属性组合被称为一组SKU、因为SKU是物理上不可分割的最小存货单元,单凭尺寸或者颜色是没有办法确认这款商品的库存情况。同理商家进货补货也是通过SKU来完成的,试问淘宝店家跟供货商说我要100件红色女士风衣?供应商知道该怎么给他备货吗?显然是不知道的。因为还欠缺了另外的一个销售属性【尺码】。

1.3、spu和sku都是属性值的集合

SPU 属性(不会影响到库存和价格的属性, 又叫关键属性)

Oppo R17这是商品的SPU,但Oppo R17只是一个名词,单纯的理解这个名词是没有意义的。

Oppo R17是这个商品的SPU,这里的SPU是一组商品的属性组合。如下所示

【硬件参数】:

CPU 型号:高通骁龙™ 670

CPU 频率:2.0GHz

核心数:八核

处理器位数:64 位

GPU 型号:Adreno™ 615

电池容量:3500mAh(典型值)*

【尺寸】:

长:约 157.5mm

宽:约 74.9mm

厚:约 7.5mm

重:约 182g

以及包括【摄像头】、【显示屏】、【操作系统】等等这些属性构成了一个SPU、这个SPU属性组合的名称叫做Oppo R17。

spu : 包含在每一部 oppo r17 的属性集合, 与商品是一对一的关系(产地:中国, 毛重:182g...)

SKU 属性(会影响到库存和价格的属性, 又叫销售属性)

sku : 影响价格和库存的 属性集合, 与商品是多对一的关系,即一个商品有多个SKU。

如流光蓝(三种颜色:流光蓝、霓光紫、霓光渐变色)+8G+128G(两种配置:8G+128G、6G+128G)。

即Oppo R17有一个SPU、6种SKU。

单品 : 国人对于SKU的另外一种叫法。

1.4、SKU和商品之间的关系

1)SKU(或称商品SKU)指的是商品子实体。

2)商品SPU和商品SKU是包含关系,一个商品SPU包含若干个商品SKU子实体,商品SKU从属于商品SPU。

3)SKU不是编码,每个SKU包含一个唯一编码,即SKU Code,用于管理。

上面是从网上搞的,这么看可能还不是太明白,简单的程序思想说,如果所以手机信息存在一张表里,但对于同一家手机来说,厂家,分类及品牌都是相同的信息,假设这此相同的信息每条占10M内存,如果数据多了,这就形成了数据冗余,冗余数据占了不必要的内存那就要优化了,因为对电商来说内存是很宝贵的,那这时SPU概念就出来了,SPU概念就是把这些共性的数据抽取来,这一抽后,本来占30M的内存我只用存一条,那内存就节约了2/3,对于不内的信息比喻内存、颜色、价格的我单独再存一张表中这样性能就好很多,不同的属性就是SKU。这大白话一说大概就明白了。

二、数据库表的设计

前面说了SPU和SKU的关系和概念,下面就来学着电商网站设计一下SPU及SKU表,表结构如下

掌握SKU和SPU关系及表设计(三)
一、掌握SKU和SPU关系
二、数据库表的设计

SPU:

-- ----------------------------
DROP TABLE IF EXISTS `spu`;
CREATE TABLE `spu` (
  `id` varchar(60) NOT NULL COMMENT '主键',
  `name` varchar(100) DEFAULT NULL COMMENT 'SPU名',
  `intro` varchar(200) DEFAULT NULL COMMENT '简介',
  `brand_id` int(11) DEFAULT NULL COMMENT '品牌ID',
  `category_one_id` int(20) DEFAULT NULL COMMENT '一级分类',
  `category_two_id` int(10) DEFAULT NULL COMMENT '二级分类',
  `category_three_id` int(10) DEFAULT NULL COMMENT '三级分类',
  `images` varchar(1000) DEFAULT NULL COMMENT '图片列表',
  `after_sales_service` varchar(50) DEFAULT NULL COMMENT '售后服务',
  `content` longtext COMMENT '介绍',
  `attribute_list` varchar(3000) DEFAULT NULL COMMENT '规格列表',
  `is_marketable` int(1) DEFAULT '0' COMMENT '是否上架,0已下架,1已上架',
  `is_delete` int(1) DEFAULT '0' COMMENT '是否删除,0:未删除,1:已删除',
  `status` int(1) DEFAULT '0' COMMENT '审核状态,0:未审核,1:已审核,2:审核不通过',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SKU;

DROP TABLE IF EXISTS `sku`;
CREATE TABLE `sku` (
  `id` varchar(60) NOT NULL COMMENT '商品id',
  `name` varchar(200) NOT NULL COMMENT 'SKU名称',
  `price` int(20) NOT NULL DEFAULT '1' COMMENT '价格(分)',
  `num` int(10) DEFAULT '100' COMMENT '库存数量',
  `image` varchar(200) DEFAULT NULL COMMENT '商品图片',
  `images` varchar(2000) DEFAULT NULL COMMENT '商品图片列表',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `spu_id` varchar(60) DEFAULT NULL COMMENT 'SPUID',
  `category_id` int(10) DEFAULT NULL COMMENT '类目ID',
  `category_name` varchar(200) DEFAULT NULL COMMENT '类目名称',
  `brand_id` int(11) DEFAULT NULL COMMENT '品牌id',
  `brand_name` varchar(100) DEFAULT NULL COMMENT '品牌名称',
  `sku_attribute` varchar(200) DEFAULT NULL COMMENT '规格',
  `status` int(1) DEFAULT '1' COMMENT '商品状态 1-正常,2-下架,3-删除',
  PRIMARY KEY (`id`),
  KEY `cid` (`category_id`),
  KEY `status` (`status`),
  KEY `updated` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';

2.1、商品发布流程分析

大家在购物时会发现,当选择商品的类别时,下面的商品名称及商品的图片和商品店家信息什么都是动态变化的。这里面就设计到设计了,一般在设计时在发布商品前,需要先选择发布商品所属分类,分类严格定义为3级分类。

分类表设计如下:

DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
  `id` int(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
  `name` varchar(50) DEFAULT NULL COMMENT '分类名称',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  `parent_id` int(20) DEFAULT NULL COMMENT '上级ID',
  PRIMARY KEY (`id`),
  KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11182 DEFAULT CHARSET=utf8 COMMENT='商品类目';
分类选择完成后,需要加载品牌,品牌加载并非一次性加载完成,而是根据选择的分类进行加载。分类品牌关系表:
DROP TABLE IF EXISTS `category_brand`;
CREATE TABLE `category_brand` (
  `category_id` int(11) NOT NULL COMMENT '分类ID',
  `brand_id` int(11) NOT NULL COMMENT '品牌ID',
  PRIMARY KEY (`brand_id`,`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
品牌表:
DROP TABLE IF EXISTS `brand`;
CREATE TABLE `brand` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
  `name` varchar(100) NOT NULL COMMENT '品牌名称',
  `image` varchar(1000) DEFAULT '' COMMENT '品牌图片地址',
  `initial` varchar(1) DEFAULT '' COMMENT '品牌的首字母',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='品牌表';
当选择分类后,会加载分类对应的属性。
分类属性表:
DROP TABLE IF EXISTS `category_attr`;
CREATE TABLE `category_attr` (
  `category_id` int(11) NOT NULL,
  `attr_id` int(11) NOT NULL COMMENT '属性分类表',
  PRIMARY KEY (`category_id`,`attr_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
属性表:
DROP TABLE IF EXISTS `sku_attribute`;
CREATE TABLE `sku_attribute` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(50) DEFAULT NULL COMMENT '属性名称',
  `options` varchar(2000) DEFAULT NULL COMMENT '属性选项',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;

上面表设计好了,下面就将它的实体类全部写好

掌握SKU和SPU关系及表设计(三)
一、掌握SKU和SPU关系
二、数据库表的设计

@Data
@NoArgsConstructor
@AllArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "brand")
public class Brand implements Serializable {
    //    品牌ID
    //    MyBatisPlus主键策略注解
    @TableId(type= IdType.AUTO)
    private Integer id;
    //    品牌名字
    private String name;
    //    品牌图片
    private String image;
    //    品牌首字母
    private String initial;
    //    品牌排序
    private Integer sort;

    //分类
    @TableField(exist = false)
    private List<Category> categories;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "category")
public class Category implements Serializable {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer sort;
    private Integer parentId;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "category_attr")
public class CategoryAttr {

    @TableField
    private Integer categoryId;

    @TableField
    private Integer attrId;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "category_brand")
public class CategoryBrand {

    @TableField
    private Integer categoryId;

    @TableField
    private Integer brandId;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    // Spu
    private Spu spu;
    // Sku
    private List<Sku> skus;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "sku")
public class Sku {

    @TableId(type = IdType.ASSIGN_ID)
    private String id;
    private String name;
    private Integer price;
    private Integer num;
    private String image;
    private String images;
    private Date createTime;
    private Date updateTime;
    private String spuId;
    private Integer categoryId;
    private String categoryName;
    private Integer brandId;
    private String brandName;
    private String skuAttribute;
    private Integer status;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "sku_attribute")
public class SkuAttribute implements Serializable {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private String options;
    private Integer sort;

    //对应分类
    @TableField(exist = false)
    private List<Category> categories;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "spu")
public class Spu {

    @TableId(type = IdType.ASSIGN_ID)
    private String id;
    private String name;
    private String intro;
    private Integer brandId;
    private Integer categoryOneId;
    private Integer categoryTwoId;
    private Integer categoryThreeId;
    private String images;
    private String afterSalesService;
    private String content;
    private String attributeList;
    private Integer isMarketable;
    private Integer isDelete;
    private Integer status;
}

接下来写分类表的Mapper

掌握SKU和SPU关系及表设计(三)
一、掌握SKU和SPU关系
二、数据库表的设计

接着写业务层代码,这块代码全在spring-cloud-goods-service项目中

public interface CategoryService extends IService<Category> {

    /***
     * 根据分类父ID查询所有子类
     */
    List<Category> findByParentId(Integer pid);
}
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService {

    @Resource
    private CategoryMapper categoryMapper;

    /***
     * 根据分类父ID查询所有子类
     */
    @Override
    public List<Category> findByParentId(Integer pid) {
        //条件封装对象
        QueryWrapper<Category> queryWrapper = new QueryWrapper<Category>();
        queryWrapper.eq("parent_id",pid);
        return categoryMapper.selectList(queryWrapper);
    }
}

最后写上控制层代码

@RestController
@RequestMapping(value = "/category")
@CrossOrigin
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /****
     * 根据分类父ID查询子分类
     */
    @GetMapping(value = "/parent/{id}")
    public RespResult<List<Category>> findByParentId(@PathVariable("id")Integer id){
        return RespResult.ok(categoryService.findByParentId(id));
    }

}
品牌需要根据分类进行加载,当用户选择第3级分类的时候,加载品牌,品牌数据需要经过category_brand 表关联查询。所以接下来写品牌表的逻辑。实现步骤是这样的先从category_brand中指定分类对应的品牌ID集合然后从brand查出品牌集合
public interface BrandMapper extends BaseMapper<Brand> {
    /****
     * 1、根据分类ID查询品牌ID集合
     */
    @Select("SELECT brand_id FROM category_brand WHERE category_id=#{id}")
    List<Integer> queryBrandIds(Integer id);
}
public interface BrandService extends IService<Brand> {



    /***
     * 根据分类ID查询品牌集合
     */
    List<Brand> queryByCategoryId(Integer id);
}
@Service
public class BrandServiceImpl extends ServiceImpl<BrandMapper,Brand> implements BrandService {


    @Resource
    private BrandMapper brandMapper;


    /****
     * 根据分类ID查询品牌集合
     * @param id:分类ID
     * @return
     */
    @Override
    public List<Brand> queryByCategoryId(Integer id) {
        //根据分类ID查询品牌ID集合
        List<Integer> brandIds = brandMapper.queryBrandIds(id);
        //根据品牌ID集合查询品牌集合
        if(brandIds!=null && brandIds.size()>0){
            return brandMapper.selectList(new QueryWrapper<Brand>().in("id",brandIds));
        }
        return null;
    }
}
@RestController
@RequestMapping(value = "/brand")
@CrossOrigin
public class BrandController {

    @Autowired
    private BrandService brandService;



    /****
     * 根据分类ID查询品牌集合
     */
    @GetMapping(value = "/category/{pid}")
    public RespResult<List<Brand>>  categoryBrands(@PathVariable(value = "pid")Integer pid){
        List<Brand> brands = brandService.queryByCategoryId(pid);
        return RespResult.ok(brands);
    }
}

分类和品牌搞定的,接下来按电商页面的风格就是查找属性了,因为属性也需要根据分类查询,所以可以按照如下思路实现:

1、先从category_attr根据分类ID查询出当前分类拥有的属性ID集合
2、从sku_attribute中查询属性集合
有了思路接下来就是写代码了
public interface SkuAttributeMapper extends BaseMapper<SkuAttribute> {

    /****
     * 1、根据分类ID查询属性ID集合
     * 2、根据属性ID集合查询属性集合
     */
    @Select("select * from sku_attribute where id IN(SELECT attr_id FROM category_attr WHERE category_id=#{id})")
    List<SkuAttribute> queryByCategoryId(Integer id);

}
public interface SkuAttributeService extends IService<SkuAttribute> {


    /***
     * 根据分类ID查询属性加集合
     */
    List<SkuAttribute> queryList(Integer id);
}
@Service
public class SkuAttributeServiceImpl extends ServiceImpl<SkuAttributeMapper,SkuAttribute> implements SkuAttributeService {

    @Autowired
    private SkuAttributeMapper skuAttributeMapper;

    /*****
     * 根据分类ID查询属性集合
     * @param id
     * @return
     */
    @Override
    public List<SkuAttribute> queryList(Integer id) {
        return skuAttributeMapper.queryByCategoryId(id);
    }
}
@RestController
@RequestMapping(value = "/skuAttribute")
@CrossOrigin
public class SkuAttributeController {

    @Autowired
    private SkuAttributeService skuAttributeService;

    /*****
     * 根据分类ID查询属性集合
     */
    @GetMapping(value = "/category/{id}")
    public RespResult<List<SkuAttributeController>> categorySkuAttributeList(@PathVariable(value = "id")Integer id){
        List<SkuAttribute> skuAttributes = skuAttributeService.queryList(id);
        return RespResult.ok(skuAttributes);
    }
}

上面的步骤搞完后,接下来就是产品的发布保存了,现在想要了解的是在产品保存时,哪些东西需要保存,首先,商品发布保存时一定存在Sku和Spu,因此后端能有一个对象同时能接到Spu和多个Sku,方法有很多种,可以直接在Spu中写一个 List<Sku> ,但这种方法不推荐,按照对象设计原则,对一个对象进行扩展时,尽量避免对原始对象造成改变,因此可以使用复合类,可以创建一个 Prodcut 类,该类中有Spu也有 List<Sku> ,代码如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    // Spu
    private Spu spu;
    // Sku
    private List<Sku> skus;
}
public interface SkuMapper extends BaseMapper<Sku> {
}
public interface SpuMapper extends BaseMapper<Spu> {
}
public interface SpuService extends IService<Spu> {

    /****
     * 产品保存
     */
    void saveProduct(Product product);
}
 
@Service
public class SpuServiceImpl extends ServiceImpl<SpuMapper, Spu> implements SpuService {

    @Resource
    private SpuMapper spuMapper;

    @Resource
    private SkuMapper skuMapper;

    @Resource
    private CategoryMapper categoryMapper;

    @Resource
    private BrandMapper brandMapper;
    @Override
    public void saveProduct(Product product) {
//1.保存Spu
        Spu spu = product.getSpu();


            spu.setIsMarketable(1); //已上架
            spu.setIsDelete(0); //未删除
            spu.setStatus(1);   //审核已通过
            spuMapper.insert(spu);


        //2.保存List<Sku>
        Date date = new Date();
        //查询分类
        Category category = categoryMapper.selectById(spu.getCategoryThreeId());
        //查询品牌
        Brand brand = brandMapper.selectById(spu.getBrandId());
        for (Sku sku : product.getSkus()) {
            //SKU名称
            String name = spu.getName();
            Map<String,String> skuattrMap = JSON.parseObject(sku.getSkuAttribute(),Map.class);
            for (Map.Entry<String, String> entry : skuattrMap.entrySet()) {
                name+="  "+entry.getValue();
            }
            sku.setName(name);
            //创建时间
            sku.setCreateTime(date);
            //修改时间
            sku.setUpdateTime(date);
            //分类ID
            sku.setCategoryId(spu.getCategoryThreeId());
            //分类名字,这个是需要查询得到的
            sku.setBrandName(brand.getName());
            //品牌ID
            sku.setBrandId(spu.getBrandId());
            //品牌名字
            sku.setCategoryName(category.getName());
            //spuid
            sku.setSpuId(spu.getId());
            //状态 商品状态 1-正常,2-下架,3-删除
            sku.setStatus(1);

            //添加
            skuMapper.insert(sku);
        }
    }
}
@RestController
@RequestMapping(value = "/spu")
@CrossOrigin
public class SpuController {

    @Autowired
    private SpuService spuService;

    /*****
     * 产品保存
     */
    @PostMapping(value = "/save")
    public RespResult save(@RequestBody Product product){
        spuService.saveProduct(product);
        return RespResult.ok();
    }
}

上面是产品的保存功能,但是不管是什么平台,后台都是支持内容的修改的,电商产品平台也一样,也具有修改功能修改代码其实也就是在上面代码改点东西而已,为减少接口我偷下懒,用一个概念偷换,因为增加的数据Id一定是空的,但是修改的数据他的id是有值的,就用这个概念来共用一个接口来实现修改功能;修改下SpuServiceImpl类

@Service
public class SpuServiceImpl extends ServiceImpl<SpuMapper, Spu> implements SpuService {

    @Resource
    private SpuMapper spuMapper;

    @Resource
    private SkuMapper skuMapper;

    @Resource
    private CategoryMapper categoryMapper;

    @Resource
    private BrandMapper brandMapper;
    @Override
    public void saveProduct(Product product) {
        //1.保存Spu
        Spu spu = product.getSpu();

        if(StringUtils.isEmpty(spu.getId())){
            spu.setIsMarketable(1); //已上架
            spu.setIsDelete(0); //未删除
            spu.setStatus(1);   //审核已通过
            spuMapper.insert(spu);
        }else{
            //修改
            spuMapper.updateById(spu);
            //删除Sku集合
            skuMapper.delete(new QueryWrapper<Sku>().eq("spu_id",spu.getId()));
        }

        //2.保存List<Sku>
        Date date = new Date();
        Category category = categoryMapper.selectById(spu.getCategoryThreeId());
        Brand brand = brandMapper.selectById(spu.getBrandId());
        for (Sku sku : product.getSkus()) {
            //SKU名称
            String name = spu.getName();
            Map<String,String> skuattrMap = JSON.parseObject(sku.getSkuAttribute(),Map.class);
            for (Map.Entry<String, String> entry : skuattrMap.entrySet()) {
                name+="  "+entry.getValue();
            }
            sku.setName(name);
            //创建时间
            sku.setCreateTime(date);
            //修改时间
            sku.setUpdateTime(date);
            //分类ID
            sku.setCategoryId(spu.getCategoryThreeId());
            //分类名字
            sku.setBrandName(brand.getName());
            //品牌ID
            sku.setBrandId(spu.getBrandId());
            //品牌名字
            sku.setCategoryName(category.getName());
            //spuid
            sku.setSpuId(spu.getId());
            //状态 商品状态 1-正常,2-下架,3-删除
            sku.setStatus(1);

            //添加
            skuMapper.insert(sku);
        }
    }
}

git源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/spring-cloud-alibaba1.git