一、掌握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