01、课程目标
-
了解SPU与SKU数据结构设计思路
-
独立实现商品查询
-
独立实现商品新增后台
02、SPU与SKU:概念说明与表结构
1)概念说明
SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品
以图为例来看:
- 本页的 华为Mate10 就是一个商品集(SPU)
- 因为颜色、内存等不同,而细分出不同的Mate10,如亮黑色128G版。(SKU)
可以看出:
- SPU是一个抽象的商品集概念,为了方便后台的管理。
- SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU
2)SPU与SKU表结构分析
1)思考分析
弄清楚了SPU和SKU的概念区分,接下来我们一起思考一下该如何设计数据库表。
首先来看SPU,大家一起思考下SPU应该有哪些字段来描述?
id:主键
title:标题
description:描述
specification:规格
packaging_list:包装
after_service:售后服务
comment:评价
category_id:商品分类
brand_id:品牌
似乎并不复杂.
再看下SKU,大家觉得应该有什么字段?
id:主键
spu_id:关联的spu
price:价格
images:图片
stock:库存
颜色?
内存?
硬盘?
sku的特有属性也是变化的,不同商品,特有属性不一定相同,那么我们的表字段岂不是不确定?
sku的这个特有属性该如何设计呢?
2)SKU的特有属性
SPU中会有一些特殊属性,用来区分不同的SKU,我们称为SKU特有属性。如华为META10的颜色、内存属性。
不同种类的商品,一个手机,一个衣服,其SKU属性不相同。
同一种类的商品,比如都是衣服,SKU属性基本是一样的,都是颜色、尺码等。
这样说起来,似乎SKU的特有属性也是与分类相关的?事实上,仔细观察你会发现,SKU的特有属性是商品规格参数的一部分:
也就是说,我们没必要单独对SKU的特有属性进行设计,它可以看做是规格参数中的一部分。这样规格参数中的属性可以标记成两部分:
- spu下所有sku共享的规格属性(称为通用属性)
- spu下每个sku不同的规格属性(称为特有属性)
回忆一下之前我们设计的tb_spec_param表,是不是有一个字段,名为generic,标记通用和特有属性。就是为了这里使用。
这样以来,商品SKU表就只需要设计规格属性以外的其它字段了,规格属性由之前的规格参数表tb_spec_param
来保存。
但是,规格属性的值依然是需要与商品相关联的。
03、SPU与SKU:spu与spu_detail表结构分析
1)数据库表切分说明
-
横向切分【水平拆分】
一种情况是 数据量太大了【2000万左右】
一种情况是 历史数据使用的几率很低
-
纵向切分【垂直拆分】
一种情况是 表的字段太多了【50个左右】
一种情况是 表中有一些大字段,比如:blob,clob,增删改的效率低【long】,需要将这些大字段单独分离出去
2)表结构
spu表
CREATE TABLE `tb_spu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
`name` varchar(256) NOT NULL DEFAULT '' COMMENT '商品名称',
`sub_title` varchar(256) DEFAULT '' COMMENT '副标题,一般是促销信息',
`cid1` bigint(20) NOT NULL COMMENT '1级类目id',
`cid2` bigint(20) NOT NULL COMMENT '2级类目id',
`cid3` bigint(20) NOT NULL COMMENT '3级类目id',
`brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
`saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=183 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象性的商品,比如 iphone8';
spu_detail表
与我们前面分析的基本类似,但是似乎少了一些字段,比如商品描述。
我们做了表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail
CREATE TABLE `tb_spu_detail` (
`spu_id` bigint(20) NOT NULL,
`description` text COMMENT '商品描述信息',
`generic_spec` varchar(2048) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
`special_spec` varchar(1024) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
`packing_list` varchar(1024) DEFAULT '' COMMENT '包装清单',
`after_service` varchar(1024) DEFAULT '' COMMENT '售后服务',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。
需要注意的是这两个字段:generic_spec和special_spec。
3)spu中的规格参数:generic_spec字段
前面讲过规格参数与商品分类绑定,同一分类的商品,会有一套相同的规格参数key(规格参数模板),但是这个分类下每个商品的规格参数值都不相同,因此要满足下面几点:
- 我们有一个规格参数表,跟分类关联,保存的就是某分类下的规格参数模板。
- 我们还需要表,跟商品关联,保存某个商品,相关联的规格参数的值。
- 规格参数因为分成了通用规格参数和特有规格参数,因此规格参数值也需要分别于SPU和SKU关联:
- 通用的规格参数值与SPU关联。
- 特有规格参数值与SKU关联。
但是我们并没有增加新的表,来看下我们的 表如何存储这些信息:
generic_spec字段
如果要设计一张表,来表示spu中的通用规格属性的值,至少需要下面的字段:
spu_id:与哪个商品关联
param_id:是商品的哪个规格参数
value:具体的值
我们并没有这么设计。而是把与某个商品相关的规格属性值,直接保存到这个商品spu表中,因此这些规格属性关联的商品就一目了然,那么上述3个属性中的spu_id
就无需保存了,而剩下的就是param_id
和规格参数值了。两者刚好是一一对应关系,组成一个键值对。我们刚好可以用一个json结构来标示。
是也就是spuDetail表中的generic_spec
,其中保存通用规格参数信息的值:
整体来看:
json结构,其中都是键值对:
- key:对应的规格参数的
spec_param
的id - value:对应规格参数的值
4)spu中的规格参数:special_spec字段
我们说spu中只保存通用规格参数,那么为什么有多出了一个special_spec
字段呢?
以手机为例,品牌、操作系统等肯定是通用规格属性,内存、颜色等肯定是特有属性。
当你确定了一个SPU,比如小米的:红米4X,因为颜色内存等不同,会形成多个sku。如果把每个sku的颜色、内存等信息都整理一下,会形成下面的结果:
颜色:[香槟金, 樱花粉, 磨砂黑]
内存:[2G, 3G]
机身存储:[16GB, 32GB]
也就是说这里把一个spu下的每个sku的特有规格属性值聚合在了一起!这个就是special_spec字段了。
来看数据格式:
也是json结构:
- key:规格参数id
- value:spu属性的数组
那么问题来:为什么要在spu中把所有sku的规格属性聚合起来保存呢?
因为我们有时候需要把所有规格参数都查询出来,而不是只查询1个sku的属性。比如,商品详情页展示可选的规格参数时:
刚好符号我们的结构,这样页面渲染就非常方便了。
综上所述,spu与商品规格参数模板的关系如图所示:
04、SPU与SKU:sku表结构分析
1)表结构
CREATE TABLE `tb_sku` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
`spu_id` bigint(20) NOT NULL COMMENT 'spu id',
`title` varchar(256) NOT NULL COMMENT '商品标题',
`images` varchar(1024) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
`stock` int(8) DEFAULT '9999' COMMENT '库存',
`price` bigint(16) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
`indexes` varchar(32) DEFAULT '' COMMENT '特有规格属性在spu属性模板中的对应下标组合',
`own_spec` varchar(1024) DEFAULT '' COMMENT 'sku的特有规格参数键值对,json格式,反序列化时请使用linkedHashMap,保证有序',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
PRIMARY KEY (`id`),
KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27359021554 DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的 64g的iphone 8';
特别需要注意的是sku表中的indexes
字段和own_spec
字段。sku中应该保存特有规格参数的值,就在这两个字段中。
2)sku中的特有规格参数:indexes字段
在SPU表中,已经对特有规格参数及可选项进行了保存,结构如下:
{
"4": [
"香槟金",
"樱花粉",
"磨砂黑"
],
"12": [
"2GB",
"3GB"
],
"13": [
"16GB",
"32GB"
]
}
这些特有属性如果排列组合,会产生12个不同的SKU,而不同的SKU,其属性就是上面备选项中的一个。
比如:
- 红米4X,香槟金,2GB内存,16GB存储
- 红米4X,磨砂黑,2GB内存,32GB存储
你会发现,每一个属性值,对应于SPUoptions数组的一个选项,如果我们记录下角标,就是这样:
- 红米4X,0,0,0
- 红米4X,2,0,1
既然如此,我们是不是可以将不同角标串联起来,作为SPU下不同SKU的标示。这就是我们的indexes字段。
这个设计在商品详情页会特别有用:
当用户点击选中一个特有属性,你就能根据 角标快速定位到sku。
3)sku中的特有规格参数:own_spec字段
看结构:
{"4":"香槟金","12":"2GB","13":"16GB"}
保存的是特有属性的键值对。
SPU中保存的是可选项,但不确定具体的值,而SKU中的保存的就是具体的值。
05、商品查询:商品对象与表的关系
商品对象所需表:商品对象对应数据库中多张表
商品对象比较复杂,需要多张表组合起来,开发中,复杂的业务是会这样的,大家一定要捋清晰这些表的关系。
商品对象的创建需要六张表的数据。
06、商品查询:商品列表数据来源分析
接下来,我们实现商品管理的页面,先看下我们要实现的效果:
可以看出整体是一个table,然后有新增按钮。是不是跟之前写品牌管理很像?
点击页面菜单中的商品列表页面:
页面右侧是一个空白的数据表格:
看到浏览器发起已经发起了查询商品数据的请求:
因此接下来,我们编写接口即可。
那么这些列表的数据分别来自那些表?
列表中我们只能先展示这几个数据,商品表我们之前分析了来源于六张表,那在哪里展示呢?
我们可以在商品详情页去展示。
07、商品查询:商品模块准备工作
1)SPU的entity
package com.leyou.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("tb_spu")
public class Spu {
@TableId(type = IdType.AUTO)
private Long id;
private Long brandId;
private Long cid1;// 1级类目
private Long cid2;// 2级类目
private Long cid3;// 3级类目
private String name;// 商品名称
private String subTitle;// 子标题
private Boolean saleable;// 是否上架
private Date createTime;// 创建时间
private Date updateTime;// 最后修改时间
}
2)SpuDetail的entity
package com.leyou.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("tb_spu_detail")
public class SpuDetail {
@TableId(type = IdType.INPUT)
private Long spuId;// 对应的SPU的id
private String description;// 商品描述
private String specialSpec;// 商品特殊规格的名称及可选值模板
private String genericSpec;// 商品的全局规格属性
private String packingList;// 包装清单
private String afterService;// 售后服务
private Date createTime;// 创建时间
private Date updateTime;// 最后修改时间
}
3)Sku的entity
package com.leyou.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("tb_sku")
public class Sku {
@TableId(type = IdType.AUTO)
private Long id;
private Long spuId;
private String title;
private String images;
private Long price;
private Integer stock;
private String ownSpec;// 商品特殊规格的键值对
private String indexes;// 商品特殊规格的下标
private Boolean enable;// 是否有效,逻辑删除用
private Date createTime;// 创建时间
private Date updateTime;// 最后修改时间
}
4)SPU的DTO(*)
先分析:
-
请求方式:GET
-
请求路径:/spu/page
-
请求参数:
- page:当前页
- rows:每页大小
- key:过滤条件
- saleable:上架或下架
-
返回结果:通过页面能看出来,查询要展示的数据都是SPU数据,而且因为是分页查询,我们可以返回与之前品牌查询一样的PageResult。
-
要注意,页面展示的中需要是商品分类和品牌名称,而SPU表中中保存的是id,我们需要在DTO中处理这些字段。
我们可以对Spu拓展categoryName和brandName属性:
-
package com.leyou.item.dto;
import lombok.Data;
import java.util.Date;
/**
* 封装Spu的相关数据
*/
@Data
public class SpuDTO {
private Long id;
private Long brandId;
private Long cid1;// 1级类目
private Long cid2;// 2级类目
private Long cid3;// 3级类目
private String name;// 商品名称
private String subTitle;// 子标题
private Boolean saleable;// 是否上架
private Date createTime;// 创建时间
private Date updateTime;// 最后修改时间
//分类名称 格式:手机通讯/手机/手机
private String categoryName;
//品牌名称
private String brandName;
}
5)SPU的Mapper
package com.leyou.item.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.entity.Spu;
public interface SpuMapper extends BaseMapper<Spu> {
}
6)SpuDetail的Mapper
package com.leyou.item.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.entity.SpuDetail;
public interface SpuDetailMapper extends BaseMapper<SpuDetail> {
}
7)Sku的Mapper
package com.leyou.item.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.entity.Sku;
public interface SkuMapper extends BaseMapper<Sku> {
}
8)Goods商品的Service
package com.leyou.item.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.leyou.item.mapper.SkuMapper;
import com.leyou.item.mapper.SpuDetailMapper;
import com.leyou.item.mapper.SpuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 商品业务Service
*/
@Service
@Transactional
public class GoodsService{
@Autowired
private SpuMapper spuMapper;
@Autowired
private SpuDetailMapper spuDetailMapper;
@Autowired
private SkuMapper skuMapper;
}
9)Goods商品的Controller
package com.leyou.item.controller;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
/**
* 商品Controller
*/
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
}
08、商品查询:品牌和分类名称显示的准备工作
1)根据品牌id查询品牌对象
提供处理器
/**
* 根据id查询品牌
*/
@GetMapping("/brand/{id}")
public ResponseEntity<Brand> findBrandById(@PathVariable("id") Long id){ // @PathVariable接收URL的参数
Brand brand = brandService.findBrandById(id);
return ResponseEntity.ok(brand);
}
提供service
public Brand findBrandById(Long id) {
Brand brand = brandMapper.selectById(id);
if(brand==null){
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
return brand;
}
2)根据分类id的集合查询分类对象的列表
提供处理器
/**
* 根据分类ID查询分类集合
*/
@GetMapping("/category/list")
public ResponseEntity<List<Category>> findCategoriesByIds(@RequestParam("ids") List<Long> ids){
List<Category> categories = categoryService.findCategoriesByIds(ids);
return ResponseEntity.ok(categories);
}
提供service
public List<Category> findCategoriesByIds(List<Long> ids) {
List<Category> categories = categoryMapper.selectBatchIds(ids);
if(CollectionUtils.isEmpty(categories)){
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
}
return categories;
}
09、商品查询:Spu分页查询(*)
1)GoodsController添加方法
先分析:
- 请求方式:GET
- 请求路径:/spu/page
- 请求参数:
- page:当前页
- rows:每页大小
- key:过滤条件
- saleable:上架或下架
- 返回结果:PageResult
package com.leyou.item.controller;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 商品
*/
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
package com.leyou.item.controller;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 商品
*/
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
/**
* 分页查询商品
*/
@GetMapping("/spu/page")
public ResponseEntity<PageResult<SpuDTO>> spuPageQuery(
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "key",required = false) String key,
@RequestParam(value = "saleable",required = false) Boolean saleable
){
PageResult<SpuDTO> pageResult = goodsService.spuPageQuery(page,rows,key,saleable);
return ResponseEntity.ok(pageResult);
}
}
}
2)GoodsService添加方法
package com.leyou.item.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.leyou.common.pojo.PageResult;
import com.leyou.common.utils.BeanHelper;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.mapper.SkuMapper;
import com.leyou.item.mapper.SpuDetailMapper;
import com.leyou.item.mapper.SpuMapper;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import com.leyou.item.pojo.Spu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 商品业务
*/
@Service
@Transactional
public class GoodsService {
@Autowired
private SpuMapper spuMapper;
@Autowired
private SpuDetailMapper spuDetailMapper;
@Autowired
private SkuMapper skuMapper;
@Autowired
private BrandService brandService;
@Autowired
private CategoryService categoryService;
public PageResult<SpuDTO> spuPageQuery(Integer page, Integer rows, String key, Boolean saleable) {
//1.封装条件
//1.1 封装分页参数
IPage<Spu> iPage = new Page<>(page,rows);
//1.2 封装查询条件
QueryWrapper<Spu> queryWrapper = Wrappers.query();
//拼接条件
//SELECT * FROM tb_spu WHERE (NAME LIKE '%华为%' OR sub_title LIKE '%华为%') AND saleable=1
//处理key
if(StringUtils.isNotEmpty(key)){
// where name like '%xx%' or sub_title like '%xx%'
//and:该方法可以把一些拼接的条件(sql语句)作为一个整体提高sql执行优先级(给一些条件加括号)
queryWrapper.and(
i->
i.like("name",key)
.or()
.like("sub_title",key)
);
}
//处理saleable
if(saleable!=null){
// saleable=true/false
queryWrapper.eq("saleable",saleable);
}
//2.查询数据,获取结果
iPage = spuMapper.selectPage(iPage,queryWrapper);
//3.处理并返回结果
//3.1 取出所有Spu对象
List<Spu> spuList = iPage.getRecords();
//3.2 拷贝数据,从Spu对象拷贝到SpuDTO对象
List<SpuDTO> spuDTOList = BeanHelper.copyWithCollection(spuList,SpuDTO.class);
//3.3 封装分类名称和品牌名称
getCategoryNameAndBrandName(spuDTOList);
//3.4 封装PageResult
PageResult<SpuDTO> pageResult = new PageResult<>(iPage.getTotal(),iPage.getPages(),spuDTOList);
return pageResult;
}
/**
* 在spuDTOList集合中添加分类名称和品牌名称两个属性
* @param spuDTOList
*/
public void getCategoryNameAndBrandName(List<SpuDTO> spuDTOList) {
spuDTOList.forEach(spuDTO -> {
//1.处理品牌
Brand brand = brandService.findBrandById(spuDTO.getBrandId());
spuDTO.setBrandName(brand.getName());
//2.处理分类
//2.1 根据分类ID集合查询分类对象集合
List<Category> categoryList = categoryService.findCategoriesByIds(Arrays.asList(spuDTO.getCid1(), spuDTO.getCid2(), spuDTO.getCid3()));
//2.2 格式: 手机通讯/手机/手机
String categoryName = categoryList.stream().map(Category::getName).collect(Collectors.joining("/"));
spuDTO.setCategoryName(categoryName);
});
}
}
编写完成后,重启微服务,然后打开页面测试:
10、商品保存:业务分析
1)页面预览
当我们点击新增商品按钮:
就会出现一个弹窗:
里面把商品的数据分为了4部分来填写:
- 基本信息:主要是一些简单的文本数据,包含了SPU和SpuDetail的部分数据,如
- 商品分类:是SPU中的
cid1
,cid2
,cid3
属性 - 品牌:是spu中的
brandId
属性 - 标题:是spu中的
title
属性 - 子标题:是spu中的
subTitle
属性 - 售后服务:是SpuDetail中的
afterService
属性 - 包装列表:是SpuDetail中的
packingList
属性
- 商品分类:是SPU中的
- 商品描述:是SpuDetail中的
description
属性,数据较多,所以单独放一个页面 - 规格参数:商品规格信息,对应SpuDetail中的
genericSpec
属性 - SKU属性:spu下的所有Sku信息
也就是说这个页面包含了商品相关的三张表中的数据:
- tb_spu
- tb_spu_detail
- tb_sku
2)商品分类
商品分类的级联选框我们之前在品牌查询已经做过,是要根据分类的pid查询分类,所以这里的级联选框已经实现完成:
刷新页面,可以看到请求已经发出:
需要注意的是,这里选中以后会显示3级分类,因为数据库中保存的就是商品的1~3级类目。
注意:当选定了一个分类后,那么其实就已经确定了品牌的范围和规格参数的范围。
3)品牌选择
品牌也是一个下拉选框,不过其选项是不确定的,只有当用户选择了商品分类,才会把这个分类下的所有品牌展示出来。
所以页面编写了watch函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表。刷新页面,当选中一个分类时,可以看到请求发起:
接下来,我们只要编写后台接口,根据商品分类id,查询对应品牌即可。
11、商品保存:根据分类查询品牌
页面需要去后台查询品牌信息,我们自然需要提供:
1)controller
/**
* 根据分类id查询品牌
*/
@GetMapping("/brand/of/category")
public ResponseEntity<List<Brand>> findBrandsByCid(@RequestParam("id") Long id){
List<Brand> brands = brandService.findBrandsByCid(id);
return ResponseEntity.ok(brands);
}
2)service
public List<Brand> findBrandsByCid(Long id) {
//1.根据分类id查询品牌
List<Brand> brands = brandMapper.findBrandsByCid(id);
//2.处理结果
if(CollectionUtils.isEmpty(brands)){
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
return brands;
}
3)mapper
根据分类查询品牌有中间表,需要自己编写Sql:
package com.leyou.item.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface BrandMapper extends BaseMapper<Brand> {
public void saveCategoryAndBrand(@Param("bid") Long bid,@Param("cids") List<Long> cids);
/**
* 根据分类id查询品牌
* @param id
* @return
*/
@Select("SELECT b.* FROM tb_brand b,tb_category_brand cb WHERE b.`id` = cb.brand_id AND cb.category_id=#{categoryId}")
List<Brand> findBrandsByCid(Long id);
}
效果:
12、商品保存:商品描述及富文本编辑器
1)富文本编辑器介绍
标题、子标题等都是普通文本框,我们直接填写即可,没有需要特别注意的,商品描述信息比较复杂,而且图文并茂,甚至包括视频。
这样的内容,一般都会使用富文本编辑器。
什么是富文本编辑器?
百度百科:
通俗来说:富文本,就是比较丰富的文本编辑器。普通的框只能输入文字,而富文本还能给文字加颜色样式等。
富文本编辑器有很多,例如:KindEditor、Ueditor。但并不原生支持vue
但是我们今天要说的,是一款支持Vue的富文本编辑器:vue-quill-editor
2)Vue-Quill-Editor介绍
GitHub的主页:https://github.com/surmon-china/vue-quill-editor
Vue-Quill-Editor是一个基于Quill的富文本编辑器:Quill的官网
3)Vue-Quill-Editor使用指南
使用非常简单:
第一步:安装,使用npm命令:
npm install vue-quill-editor --save
第二步:加载,在js中引入:
全局使用:
import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'
const options = {}; /* { default global options } */
Vue.use(VueQuillEditor, options); // options可选
局部使用:
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import {quillEditor} from 'vue-quill-editor'
var vm = new Vue({
components:{
quillEditor
}
})
第三步:页面引用:
<quill-editor v-model="goods.spuDetail.description" :options="editorOption"/>
4)自定义富文本编辑器
不过这个组件有个小问题,就是图片上传的无法直接上传到后台,因此我们对其进行了封装,支持了图片的上传。
使用也非常简单:
<v-stepper-content step="2">
<v-editor v-model="goods.spuDetail.description" url="/upload/signature" needSignature/>
</v-stepper-content>
- url:是图片上传的路径或者上传阿里OSS时的签名路径,这里输入的是签名路径
- v-model:双向绑定,将富文本编辑器的内容绑定到goods.spuDetail.description
5)效果
13、商品保存:规格参数与SKU属性
1)规格参数
规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过watch监控来实现:
页面如下:
2)SKU属性
Sku属性是SPU下的每个商品的不同特征,如图:
当我们填写一些属性后,会在页面下方生成一个sku表格,大家可以计算下会生成多少个不同属性的Sku呢?
当你选择了上图中的这些选项时:
- 颜色共2种:夜空黑,绚丽红
- 内存共2种:4GB,6GB
- 机身存储1种:64GB
此时会产生多少种SKU呢? 应该是 2 * 2 * 1 = 4种,这其实就是在求笛卡尔积。
我们会在页面下方生成一个sku的表格:
这个表格中就包含了以上颜色内存的所有可能组合,剩下的价格等信息就需要用户自己来完成了。
注意,页面中的sku的图片上传,默认是上传到阿里云,当然可以根据自己情况修改上传到本地nginx。
最后我们可以根据填写号的特有规格参数属性,来选定最终我们要保存几个sku:
14、商品保存:修改SpuDTO对象
当我们发起保存提交的时候,页面向后台提交的数据如下:
我们可以修改SpuDTO,在里面添加两个属性,来接收当前所有提交的数据:
package com.leyou.item.dto;
import com.baomidou.mybatisplus.annotation.TableName;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 封装商品展示列表需要的数据
*/
@Data
public class SpuDTO{
private Long id;
private Long brandId;
private Long cid1;// 1级类目
private Long cid2;// 2级类目
private Long cid3;// 3级类目
private String name;// 商品名称
private String subTitle;// 子标题
private Boolean saleable;// 是否上架
private Date createTime;// 创建时间
private Date updateTime;// 最后修改时间
private String brandName;//品牌名称
private String categoryName;//分类名称
private SpuDetail spuDetail; //用于接收添加商品的SpuDetail数据
private List<Sku> skus; // 用于接收添加商品的SpuDetail数据
}
15、商品保存:完成商品保存逻辑
1)改造GoodsService
为批量添加Sku做准备
@Service
@Transactional
public class GoodsService extends ServiceImpl<SkuMapper, Sku> {
……
}
2)提供处理器
/**
* 添加商品
*/
@PostMapping("/goods")
public ResponseEntity<Void> saveGoods(@RequestBody SpuDTO spuDTO){
goodsService.saveGoods(spuDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
3)提供service
public void saveGoods(SpuDTO spuDTO) {
//1.保存spu表
//数据拷贝
try {
Spu spu = BeanHelper.copyProperties(spuDTO, Spu.class);
//商品默认下架
spu.setSaleable(false);
spuMapper.insert(spu);
//2.保存spuDetail表
SpuDetail spuDetail = spuDTO.getSpuDetail();
//必须记得设置spu的ID
spuDetail.setSpuId(spu.getId());
spuDetailMapper.insert(spuDetail);
//3.保存sku表
List<Sku> skus = spuDTO.getSkus();
skus.forEach(sku -> {
//必须记得设置spu的ID
sku.setSpuId(spu.getId());
//保存sku
//skuMapper.insert(sku);
});
//调用MyBatis-Plus的批量保存
saveBatch(skus);
} catch (Exception e) {
e.printStackTrace();
throw new LyException(ExceptionEnum.INSERT_OPERATION_FAIL);
}
}
如果使用批量增加,需要继承ServiceImpl类
4)重启服务器查看保存商品效果
16、商品修改:商品上下架操作
在商品详情页,每一个商品后面,都会有一个编辑按钮:
点击这个按钮,并没有打开商品编辑窗口,而是弹出了一个提示窗口:
已经上架的商品用户可能正在购买,所以不能修改。必须要先下架才可以。
1)页面请求
此时打开控制台,可以看到请求已经发出了:
请求方式:PUT
请求路径:/spu/saleable
参数有两个 :
- id:应该是spu的id
- saleable:布尔值,代表上架或下架
返回结果:应该是无
2)后台实现:Controller
接下来我们在服务端接收请求,并且修改spu的saleable属性。
需要注意的是,我们在修改spu的上下架状态时,无需修改sku的enable属性,sku的enable属性只与当前sku的库存量有关,如果库存大于0,就是有效,否则是无效。
/**
* 商品上下架
*/
@PutMapping("/spu/saleable")
public ResponseEntity<Void> updateSaleable(@RequestParam("id") Long id,@RequestParam("saleable") Boolean saleable){
goodsService.updateSaleable(id,saleable);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
3)后台实现:Service
public void updateSaleable(Long id, Boolean saleable) {
try {
Spu spu = new Spu();
spu.setId(id);//必须设置数据库存在ID
spu.setSaleable(saleable);
//修改状态
spuMapper.updateById(spu); // updateById: MyBatis-Plus底层自动判断非NULL值才更新
} catch (Exception e) {
e.printStackTrace();
throw new LyException(ExceptionEnum.UPDATE_OPERATION_FAIL);
}
}
17、门户网站:搭建门户网站
后台系统的内容暂时告一段落,有了商品,接下来我们就要在页面展示商品,给用户提供浏览和购买的入口,那就是我们的门户系统。
门户系统面向的是用户,安全性很重要,而且搜索引擎对于单页应用并不友好。因此我们的门户系统不再采用与后台系统类似的SPA(单页应用)。
依然是前后端分离,不过前端的页面会使用独立的html,在每个页面中使用vue来做页面渲染。
1)静态资源
webpack打包多页应用配置比较繁琐,项目结构也相对复杂。这里为了简化开发(毕竟我们不是专业的前端人员),我们不在使用webpack,而是直接编写原生的静态HTML。
将资料中的leyou-portal解压,并把结果赋值到工作空间的目录
链接:https://pan.baidu.com/s/1S_47cnOdsBrNRlu50cfwJQ
提取码:kp60
解压缩:
然后通过idea打开,可以看到项目结构:
2)live-server
简介
没有webpack,我们就无法使用webpack-dev-server运行这个项目,实现热部署。
所以,这里我们使用另外一种热部署方式:live-server
地址;https://www.npmjs.com/package/live-server
这是一款带有热加载功能的小型开发服务器。用它来展示你的HTML / JavaScript / CSS,但不能用于部署最终的网站。
安装和运行参数
安装,使用npm命令即可,这里建议全局安装,以后任意位置可用
npm install -g live-server
运行时,直接输入命令:
live-server
另外,你可以在运行命令后,跟上一些参数以配置:
--port=NUMBER
- 选择要使用的端口,默认值:PORT env var或8080--host=ADDRESS
- 选择要绑定的主机地址,默认值:IP env var或0.0.0.0(“任意地址”)--no-browser
- 禁止自动Web浏览器启动--browser=BROWSER
- 指定使用浏览器而不是系统默认值--quiet | -q
- 禁止记录--verbose | -V
- 更多日志记录(记录所有请求,显示所有侦听的IPv4接口等)--open=PATH
- 启动浏览器到PATH而不是服务器root--watch=PATH
- 用逗号分隔的路径来专门监视变化(默认值:观看所有内容)--ignore=PATH
- 要忽略的逗号分隔的路径字符串(anymatch -compatible definition)--ignorePattern=RGXP
-文件的正则表达式忽略(即.*\.jade
)(不推荐使用赞成--ignore
)--middleware=PATH
- 导出要添加的中间件功能的.js文件的路径; 可以是没有路径的名称,也可以是引用middleware
文件夹中捆绑的中间件的扩展名--entry-file=PATH
- 提供此文件(服务器根目录)代替丢失的文件(对单页应用程序有用)--mount=ROUTE:PATH
- 在定义的路线下提供路径内容(可能有多个定义)--spa
- 将请求从/ abc转换为/#/ abc(方便单页应用)--wait=MILLISECONDS
- (默认100ms)等待所有更改,然后重新加载--htpasswd=PATH
- 启用期待位于PATH的htpasswd文件的http-auth--cors
- 为任何来源启用CORS(反映请求源,支持凭证的请求)--https=PATH
- 到HTTPS配置模块的路径--proxy=ROUTE:URL
- 代理ROUTE到URL的所有请求--help | -h
- 显示简洁的使用提示并退出--version | -v
- 显示版本并退出
测试
我们进入leyou-portal目录,输入命令:
live-server --port=9002
3)在package.json中配置启动命令
初始化npm,其中-y表示一路yes安装下去
npm init -y
安装vue
npm install vue --save
配置启动脚本:
进入package.json文件,在script中添加启动脚本:
{
"name": "leyou-portal",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "live-server --port=9002"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vue": "^2.6.10"
}
}
以后可以用 npm run serve启动
4)域名访问
现在我们访问的是:http://127.0.0.1:9002
之前我们已经配置好了9002端口对应的域名:http://www.leyou.com
所以今后我们可以直接使用域名访问
5)在网关中给门户网站跨域权限
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins:
- "http://manage.leyou.com"
- "http://www.leyou.com"
allowedHeaders:
- "*"
allowCredentials: true
maxAge: 360000
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTIONS
- HEAD