第十三章:商品管理

此博客用于个人学习,来源于网上,对知识点进行一个整理。

1. 商品新增:

把商品的数据分为了4部分来填写:

  • 基本信息:主要是一些简单的文本数据,包含了 SPU 和 SpuDetail 的部分数据,如
    • 商品分类:是 SPU 中的 cid1 , cid2 , cid3 属性
    • 品牌:是 spu 中的 brandId 属性
    • 标题:是 spu 中的 title 属性
    • 子标题:是 spu 中的 subTitle 属性
    • 售后服务:是 SpuDetail 中的 afterService 属性
    • 包装列表:是 SpuDetail 中的 packingList 属性
  • 商品描述:是 SpuDetail 中的 description 属性,数据较多,所以单独放一个页面
  • 规格参数:商品规格信息,对应SpuDetail中的 genericSpec 属性
  • SKU 属性:spu 下的所有 Sku 信息

对应到页面中的四个 stepper-content :

在这里插入图片描述

1.1 弹窗事件:

弹窗是一个独立组件,并且在 Goods 组件中已经引用它:

在这里插入图片描述
并且在页面中渲染:

在这里插入图片描述
在新增商品按钮的点击事件中,改变这个 dialog 的 show 属性:

在这里插入图片描述
在这里插入图片描述

1.2 基本数据:

商品分类信息查询我们之前已经做过,所以这里的级联选框已经实现完成:

在这里插入图片描述
1)品牌选择:

品牌也是一个下拉选框,不过其选项是不确定的,只有当用户选择了商品分类,才会把这个分类下的所有品牌展示出来。所以页面编写了 watch 函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表:

在这里插入图片描述
选择商品分类后,可以看到请求发起。接下来,我们只要编写后台接口,根据商品分类 id,查询对应品牌即可。

2)后台接口:

请求方式:GET

请求路径:/brand/cid/{cid}

请求参数:cid

响应数据:品牌集合

BrandController:

/**
 * 根据分类id查询品牌列表
 * @param cid
 * @return
 */
@GetMapping("cid/{cid}")
public ResponseEntity<List<Brand>> queryBrandsByCid(@PathVariable("cid")Long cid){
    List<Brand> brands = this.brandService.queryBrandsByCid(cid);
    if (CollectionUtils.isEmpty(brands)){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(brands);
}

BrandService:

/**
 * 根据分类id查询品牌列表
 * @param cid
 * @return
 */
public List<Brand> queryBrandsByCid(Long cid) {
    return brandMapper.selectBrandsByCid(cid);
}

BrandMapper:根据分类查询品牌有中间表,需要自己编写 Sql。

@Select("SELECT * FROM tb_brand a INNER JOIN tb_category_brand b on a.id = b.brand_id where b.category_id=#{cid}")
List<Brand> selectBrandsByCid(Long cid);

剩余的几个属性:标题、子标题等都是普通文本框,我们直接填写即可,没有需要特别注意的。

1.3 商品描述:

商品描述信息比较复杂,而且图文并茂,甚至包括视频,这样的内容,一般都会使用富文本编辑器。富文本,就是比较丰富的文本编辑器。普通的框只能输入文字,而富文本还能给文字加颜色样式等。

这里使用一个基于 Quill 的富文本编辑器的 Vue-Quill-Editor。

1)使用:

全局引入:

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"/>

2)自定义的富文本编辑器:

这个组件有个小问题,就是图片上传的无法直接上传到后台,因此我们对其进行了封装,支持了图片的上传。

<v-stepper-content step="2">
    <v-editor v-model="goods.spuDetail.description" upload-url="/upload/image"/>
</v-stepper-content>
  • upload-url:是图片上传的路径
  • v-model:双向绑定,将富文本编辑器的内容绑定到 goods.spuDetail.description

1.4 商品规格参数:

规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过 watch 监控来实现:

在这里插入图片描述
可以看到这里是根据商品分类 id 查询规格参数:SpecParam。之前写过一个根据 gid(分组id)来查询规格参数的接口,我们接下来完成根据分类 id 查询规格参数,接下来改造查询规格参数接口。

我们在原来的根据 gid(规格组id)查询规格参数的接口上,添加一个参数:cid,即商品分类id。考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件:

