1. 程式人生 > >樂優商城(五)品牌管理(後端)

樂優商城(五)品牌管理(後端)

目錄

後臺功能——品牌管理(後端)

二、後端介面實現

主要就是對資料庫的抽插,難點在於和前端頁面的聯調,介面本身不復雜。

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 測試

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("刪除已取消!");
            });
          }
        }

三、功能演示