1. 程式人生 > 實用技巧 >樂優商城--商品規格(資料庫分析、商品查詢)

樂優商城--商品規格(資料庫分析、商品查詢)

0.學習目標

  • 瞭解商品規格資料結構設計思路

  • 實現商品規格查詢

  • 瞭解SPU和SKU資料結構設計思路

  • 實現商品查詢

  • 瞭解商品新增的頁面實現

  • 獨立編寫商品新增後臺功能

1.商品規格資料結構

樂優商城是一個全品類的電商網站,因此商品的種類繁多,每一件商品,其屬性又有差別。為了更準確描述商品及細分差別,抽象出兩個概念:SPU和SKU,瞭解一下:

1.1.SPU和SKU

SPU:Standard Product Unit (標準產品單位) ,一組具有共同屬性的商品集

SKU:Stock Keeping Unit(庫存量單位),SPU商品集因具體特性不同而細分的每個商品

以圖為例來看:

  • 本頁的 華為Mate10 就是一個商品集(SPU)

  • 因為顏色、記憶體等不同,而細分出不同的Mate10,如亮黑色128G版。(SKU)

可以看出:

  • SPU是一個抽象的商品集概念,為了方便後臺的管理。

  • SKU才是具體要銷售的商品,每一個SKU的價格、庫存可能會不一樣,使用者購買的是SKU而不是SPU

1.2.資料庫設計分析

1.2.1.思考並發現問題

弄清楚了SPU和SKU的概念區分,接下來我們一起思考一下該如何設計資料庫表。

首先來看SPU,大家一起思考下SPU應該有哪些欄位來描述?

id:主鍵
title:標題
description:描述
specification:規格
packaging_list:包裝
after_service:售後服務
comment:評價
category_id:商品分類
brand_id:品牌

似乎並不複雜,但是大家仔細思考一下,商品的規格欄位你如何填寫?


不同商品的規格不一定相同,資料庫中要如何儲存?

再看下SKU,大家覺得應該有什麼欄位?

id:主鍵
spu_id:關聯的spu
price:價格
images:圖片
stock:庫存
顏色?
記憶體?
硬碟?

碰到難題了,不同的商品分類,可能屬性是不一樣的,比如手機有記憶體,衣服有尺碼,我們是全品類的電商網站,這些不同的商品的不同屬性,如何設計到一張表中?

其實顏色、記憶體、硬碟屬性都是規格引數中的欄位。所以,要解決這個問題,首先要能清楚規格引數。

1.2.2.分析規格引數

仔細檢視每一種商品的規格你會發現:

雖然商品規格千變萬化,但是同一類商品(如手機)的規格是統一的,有圖為證:

華為的規格:


三星的規格:


1.2.3.SKU的特有屬性

SPU中會有一些特殊屬性,用來區分不同的SKU,我們稱為SKU特有屬性。如華為META10的顏色、記憶體屬性。

不同種類的商品,一個手機,一個衣服,其SKU屬性不相同。

同一種類的商品,比如都是衣服,SKU屬性基本是一樣的,都是顏色、尺碼等。

這樣說起來,似乎SKU的特有屬性也是與分類相關的?事實上,仔細觀察你會發現,SKU的特有屬性是商品規格引數的一部分

也就是說,我們沒必要單獨對SKU的特有屬性進行設計,它可以看做是規格引數中的一部分。這樣規格引數中的屬性可以標記成兩部分:

  • spu下所有sku共享的規格屬性(稱為全域性屬性)

  • 每個sku不同的規格屬性(稱為特有屬性)

1.2.4.搜尋屬性

開啟一個搜尋頁,我們來看看過濾的條件:

你會發現,過濾條件中的螢幕尺寸、執行記憶體、網路、機身記憶體、電池容量、CPU核數等,在規格引數中都能找到:


也就是說,規格引數中的資料,將來會有一部分作為搜尋條件來使用。我們可以在設計時,將這部分屬性標記出來,將來做搜尋的時候,作為過濾條件。要注意的是,無論是SPU的全域性屬性,還是SKU的特有屬性,都有可能作為搜尋過濾條件的,並不衝突,而是有一個交集:


1.3.規格引數表

1.3.1.表結構

我們看下規格引數的格式:


可以看到規格引數是分組的,每一組都有多個引數鍵值對。不過對於規格引數的模板而言,其值現在是不確定的,不同的商品值肯定不同,模板中只要儲存組資訊、組內參數資訊即可。