/**
 * 根据条件查询规格参数
 * @param gid
 * @return
 */
@GetMapping("params")
public ResponseEntity<List<SpecParam>> queryParams(
        @RequestParam(value = "gid",required = false)Long gid,
        @RequestParam(value = "cid",required = false)Long cid,
        @RequestParam(value = "generic",required = false)Boolean generic,
        @RequestParam(value = "searching",required = false)Boolean searching
){
    List<SpecParam> params = this.specificationService.queryParamsByGid(gid,cid,generic,searching);
    if (CollectionUtils.isEmpty(params)){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(params);
}

改造 SpecificationService:

/**
 * 根据gid查询规格参数
 * @param gid
 * @return
 */
public List<SpecParam> queryParams(Long gid, Long cid, Boolean generic, Boolean searching) {
    SpecParam record = new SpecParam();
    record.setGroupId(gid);
    record.setCid(cid);
    record.setGeneric(generic);
    record.setSearching(searching);
    return this.specParamMapper.select(record);
}

如果 param 中有属性为 null,则不会把属性作为查询条件,因此该方法具备通用性,即可根据 gid 查询,也可根据 cid 查询。

1.5 页面表单提交:

Sku 属性是 SPU 下的每个商品的不同特征,当我们填写一些属性后,会在页面下方生成一个 sku 表格。

在 sku 列表的下方,有一个提交按钮,并且绑定了点击事件:

在这里插入图片描述
点击后会组织数据并向后台提交:

submit() {
      // 表单校验。
      if(!this.$refs.basic.validate){
        this.$message.error("请先完成表单内容!");
      }
      // 先处理goods,用结构表达式接收,除了categories外,都接收到goodsParams中
      const {
        categories: [{ id: cid1 }, { id: cid2 }, { id: cid3 }],
        ...goodsParams
      } = this.goods;
      // 处理规格参数
      const specs = {};
      this.specs.forEach(({ id,v }) => {
        specs[id] = v;
      });
      // 处理特有规格参数模板
      const specTemplate = {};
      this.specialSpecs.forEach(({ id, options }) => {
        specTemplate[id] = options;
      });
      // 处理sku
      const skus = this.skus
        .filter(s => s.enable)
        .map(({ price, stock, enable, images, indexes, ...rest }) => {
          // 标题,在spu的title基础上,拼接特有规格属性值
          const title = goodsParams.title + " " + Object.values(rest).map(v => v.v).join(" ");
          const obj = {};
          Object.values(rest).forEach(v => {
            obj[v.id] = v.v;
          });
          return {
            price: this.$format(price), // 价格需要格式化
            stock,
            indexes,
            enable,
            title, // 基本属性
            images: images ? images.join(",") : '', // 图片
            ownSpec: JSON.stringify(obj) // 特有规格参数
          };
        });
      Object.assign(goodsParams, {
        cid1,
        cid2,
        cid3, // 商品分类
        skus // sku列表
      });
      goodsParams.spuDetail.genericSpec = JSON.stringify(specs);
      goodsParams.spuDetail.specialSpec = JSON.stringify(specTemplate);

      // 提交到后台
      this.$http({
        method: this.isEdit ? "put" : "post",
        url: "/item/goods",
        data: goodsParams
      })
        .then(() => {
          // 成功,关闭窗口
          this.$emit("close");
          // 提示成功
          this.$message.success("保存成功了");
        })
        .catch(() => {
          this.$message.error("保存失败!");
        });
    }

点击提交,查看控制台提交的数据格式,整体是一个 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.6 后台实现:

1)实体类:

SPU 和 SpuDetail 实体类已经添加过,添加 Sku 和 Stock 对象:

