前言
做過電商專案前端售賣的應該都遇見過不同規格產品庫存的計算問題,業界名詞叫做sku(stock Keeping Unit)
,庫存量單元對應我們售賣的具體規格,比如一部手機具體型號規格,其中iphone6s 4G 紅色
就是一個sku
。這裡我們區別spu(Standard Product Unit)
,標準化產品單元,比如一部手機型號iphone6s
就是一個spu
。
sku 演算法
在前端展示商品時,根據使用者選擇的不同sku
,我們需要計算出不同的庫存量動態展示給使用者,這裡就衍生出了sku
演算法。
資料結構
我們先看看在後端伺服器儲存庫存的資料結構一般是長怎麼樣的:
// 庫存列表
const skuList = [
{
skuId: '0',
skuGroup: ['紅色', '大'],
remainStock: 7,
price: 2,
picUrl: 'https://dummyimage.com/100x100/ff00b4/ffffff&text=大'
},
{
skuId: '1',
skuGroup: ['紅色', '小'],
remainStock: 3,
price: 4,
picUrl: 'https://dummyimage.com/100x100/ff00b4/ffffff&text=小'
},
{
skuId: '2',
skuGroup: ['藍色', '大'],
remainStock: 0,
price: 0.01,
picUrl: 'https://dummyimage.com/100x100/0084ff/ffffff&text=大'
},
{
skuId: '3',
skuGroup: ['藍色', '小'],
remainStock: 1,
price: 1,
picUrl: 'https://dummyimage.com/100x100/0084ff/ffffff&text=小'
}
];
// 規格列表
const skuNameList = [
{
skuName: "顏色",
skuValues: ["紅色", "藍色"]
},
{
skuName: "尺寸",
skuValues: ["大", "小"]
}
];
演算法演示
在前端使用者選擇單個規格或多個規格後,我們需要動態計算出此時其他按鈕是否還能點選(組合有庫存),以及當前狀態對應的總庫存量,封面圖和價格區間。
以上面的資料舉個 ?
開始時什麼都沒有選擇,展示預設圖片,規格列表中的第一項組合(['紅色-大'])對應的圖片,庫存為商品總庫存,價格為商品的價格區間。然後在使用者選擇某個屬性或幾個屬性的時候實時計算對應的圖片,庫存,價格區間。
同時根據當前已選屬性,置灰不可選擇的屬性。在本例中,藍色 大
的產品對應的庫存為 0,所以當我們選擇其中一項 藍色 或者 大 的時候,需要置灰另一個屬性選項。
實現思路-第一種演算法
為了大家能看清下面的分析,在此定義下相關名詞,庫存列表:skuList,規格列表:skuNameList,屬性:skuNameList-skuValues陣列下的單個元素,規格:skuNameList下的單個元素
sku 演算法的難點在於如何根據當前已選屬性(未選,選擇部分,全選)來判斷當前其它屬性是否可選
在網上查詢相關演算法時比較廣泛流傳的是來自淘寶前端的兩種演算法,下面將實現第一種演算法:
主要思路就是遍歷當前的規格列表下的屬性,根據已選屬性及庫存列表判斷當前屬性按鈕是否有庫存(可選):
-
將規格列表下的已選屬性集合作為入參
selected
,如果在當前規格未選擇相關屬性則傳入空字串,即最開始時selected === ['', '']
-
初始化相關變數,庫存,價格區間,圖片,及屬性物件
attrState
(attrState 用於標記每個屬性是否可選) -
遍歷規格列表,移除存在與當前規格屬性陣列中已選屬性(假設當前規格未選擇)
-
遍歷規格屬性,依次將當前屬性和已選屬性組合
-
將上步的組合作為最終屬性組合,遍歷庫存列表是否存在庫存(屬於子集關係且庫存>0),有庫存則標記可選(true)
-
最後再次遍歷庫存列表,用來獲取當前庫存,圖片,價格等 sku 資料
/**
* 獲取sku資訊
* @param {Array} selected 已選屬性陣列
* @return {Object} skuInfo
*
*/
const getSkuInfo = (selected) => {
// 用以記錄每個按鈕狀態的,例如 itemState['紅色'] = true 表示高亮
const attrState = {};
let picUrl = '';
let minPrice = Number.MAX_VALUE;
let maxPrice = 0;
let remainStock = 0;
// in array not in others
const difference = (array, others) => {
return array.filter((item) => others.indexOf(item) === -1);
};
// every in array in others
const isSubset = (array, others) => {
return array.every((item) => others.indexOf(item) > -1);
};
// 遍歷規格列表
skuNameList.forEach((spec) => {
// 移除當前遍歷規格下的已選屬性
const tempSelected = difference(selected, spec.skuValues).filter(name => name);
// 遍歷規格屬性
spec.skuValues.forEach((name) => {
const willSelected = [...tempSelected, name];
// 預設無庫存不可選
attrState[name] = false;
// 在庫存列表尋找匹配庫存
for (let i = 0, len = skuList.length; i < len; i++) {
const skuGroup = skuList[i].skuGroup;
if (isSubset(willSelected, skuGroup) && skuList[i].remainStock) {
attrState[name] = true;
break;
}
}
});
});
// 實際已選屬性,過濾空字串
const realSelected = selected.filter((item) => item);
// 預設選擇用於匹配當前圖片
const defaultSelected = selected.map(
(name, idx) => name || skuNameList[idx].skuValues[0]
);
skuList.forEach((sku) => {
if (isSubset(realSelected, sku.skuGroup)) {
remainStock += sku.remainStock;
maxPrice = maxPrice > sku.price ? maxPrice : sku.price;
minPrice = minPrice < sku.price ? minPrice : sku.price;
// 取當前圖片
if (isSubset(defaultSelected, sku.skuGroup)) {
picUrl = sku.picUrl;
}
}
});
return {
attrState,
picUrl,
minPrice,
maxPrice,
remainStock,
};
};
總結
做過電商專案的應該都處理或者聽說過sku,學習相關概念和真正理解如何計算sku可以幫助我們更加熟悉業務,提升自己對於相關業務的處理能力。以後在面試中遇到面試官的提問也能更穩一些。後面將介紹第二種演算法,其思路更容易理解,也是比較多同學在業務中採用的方式。
參考
歡迎到前端學習打卡群一起學習~516913974