title: 乐优商城学习笔记七-商品管理(添加商品)
     
     date: 2019-04-17 16:18:10
     
     tags:
     
     – 乐优商城
     
     – java
     
     – springboot
     
     categories:
     
     – 乐优商城
    
     0.学习目标
    
- 独立实现商品新增后台
- 独立实现商品编辑后台
- 独立搭建前台系统页面
     1.商品新增
    
     1.1.页面预览
    
当我们点击新增商品按钮:
 
      就会出现一个弹窗:
[图片上传失败…(image-55dc49-1555491859890)]
里面把商品的数据分为了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
     
     :
    
[图片上传失败…(image-4fa092-1555491859890)]
     1.2.弹窗事件
    
弹窗是一个独立组件:
[图片上传失败…(image-591a50-1555491859890)]
并且在Goods组件中已经引用它:
[图片上传失败…(image-465e8e-1555491859890)]
并且在页面中渲染:
[图片上传失败…(image-44bb64-1555491859890)]
     在
     
      新增商品
     
     按钮的点击事件中,改变这个
     
      dialog
     
     的
     
      show
     
     属性:
    
[图片上传失败…(image-4e78e8-1555491859890)]
[图片上传失败…(image-d90975-1555491859890)]
     1.3.基本数据
    
我们先来看下基本数据:
[图片上传失败…(image-1e3aeb-1555491859890)]
     1.3.1.商品分类
    
商品分类信息查询我们之前已经做过,所以这里的级联选框已经实现完成:
[图片上传失败…(image-211bab-1555491859890)]
刷新页面,可以看到请求已经发出:
[图片上传失败…(image-47d990-1555491859890)]
[图片上传失败…(image-4d6430-1555491859890)]
[图片上传失败…(image-4a702e-1555491859890)]
     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;
            });
刷新页面,可以看到请求发起:
[图片上传失败…(image-478a3f-1555491859890)]
接下来,我们只要编写后台接口,根据商品分类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查询。
测试
 
      刷新页面测试:
[图片上传失败…(image-abff9f-1555491859890)]
     1.5.SKU信息
    
Sku属性是SPU下的每个商品的不同特征,如图:
 
      当我们填写一些属性后,会在页面下方生成一个sku表格,大家可以计算下会生成多少个不同属性的Sku呢?
当你选择了上图中的这些选项时:
- 颜色共2种:夜空黑,绚丽红
- 内存共2种:4GB,6GB
- 机身存储1种:64GB
此时会产生多少种SKU呢? 应该是 2 * 2 * 1 = 4种,这其实就是在求笛卡尔积。
我们会在页面下方生成一个sku的表格:
 
      
     1.7.页面表单提交
    
在sku列表的下方,有一个提交按钮:
[图片上传失败…(image-3f3a7a-1555491859890)]
并且绑定了点击事件:
[图片上传失败…(image-8f8986-1555491859890)]
点击后会组织数据并向后台提交:
[图片上传失败…(image-4f782d-1555491859890)]
提交:
[图片上传失败…(image-b00b0a-1555491859890)]
点击提交,查看控制台提交的数据格式:
[图片上传失败…(image-619613-1555491859890)]
- 
      整体是一个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,略
 