@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;// 库存

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getSpuId() {
        return spuId;
    }

    public void setSpuId(Long spuId) {
        this.spuId = spuId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImages() {
        return images;
    }

    public void setImages(String images) {
        this.images = images;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

    public String getOwnSpec() {
        return ownSpec;
    }

    public void setOwnSpec(String ownSpec) {
        this.ownSpec = ownSpec;
    }

    public String getIndexes() {
        return indexes;
    }

    public void setIndexes(String indexes) {
        this.indexes = indexes;
    }

    public Boolean getEnable() {
        return enable;
    }

    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getLastUpdateTime() {
        return lastUpdateTime;
    }

    public void setLastUpdateTime(Date lastUpdateTime) {
        this.lastUpdateTime = lastUpdateTime;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }
}

注意:这里保存了一个库存字段,在数据库中是另外一张表保存的,方便查询。

@Table(name = "tb_stock")
public class Stock {
    @Id
    private Long skuId;
    private Integer seckillStock;// 秒杀可用库存
    private Integer seckillTotal;// 已秒杀数量
    private Integer stock;// 正常库存

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public Integer getSeckillStock() {
        return seckillStock;
    }

    public void setSeckillStock(Integer seckillStock) {
        this.seckillStock = seckillStock;
    }

    public Integer getSeckillTotal() {
        return seckillTotal;
    }

    public void setSeckillTotal(Integer seckillTotal) {
        this.seckillTotal = seckillTotal;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }
}

2)GoodsController:

  • 请求方式:POST

  • 请求路径:/goods

  • 请求参数:Spu 的 json 格式的对象,spu 中包含 spuDetail 和 Sku 集合,在编写逻辑之前,先对 SpuBo 进行扩展:

public class SpuBo extends Spu {

    private String cname;

    private String bname;

    private SpuDetail spuDetail;

    private List<Sku> skus;

    public SpuDetail getSpuDetail() {
        return spuDetail;
    }

    public void setSpuDetail(SpuDetail spuDetail) {
        this.spuDetail = spuDetail;
    }

    public List<Sku> getSkus() {
        return skus;
    }

    public void setSkus(List<Sku> skus) {
        this.skus = skus;
    }

    public String getCname() {
        return cname;
    }

    public void setCname(String cname) {
        this.cname = cname;
    }

    public String getBname() {
        return bname;
    }

    public void setBname(String bname) {
        this.bname = bname;
    }
}
  • 返回类型:无

在 GoodsController 中添加新增商品的代码,通过 @RequestBody 注解来接收 Json 请求。

/**
 * 更新商品信息
 * @param spuBo
 * @return
 */
@PutMapping("goods")
public ResponseEntity<Void> updateGoods(@RequestBody SpuBo spuBo){
    this.goodService.updateGoods(spuBo);
    return ResponseEntity.noContent().build();
}

3)GoodsService:

除了要对 SPU 新增以外,还要对 SpuDetail、Sku、Stock 进行保存。

/**
 * 新增商品
 * @param spuBo
 */
@Transactional
public void saveGoods(SpuBo spuBo) {
    //先新增spu
    spuBo.setId(null);
    spuBo.setSaleable(true);
    spuBo.setValid(true);
    spuBo.setCreateTime(new Date());
    spuBo.setLastUpdateTime(spuBo.getCreateTime());
    this.spuMapper.insertSelective(spuBo);

    //再去新增spuDetail
    SpuDetail spuDetail = spuBo.getSpuDetail();
    spuDetail.setSpuId(spuBo.getId());
    this.spuDetailMapper.insertSelective(spuDetail);

    saveSkuAndStock(spuBo);

    sendMsg("insert",spuBo.getId());
}

private void saveSkuAndStock(SpuBo spuBo) {
    spuBo.getSkus().forEach(sku -> {
        //新增sku
        sku.setId(null);
        sku.setSpuId(spuBo.getId());
        sku.setCreateTime(new Date());
        sku.setLastUpdateTime(sku.getCreateTime());
        this.skuMapper.insertSelective(sku);
        //新增stock
        Stock stock = new Stock();
        stock.setSkuId(sku.getId());
        stock.setStock(sku.getStock());
        this.stockMapper.insertSelective(stock);
    });
}

2. 商品修改:

2.1 编辑按钮点击事件:

在商品详情页,每一个商品后面,都会有一个编辑按钮。点击这个按钮,就会打开一个商品编辑窗口,我们看下它所绑定的点击事件:(在item/Goods.vue)

在这里插入图片描述
对应的方法:

在这里插入图片描述
可以看到这里发起了两个请求,在查询商品详情和 sku 信息。因为在商品列表页面,只有 spu 的基本信息:id、标题、品牌、商品分类等。比较复杂的商品详情(spuDetail) 和 sku 信息都没有,编辑页面要回显数据,就需要查询这些内容。

