淘寶 sku 前端算法

在工作中遇到的計算sku的需求, 記錄下來思路和核心代碼以防不時之需。
使用的技術(shù)有 react框架lodash庫

sku.gif

確認后端返回的數(shù)據(jù)

"itemSkus": {
    "listData": [
      {
        "id": 598,
        "pictureUrl": "", //
        "sellPrice": 11.00, // 售價
        "totalInventory": 12, // 庫存
        "attributesIdArr": [200, 445] // 屬性組合
      },
      {
        // ...
        "sellPrice": 13, // 售價
        "attributesIdArr": [200, 447] // 屬性組合
      },
      {
        // ...
        "sellPrice": 31, // 售價
        "attributesIdArr": [302, 445] // 屬性組合
      },
      {
        // ...
        "sellPrice": 33, // 售價
        "attributesIdArr": [302, 447] // 屬性組合
      },
      {
        // ...
        "sellPrice": 20, // 售價
        "attributesIdArr": [201, 445] // 屬性組合
      },
      {
        // ...
        "sellPrice": 22, // 售價
        "attributesIdArr": [201, 446] // 屬性組合
      },
    ]
  },
  "attrs": {
    "listData": [
      {
        "id": 23,
        "name": "套餐",
        "attrValues": {
          "listData": [
            {
              "id": 200,
              "name": "一年簽證"
            },
            {
              "id": 201,
              "name": "兩年簽證"
            },
            {
              "id": 302,
              "name": "三年簽證"
            }
          ]
        }
      },
      {
        "id": 67,
        "name": "時效",
        "attrValues": {
          "listData": [
            {
              "id": 445,
              "name": "一年"
            },
            {
              "id": 446,
              "name": "兩年"
            },
            {
              "id": 447,
              "name": "三年"
            },
            {
              "id": 448,
              "name": "四年"
            }
          ]
        }
      }
     // ...
    ]
  },

現(xiàn)在我們有三組屬性和六種組合

<--  商品規(guī)格 -->
套餐 :  一年簽(200)    兩年簽(201)    三年簽(302)
時效 :  一年 (445)      兩年(446)      三年(447)     四年(448) 
服務 :    普通(500)      專享(501)

<-- sku -->
[200, 445]  -  一年簽 / 一年 
[200, 447]  -  一年簽 / 三年 
[302, 445]  -  三年簽 / 一年 
[302, 447]  -  三年簽 / 三年 
[201, 445]  -  兩年簽 / 一年 
[201, 446]  -  兩年簽 / 兩年 

接下來我們將從以下三步進行分析, 實現(xiàn)上圖的效果

1. 轉(zhuǎn)換商品規(guī)格的數(shù)據(jù)

這一步我們做兩件事情

  1. 過濾沒用的屬性
    從sku的組合中可知, "時效"的第四個屬性(四年) 和 "服務"整條屬性并未被用到, 所以最開始需要先過濾一遍商品規(guī)格
    (如果你的業(yè)務中每個屬性都被用到的話, 那么這一步不是必須的)
    【實現(xiàn)思路】
    • 循環(huán) sku 的組合, 將每個組合合并到 allAttributesIdArr 數(shù)組中, 即使數(shù)據(jù)重復也么關(guān)系
      image.png
    • 循環(huán)商品屬性, 判斷每一個規(guī)格的 id 是否存在 allAttributesIdArr 的數(shù)組中, 如果沒在的話則刪除這個規(guī)格的子屬性, 若每個子屬性都沒被用到則刪除整條規(guī)格

  2. 給每個屬性初始狀態(tài)
    我們需要有一個status字段控制標簽的 選中, 未選中 和 無效狀態(tài)
    初始化的時候每個標簽都是未選中狀態(tài)

實現(xiàn)中用的
cloneDeep 和 pullAt 方法來自于 lodash;
isInArray 則是自己封裝的工具類, 用于判斷某個值是否存在數(shù)組中

// 顯示狀態(tài) status 的常量
const STATUS_UNSELECTED = 0; // 未選中狀態(tài)
const STATUS_SELECTED = 1; // 選中狀態(tài)
const STATUS_DISABLED = 2; // 無效狀態(tài)