因此我們設計了兩張表:

  • tb_spec_group:組,與商品分類關聯

  • tb_spec_param:引數名,與組關聯,一對多

1.3.2.規格組

規格引數分組表:tb_spec_group

CREATE TABLE `tb_spec_group` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `cid` bigint(20) NOT NULL COMMENT '商品分類id,一個分類下有多個規格組',
  `name` varchar(50) NOT NULL COMMENT '規格組的名稱',
  PRIMARY KEY (`id`),
  KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='規格引數的分組表,每個商品分類下有多個規格引數組';

規格組有3個欄位:

  • id:主鍵

  • cid:商品分類id,一個分類下有多個模板

  • name:該規格組的名稱。

1.3.2.規格引數

規格引數表:tb_spec_param

CREATE TABLE `tb_spec_param` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `cid` bigint(20) NOT NULL COMMENT '商品分類id',
  `group_id` bigint(20) NOT NULL,
  `name` varchar(255) NOT NULL COMMENT '引數名',
  `numeric` tinyint(1) NOT NULL COMMENT '是否是數字型別引數,true或false',
  `unit` varchar(255) DEFAULT '' COMMENT '數字型別引數的單位,非數字型別可以為空',
  `generic` tinyint(1) NOT NULL COMMENT '是否是sku通用屬性,true或false',
  `searching` tinyint(1) NOT NULL COMMENT '是否用於搜尋過濾,true或false',
  `segments` varchar(1000) DEFAULT '' COMMENT '數值型別引數,如果需要搜尋,則新增分段間隔值,如CPU頻率間隔:0.5-1.0',
  PRIMARY KEY (`id`),
  KEY `key_group` (`group_id`),
  KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='規格引數組下的引數名';

按道理來說,我們的規格引數就只需要記錄引數名、組id、商品分類id即可。但是這裡卻多出了很多欄位,為什麼?

還記得我們之前的分析吧,規格引數中有一部分是 SKU的通用屬性,一部分是SKU的特有屬性,而且其中會有一些將來用作搜尋過濾,這些資訊都需要標記出來。

通用屬性

用一個布林型別欄位來標記是否為通用:

  • generic來標記是否為通用屬性:

    • true:代表通用屬性

    • false:代表sku特有屬性

搜尋過濾

與搜尋相關的有兩個欄位:

  • searching:標記是否用作過濾

    • true:用於過濾搜尋

    • false:不用於過濾

  • segments:某些數值型別的引數,在搜尋時需要按區間劃分,這裡提前確定好劃分區間

    • 比如電池容量,0~2000mAh,2000mAh~3000mAh,3000mAh~4000mAh

數值型別

某些規格引數可能為數值型別,這樣的資料才需要劃分區間,我們有兩個欄位來描述:

  • numberic:是否為數值型別

    • true:數值型別

    • false:不是數值型別

  • unit:引數的單位

2.商品規格引數管理

2.1.頁面佈局

2.1.1.整體佈局

開啟規格引數頁面,看到如下內容:

商品分類樹我們之前已經做過,所以這裡可以直接展示出來。

因為規格是跟商品分類繫結的,因此首先會展現商品分類樹,並且提示你要選擇商品分類,才能看到規格引數的模板。一起了解下頁面的實現:


頁面結構:

這裡使用了v-layout來完成頁面佈局,並且添加了row屬性,代表接下來的內容是行佈局(左右)。

可以看出頁面分成2個部分:

  • <v-flex xs3>:左側,內部又分上下兩部分:商品分類樹及標題

    • v-card-title:標題部分,這裡是提示資訊,告訴使用者要先選擇分類,才能看到模板

    • v-tree:這裡用到的是我們之前講過的樹元件,展示商品分類樹,

  • <v-flex xs9 class="px-1">:右側:內部是規格引數展示

2.1.2.右側規格

當我們點選一個分類時,最終要達到的效果:

可以看到右側分為上下兩部分:

  • 上部:麵包屑,顯示當前選中的分類

  • 下部:table,顯示規格引數資訊

頁面實現:

可以看到右側並不是我們熟悉的 v-data-table,而是一個spec-group元件(規格組)和spec-param元件(規格引數),這是我們定義的獨立元件:


在SpecGroup中定義了表格:

2.2.規格組的查詢

2.2.1.樹節點的點選事件

