zoukankan      html  css  js  c++  java
  • 掌握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表,表结构如下

    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;

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

    @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

    接着写业务层代码,这块代码全在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

     
    这短短的一生我们最终都会失去,不妨大胆一点,爱一个人,攀一座山,追一个梦
  • 相关阅读:
    gzip:stdin:not in gzip format的解决办法
    Oracle内存结构:SGA PGA UGA
    Oracle存储结构
    数据库的启动和关闭过程
    Linux上实现Windows的SQLPlus保存SQL历史记录功能
    oracle数据库,怎么给已有数据的表添加自增字段
    Centos 6下安装Oracle 11gR2
    CentOS 6下 Oracle11gR2 设置开机自启动
    从join on和where执行顺序认识T-SQL查询处理执行顺序
    虚拟机安装VBoxAdditions增强功能
  • 原文地址:https://www.cnblogs.com/xing1/p/14924873.html
Copyright © 2011-2022 走看看