1. 程式人生 > >樂優商場開發第九天筆記

樂優商場開發第九天筆記

0.學習目標

  • 瞭解商品規格資料結構設計思路
  • 實現商品規格查詢
  • 瞭解SPU和SKU資料結構設計思路
  • 實現商品查詢
  • 瞭解商品新增的頁面實現
  • 獨立編寫商品新增後臺功能

1.商品規格資料結構

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

1.1.SPU和SKU

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

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

以圖為例來看:

1526085541996

  • 本頁的 華為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:品牌

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

1526086539789

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

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

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

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

1.2.2.分析規格引數

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

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

華為的規格:

1526087063700

三星的規格:

1526087142454

也就是說,商品的規格引數應該是與分類繫結的。每一個分類都有統一的規格引數模板,但不同商品其引數值可能不同

如下圖所示:

1526088168565

1.2.3.SKU的特有屬性

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

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

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

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

1526088981953

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

  • 所有sku共享的規格屬性(稱為全域性屬性)
  • 每個sku不同的規格屬性(稱為特有屬性)

1526089506566

1.2.4.搜尋屬性

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

1526090072535

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

1526090228171

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

1526091216124

1.3.規格引數表

1.3.1.表結構

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

1526092179381

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

因此我們設計了兩張表:

  • 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:某些數值型別的引數,在搜尋時需要按區間劃分,這裡提前確定好劃分區間
    • 比如電池容量,02000mAh,2000mAh3000mAh,3000mAh~4000mAh

數值型別

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

  • numberic:是否為數值型別
    • true:數值型別
    • false:不是數值型別
  • unit:引數的單位

2.商品規格引數管理

2.1.頁面佈局

2.1.1.整體佈局

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

1529549099049

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

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

1529549521623

頁面結構:

1528423299102

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

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

  • <v-flex xs3>:左側,內部又分上下兩部分:商品分類樹及標題
    • v-card-title:標題部分,這裡是提示資訊,告訴使用者要先選擇分類,才能看到模板
    • v-tree:這裡用到的是我們之前講過的樹元件,展示商品分類樹,
  • <v-flex xs9 class="px-1">:右側:內部是規格引數展示

2.1.2.右側規格

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

1529550402376

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

  • 上部:麵包屑,顯示當前選中的分類
  • 下部:table,顯示規格引數資訊

頁面實現:

1529550694218

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

1529550775415

在SpecGroup中定義了表格:

1528427514717

2.2.規格組的查詢

2.2.1.樹節點的點選事件

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

1528428028513

我們來看下handleClick方法:

1528428221957

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

  • 記錄當前選中的節點,選中的就是商品分類
  • showGroup被置為true,則規格組就會顯示了。

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

1528428308113

2.2.2.頁面查詢規格組

來看下SpecGroup.vue中的實現:

1528428617046

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

1528428699027

2.2.3.後端程式碼

實體類

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

1528429029149

內容:

@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中編寫業務:

1528433648595

mapper

public interface SpecGroupMapper extends Mapper<SpecGroup> {
}

controller

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

  • 請求方式:查詢,肯定是get

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

  • 請求引數:商品分類id

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

    1528429342796

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

程式碼:

@RestController
@RequestMapping("spec")
public class SpecificationController {

    @Autowired
    private SpecificationService specificationService;

    @GetMapping("groups/{cid}")
    public ResponseEntity<List<SpecGroup>> querySpecGroups(@PathVariable("cid") Long cid){
        List<SpecGroup> list = this.specificationService.querySpecGroups(cid);
        if(list == null || list.size() == 0){
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(list);
    }
}

service

@Service
public class SpecificationService {

    @Autowired
    private SpecGroupMapper specGroupMapper;

    public List<SpecGroup> querySpecGroups(Long cid) {
        SpecGroup t = new SpecGroup();
        t.setCid(cid);
        return this.specGroupMapper.select(t);
    }
}

頁面訪問測試:

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

1528433697492

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

1528433733340

然後在後臺系統中測試:
1528433821121

2.3.規格引數查詢

2.3.1.表格切換

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

1528429949504

我們看下事件處理:

1528429988410

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

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

1528430052736

事件處理:

1528430100166

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

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

1528430147829

2.3.2.頁面查詢規格引數

我們來看SpecParam.vue的實現:

1528430319461

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

1528430407298

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

2.3.3.後臺實現

controller

分析:

