一、掌握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)); } }
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); } }
分类和品牌搞定的,接下来按电商页面的风格就是查找属性了,因为属性也需要根据分类查询,所以可以按照如下思路实现:
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