乐优商城(五)

目录

后台功能——品牌管理(后端)

二、后端接口实现

2.1 品牌查询

2.1.1 数据库表

2.1.2 实体类

2.1.3 Mapper

2.1.4 Controller

2.1.5 Service

2.1.6 测试

2.1.7 前端请求

2.2 品牌增加

2.2.1 Controller

2.2.2 Service

2.2.3 Mapper

2.2.4 前端的细节问题

2.2.5 图片的上传

2.3 品牌修改

2.3.1 点击编辑出现弹窗

2.3.2 数据回显

2.3.4 商品分类回显

2.4 品牌删除

2.4.1 逻辑

2.4.2 后端接口实现

2.4.3 前端请求

三、功能演示


后台功能——品牌管理(后端)

二、后端接口实现

主要就是对数据库的抽插,难点在于和前端页面的联调,接口本身不复杂。

2.1 品牌查询

2.1.1 数据库表

CREATE TABLE `tb_brand` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
  `name` varchar(50) NOT NULL COMMENT '品牌名称',
  `image` varchar(200) DEFAULT '' COMMENT '品牌图片地址',
  `letter` char(1) DEFAULT '' COMMENT '品牌的首字母',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325400 DEFAULT CHARSET=utf8 COMMENT='品牌表,一个品牌下有多个商品(spu),一对多关系';

品牌和商品分类之间是多对多关系。因此我们有一张中间表,来维护两者间关系:

CREATE TABLE `tb_category_brand` (
  `category_id` bigint(20) NOT NULL COMMENT '商品类目id',
  `brand_id` bigint(20) NOT NULL COMMENT '品牌id',
  PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';

但是,你可能会发现,这张表中并没有设置外键约束,似乎与数据库的设计范式不符。为什么这么做?

  • 外键会严重影响数据库读写的效率

  • 数据删除时会比较麻烦

在电商行业,性能是非常重要的。我们宁可在代码中通过逻辑来维护表关系,也不设置外键。

2.1.2 实体类

@Table(name = "tb_brand")
/**
 * @author:li
 *
 */
public class Brand implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    /**
     * 品牌名称
     */
    private String name;
    /**
     * 品牌图片
     */
    private String image;
    private Character letter;
    
    //省略get和set
}

2.1.3 Mapper

/**
 * @Author: 98050
 * @Time: 2018-08-07 19:15
 * @Feature:
 */
@org.apache.ibatis.annotations.Mapper
public interface BrandMapper extends Mapper<Brand> {
}

2.1.4 Controller

编写controller先思考四个问题:

  • 请求方式:查询,肯定是Get

  • 请求路径:分页查询,/brand/page

  • 请求参数:根据我们刚才编写的页面,有分页功能,有排序功能,有搜索过滤功能,因此至少要有5个参数:

    • page:当前页,int

    • rows:每页大小,int

    • sortBy:排序字段,String

    • desc:是否为降序,boolean

    • key:搜索关键词,String

  • 响应结果:分页结果一般至少需要两个数据

    • total:总条数

    • items:当前页数据

    • totalPage:有些还需要总页数

为了方便,需要封装一个类,表示分页结果:

package com.leyou.common.pojo;

import java.util.List;

/**
 * @author li
 * @param <T>
 */
public class PageResult<T> {

    /**
     * 总条数
     */
    private Long total;
    /**
     * 总页数
     */
    private Long totalPage;
    /**
     * 当前页数据
     */
    private List<T> items;

    public PageResult() {
    }

    public PageResult(Long total, List<T> items) {
        this.total = total;
        this.items = items;
    }

    public PageResult(Long total, Long totalPage, List<T> items) {
        this.total = total;
        this.totalPage = totalPage;
        this.items = items;
    }

    public Long getTotal() {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total;
    }

    public List<T> getItems() {
        return items;
    }

    public void setItems(List<T> items) {
        this.items = items;
    }

    public Long getTotalPage() {
        return totalPage;
    }

    public void setTotalPage(Long totalPage) {
        this.totalPage = totalPage;
    }
}

