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表

  • 相关阅读:
    Mysql 从库的备份中恢复一张表
    my.cnf 配置文件参数解释
    利用mvn deploy命令上传包(转)
    IntelliJ IDEA 项目文件旁边都有0%classes,0% lines covered
    idea启动java Maven项目,出现" java: 程序包xxxx不存在"
    org/apache/poi/POIXMLTypeLoader或者java.lang.NoSuchFieldError: RETURN_NULL_AND_BLANK
    elasticsearch,kibana,logstash.下载
    idea显示 RunDashboard ,多个启动项时列表显示
    写for循环快捷生成方式
    Could not transfer artifact xxx from/to xxx解决方案
  • 原文地址:https://www.cnblogs.com/luckyplj/p/11537052.html
Copyright © 2011-2022 走看看