因此,接下来我们就编写后台接口,提供查询服务接口。

2.2 查询 SpuDetail 接口:

1)GoodsController:

  • 请求方式:GET
  • 请求路径:/spu/detail/{id}
  • 请求参数:id,应该是 spu 的 id
  • 返回结果:SpuDetail 对象
/**
 * 根据spuId查询spuDetail
 * @param spuId
 * @return
 */
@GetMapping("spu/detail/{spuId}")
public ResponseEntity<SpuDetail> querySpuDetailBySpuId(@PathVariable("spuId")Long spuId){
    SpuDetail spuDetail = this.goodService.querySpuDatilBySpuId(spuId);
    if (spuDetail == null){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(spuDetail);
}

2)GoodsService:

/**
 * 根据spuId查询spuDetail
 * @param spuId
 * @return
 */
public SpuDetail querySpuDatilBySpuId(Long spuId) {
    return this.spuDetailMapper.selectByPrimaryKey(spuId);
}

2.3 查询 sku:

  • 请求方式:Get
  • 请求路径:/sku/list
  • 请求参数:id,应该是 spu 的 id
  • 返回结果:sku 的集合

1)GoodsController:

/**
 * 根据spuId查询sku集合
 * @param spuId
 * @return
 */
@GetMapping("spu/list")
public ResponseEntity<List<Sku>> querySkusBySpuId(@RequestParam("id")Long spuId){
    List<Sku> skus = this.goodService.querySkusBySpuId(spuId);
    if (CollectionUtils.isEmpty(skus)){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(skus);
}

2)GoodsService:需要注意的是,为了页面回显方便,我们一并把 sku 的库存 stock 也查询出来。

/**
 * 根据spuId查询sku集合,为了页面回显方便,我们一并把sku的库存stock也查询出来
 * @param spuId
 * @return
 */
public List<Sku> querySkusBySpuId(Long spuId) {
    Sku record = new Sku();
    record.setSpuId(spuId);
    List<Sku> skus = this.skuMapper.select(record);
    //库存信息保存在stock表里面
    skus.forEach(sku -> {
        Stock stock = this.stockMapper.selectByPrimaryKey(sku.getId());
        sku.setStock(stock.getStock());
    });
    return skus;
}

2.4 页面提交:

这里的保存按钮与新增其实是同一个,因此提交的逻辑也是一样的。

2.5 后台实现:

编写后台,实现修改商品接口。

1)GoodsController:

  • 请求方式:PUT
  • 请求路径:/
  • 请求参数:Spu 对象
  • 返回结果:无
/**
 * 更新商品信息
 * @param spuBo
 * @return
 */
@PutMapping("goods")
public ResponseEntity<Void> updateGoods(@RequestBody SpuBo spuBo){
    this.goodService.updateGoods(spuBo);
    return ResponseEntity.noContent().build();
}

2)GoodsService:

spu 数据可以修改,但是 SKU 数据无法修改,因为有可能之前存在的 SKU 现在已经不存在了,或者以前的 sku 属性都不存在了。因此这里直接删除以前的 SKU,然后新增即可。

/**
 * 更新商品信息
 * @param spuBo
 */
@Transactional
public void updateGoods(SpuBo spuBo) {
    //根据spuId查询要删除的sku
    Sku record = new Sku();
    record.setSpuId(spuBo.getId());
    List<Sku> skus = this.skuMapper.select(record);
    skus.forEach(sku -> {
        //先删除stock
        this.stockMapper.deleteByPrimaryKey(sku.getId());
    });

    //删除sku
    Sku sku = new Sku();
    sku.setSpuId(spuBo.getId());
    this.skuMapper.delete(sku);

    //新增sku和stock
    this.saveSkuAndStock(spuBo);

    //更新spu和spuDetail,但有些属性不能直接更新,要修改一下先
    spuBo.setCreateTime(null);
    spuBo.setLastUpdateTime(new Date());
    spuBo.setValid(null);
    spuBo.setSaleable(null);
    this.spuMapper.updateByPrimaryKey(spuBo);

    this.spuDetailMapper.updateByPrimaryKeySelective(spuBo.getSpuDetail());

    sendMsg("update",spuBo.getId());
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值