// 轉(zhuǎn)換商品規(guī)格的數(shù)據(jù)
convertAttrs = (record) => {
  const { attrs, itemSkus, itemCategoryId } = record;

  // itemSkus 中用到的所有屬性的 id 數(shù)組
  let allAttributesIdArr = [];
  for (let elem of Array.values(itemSkus.listData)) {
    allAttributesIdArr = attributesIdArr.concat(attributesIdArr);
  }

  // 遍歷 attrs 添加默認顯示狀態(tài)(未選中狀態(tài))同時刪除沒用的屬性或整條屬性
  let _attrs = cloneDeep(attrs);
  let deleteAttrIndex = [];
  for (let [attrIndex, elem] of Array.entries(attrs.listData)) {
    let deleteIndex = [];
    const _attrsElem = _attrs.listData[attrIndex].attrValues.listData;
    const attrsElemLen = _attrsElem.length;
    for (let [index, attrElem] of Array.entries(elem.attrValues.listData)) {
      // 添加初始狀態(tài)
      _attrsElem[index].status = STATUS_UNSELECTED;
      if (!isInArray(allAttributesIdArr, attrElem.id)) deleteIndex.push(index);
    }
    // 如果刪除數(shù)據(jù)的長度等于這條屬性的長度 那么刪除整條屬性 反之則刪除某些屬性
    attrsElemLen === deleteIndex.length
      ? deleteAttrIndex.push(attrIndex)
      : pullAt(_attrsElem, deleteIndex);
  }
  pullAt(_attrs.listData, deleteAttrIndex);
  return { attrs: _attrs, itemSkus };
};

2. 初始化數(shù)據(jù)

這一步我們同樣需要做兩件事情

  1. 計算出用戶可能選擇的所有組合(在數(shù)學上也稱為: 冪集)
    【實現(xiàn)思路】
  • 獲取單個組合可能產(chǎn)生的所有組合
    例如 [200, 445] 這個組合可能產(chǎn)生 [ [200], [445], [200, 445] ] 這三種組合
    具體算法參考這段代碼


    image.png

    上面代碼的計算過程是這樣

attributesIdArrItem    powerSet
                       [[]]   // 循環(huán)前的值
200                    [[], [200]]   // 將 200 和 powerSet 的元素合并, 并將得到的結(jié)果push到powerSet中得到新的powerSet = [[], [200]]
445                    [[], [200], [445], [200, 445]]   //將 400 和上一次的 powerSet 的每個元素合并, 將得到的結(jié)果push到powerSet中得到新的powerSet =  [[], [200], [445], [200, 445]]

  • 獲得這個組合的產(chǎn)生的所有可能組合后將他轉(zhuǎn)換成json格式, 并且將這個組合對應的數(shù)據(jù)放入完整的選擇中, 方便后續(xù)的取值
    image.png
  • 循環(huán) sku 計算出全部的可能
    image.png
  1. 未選中時頁面顯示的默認數(shù)據(jù), 如價格區(qū)間、庫存、圖片等 (本文只計算價格區(qū)間)
    循環(huán) sku 獲得所有價格的數(shù)組, 從中取出最大最小值就是我們想要的價格區(qū)間了

實現(xiàn)中用的
min 和 max 方法來自于 lodash;

