zoukankan      html  css  js  c++  java
  • 023 商品管理功能02-----商品新增

    当我们点击新增商品按钮:

    就会出现一个弹窗:

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

    • 基本信息:主要是一些简单的文本数据,包含了SPU和SpuDetail的部分数据,如

      • 商品分类:是SPU中的cid1cid2cid3属性

      • 品牌:是spu中的brandId属性

      • 标题:是spu中的title属性

      • 子标题:是spu中的subTitle属性

      • 售后服务:是SpuDetail中的afterService属性

      • 包装列表:是SpuDetail中的packingList属性

    • 商品描述:是SpuDetail中的description属性,数据较多,所以单独放一个页面

    • 规格参数:商品规格信息,对应SpuDetail中的genericSpec属性

    • SKU属性:spu下的所有Sku信息

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

    1.弹窗事件

    弹窗是一个独立组件:

     

    并且在Goods组件中已经引用它:

    并且在页面中渲染:

    新增商品按钮的点击事件中,改变这个dialogshow属性:

     

    2.基本信息栏

    我们先来看下基本信息栏:

    (1)商品分类

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

    刷新页面,可以看到请求已经发出:

    效果图:

    (2)品牌选择

    <1>页面

    品牌也是一个下拉选框,不过其选项是不确定的,只有当用户选择了商品分类,才会把这个分类下的所有品牌展示出来。

    所以页面编写了watch函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表:

     

    选择商品分类后,可以看到请求发起:

    <2>后台接口

    根据商品分类id,查询对应品牌即可。

    页面需要去后台查询品牌信息,我们自然需要提供:

    请求方式:GET

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

    请求参数:cid

    响应数据:品牌集合

    (1)BrandController中添加代码

    /**
         * 注意:@GetMapping(path = "/cid/{cid}") 中的{cid}为占位符 对应url中的 /cid/77中的77
         * @param cid
         * @return
         */
        @GetMapping(path = "/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);
        }

    (2)BrandServiceImpl中添加方法

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

    (3)BrandMapper中添加方法(需要人为的编写sql语句)

     /**
         * 根据分类cid,筛选出该分类下所有的品牌信息
         * sql语句解读:INNER JOIN 内连接 将tb_brand表和tb_category_brand关联起来,on为条件语句
         * @param cid
         * @return
         */
        @Select("SELECT b.* from tb_brand b INNER JOIN tb_category_brand cb on b.id=cb.brand_id where cb.category_id=#{cid}")
        List<Brand> selectBrandByCid(Long cid);

    <3>效果图

    3.商品描述栏

    商品描述信息比较复杂,而且图文并茂,甚至包括视频。

    这样的内容,一般都会使用富文本编辑器。

    (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的富文本编辑器

    (3)使用

    使用非常简单:已经在项目中集成。

    以下步骤不需操作,仅供参考:

    <1>安装,使用npm命令:

    npm install vue-quill-editor --save

    <2>加载,在js中引入:

    <3>页面使用:

    <quill-editor v-model="goods.spuDetail.description" :options="editorOption"/>

    (4)自定义的富文本编辑器

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

    使用也非常简单:

    <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

    范例(GoodsForm.vue):

    (5)效果图

    4.规格参数栏

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

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

    (1)改造规格参数代码

    <1>SpecificationController.java类中的queryParams方法

    我们在原来的根据 gid(规格组id)查询规格参数的接口上,添加一个参数:cid,即商品分类id。

    等一下, 考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件:

    /**
         * 根据条件查询规格参数
         * @param gid
         * @return
         */
        @GetMapping(path = "/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.queryParams(gid, cid, generic, searching);
            if (CollectionUtils.isEmpty(params)){
                return ResponseEntity.notFound().build();
            }
            return ResponseEntity.ok(params);
        }

    <2>改造SpecificationService:

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

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

    (2)效果图

    5.SKU属性栏

    Sku属性是SPU下的每个商品的不同特征,如图:

    当我们填写一些属性后,会在页面下方生成一个sku表格,大家可以计算下会生成多少个不同属性的Sku呢?

    当你选择了上图中的这些选项时:

    • 颜色共2种:迷夜黑,勃艮第红,绚丽蓝

    • 内存共2种:4GB,6GB

    • 机身存储1种:64GB,128GB

    此时会产生多少种SKU呢? 应该是 3 * 2 * 2 = 12种,这其实就是在求笛卡尔积。

    我们会在页面下方生成一个sku的表格:

     

    6.页面表单提交

    在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:特有规格参数的下标

    7.后台实现

     (1)实体类

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

    <1>Sku

    package lucky.leyou.item.domain;
    
    import javax.persistence.*;
    import java.util.Date;
    
    @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表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性. 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,
        @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;
        }
    }

    <2>Stock

    package lucky.leyou.item.domain;
    
    import javax.persistence.Id;
    import javax.persistence.Table;
    
    @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)Mapper

    利用通用mapper,可以直接调用通用mapper工具包封装的方法直接操作数据库,避免了sql语句的编写。

    注意:SkuMapper、StockMapper继承通用Mapper后,也可以自定义方法用来操作数据库表

    <1>SkuMapper

    package lucky.leyou.item.mapper;
    
    import lucky.leyou.item.domain.Sku;
    import tk.mybatis.mapper.common.Mapper;
    
    public interface SkuMapper extends Mapper<Sku> {
    
    }

    <2>StockMapper

    package lucky.leyou.item.mapper;
    
    import lucky.leyou.item.domain.Stock;
    import tk.mybatis.mapper.common.Mapper;
    
    public interface StockMapper extends Mapper<Stock> {
    }

    (3)Controller

    <1>修改SpuBo类

    Spu的json格式的对象,spu中包含spuDetail和Sku集合。这里我们该怎么接收?我们之前定义了一个SpuBo对象,作为业务对象。这里也可以用它,不过需要再扩展spuDetail和skus字段:

    package lucky.leyou.item.bo;
    
    import lucky.leyou.item.domain.Sku;
    import lucky.leyou.item.domain.Spu;
    import lucky.leyou.item.domain.SpuDetail;
    
    import java.util.List;
    
    /**
     * Bo为business object 业务对象
     * SpuBo这个类用来封装分页查询商品的结果集,继承了spu这个类,并扩展出了cname,和bname属性
     */
    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;
        }
    }

    <2>在GoodsController中添加新增商品的方法

    /**
         * 商品保存
         * @param spuBo 注意:利用@RequestBody注解接收json数据
         * @return
         */
        @PostMapping("goods")
        public ResponseEntity<Void> saveGoods(@RequestBody SpuBo spuBo){
            this.goodsService.saveGoods(spuBo);
            return ResponseEntity.status(HttpStatus.CREATED).build();
        }

    注意:通过@RequestBody注解来接收Json请求

    (4)service

    这里的逻辑比较复杂,我们除了要对SPU新增以外,还要对SpuDetail、Sku、Stock进行保存

    package lucky.leyou.item.service.impl;
    
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import lucky.leyou.common.domain.PageResult;
    import lucky.leyou.item.bo.SpuBo;
    import lucky.leyou.item.domain.Spu;
    import lucky.leyou.item.domain.SpuDetail;
    import lucky.leyou.item.domain.Stock;
    import lucky.leyou.item.mapper.*;
    import lucky.leyou.item.service.ICategoryService;
    import lucky.leyou.item.service.IGoodsService;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.BeanUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import tk.mybatis.mapper.entity.Example;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;
    
    @Service
    public class GoodsServiceImpl implements IGoodsService {
    
        @Autowired
        private SpuMapper spuMapper;
    
        @Autowired
        private SpuDetailMapper spuDetailMapper;
    
        @Autowired
        private BrandMapper brandMapper;
    
        @Autowired
        private ICategoryService categoryService;
    
        @Autowired
        private SkuMapper skuMapper;
    
        @Autowired
        private StockMapper stockMapper;
    
        @Override
        public PageResult<SpuBo> querySpuBoByPage(String key, Boolean saleable, Integer page, Integer rows) {
            Example example = new Example(Spu.class);
            Example.Criteria criteria = example.createCriteria(); //查询条件
            // 01 添加文本框中用户输入的搜索条件
            if (StringUtils.isNotBlank(key)) {
                //注意:criteria.andLike该方法的参数1是数据库表的字段名,参数2为模糊查询的表达式
                criteria.andLike("title", "%" + key + "%");
            }
    
            //02 添加上下架的过滤条件
            if (saleable != null) {
                criteria.andEqualTo("saleable", saleable);
            }
    
            // 03 分页条件
            PageHelper.startPage(page, rows);
    
            // 04 执行查询,获取spu集合
            List<Spu> spus = this.spuMapper.selectByExample(example);
            PageInfo<Spu> pageInfo = new PageInfo<>(spus);
    
            List<SpuBo> spuBos = new ArrayList<>();
    
            //05 spu集合转化为spubo集合
            //java8 foreach循环
            spus.forEach(spu->{
                SpuBo spuBo = new SpuBo();
                // copy共同属性的值到新的对象
                BeanUtils.copyProperties(spu, spuBo);
                // 查询分类名称
                List<String> names = this.categoryService.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
                spuBo.setCname(StringUtils.join(names, "/")); //StringUtils.join将集合元素用指定的分隔符连接成字符串
    
                // 查询品牌的名称
                spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName());
    
                spuBos.add(spuBo);
            });
    
            //06 利用PageResult的构造方法返回PageResult对象
            return new PageResult<>(pageInfo.getTotal(), spuBos);
        }
    
        /**
         * 新增商品
         * @param spuBo
         */
        @Override
        @Transactional  //添加事务
        public void saveGoods(SpuBo spuBo) {
            // 01 新增spu
            // 设置默认字段
            spuBo.setId(null);
            spuBo.setSaleable(true);  //设置是否可售
            spuBo.setValid(true);
            spuBo.setCreateTime(new Date());  //设置创建时间
            spuBo.setLastUpdateTime(spuBo.getCreateTime()); //设置更新时间
            this.spuMapper.insertSelective(spuBo);
    
            // 02 新增spuDetail
            SpuDetail spuDetail = spuBo.getSpuDetail();
            spuDetail.setSpuId(spuBo.getId());
            this.spuDetailMapper.insertSelective(spuDetail);
    
            saveSkuAndStock(spuBo);
        }
    
        private void saveSkuAndStock(SpuBo spuBo) {
            spuBo.getSkus().forEach(sku -> {
                // 03 新增sku
                sku.setSpuId(spuBo.getId());
                sku.setCreateTime(new Date());
                sku.setLastUpdateTime(sku.getCreateTime());
                this.skuMapper.insertSelective(sku);
    
                // 04 新增库存
                Stock stock = new Stock();
                stock.setSkuId(sku.getId());
                stock.setStock(sku.getStock());
                this.stockMapper.insertSelective(stock);
            });
        }
    }

    8.最终效果图

    数据库表数据:

    spu表

    spu_detail表

  • 相关阅读:
    learning scala view collection
    scala
    learning scala dependency injection
    learning scala implicit class
    learning scala type alise
    learning scala PartialFunction
    learning scala Function Recursive Tail Call
    learning scala Function Composition andThen
    System.Threading.Interlocked.CompareChange使用
    System.Threading.Monitor的使用
  • 原文地址:https://www.cnblogs.com/luckyplj/p/11537052.html
Copyright © 2011-2022 走看看