商城系统中的商品信息肯定避免不了SPU和SKU这两个概念,本节就给大家详细介绍下这块的内容
SPU = Standard Product Unit (标准化产品单元)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
SKU=stock keeping unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
举个例子:
购买手机的时候,你可以选择华为Mate40系列手机,Mate40系列手机的生产制造商是华为,品牌是华为,手机分类也是华为,不过Mate40系列手机有多款,比如 Mate40 、Mate40 Pro 、 Mate40 Pro +,每款手机的架构也不一样,颜色也不一定一样,那么这个例子中哪些是Spu哪些是Sku呢?
Spu:
手机系列:Mate40系列
厂家:华为
品牌:华为
分类:手机
Sku:
价格
颜色
网络格式
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:
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='商品表';
商品发布流程如下:
1)分类选择
发布商品前,需要先选择发布商品所属分类,分类严格定义为3级分类。
分类表:
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='商品类目';
2)选择品牌
分类选择完成后,需要加载品牌,品牌加载并非一次性加载完成,而是根据选择的分类进行加载。
分类品牌关系表:
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;
品牌表:
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='品牌表';
3)属性加载
当选择分类后,加载分类对应的属性。
分类属性表:
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;
属性表:
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;
对应的Bean我们已经提前写好,如下
package com.bobo.vip.mall.goods.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/*****
* @Author: 波波
* @Description: 云商城
****/
@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;
}
package com.bobo.vip.mall.goods.model;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/*****
* @Author: 波波
* @Description: 云商城
****/
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "category_attr")
public class CategoryAttr {
@TableField
private Integer categoryId;
@TableField
private Integer attrId;
}
/*****
* @Author: 波波
* @Description: 云商城
****/
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "category_brand")
public class CategoryBrand {
@TableField
private Integer categoryId;
@TableField
private Integer brandId;
}
/*****
* @Author: 波波
* @Description: 云商城
****/
@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;
}
/*****
* @Author: 波波
* @Description: 云商城
****/
@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;
}
分类功能需要实现按照父ID查询,最开始初始化加载的是顶级父类,parent_id=0,后面每次点击的时候都根据传入的id查询子分类。
1)Mapper
创建com.bobo.vip.mall.goods.mapper.CategoryMapper
,代码如下:
public interface CategoryMapper extends BaseMapper<Category> {
}
2)Service
接口:com.bobo.vip.mall.goods.service.CategoryService
代码如下:
public interface CategoryService extends IService<Category> {
/**
* 根据父ID查询子分类
* @param pid
* @return
*/
List<Category> queryByParentId(Integer pid);
}
实现类:com.bobo.vip.mall.goods.service.impl.CategoryServiceImpl
代码如下:
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
/***
* 根据父ID查询子分类
* @param pid
* @return
*/
@Override
public List<Category> queryByParentId(Integer pid) {
//条件封装
QueryWrapper<Category> queryWrapper = new QueryWrapper<Category>();
queryWrapper.eq("parent_id",pid);
return categoryMapper.selectList(queryWrapper);
}
}
3)Controller
创建com.bobo.vip.mall.goods.controller.CategoryController
代码如下;
@RestController
@RequestMapping(value = "/category")
@CrossOrigin
public class CategoryController {
@Autowired
private CategoryService categoryService;
/****
* 根据父ID查询子分类
*/
@GetMapping(value = "/parent/{pid}")
public RespResult<List<Category>> list(@PathVariable(value = "pid")Integer pid){
List<Category> categories = categoryService.queryByParentId(pid);
return RespResult.ok(categories);
}
}
品牌需要根据分类进行加载,当用户选择第3级分类的时候,加载品牌,品牌数据需要经过category_brand
表关联查询。
我们可以按照如下步骤实现:
1、查询category_brand中指定分类对应的品牌ID集合
2、从brand查出品牌集合
1)Mapper
修改com.bobo.vip.mall.goods.mapper.BrandMapper
,添加根据分类ID查询品牌ID集合:
//根据分类ID查询品牌集合
@Select("select brand_id from category_brand where category_id=#{id}")
List<Integer> queryBrandIds(Integer id);
2)Service
接口:com.bobo.vip.mall.goods.service.BrandService
中添加根据分类ID查询品牌集合方法
//根据分类ID查询品牌
List<Brand> queryByCategoryId(Integer id);
实现类:com.bobo.vip.mall.goods.service.impl.BrandServiceImpl
/***
* 根据分类ID查询品牌
* @param id
* @return
*/
@Override
public List<Brand> queryByCategoryId(Integer id) {
//查询分类ID对应的品牌集合
List<Integer> brandIds = brandMapper.queryBrandIds(id);
//根据品牌ID集合查询品牌信息
List<Brand> brands = brandMapper.selectBatchIds(brandIds);
return brands;
}
3)Controller
修改com.bobo.vip.mall.goods.controller.BrandController
添加根据分类ID查询品牌集合
/****
* 根据分类ID查询品牌
*/
@GetMapping(value = "/category/{id}")
public RespResult<List<Brand>> categoryBrands(@PathVariable(value = "id")Integer id){
List<Brand> brands = brandService.queryByCategoryId(id);
return RespResult.ok(brands);
}
属性也称为规格,属性也需要根据分类查询,我们可以按照如下思路实现:
1、先从category_attr根据分类ID查询出当前分类拥有的属性ID集合
2、从sku_attribute中查询属性集合
1)Mapper
创建com.bobo.vip.mall.goods.mapper.SkuAttributeMapper
实现根据分类ID查询属性信息。
/***
* 根据分类ID查询属性集合
* @param id
* @return
*/
@Select("SELECT * FROM sku_attribute WHERE id IN(SELECT attr_id FROM category_attr WHERE category_id=#{id})")
List<SkuAttribute> queryByCategoryId(Integer id);
2)Service
接口:com.bobo.vip.mall.goods.service.SkuAttributeService
添加根据分类ID查询属性集合方法
//根据分类ID查询属性集合
List<SkuAttribute> queryList(Integer id);
实现类:com.bobo.vip.mall.goods.service.impl.SkuAttributeServiceImpl
添加如下实现方法
/***
* 根据分类ID查询属性集合
* @param id
* @return
*/
@Override
public List<SkuAttribute> queryList(Integer id) {
return skuAttributeMapper.queryByCategoryId(id);
}
3)Controller
创建com.bobo.vip.mall.goods.controller.SkuAttributeController
,添加如下方法
/***
* 根据分类ID查询
*/
@GetMapping(value = "/category/{id}")
public RespResult<SkuAttribute> categoryAttributeList(@PathVariable(value = "id")Integer id){
//根据分类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;
}
添加商品的时候,我们需要保存Spu,同时需要添加多个Sku。我们可以在华为商城中看看真实电商中Sku名字特征,每次点击不同属性的时候,前部分名字一样,只是将名字中的规格替换了,也就是说Sku的名字其实是组合成的,一部分是Spu的一部分是Sku的,可以进行组合。
1)名字分析
添加商品的时候,会将商品的属性传入后台,格式如下,如果把规格名字添加到名字中,那就是华为商城中的效果了,我们可以这么做,把属性解析成Map,然后每个属性值添加到商品名字中即可。
{"适合人群":"有一定java基础的人","书籍分类":"软件编程"}
2)实现代码
Mapper
com.bobo.vip.mall.goods.mapper.SpuMapper
代码如下:
public interface SpuMapper extends BaseMapper<Spu> {
}
com.bobo.vip.mall.goods.mapper.SkuMapper
代码如下:
public interface SkuMapper extends BaseMapper<Sku> {
}
Service
com.bobo.vip.mall.goods.service.SpuService
中添加产品方法如下
public interface SpuService extends IService<Spu> {
//保存商品
void saveProduct(Product product);
}
com.bobo.vip.mall.goods.service.impl.SpuServiceImpl
中添加产品方法如下:
@Service
public class SpuServiceImpl extends ServiceImpl<SpuMapper,Spu> implements SpuService {
@Autowired
private SkuMapper skuMapper;
@Autowired
private SpuMapper spuMapper;
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private BrandMapper brandMapper;
// 保存商品
@Override
public void saveProduct(Product product) {
//Spu
Spu spu = product.getSpu();
//上架
spu.setIsMarketable(1);
//未删除
spu.setIsDelete(0);
//状态
spu.setStatus(1);
//添加
spuMapper.insert(spu);
//查询三级分类
Category category = categoryMapper.selectById(spu.getCategoryThreeId());
//查询品牌
Brand brand = brandMapper.selectById(spu.getBrandId());
//当前时间
Date now = new Date();
//新增Sku集合
for (Sku sku : product.getSkus()) {
//设置名字
String skuName = spu.getName();
Map<String,String> attrMap = JSON.parseObject(sku.getSkuAttribute(), Map.class);
for (Map.Entry<String, String> entry : attrMap.entrySet()) {
skuName+= " "+entry.getValue();
}
sku.setName(skuName);
//设置图片
sku.setImages(spu.getImages());
//设置状态
sku.setStatus(1);
//设置类目ID
sku.setCategoryId(spu.getCategoryThreeId());
//设置类目名称
sku.setCategoryName(category.getName());
//设置品牌ID
sku.setBrandId(brand.getId());
//设置品牌名称
sku.setBrandName(brand.getName());
//设置Spuid
sku.setSpuId(spu.getId());
//时间
sku.setCreateTime(now);
sku.setUpdateTime(now);
//增加
skuMapper.insert(sku);
}
}
}
Controller
创建com.bobo.vip.mall.goods.controller.SpuController
,添加产品代码如下:
@Autowired
private SpuService spuService;
/***
* 保存
*/
@PostMapping(value = "/save")
public RespResult save(@RequestBody Product product){
//保存
spuService.saveProduct(product);
return RespResult.ok();
}
产品修改其实和产品添加几乎一致,只需要做小改动即可,实现步骤如下:
1、如果Spu的id值不为空,说明是修改操作
2、如果是修改操作,先删除之前对应的Sku集合
3、其他流程和添加商品一致
修改com.bobo.vip.mall.goods.service.impl.SpuServiceImpl
的save
方法,代码如下:
在这里插入图片描述
源码如下:
@Override
public void saveProduct(Product product) {
//Spu
Spu spu = product.getSpu();
//如果ID为空,则增加
if(StringUtils.isEmpty(spu.getId())){
//上架
spu.setIsMarketable(1);
//未删除
spu.setIsDelete(0);
//状态
spu.setStatus(1);
//添加
spuMapper.insert(spu);
}else{
//ID 不为空,则修改
spuMapper.updateById(spu);
//删除之前的Sku记录
skuMapper.delete(new QueryWrapper<Sku>().eq("spu_id",spu.getId()));
}
//查询三级分类
Category category = categoryMapper.selectById(spu.getCategoryThreeId());
//查询品牌
Brand brand = brandMapper.selectById(spu.getBrandId());
//当前时间
Date now = new Date();
//新增Sku集合
for (Sku sku : product.getSkus()) {
//设置名字
String skuName = spu.getName();
Map<String,String> attrMap = JSON.parseObject(sku.getSkuAttribute(), Map.class);
for (Map.Entry<String, String> entry : attrMap.entrySet()) {
skuName+= " "+entry.getValue();
}
sku.setName(skuName);
//设置图片
sku.setImages(spu.getImages());
//设置状态
sku.setStatus(1);
//设置类目ID
sku.setCategoryId(spu.getCategoryThreeId());
//设置类目名称
sku.setCategoryName(category.getName());
//设置品牌ID
sku.setBrandId(brand.getId());
//设置品牌名称
sku.setBrandName(brand.getName());
//设置Spuid
sku.setSpuId(spu.getId());
//时间
sku.setCreateTime(now);
sku.setUpdateTime(now);
//增加
skuMapper.insert(sku);
}
}
我们可以发现个问题,刚才写的很多增删改查代码都比较简单,比较枯燥,重复写一些类的创建、单表增删改查非常类,而创建对象和单标操作的代码,在开发中几乎占用了开发时间的80%,如果能够用工具生成就可以大大节省我们开发成本了。
AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
学习网址 https://baomidou.com/guide/generator.html
1)引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
2)代码生成
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java"); // 文件输出路径
gc.setAuthor("bobo"); //作者
gc.setOpen(false); //生成之后是否打开目录
gc.setIdType(IdType.NONE); //主键策略
gc.setServiceName("%sService"); //名字设置 %s是占位符,可以理解成类的名字
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://192.168.100.140:3306/shop_goods?useUnicode=true&useSSL=false&characterEncoding=utf8");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("mall-goods");
pc.setParent("com.bobo.code");
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel); //驼峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel); //驼峰命名
strategy.setEntityLombokModel(true); //是否使用Lombok
strategy.setRestControllerStyle(true); //是否生成RestController
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id"); //公共字段定义
strategy.setControllerMappingHyphenStyle(true); //驼峰转连字符
strategy.setTablePrefix(pc.getModuleName() + "_"); //表前缀
mpg.setStrategy(strategy);
mpg.execute();
}
效果
有红色的提示是因为没有引入依赖,我们可以把生成的相关内容拷贝到合适的项目位置即可。