當我們點選樹節點時,要將v-dialog開啟,因此必須繫結一個點選事件:(Specification.vue)

我們來看下handleClick方法:(Specification.vue)

點選事件發生時,發生了兩件事:

  • 記錄當前選中的節點,選中的就是商品分類

  • showGroup被置為true,則規格組就會顯示了。

同時,我們把被選中的節點(商品分類)的id傳遞給了SpecGroup元件:(Specification.vue)

2.2.2.頁面查詢規格組

來看下SpecGroup.vue中的實現:

我們檢視頁面控制檯,可以看到請求已經發出:

2.2.3.後端程式碼

實體類

leyou-item-interface中新增實體類:


內容:

@Table(name = "tb_spec_group")
public class SpecGroup {
​
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
​
    private Long cid;
​
    private String name;
​
    @Transient
    private List<SpecParam> params;
​
   // getter和setter省略
}

@Table(name = "tb_spec_param")
public class SpecParam {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long cid;
    private Long groupId;
    private String name;
    @Column(name = "`numeric`")
    private Boolean numeric;
    private String unit;
    private Boolean generic;
    private Boolean searching;
    private String segments;
    
    // getter和setter ...
}

leyou-item-service中編寫業務:


mapper

public interface SpecGroupMapper extends Mapper<SpecGroup> {
}

controller

先分析下需要的東西,在頁面的ajax請求中可以看出:

  • 請求方式:get

  • 請求路徑:/spec/groups/{cid} ,這裡通過路徑佔位符傳遞商品分類的id

  • 請求引數:商品分類id

  • 返回結果:頁面是直接把resp.data賦值給了groups:

    那麼我們返回的應該是規格組SpecGroup的集合

程式碼:

@RestController
@RequestMapping("spec")
public class SpecificationController {
​
    @Autowired
    private SpecificationService specificationService;
​
    /**
     * 根據分類id查詢分組
     * @param cid
     * @return
     */
    @GetMapping("groups/{cid}")
    public ResponseEntity<List<SpecGroup>> queryGroupsByCid(@PathVariable("cid")Long cid){
        List<SpecGroup> groups = this.specificationService.queryGroupsByCid(cid);
        if (CollectionUtils.isEmpty(groups)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(groups);
    }
}

service

@Service
public class SpecificationService {

    @Autowired
    private SpecGroupMapper groupMapper;

    /**
     * 根據分類id查詢分組
     * @param cid
     * @return
     */
    public List<SpecGroup> queryGroupsByCid(Long cid) {
        SpecGroup specGroup = new SpecGroup();
        specGroup.setCid(cid);
        return this.groupMapper.select(specGroup);
    }
}

頁面訪問測試:

目前,我們資料庫只為手機分類(76)提供了規格組:

我們訪問:http://api.leyou.com/api/item/spec/groups/76

然後在後臺系統中測試:

2.3.規格引數查詢

2.3.1.表格切換

當我們點選規格組,會切換到規格引數顯示,肯定是在規格組中綁定了點選事件:

我們看下事件處理:

可以看到這裡是使用了父子通訊,子元件觸發了select事件:

再來看下父元件的事件繫結:

事件處理:

這裡我們記錄了選中的分組,並且把標記設定為false,這樣規格組就不顯示了,而是顯示:SpecParam

並且,我們把group也傳遞到spec-param元件:

2.3.2.頁面查詢規格引數

我們來看SpecParam.vue的實現:

檢視頁面控制檯,發現請求已經發出:

報404,因為我們還沒有實現後臺邏輯,接下來就去實現。

2.3.3.後臺實現

SpecificationController

分析:

  • 請求方式:GET

  • 請求路徑:/spec/params

  • 請求引數:gid,分組id

