0.学习目标
- 独立实现商品新增后台
- 独立实现商品编辑后台
- 独立搭建前台系统页面
1.商品新增
1.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信息
对应到页面中的四个stepper-content
:
1.2.弹窗事件
弹窗是一个独立组件:
并且在Goods组件中已经引用它:
并且在页面中渲染:
在新增商品
按钮的点击事件中,改变这个dialog
的show
属性:
1.3.基本数据
我们先来看下基本数据:
1.3.1.商品分类
商品分类信息查询我们之前已经做过,所以这里的级联选框已经实现完成:
刷新页面,可以看到请求已经发出:
1.3.2.品牌选择
页面
品牌也是一个下拉选框,不过其选项是不确定的,只有当用户选择了商品分类,才会把这个分类下的所有品牌展示出来。
所以页面编写了watch函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表:
"goods.categories": {
deep: true,
handler(val) {
// 判断商品分类是否存在,存在才查询
if (val && val.length > 0) {
// 根据分类查询品牌
this.$http
.get("/item/brand/cid/" + this.goods.categories[2].id)
.then(({ data }) => {
this.brandOptions = data;
});
刷新页面,可以看到请求发起:
接下来,我们只要编写后台接口,根据商品分类id,查询对应品牌即可。
后台接口
页面需要去后台查询品牌信息,我们自然需要提供:
controller
/**
* 根据分类查询品牌
* @param cid
* @return
*/
@GetMapping("cid/{cid}")
public ResponseEntity<List<Brand>> queryBrandByCategory(@PathVariable("cid") Long cid) {
List<Brand> list = this.brandService.queryBrandByCategory(cid);
if(list == null){
new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(list);
}
service
public List<Brand> queryBrandByCategory(Long cid) {
return this.brandMapper.queryByCategoryId(cid);
}
mapper
根据分类查询品牌有中间表,需要自己编写Sql:
@Select("SELECT b.* FROM tb_category_brand cb LEFT JOIN tb_brand b ON cb.brand_id = b.id WHERE cb.category_id = #{cid}")
List<Brand> queryByCategoryId(Long cid);
1.4.商品规格参数
规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过watch监控来实现:
可以看到这里是根据商品分类id查询规格参数:SpecParam。我们之前写过一个根据gid(分组id)来查询规格参数的接口,我们可以对其进行扩展:
改造查询规格参数接口
我们在原来的根据 gid(规格组id)查询规格参数的接口上,添加一个参数:cid,即商品分类id。
等一下, 考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件:
@GetMapping("/params")
public ResponseEntity<List<SpecParam>> querySpecParam(
@RequestParam(value="gid", required = false) Long gid,
@RequestParam(value="cid", required = false) Long cid,
@RequestParam(value="searching", required = false) Boolean searching,
@RequestParam(value="generic", required = false) Boolean generic
){
List<SpecParam> list =
this.specificationService.querySpecParams(gid,cid,searching,generic);
if(list == null || list.size() == 0){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(list);
}
改造service:
public List<SpecParam> querySpecParams(Long gid, Long cid, Boolean searching, Boolean generic) {
SpecParam param = new SpecParam();
param.setGroupId(gid);
param.setCid(cid);
param.setSearching(searching);
param.setGeneric(generic);
return this.specParamMapper.select(param);
}
如果param中有属性为null,则不会吧属性作为查询条件,因此该方法具备通用性,即可根据gid查询,也可根据cid查询。
测试
刷新页面测试:
1.5.SKU信息
Sku属性是SPU下的每个商品的不同特征,如图:
当我们填写一些属性后,会在页面下方生成一个sku表格,大家可以计算下会生成多少个不同属性的Sku呢?
当你选择了上图中的这些选项时:
- 颜色共2种:夜空黑,绚丽红
- 内存共2种:4GB,6GB
- 机身存储1种:64GB
此时会产生多少种SKU呢? 应该是 2 * 2 * 1 = 4种,这其实就是在求笛卡尔积。
我们会在页面下方生成一个sku的表格:
1.7.页面表单提交
在sku列表的下方,有一个提交按钮:
并且绑定了点击事件:
点击后会组织数据并向后台提交:
提交:
点击提交,查看控制台提交的数据格式:
- 整体是一个json格式数据,包含Spu表所有数据:
- brandId:品牌id
- cid1、cid2、cid3:商品分类id
- subTitle:副标题
- title:标题
- spuDetail:是一个json对象,代表商品详情表数据
- afterService:售后服务
- description:商品描述
- packingList:包装列表
- specialSpec:sku规格属性模板
- genericSpec:通用规格参数
- skus:spu下的所有sku数组,元素是每个sku对象:
- title:标题
- images:图片
- price:价格
- stock:库存
- ownSpec:特有规格参数
- indexes:特有规格参数的下标
1.7.后台实现
实体类
Spu
@Table(name = "tb_spu")
public class Spu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long brandId;
private Long cid1;// 1级类目
private Long cid2;// 2级类目
private Long cid3;// 3级类目
private String title;// 标题
private String subTitle;// 子标题
private Boolean saleable;// 是否上架
private Boolean valid;// 是否有效,逻辑删除用
private Date createTime;// 创建时间
private Date lastUpdateTime;// 最后修改时间
}
SpuDetail
@Table(name="tb_spu_detail")
public class SpuDetail {
@Id
private Long spuId;// 对应的SPU的id
private String description;// 商品描述
private String specTemplate;// 商品特殊规格的名称及可选值模板
private String specifications;// 商品的全局规格属性
private String packingList;// 包装清单
private String afterService;// 售后服务
}
Sku
@Table(name = "tb_sku")
public class Sku {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long spuId;
private String title;
private String images;
private Long price;
private String ownSpec;// 商品特殊规格的键值对
private String indexes;// 商品特殊规格的下标
private Boolean enable;// 是否有效,逻辑删除用
private Date createTime;// 创建时间
private Date lastUpdateTime;// 最后修改时间
@Transient
private Integer stock;// 库存
}
注意:这里保存了一个库存字段,在数据库中是另外一张表保存的,方便查询。
Stock
@Table(name = "tb_stock")
public class Stock {
@Id
private Long skuId;
private Integer seckillStock;// 秒杀可用库存
private Integer seckillTotal;// 已秒杀数量
private Integer stock;// 正常库存
}
Controller
四个问题:
-
请求方式:POST
-
请求路径:/goods
-
请求参数:Spu的json格式的对象,spu中包含spuDetail和Sku集合。这里我们该怎么接收?我们之前定义了一个SpuBo对象,作为业务对象。这里也可以用它,不过需要再扩展spuDetail和skus字段:
public class SpuBo extends Spu { @Transient String cname;// 商品分类名称 @Transient String bname;// 品牌名称 @Transient SpuDetail spuDetail;// 商品详情 @Transient List<Sku> skus;// sku列表 }
-
返回类型:无
代码:
/**
* 新增商品
* @param spu
* @return
*/
@PostMapping
public ResponseEntity<Void> saveGoods(@RequestBody Spu spu) {
try {
this.goodsService.save(spu);
return new ResponseEntity<>(HttpStatus.CREATED);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
注意:通过@RequestBody注解来接收Json请求
Service
这里的逻辑比较复杂,我们除了要对SPU新增以外,还要对SpuDetail、Sku、Stock进行保存
@Transactional
public void save(SpuBo spu) {
// 保存spu
spu.setSaleable(true);
spu.setValid(true);
spu.setCreateTime(new Date());
spu.setLastUpdateTime(spu.getCreateTime());
this.spuMapper.insert(spu);
// 保存spu详情
spu.getSpuDetail().setSpuId(spu.getId());
this.spuDetailMapper.insert(spu.getSpuDetail());
// 保存sku和库存信息
saveSkuAndStock(spu.getSkus(), spu.getId());
}
private void saveSkuAndStock(List<Sku> skus, Long spuId) {
for (Sku sku : skus) {
if (!sku.getEnable()) {
continue;
}
// 保存sku
sku.setSpuId(spuId);
// 初始化时间
sku.setCreateTime(new Date());
sku.setLastUpdateTime(sku.getCreateTime());
this.skuMapper.insert(sku);
// 保存库存信息
Stock stock = new Stock();
stock.setSkuId(sku.getId());
stock.setStock(sku.getStock());
this.stockMapper.insert(stock);
}
}
Mapper
都是通用Mapper,略