// 獲取組合的冪集 && 計算價格區(qū)間
getSkuInit = (record) => {
  const { attrs, itemSkus } = this.convertAttrs(record);
  let skuAvailableSet = {}; // 所有可能的屬性組合
  let sellPriceArr = []; // 保存所有的價格
  let attrsLen = attrs.listData.length;

  // 獲取組合的冪集
  for (let skusItem of Array.values(itemSkus.listData)) {
    sellPriceArr.push(skusItem.sellPrice);

    // 儲存冪集
    let powerSet = [[]];
    for (let attributesIdArrItem of Array.values(skusItem.attributesIdArr)) {
      let len = powerSet.length;
      for (let i = 0; i < len; i++) {
        powerSet.push(powerSet[i].concat(attributesIdArrItem));
      }
    }

    for (let powerSetItem of Array.values(powerSet)) {
      const tmpSet = powerSetItem.join(',');
      // skuAvailableSet存成 [可能組合的id字符串]: [商品屬性], 后續(xù)取值的時候方便
      if (attrsLen === powerSetItem.length) {
        skuAvailableSet[tmpSet] = skusItem;
      } else if (tmpSet) {
        skuAvailableSet[tmpSet] = {};
      }
    }
  }
  // 計算價格區(qū)間
  const minsellPrice = min(sellPriceArr);
  const maxsellPrice = max(sellPriceArr);
  this.sellPriceRange =
    minsellPrice === maxsellPrice ? minsellPrice : `${minsellPrice}-${maxsellPrice}`;

  return { skuAvailableSet, sellPrice: this.sellPriceRange, attrs, itemSkus };
};

3. 點擊屬性

這一步我們需要判斷用戶點擊時, 其他屬性的顯示狀態(tài)(不可選還是未選中) 以及價格(或者圖片, 庫存等) 的變化
【實現(xiàn)思路】
!!!! 不行了 寫不動了, 明年回來再寫吧 !!!!

/*
    attrValue: 點擊的屬性
    attrIndex: 被點擊的屬性組的坐標(縱坐標)
    attrValuesIndex: 被點擊的屬性組下value的坐標(橫坐標)
*/
doSkuSet = (attrValue, attrIndex, attrValuesIndex) => {
  let { record, selectedValIdArr, skuAvailableSet, sellPrice } = this.state;
  let { attrs = { listData: [] }, ...other } = record;

  // 設置當前屬性的顯示狀態(tài)
  const status = attrValue.status;
  switch (status) {
    case STATUS_UNSELECTED:
      selectedValIdArr[attrIndex] = attrValue.id;
      attrs.listData[attrIndex].attrValues.listData[attrValuesIndex].status = STATUS_SELECTED;
      break;
    case STATUS_SELECTED:
      selectedValIdArr[attrIndex] = '';
      attrs.listData[attrIndex].attrValues.listData[attrValuesIndex].status = STATUS_UNSELECTED;
      break;
    case STATUS_DISABLED:
      return;
    default:
      break;
  }

  // 設置標簽的狀態(tài) & 商品的價格
  const selectedValIdTrueArr = compact(selectedValIdArr);
  if (selectedValIdTrueArr.length) {
    // 設置標簽顯示狀態(tài)
    for (let [attrsIndex, attrsItem] of Array.entries(attrs.listData)) {
      for (let attrValueElem of Array.values(attrsItem.attrValues.listData)) {
        const attrValueId = attrValueElem.id;
        //  當前屬性是否在已選中的 selectedValIdArr 中, 如果是的話不用重新設置狀態(tài)
        if (attrValueId === selectedValIdArr[attrsIndex]) continue;

        //  構(gòu)造當前屬性與其他attr已選中的屬性的組合
        let tmpSet = cloneDeep(selectedValIdArr);
        tmpSet[attrsIndex] = attrValueId;

        // 判斷tmpSet的組合是否可選
        if (skuAvailableSet[compact(tmpSet).join(',')]) {
          attrValueElem.status = STATUS_UNSELECTED;
        } else {
          attrValueElem.status = STATUS_DISABLED;
        }
      }
    }

    // 設置顯示價格
    const selecteSellPrice = skuAvailableSet[selectedValIdTrueArr.join(',')].sellPrice;
    sellPrice = selecteSellPrice ? selecteSellPrice : this.sellPriceRange;
  } else {
    // 全不選的時候?qū)⑺袑傩栽O置為初始值
    for (let attrsItem of Array.values(attrs.listData)) {
      for (let attrValueElem of Array.values(attrsItem.attrValues.listData)) {
        attrValueElem.status = STATUS_UNSELECTED;
      }
    }

    sellPrice = this.sellPriceRange;
  }

  this.setState({ selectedValIdArr, sellPrice, record: { attrs, ...other } });
  this.forceUpdate();
};
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容