  • 請求方式:GET
  • 請求路徑:/spec/params
  • 請求引數:gid,分組id
  • 返回結果:該分組下的規格引數集合List<SpecParam>

程式碼:

@GetMapping("/params")
public ResponseEntity<List<SpecParam>> querySpecParam(
    @RequestParam(value="gid", required = false) Long gid
        ){
        List<SpecParam> list =
                this.specificationService.querySpecParams(gid);
        if(list == null || list.size() == 0){
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(list);
    }

service

@Autowired
private SpecParamMapper specParamMapper;

public List<SpecParam> querySpecParams(Long gid){
    SpecParam t = new SpecParam();
    t.setGroupId(gid);
    return this.specParamMapper.select(t);
}

mapper

public interface SpecParamMapper extends Mapper<SpecParam> {
}

測試:

1528442972016

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格式:

整體來看:

1529554390912

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中,會把特有屬性的所有值都記錄下來,形成一個數組:

裡面又有哪些內容呢?

來看資料格式:

1529554916252

也是json結構:

  • key:規格引數id
  • value:spu屬性的陣列

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

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

1526267828817

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

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欄位。

1526266901335

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

1526267180997

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

3.2.2.own_spec欄位

看結構:

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

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

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

3.3.匯入圖片資訊

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

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

1528445149890

1528479229165

然後,使用命令解壓縮:

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.效果預覽

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

1526268595873

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

4.2.頁面請求

先看整體頁面結構:

1528447301687

並且在Vue例項掛載後就會發起查詢:

1528447431959

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

1528447591939

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

4.3.後臺提供介面

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

4.3.1.實體類

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,業務層也是這樣

@RestController
public class GoodsController {

    @Autowired
    private GoodsService goodsService;

    @GetMapping("spu/page")
    public ResponseEntity<PageResult<SpuBo>> querySpuByPage(
            @RequestParam(value = "page", defaultValue = "1") Integer page,
            @RequestParam(value = "rows", defaultValue = "5") Integer rows,
            @RequestParam(value = "saleable", required = false) Boolean saleable,
            @RequestParam(value = "key", required = false) String key) {
        PageResult<SpuBo> result = this.goodsService.querySpuPage(page, rows, saleable, key);
        if (result == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
        return ResponseEntity.ok(result);
    }
}

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> querySpuByPageAndSort(Integer page, Integer rows, Boolean saleable, String key) {
        // 1、查詢SPU
        // 分頁,最多允許查100條
        PageHelper.startPage(page, Math.min(rows, 100));

        // 建立查詢條件
        Example example = new Example(Spu.class);
        Example.Criteria criteria = example.createCriteria();

        // 是否過濾上下架
        if (saleable != null) {
            criteria.orEqualTo("saleable", saleable);
        }
        
        // 是否模糊查詢
        if (StringUtils.isNotBlank(key)) {
            criteria.andLike("title", "%" + key + "%");
        }
        Page<Spu> pageInfo = (Page<Spu>) this.spuMapper.selectByExample(example);

        List<SpuBo> list = pageInfo.getResult().stream().map(spu -> {
            // 把spu變為 spuBo
            SpuBo spuBo = new SpuBo();
            // 屬性拷貝
            BeanUtils.copyProperties(spu, spuBo);

            // 2、查詢spu的商品分類名稱,要查三級分類
            List<String> names = this.categoryService.queryNameByIds(
                    Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
            // 將分類名稱拼接後存入
            spuBo.setCname(StringUtils.join(names, "/"));

            // 3、查詢spu的品牌名稱
            Brand brand = this.brandMapper.selectByPrimaryKey(spu.getBrandId());
            spuBo.setBname(brand.getName());
            return spuBo;
        }).collect(Collectors.toList());

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

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

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