并且这个封装类在其他微服务中也会使用,所以将其抽取到ly-common中,提高复用性:

因为传递的参数比较多,所以专门封装一个参数类:

package com.leyou.parameter.pojo;

/**
 * @Author: 98050
 * Time: 2018-08-08 11:38
 * Feature:
 */
public class BrandQueryByPageParameter {

    /*
    *   - page:当前页,int
        - rows:每页大小,int
        - sortBy:排序字段,String
        - desc:是否为降序,boolean
        - key:搜索关键词,String
    * */

    private Integer page;
    private Integer rows;
    private String sortBy;
    private Boolean desc;
    private String key;

    public Integer getPage() {
        return page;
    }

    public void setPage(Integer page) {
        this.page = page;
    }

    public Integer getRows() {
        return rows;
    }

    public void setRows(Integer rows) {
        this.rows = rows;
    }

    public String getSortBy() {
        return sortBy;
    }

    public void setSortBy(String sortBy) {
        this.sortBy = sortBy;
    }

    public Boolean getDesc() {
        return desc;
    }

    public void setDesc(Boolean desc) {
        this.desc = desc;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public BrandQueryByPageParameter(Integer page, Integer rows, String sortBy, Boolean desc, String key) {
        this.page = page;
        this.rows = rows;
        this.sortBy = sortBy;
        this.desc = desc;
        this.key = key;
    }

    public BrandQueryByPageParameter(){
        super();
    }

    @Override
    public String toString() {
        return "BrandQueryByPageParameter{" +
                "page=" + page +
                ", rows=" + rows +
                ", sortBy='" + sortBy + '\'' +
                ", desc=" + desc +
                ", key='" + key + '\'' +
                '}';
    }
}

编写Controller

/**
 * @Author: 98050
 * Time: 2018-08-07 19:18
 * Feature:
 */
@RestController
@RequestMapping("brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 分页查询品牌
     * @param page
     * @param rows
     * @param sortBy
     * @param desc
     * @param key
     * @return
     */
    @GetMapping("page")
    public ResponseEntity<PageResult<Brand>> queryBrandByPage( @RequestParam(value = "page", defaultValue = "1") Integer page,
                                                               @RequestParam(value = "rows", defaultValue = "5") Integer rows,
                                                               @RequestParam(value = "sortBy", required = false) String sortBy,
                                                               @RequestParam(value = "desc", defaultValue = "false") Boolean desc,
                                                               @RequestParam(value = "key", required = false) String key){
        BrandQueryByPageParameter brandQueryByPageParameter=new BrandQueryByPageParameter(page,rows,sortBy,desc,key);
        PageResult<Brand> result = this.brandService.queryBrandByPage(brandQueryByPageParameter);
        if(result == null){
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
        return ResponseEntity.ok(result);
    }
}

2.1.5 Service

接口

  /**
     * 分页查询
     * @param brandQueryByPageParameter
     * @return
     */
    PageResult<Brand> queryBrandByPage(BrandQueryByPageParameter brandQueryByPageParameter);

实现类

    @Override
    public PageResult<Brand> queryBrandByPage(BrandQueryByPageParameter brandQueryByPageParameter) {

        /**
         * 1.分页
         */
        PageHelper.startPage(brandQueryByPageParameter.getPage(),brandQueryByPageParameter.getRows());

        /**
         *  2.排序
         */
        Example example = new Example(Brand.class);
        if (StringUtils.isNotBlank(brandQueryByPageParameter.getSortBy())){
            example.setOrderByClause(brandQueryByPageParameter.getSortBy()+(brandQueryByPageParameter.getDesc()? " DESC":" ASC"));
        }
        /**
         * 3.查询
         */
        if(StringUtils.isNotBlank(brandQueryByPageParameter.getKey())) {
            example.createCriteria().orLike("name", brandQueryByPageParameter.getKey()+"%").orEqualTo("letter", brandQueryByPageParameter.getKey().toUpperCase());
        }
        List<Brand> list=this.brandMapper.selectByExample(example);

        /**
         * 4.创建PageInfo
         */
        PageInfo<Brand> pageInfo = new PageInfo<>(list);
        /**
         * 5.返回分页结果
         */
        return new PageResult<>(pageInfo.getTotal(),pageInfo.getList());
    }

2.1.6 测试

访问http://api.leyou.com/api/item/brand/page

2.1.7 前端请求

在页面创建的时候需要加载数据,所以将数据请求单独放在一个函数里面,方便以后实时刷新数据。

        getDataFromServer(){

          // 开启进度条
          this.loading = true;

          //发起ajax请求
          // 分页查询page,rows,key,sortBy,desc

          this.$http.get("/item/brand/page",{
            params:{
              page:this.pagination.page,
              rows:this.pagination.rowsPerPage,
              sortBy:this.pagination.sortBy,
              desc:this.pagination.descending,
              key:this.search,
            }
          }).then(resp =>{
            console.log(resp)
            this.brands=resp.data.items;
            this.totalBrands = resp.data.total;
            //关闭进度条
            this.loading = false;
          })

        }

2.2 品牌增加

2.2.1 Controller

还是一样,先分析四个内容:

  • 请求方式:刚才看到了是POST

  • 请求路径:/brand

  • 请求参数:brand对象,外加商品分类的id(最后一级id)数组cids

  • 返回值:无

代码:

    /**
     * 品牌新增
     * @param brand
     * @param categories
     * @return
     */
    @PostMapping
    public ResponseEntity<Void>  saveBrand(Brand brand, @RequestParam("categories") List<Long> categories){
        this.brandService.saveBrand(brand, categories);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

2.2.2 Service

接口:


    /**
     * 新增brand,并且维护中间表
     * @param brand
     * @param cids
     */
    void saveBrand(Brand brand, List<Long> cids);

实现类:

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveBrand(Brand brand, List<Long> categories) {
        System.out.println(brand);
        // 新增品牌信息
        this.brandMapper.insertSelective(brand);
        // 新增品牌和分类中间表
        for (Long cid : categories) {
            this.brandMapper.insertCategoryBrand(cid, brand.getId());
        }
    }

这里调用了brandMapper中的一个自定义方法insertCategoryBrand,来实现中间表的数据新增 。

2.2.3 Mapper

通用Mapper只能处理单表,也就是Brand的数据,因此我们手动编写一个方法及sql,实现中间表的新增

/**
 * @Author: 98050
 * @Time: 2018-08-07 19:15
 * @Feature:
 */
@org.apache.ibatis.annotations.Mapper
public interface BrandMapper extends Mapper<Brand> {
    /**
     * 新增商品分类和品牌中间表数据
     * @param cid 商品分类id
     * @param bid 品牌id
     * @return
     */
    @Insert("INSERT INTO tb_category_brand (category_id, brand_id) VALUES (#{cid},#{bid})")
    void insertCategoryBrand(@Param("cid") Long cid, @Param("bid") Long bid);
}

2.2.4 前端的细节问题

新增完成后关闭当前窗口(Vue组件之间的通信),控制窗口关闭是在父组件MyBrand.vue中。

  • 第一步,在父组件中定义一个函数,用来关闭窗口,不过之前已经定义过了,我们优化一下,关闭的同时重新加载数据:
        reload(){
          //关闭对话框
          this.show=false;
          //刷新页面
          this.getDataFromServer();
        },
  • 第二步,父组件在使用子组件时,绑定事件,关联到这个函数

  • 第三步,子组件通过this.$emit调用父组件的函数:

2.2.5 图片的上传

刚才的新增实现中,并没有上传图片。由于文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此需要创建一个独立的微服务,专门处理各种上传。最终目的是做一个分布式文件系统,具体在下一篇介绍

2.3 品牌修改

2.3.1 点击编辑出现弹窗

给编辑按钮绑定一个事件即可,并且把当前brand的信息传递给editBrand方法。

2.3.2 数据回显

回显数据,就是把当前点击的品牌数据传递到子组件(MyBrandForm)。而父组件给子组件传递数据,通过props属性。

  • 第一步:在编辑时获取当前选中的品牌信息,并且记录到oldBrand中。

在data中定义oldBrand属性,用来接收要编辑的brand数据:

  • 第二步:在触发编辑事件时,把当前的brand传递给editBrand方法方法,然后赋值给oldBrand。
editBrand(oldBrand){
  // 控制弹窗可见:
  this.show = true;
  // 获取要编辑的brand
  this.oldBrand = oldBrand;
},
  • 第三步:把获取的brand数据 传递给子组件

  • 第四步:在子组件中通过props接收要编辑的brand数据,Vue会自动完成回显

接收数据:

  • 第五步:通过watch函数监控oldBrand的变化,把值copy到本地的brand。
      watch:{
        oldBrand:{
          deep:true,
          handler(val){
            if(val){
              this.brand=Object.deepCopy(val);
            }else{
              this.clear();
            }
          }
        }
      },

Object.deepCopy 自定义的对对象进行深度复制的方法。

需要判断监听到的是否为空,如果为空,应该进行初始化,初始化用到了一个函数clear:

  • 第六步:测试。除了商品分类以外,其他数据都回显了。

2.3.4 商品分类回显

商品分类信息在tb_brand中是没有的,需要通过中间表tb_category_brand和tb_category联合查询得到。

2.3.4.1 后台接口

Controller

    /**
     * 用于修改品牌信息时,商品分类信息的回显
     * @param bid
     * @return
     */
    @GetMapping("bid/{bid}")
    public ResponseEntity<List<Category>> queryByBrandId(@PathVariable("bid") Long bid){
        List<Category> list = this.categoryService.queryByBrandId(bid);
        if(list == null || list.size() < 1){
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        return ResponseEntity.ok(list);
    }

Service

接口:

    /**
     * 根据brand id查询分类信息
     * @param bid
     * @return
     */
    List<Category> queryByBrandId(Long bid);

实现类:

    /**
     * 根据品牌id查询分类
     * @param bid
     * @return
     */
    @Override
    public List<Category> queryByBrandId(Long bid) {
        return this.categoryMapper.queryByBrandId(bid);
    }

Mapper

    /**
     * 根据品牌id查询商品分类
     * @param bid
     * @return
     */
    @Select("SELECT * FROM tb_category WHERE id IN (SELECT category_id FROM tb_category_brand WHERE brand_id = #{bid}) ")
    List<Category> queryByBrandId(@Param("bid") Long bid);

2.3.4.2 前台查询分类并渲染

在编辑页打开前,就要进行商品分类的查询,查询成功后再回显其他数据。

最终代码:

        editBrand(oldBrand){
          //根据品牌信息查询商品分类
          this.$http.get("/item/category/bid/"+oldBrand.id).then(
            ({data}) => {
              this.isEdit=true;
              //显示弹窗
              this.show=true;
              //获取要编辑的brand
              this.oldBrand=oldBrand;
              this.oldBrand.categories = data;
            }
          ).catch();

        },

测试:

2.3.4.3 新增窗口数据干扰

但是,此时却产生了新问题:新增窗口竟然也有数据。

原因:如果之前打开过编辑,那么在父组件中记录的oldBrand会保留。下次再打开窗口,如果是编辑窗口到没问题,但是新增的话,就会再次显示上次打开的品牌信息了。

解决: 新增窗口打开前,把数据置空。

2.3.4.4 提交表单时要判断是新增还是修改

新增和修改是同一个页面,我们该如何判断?

父组件中点击按钮弹出新增或修改的窗口,因此父组件非常清楚接下来是新增还是修改。

因此,最简单的方案就是,在父组件中定义变量,记录新增或修改状态,当弹出页面时,把这个状态也传递给子组件。

  • 第一步:在父组件中记录状态

  • 第二步:在新增和修改前更改状态

  • 第三步:传递给子组件

  • 第四步:子组件接收标记

  • 第五步:动态化处理

标题动态化:

表单提交动态:

          submit(){
            //提交表单
              if(this.$refs.BrandForm.validate()){
                /**
                 * 使用解构表达式获取数据,除categories以外的数据都放入rest中,然后对categories使用map进行处理,得到id后重新赋值给
                 * rest里面的categories数组
                 */
                const {categories, ... rest}=this.brand;
                rest.categories=categories.map(c => c.id).join(",");
                console.log(rest)
                if(this.isEdit) {
                  this.$http.delete("/item/brand/cid_bid/" + this.oldBrand.id).then().catch();
                }
                this.$http({
                  method:this.isEdit ? 'put' :'post',
                  url:"/item/brand",
                  data:this.$qs.stringify(rest),
                }).then(
                  () =>{
                    //关闭对话框
                    this.$emit('reload');
                    this.$message.success("保存成功!");
                    this.clear();
                  }
                ).catch(
                  ()=>{
                    this.$message.success("保存失败!");
                  }
                );
              }
          },

2.4 品牌删除

删除分为两种:单个和多个。

2.4.1 逻辑

单个删除传入后端的是被删数据的id,多个删除则是将全部id用“-”连接成字符串传入后端,而后端通过判断传入的数据是否包含“-”来决定是单个删除还是删除多个。

2.4.2 后端接口实现

选中后点击删除即可。删除的时候先从tb_brand中删除数据,然后维护中间表tb_category_brand。

Controller

    /**
     * 删除tb_brand中的数据,单个删除、多个删除二合一
     * @param bid
     * @return
     */
    @DeleteMapping("bid/{bid}")
    public ResponseEntity<Void> deleteBrand(@PathVariable("bid") String bid){
        String separator="-";
        if(bid.contains(separator)){
            String[] ids=bid.split(separator);
            for (String id:ids){
                this.brandService.deleteBrand(Long.parseLong(id));
            }
        }
        else {
            this.brandService.deleteBrand(Long.parseLong(bid));
        }
        return ResponseEntity.status(HttpStatus.OK).build();
    }

Service

接口

    /**
     * 删除brand,并且维护中间表
     * @param id
     */
    void deleteBrand(Long id);

实现类

    /**
     * 品牌删除
     * @param id
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteBrand(Long id) {
        //删除品牌信息
        this.brandMapper.deleteByPrimaryKey(id);

        //维护中间表
        this.brandMapper.deleteByBrandIdInCategoryBrand(id);
    }

Mapper

维护中间表时需要自己写sql

    /**
     * 根据brand id删除中间表相关数据
     * @param bid
     */
    @Delete("DELETE FROM tb_category_brand WHERE brand_id = #{bid}")
    void deleteByBrandIdInCategoryBrand(@Param("bid") Long bid);

2.4.3 前端请求

2.4.3.1 单个删除

        deleteBrand(oldBrand){
          if (this.selected.length === 1 && this.selected[0].id === oldBrand.id) {
            this.$message.confirm('此操作将永久删除该品牌, 是否继续?').then(
              () => {
                //发起删除请求,删除单条数据
                  this.$http.delete("/item/brand/bid/" + oldBrand.id).then(() => {
                    this.getDataFromServer();
                  }).catch()
              }
            ).catch(() => {
              this.$message.info("删除已取消!");
            });
          }
        }

2.4.3.2 多个删除

        deleteAllBrand(){
          //拼接id数组
          /**
           * 加了{}就必须有return
           * @type {any[]}
           */
          const ids = this.selected.map( s => s.id);

          if (selected.length>0) {
            this.$message.confirm('此操作将永久删除所选品牌,是否继续?').then(
              () => {
                this.$http.delete("/item/brand/bid/" + ids.join("-")).then(() => {
                  this.getDataFromServer();
                }).catch();
              }
            ).catch(() => {
              this.$message.info("删除已取消!");
            });
          }
        }

三、功能演示

猜你喜欢

转载自blog.csdn.net/lyj2018gyq/article/details/82223835
今日推荐