title: 樂優商城學習筆記五-商品規格管理
date: 2019-04-15 13:29:57
tags:
- 樂優商城
- java
- springboot
- FastDFS
categories:
- 樂優商城
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的特有屬性進行設計,它可以看做是規格參數中的一部分。這樣規格參數中的屬性可以標記成兩部分:
- 所有sku共享的規格屬性(稱為全局屬性)
- 每個sku不同的規格屬性(稱為特有屬性)
1.2.4.搜索屬性
打開一個搜索頁,我們來看看過濾的條件:
你會發現,過濾條件中的屏幕尺寸、運行內存、網路、機身內存、電池容量、CPU核數等,在規格參數中都能找到:
也就是說,規格參數中的數據,將來會有一部分作為搜索條件來使用。我們可以在設計時,將這部分屬性標記出來,將來做搜索的時候,作為過濾條件。要注意的是,無論是SPU的全局屬性,還是SKU的特有屬性,都有可能作為搜索過濾條件的,并不沖突,而是有一個交集:
1.3.規格參數表
1.3.1.表結構
先看下規格參數表:
CREATE TABLE `tb_specification` (
`category_id` bigint(20) NOT NULL COMMENT '規格模板所屬商品分類id',
`specifications` varchar(3000) NOT NULL DEFAULT '' COMMENT '規格參數模板,json格式',
PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品規格參數模板,json格式。';
很奇怪是吧,只有兩個字段。特別需要注意的是第二個字段:
- specificatons:規格參數模板,json格式
為什么是一個json?我們看下規格參數的格式:
如果按照傳統數據庫設計,這里至少需要3張表:
- group:代表組,與商品分類關聯
- param_key:屬性名,與組關聯,一對多
- param_value:屬性備選值,與屬性名關聯,一對多
這樣程序的復雜度大大增加,但是提高了數據的復用性。
我們的解決方案是,采用json來保存整個規格參數模板,不需要額外的表,一個字符串就夠了。
1.3.2.json結構分析
先整體看一下:
- 因為規格參數分為很多組,所以json最外層是一個數組。
- 數組中是對象類型,每個對象代表一個組的數據,對象的屬性包括:
- group:組的名稱
- params:該組的所有屬性
接下來是params:
以主芯片
這一組為例:
group:注明,這里是主芯片
-
params:該組的所有規格屬性,因為不止一個,所以是一個數組。這里包含四個規格屬性:CPU品牌,CPU型號,CPU頻率,CPU核數。每個規格屬性都是一個對象,包含以下信息:
- k:屬性名稱
- searchable:是否作為搜索字段,將來在搜索頁面使用,boolean類型
- global:是否是SPU全局屬性,boolean類型。true為全局屬性,false為SKU的特有屬性
- options:屬性值的可選項,數組結構。起約束作用,不允許填寫可選項以外的值,比如CPU核數,有人添10000核豈不是很扯淡
- numerical:是否為數值,boolean類型,true則為數值,false則不是。為空也代表非數值
- unit:單位,如:克,毫米。如果是數值類型,那么就需要有單位,否則可以不填。
上面的截圖中所有屬性都是全局屬性,我們來看看內存,應該是特有屬性:
總結下:
- 規格參數分組,每組有多個參數
- 參數的
k
代表屬性名稱,沒有值,具體的SPU才能確定值 - 參數會有不同的屬性:是否可搜索,是否是全局、是否是數值,這些都用boolean值進行標記:
- SPU下的多個SKU共享的參數稱為全局屬性,用
global
標記 - SPU下的多個SKU特有的參數稱為特有屬性
- 如果參數是數值類型,用
numerical
標記,并且指定單位unit
- 如果參數可搜索,用
searchable
標記
- SPU下的多個SKU共享的參數稱為全局屬性,用
2.商品規格參數管理
2.1.1.頁面實現
因為規格是跟商品分類綁定的,因此首先會展現商品分類樹,并且提示你要選擇商品分類,才能看到規格參數的模板。一起了解下頁面的實現:
可以看出頁面分成3個部分:
v-card-title
:標題部分,這里是提示信息,告訴用戶要先選擇分類,才能看到模板-
v-tree
:這里用到的是我們之前講過的樹組件,展示商品分類樹,不過現在是假數據,我們只要把treeData
屬性刪除,它就會走url
屬性指定的路徑去查詢真實的商品分類樹了。<v-tree url="/item/category/list" :isEdit="false" @handleClick="handleClick" />
-
v-dialog
:Vuetify提供的對話框組件,v-model綁定的dialog屬性是boolean類型:- true則顯示彈窗
- false則隱藏彈窗
2.1.2.data中定義的屬性
接下來,看看Vue實例中data定義了哪些屬性,對頁面會產生怎樣的影響:
[圖片上傳失敗...(image-3b8613-1555314257815)]
- specifications:選中一個商品分類后,需要查詢后臺獲取規格參數信息,保存在這個對象中,Vue會完成頁面渲染。
- oldSpec:當前頁兼具了規格的增、改、查等功能,這個對象記錄被修改前的規格參數,以防用戶撤銷修改,用來恢復數據。
- dialog:是否顯示對話框的標記。true則顯示,false則不顯示
- currentNode:記錄當前選中的商品分類節點
- isInsert:判斷接下來是新增還是修改
2.2.規格參數的查詢
點擊樹節點后要顯示規格參數,因此查詢功能應該編寫在點擊事件中。
了解一下:
2.2.1.樹節點的點擊事件
當我們點擊樹節點時,要將v-dialog
打開,因此必須綁定一個點擊事件:
我們來看下handleClick
方法:
handleClick(node) {
// 判斷點擊的節點是否是父節點(只有點擊到葉子節點才會彈窗)
if (!node.isParent) {
// 如果是葉子節點,那么就發起ajax請求,去后臺查詢商品規格數據。
this.$http.get("/item/spec/" + node.id)
.then(resp => {
// 查詢成功后,把響應結果賦值給specifications屬性,Vue會進行自動渲染。
this.specifications = resp.data;
// 記錄下此時的規格數據,當頁面撤銷修改時,用來恢復原始數據
this.oldSpec = resp.data;
// 打開彈窗
this.dialog = true;
// 標記此時要進行修改操作
this.isInsert = false;
})
.catch(() => {
// 如果沒有查詢成功,那么詢問是否添加規格
this.$message.confirm('該分類還沒有規格參數,是否添加?')
.then(() => {
// 如果要添加,則將specifications初始化為空
this.specifications = [{
group: '',
params: []
}];
// 打開彈窗
this.dialog = true;
// 標記為新增
this.isInsert = true;
})
})
}
}
2.2.2.后端代碼
entity
@Data
@Table(name = "tb_specification")
public class Specification {
@Id
private Long categoryId;
private String specifications;
}
mapper
public interface SpecificationMapper extends Mapper<Specification> {
}
controller
先分析下需要的東西,在頁面的ajax請求中可以看出:
請求方式:查詢,肯定是get
請求路徑:/spec/{cid} ,這里通過路徑占位符傳遞商品分類的id
請求參數:商品分類id
-
返回結果:頁面是直接把
resp.data
賦值給了specifications:1526104087329那么我們返回的應該是規格參數的字符串
代碼:
@RestController
@RequestMapping("spec")
public class SpecificationController {
@Autowired
private SpecificationService specificationService;
@GetMapping("{id}")
public ResponseEntity<String> querySpecificationByCategoryId(@PathVariable("id") Long id){
Specification spec = this.specificationService.queryById(id);
if (spec == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(spec.getSpecifications());
}
}
service:
@Service
public class SpecificationService {
@Autowired
private SpecificationMapper specificationMapper;
public Specification queryById(Long id) {
return this.specificationMapper.selectByPrimaryKey(id);
}
}
頁面訪問測試:
我們訪問:http://api.leyou.com/api/item/spec/76
2.3 規格參數添加
2.3.1 前端代碼
//添加分組
addGroup() {
this.specifications.push({
group: '',
params: []
})
},
// 添加新模板
addParam(i) {
this.specifications[i].params.push({
k: "",
searchable: false,
global: true,
numerical:false,
unit:"",
options: []
})
},
// 添加默認值
addOption(i, j) {
this.specifications[i].params[j].options.push("")
},
重點就是saveTemplate函數了,包含對規格模板的增加、修改和刪除等功能
// 保存、修改、刪除模板
saveTemplate() {
this.dialog = true;
//模板刪除
if (this.specifications.length === 0){
//console.log("刪除:"+this.currentNode.id);
this.$http.delete("/item/spec/"+this.currentNode.id).then(() => {
this.dialog = false;
this.$message.success("刪除成功!");
this.oldSpec = [];
}).catch(() => {
this.$message.error("刪除失敗");
});
}else {
this.$http({
method: this.oldSpec.length === 0 ? 'post' : 'put',
url: '/item/spec',
data: this.$qs.stringify({
categoryId: this.currentNode.id,
specifications: JSON.stringify(this.specifications)
})
})
.then(() => {
this.dialog = false;
this.$message.success("保存成功!")
this.oldSpec = [];
})
.catch(() => {
this.$message.error("保存失敗!")
});
}
2.3.2后臺
contoller
/**
*添加規格模板
* @param specification
* @return
*/
@PostMapping
public ResponseEntity<Void> saveSpecification(Specification specification){
this.specificationService.saveSpecification(specification);
return ResponseEntity.status(HttpStatus.OK).build();
}
/**
* 更新規格模板
* @param specification
* @return
*/
@PutMapping
public ResponseEntity<Void> updateSpecification(Specification specification){
this.specificationService.updateSpecification(specification);
return ResponseEntity.status(HttpStatus.OK).build();
}
/**
* 刪除規格模板
* @param id
* @return
*/
@DeleteMapping("{id}")
public ResponseEntity<Void> deleteSpecification(@PathVariable("id")Long id){
Specification specification = new Specification();
specification.setCategoryId(id);
this.specificationService.deleteSpecification(specification);
return ResponseEntity.status(HttpStatus.OK).build();
}
mapper
/**
* @Author smallmartial
* @Date 2019/4/14
* @Email smallmarital@qq.com
*/
public interface SpecificationMapper extends Mapper<Specification> {
}
service
public void saveSpecification(Specification specification) {
this.specificationMapper.insert(specification);
}
public void updateSpecification(Specification specification) {
/**
* updateByPrimaryKeySelective會對字段進行判斷再更新(如果為Null就忽略更新),
* 如果你只想更新某一字段,可以用這個方法。
*
* updateByPrimaryKey對你注入的字段全部更新,
* 如果為字段不更新,數據庫的值就為null。
*/
this.specificationMapper.updateByPrimaryKeySelective(specification);
}
public void deleteSpecification(Specification specification) {
this.specificationMapper.deleteByPrimaryKey(specification);
}