  • 返回結果:該分組下的規格引數集合List<SpecParam>

程式碼:

/**
     * 根據條件查詢規格引數
     * @param gid
     * @return
     */
@GetMapping("params")
public ResponseEntity<List<SpecParam>> queryParams(@RequestParam("gid")Long gid){
    List<SpecParam>  params = this.specificationService.queryParams(gid);
    if (CollectionUtils.isEmpty(params)){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(params);
}

SpecificationService

@Autowired
private SpecParamMapper paramMapper;

/**
     * 根據條件查詢規格引數
     * @param gid
     * @return
     */
public List<SpecParam> queryParams(Long gid) {
    SpecParam param = new SpecParam();
    param.setGroupId(gid);
    return this.paramMapper.select(param);
}

SpecParamMapper


public interface SpecParamMapper extends Mapper<SpecParam> {
}

測試:

2.4.增、刪、改(作業)

增刪改的作業就留給大家去完成了。頁面中介面都已定義,你要做的就是實現後臺介面。

3.SPU和SKU資料結構

規格確定以後,就可以新增商品了,先看下資料庫表

3.1.SPU表

SPU表:

CREATE TABLE `tb_spu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
  `title` varchar(255) NOT NULL DEFAULT '' COMMENT '標題',
  `sub_title` varchar(255) DEFAULT '' COMMENT '子標題',
  `cid1` bigint(20) NOT NULL COMMENT '1級類目id',
  `cid2` bigint(20) NOT NULL COMMENT '2級類目id',
  `cid3` bigint(20) NOT NULL COMMENT '3級類目id',
  `brand_id` bigint(20) NOT NULL COMMENT '商品所屬品牌id',
  `saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
  `valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已刪除,1有效',
  `create_time` datetime DEFAULT NULL COMMENT '新增時間',
  `last_update_time` datetime DEFAULT NULL COMMENT '最後修改時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,該表描述的是一個抽象的商品,比如 iphone8';

與我們前面分析的基本類似,但是似乎少了一些欄位,比如商品描述。

我們做了表的垂直拆分,將SPU的詳情放到了另一張表:tb_spu_detail

CREATE TABLE `tb_spu_detail` (
  `spu_id` bigint(20) NOT NULL,
  `description` text COMMENT '商品描述資訊',
  `generic_spec` varchar(10000) NOT NULL DEFAULT '' COMMENT '通用規格引數資料',
  `special_spec` varchar(1000) NOT NULL COMMENT '特有規格引數及可選值資訊,json格式',
  `packing_list` varchar(3000) DEFAULT '' COMMENT '包裝清單',
  `after_service` varchar(3000) DEFAULT '' COMMENT '售後服務',
  PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

這張表中的資料都比較大,為了不影響主表的查詢效率我們拆分出這張表。

需要注意的是這兩個欄位:generic_spec和special_spec。

前面講過規格引數與商品分類繫結,一個分類下的所有SPU具有類似的規格引數。SPU下的SKU可能會有不同的規格引數資訊,因此我們計劃是這樣:

  • SPUDetail中儲存通用的規格引數資訊。

  • SKU中儲存特有規格引數。

來看下我們的表如何儲存這些資訊。

3.1.1.generic_spec欄位

首先是generic_spec,其中儲存通用規格引數資訊的值,這裡為了方便查詢,使用了json格式:

整體來看:


json結構,其中都是鍵值對:

  • key:對應的規格引數的spec_param的id

  • value:對應規格引數的值

3.1.2.special_spec欄位

我們說spu中只儲存通用規格引數,那麼為什麼有多出了一個special_spec欄位呢?

以手機為例,品牌、作業系統等肯定是全域性通用屬性,記憶體、顏色等肯定是特有屬性。

當你確定了一個SPU,比如小米的:紅米4X

全域性屬性值都是固定的了:

品牌:小米
型號:紅米4X

特有屬性舉例:

顏色:[香檳金, 櫻花粉, 磨砂黑]
記憶體:[2G, 3G]
機身儲存:[16GB, 32GB]

顏色、記憶體、機身儲存,作為SKU特有屬性,key雖然一樣,但是SPU下的每一個SKU,其值都不一樣,所以值會有很多,形成陣列。

我們在SPU中,會把特有屬性的所有值都記錄下來,形成一個數組:

裡面又有哪些內容呢?

來看資料格式:


也是json結構:

  • key:規格引數id

  • value:spu屬性的陣列

那麼問題來:特有規格引數應該在sku中記錄才對,為什麼在spu中也要記錄一份?

因為我們有時候需要把所有規格引數都查詢出來,而不是隻查詢1個sku的屬性。比如,商品詳情頁展示可選的規格引數時:


剛好符合我們的結構,這樣頁面渲染就非常方便了。

3.2.SKU表

CREATE TABLE `tb_sku` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
  `spu_id` bigint(20) NOT NULL COMMENT 'spu id',
  `title` varchar(255) NOT NULL COMMENT '商品標題',
  `images` varchar(1000) DEFAULT '' COMMENT '商品的圖片,多個圖片以‘,’分割',
  `price` bigint(15) NOT NULL DEFAULT '0' COMMENT '銷售價格,單位為分',
  `indexes` varchar(100) COMMENT '特有規格屬性在spu屬性模板中的對應下標組合',
  `own_spec` varchar(1000) COMMENT 'sku的特有規格引數,json格式',
  `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0無效,1有效',
  `create_time` datetime NOT NULL COMMENT '新增時間',
  `last_update_time` datetime NOT NULL COMMENT '最後修改時間',
  PRIMARY KEY (`id`),
  KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='sku表,該表表示具體的商品實體,如黑色的64GB的iphone 8';

還有一張表,代表庫存:

CREATE TABLE `tb_stock` (
  `sku_id` bigint(20) NOT NULL COMMENT '庫存對應的商品sku id',
  `seckill_stock` int(9) DEFAULT '0' COMMENT '可秒殺庫存',
  `seckill_total` int(9) DEFAULT '0' COMMENT '秒殺總數量',
  `stock` int(9) NOT NULL COMMENT '庫存數量',
  PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='庫存表,代表庫存,秒殺庫存等資訊';

問題:為什麼要將庫存獨立一張表?

因為庫存欄位寫頻率較高,而SKU的其它欄位以讀為主,因此我們將兩張表分離,讀寫不會干擾。

特別需要注意的是sku表中的indexes欄位和own_spec欄位。sku中應該儲存特有規格引數的值,就在這兩個欄位中。

3.2.1.indexes欄位

在SPU表中,已經對特有規格引數及可選項進行了儲存,結構如下:

{
    "4": [
        "香檳金",
        "櫻花粉",
        "磨砂黑"
    ],
    "12": [
        "2GB",
        "3GB"
    ],
    "13": [
        "16GB",
        "32GB"
    ]
}

這些特有屬性如果排列組合,會產生12個不同的SKU,而不同的SKU,其屬性就是上面備選項中的一個。

比如:

  • 紅米4X,香檳金,2GB記憶體,16GB儲存

  • 紅米4X,磨砂黑,2GB記憶體,32GB儲存

你會發現,每一個屬性值,對應於SPUoptions陣列的一個選項,如果我們記錄下角標,就是這樣:

  • 紅米4X,0,0,0

  • 紅米4X,2,0,1

既然如此,我們是不是可以將不同角標串聯起來,作為SPU下不同SKU的標示。這就是我們的indexes欄位。


這個設計在商品詳情頁會特別有用:


當用戶點選選中一個特有屬性,你就能根據 角標快速定位到sku。

3.2.2.own_spec欄位

看結構:

{"4":"香檳金","12":"2GB","13":"16GB"}

儲存的是特有屬性的鍵值對。

SPU中儲存的是可選項,但不確定具體的值,而SKU中的儲存的就是具體的值。

3.3.匯入圖片資訊

現在商品表中雖然有資料,但是所有的圖片資訊都是無法訪問的,我們需要把圖片匯入到虛擬機器:

首先,把課前資料提供的資料上傳到虛擬機器下:/leyou/static目錄:在leyou下建立static目錄

然後,使用命令解壓縮:

unzip images.zip

修改Nginx配置,使nginx反向代理這些圖片地址:

vim /opt/nginx/config/nginx.conf

修改成如下配置:

server {
    listen       80;
    server_name  image.leyou.com;

    # 監聽域名中帶有group的,交給FastDFS模組處理
    location ~/group([0-9])/ {
        ngx_fastdfs_module;
    }
    # 將其它圖片代理指向本地的/leyou/static目錄
    location / {
        root   /leyou/static/;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }

}

不要忘記重新載入nginx配置

nginx -s reload

4.商品查詢

4.1.效果預覽

接下來,我們實現商品管理的頁面,先看下我們要實現的效果:

可以看出整體是一個table,然後有新增按鈕。是不是跟昨天寫品牌管理很像?

4.2.頁面請求

先看整體頁面結構(Goods.vue):

並且在Vue例項掛載後就會發起查詢(mounted呼叫getDataFromServer方法初始化資料):

我們重新整理頁面,可以看到瀏覽器發起已經發起了查詢商品資料的請求:

因此接下來,我們編寫介面即可。

4.3.後臺提供介面

頁面已經準備好,接下來在後臺提供分頁查詢SPU的功能。



4.3.1.實體類

在leyou-item-interface工程中新增實體類:

SPU

@Table(name = "tb_spu")
public class Spu {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long brandId;
    private Long cid1;// 1級類目
    private Long cid2;// 2級類目
    private Long cid3;// 3級類目
    private String title;// 標題
    private String subTitle;// 子標題
    private Boolean saleable;// 是否上架
    private Boolean valid;// 是否有效,邏輯刪除用
    private Date createTime;// 建立時間
    private Date lastUpdateTime;// 最後修改時間
    // 省略getter和setter
}

SPU詳情

@Table(name="tb_spu_detail")
public class SpuDetail {
    @Id
    private Long spuId;// 對應的SPU的id
    private String description;// 商品描述
    private String specialSpec;// 商品特殊規格的名稱及可選值模板
    private String genericSpec;// 商品的全域性規格屬性
    private String packingList;// 包裝清單
    private String afterService;// 售後服務
    // 省略getter和setter
}

4.4.2.mapper

public interface SpuMapper extends Mapper<Spu> {
}

4.3.3.controller

先分析:

  • 請求方式:GET

  • 請求路徑:/spu/page

  • 請求引數:

    • page:當前頁

    • rows:每頁大小

    • key:過濾條件

    • saleable:上架或下架

  • 返回結果:商品SPU的分頁資訊。

    • 要注意,頁面展示的是商品分類和品牌名稱,而資料庫中儲存的是id,怎麼辦?

      我們可以新建一個類,繼承SPU,並且拓展cname和bname屬性,寫到leyou-item-interface

      public class SpuBo extends Spu {
      
          String cname;// 商品分類名稱
          
          String bname;// 品牌名稱
          
          // 略 。。
      }

編寫controller程式碼:

我們把與商品相關的一切業務介面都放到一起,起名為GoodsController,業務層也是這樣

@Controller
public class GoodsController {

    @Autowired
    private GoodsService goodsService;

    @GetMapping("spu/page")
    public ResponseEntity<PageResult<SpuBo>> querySpuBoByPage(
            @RequestParam(value = "key", required = false)String key,
            @RequestParam(value = "saleable", required = false)Boolean saleable,
            @RequestParam(value = "page", defaultValue = "1")Integer page,
            @RequestParam(value = "rows", defaultValue = "5")Integer rows
    ){
        PageResult<SpuBo> pageResult = this.goodsService.querySpuBoByPage(key, saleable, page, rows);
        if(CollectionUtils.isEmpty(pageResult.getItems())){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(pageResult);
    }

}

4.4.4.service

所有商品相關的業務(包括SPU和SKU)放到一個業務下:GoodsService。

@Service
public class GoodsService {

    @Autowired
    private SpuMapper spuMapper;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private BrandMapper brandMapper;

    public PageResult<SpuBo> querySpuBoByPage(String key, Boolean saleable, Integer page, Integer rows) {

        Example example = new Example(Spu.class);
        Example.Criteria criteria = example.createCriteria();
        // 搜尋條件
        if (StringUtils.isNotBlank(key)) {
            criteria.andLike("title", "%" + key + "%");
        }
        if (saleable != null) {
            criteria.andEqualTo("saleable", saleable);
        }

        // 分頁條件
        PageHelper.startPage(page, rows);

        // 執行查詢
        List<Spu> spus = this.spuMapper.selectByExample(example);
        PageInfo<Spu> pageInfo = new PageInfo<>(spus);

        List<SpuBo> spuBos = new ArrayList<>();
        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, "/"));

            // 查詢品牌的名稱
            spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName());

            spuBos.add(spuBo);
        });

        return new PageResult<>(pageInfo.getTotal(), spuBos);

    }
}

4.4.5.Category中拓展查詢名稱的功能

頁面需要商品的分類名稱需要在這裡查詢,因此要額外提供查詢分類名稱的功能,

在CategoryService中新增功能:

public List<String> queryNamesByIds(List<Long> ids) {
    List<Category> list = this.categoryMapper.selectByIdList(ids);
    List<String> names = new ArrayList<>();
    for (Category category : list) {
        names.add(category.getName());
    }
    return names;
    // return list.stream().map(category -> category.getName()).collect(Collectors.toList());
}

mapper的selectByIdList方法是來自於通用mapper。不過需要我們在mapper上繼承一個通用mapper介面:

public interface CategoryMapper extends Mapper<Category>, SelectByIdListMapper<Category, Long> { 

}

4.5.測試

重新整理頁面,檢視效果:

基本與預覽的效果一致,OK!