商品 SKU 组合查询算法实现

5,617 阅读7分钟

title: 商品 SKU 组合查询算法实现 date: 2019-09-25 08:44:01 categories: Web tags:

前言

在系统学习了98集的数据结构和算法之后,以为自己的水平已经达到了一个新的高度了。但是在做一个APP的时候,发现自己竟然处理不了加入购物车的多级SKU对应的库存问题。着实是一个不小的打击。在解决问题之后,记录下来,牢记耻辱。

什么是SKU

维基百科解释:

存货单位(英语:stock keeping unit,SKU/ˌɛsˌkeɪˈjuː/)[1],也翻译为库存单元,是一个会计学名词,定义为库存管理中的最小可用单元,例如纺织品中一个SKU通常表示规格、颜色、款式,而在连锁零售门店中有时称单品为一个SKU。最小库存管理单元可以区分不同商品销售的最小单元,是科学管理商品的采购、销售、物流和财务管理以及POS和MIS系统的数据统计的需求,通常对应一个管理信息系统的编码。

举个例子: 如图所示

该商品有四个属性单位 公母重量数量颜色分类 四个属性又有对应的 单位

{
    '公母' : ['公的', '母的'],
    '重量' : ['100克', '200克', '300克', '400克']
    '数量' : ['一对', '两对', '三对', '四对']
    '颜色分类' : ['花色', '原色', '黑色', '金色']
}

如图选中的的 ['200克', '三对'] 是一个SKU,

['公的', '花色']

['300克', '三对']

['三对']

['母的','300克', '三对']

['公的','100克', '三对','原色'] 这些组合都是一个SKU

也就是说明,公母重量数量颜色分类对应的属性,单独或者两个属性或者以上的组合都是一个SKU

业务场景

在电商项目很几率在遇上,商品 SKU 组合查询的实现。如图:

以 公母、重量组合成对应的 SKU如下表格:

公母 重量 库存
100克 10
200克 10
300克 10
400克 10
100克 10
200克 10
300克 0
400克 10

图中只是选中的 ['母'] 有库存,然后查找对照上面的表格 ['母', '300克']。当用户选择['母'] 的时候,那么对应的['母的', '300克']300克的选项应该显示为不可选择状态 (平常用淘宝的觉得这个功能很简单,但...实际开发起来自己却不会(尴尬)。)

解决方案

场景还原

 // 属性集
var key = [
    {name: '公母', item: ['公的', '母的']},
    {name: '重量', item: ['100克', '200克', '300克']},
    {name: '数量', item: ['一对', '两对', '三对']}
];

// 数据集
var sku = {
    '公的;100克;一对': {price: 100, count: 10},
    '公的;200克;一对': {price: 101, count: 0},
    '公的;300克;一对': {price: 102, count: 12},
    '母的;100克;一对': {price: 103, count: 13},
    '母的;200克;一对': {price: 104, count: 14},
    '母的;300克;一对': {price: 105, count: 3},
    '公的;100克;两对': {price: 100, count: 0},
    '公的;200克;两对': {price: 101, count: 11},
    '公的;300克;两对': {price: 102, count: 9},
    '母的;100克;两对': {price: 100, count: 0},
    '母的;200克;两对': {price: 101, count: 11},
    '母的;300克;两对': {price: 102, count: 0},
    '公的;100克;三对': {price: 100, count: 10},
    '公的;200克;三对': {price: 101, count: 11},
    '公的;300克;三对': {price: 102, count: 3},
    '母的;100克;三对': {price: 100, count: 10},
    '母的;200克;三对': {price: 101, count: 11},
    '母的;300克;三对': {price: 102, count: 2}
};

有了这两组数据,就可以渲染 DOM 结构,然后用户点击的时候,获取对应的 key 拼接,再根据key从 数据集 里面 取得对应的信息

See the Pen 商品组合SKU算法实现(基础版) by bamboo (@celverbamboo) on CodePen.

上面这个演示有个最大的问题就是,必须把每个属性都选择后,才能获取到对应的库存和价格,如果没有选择完整,就无法获取对应的数据。

原因也很简单,因为 数据集 里没有提供嘛。比如只选择了['公的'],拼接出来之后,就变成了公的;;,再看看数据集,里面没有所以需要对数据集进行处理。

对数据集进行处理

首先,我们希望得到如下数据 当前sku为 ['公的', '100克', '一对']

['公的', '', '']
['', '100克', '']
['', '', '一对']
['公的', '100克', '']
['公的', '', '一对']
['', '100克', '一对']
['公的', '100克', '一对']

如果sku['公的', '100克', '一对', '花色']

['公的', '', '', '']
['', '100克', '', '']
['', '', '一对', '']
['', '', '', '花色']

['黑', '100克', '', '']
['黑', '', '一对', '']
['黑', '', '', '花色']
['', '100克', '一对', '']
['', '100克', '', '花色']
['', '', '一对', '花色']

['黑', '100克', '一对', '']
['黑', '100克', '', '花色']
['黑', '', '一对', '花色']
['', '100克', '一对', '花色']

['黑', '100克', '一对', '花色']

由此可以得出,我们需要 从 m 个不同元素中取出 n 个元素的组合算法,然后根据该组合,并且获取对应的sku的key,把重复sku的key库存加上。

算法代码(JS版)

// 属性集
const keys = [
    ['10', '11'],
    ['20', '21'],
    ['30', '31', '32']
];

// 数据集, 一般由后台返回
const data = {
    '10;20;30': { price: 1, count: 0 },
    '10;20;31': { price: 2, count: 2 },
    '10;20;32': { price: 3, count: 3 },
    '11;20;30': { price: 4, count: 4 },
    '11;20;31': { price: 5, count: 5 },
    '11;20;32': { price: 6, count: 6 },
    '10;21;30': { price: 7, count: 0 },
    '10;21;31': { price: 8, count: 8 },
    '10;21;32': { price: 9, count: 0 },
    '11;21;30': { price: 10, count: 10 },
    '11;21;31': { price: 11, count: 11 },
    '11;21;32': { price: 12, count: 0 },
};
//保存最后的组合结果信息
const SKUResult = {};

// 调用函数
initSKU();
console.log(SKUResult);

//初始化得到结果集
function initSKU() {
    let i, j, skuKeys = getObjKeys(data); // 过滤获取所有对应的sku-key数组
    for (i = 0; i < skuKeys.length; i++) {
        const skuKey = skuKeys[i];//一条SKU信息key
        const sku = data[skuKey];	//一条SKU信息value
        const skuKeyAttrs = skuKey.split(';'); //SKU信息key属性值数组
        skuKeyAttrs.sort(function(value1, value2) {
            return parseInt(value1) - parseInt(value2);
        });

        //对每个SKU信息key属性值进行拆分组合
        const combArr = combInArray(skuKeyAttrs);
        for (j = 0; j < combArr.length; j++) {
            addSKUResult(combArr[j], sku);
        }

        //结果集接放入SKUResult
        SKUResult[skuKeyAttrs.join(';')] = {
            count: sku.count,
            prices: [sku.price]
        }
    }
}

//获得对象的key
function getObjKeys(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    const keys = [];
    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
                keys[keys.length] = key;
        }
    }
    return keys;
}

//把组合的key放入结果集SKUResult
function addSKUResult(combArrItem, sku) {
    const key = combArrItem.join(';');
    if (SKUResult[key]) {//SKU信息key属性·
        SKUResult[key].count += sku.count;
        SKUResult[key].prices.push(sku.price);
    } else {
        SKUResult[key] = {
            count: sku.count,
            prices: [sku.price]
        };
    }
}

/**
 * 得到从 m 元素中取 n 元素的所有组合
 * 结果为[0,1...]形式的数组, 1表示选中,0表示不选
 */
function getCombFlags(m, n) {
    if (!n || n < 1) {
        return [];
    }

    const aResult = [];
    const aFlag = [];
    let bNext = true;
    let i, j, iCnt1;

    for (i = 0; i < m; i++) {
        aFlag[i] = i < n ? 1 : 0;
    }

    aResult.push(aFlag.concat());

    while (bNext) {
        iCnt1 = 0;
        for (i = 0; i < m - 1; i++) {
            if (aFlag[i] === 1 && aFlag[i + 1] === 0) {
                for (j = 0; j < i; j++) {
                    aFlag[j] = j < iCnt1 ? 1 : 0;
                }
                aFlag[i] = 0;
                aFlag[i + 1] = 1;
                const aTmp = aFlag.concat();
                aResult.push(aTmp);
                if (aTmp.slice(-n).join('').indexOf('0') === -1) {
                    bNext = false;
                }
                break;
            }
            aFlag[i] === 1 && iCnt1++;
        }
    }
    return aResult;
}

/**
 * 从数组中生成指定长度的组合
 * 方法: 先生成[0,1...]形式的数组, 然后根据0,1从原数组取元素,得到组合数组
 */
function combInArray(aData) {
    if (!aData || !aData.length) {
        return [];
    }

    const len = aData.length;
    const aResult = [];

    for (let n = 1; n < len; n++) {
        const aaFlags = getCombFlags(len, n);
        // console.log(JSON.stringify(aaFlags))
        while (aaFlags.length) {
            const aFlag = aaFlags.shift();
            const aComb = [];
            for (let i = 0; i < len; i++) {
                aFlag[i] && aComb.push(aData[i]);
            }
            aResult.push(aComb);
        }
    }
    return aResult;
}

在调用initSKU之后,打印 SKUResult则获得以下结果。

完整版

在获得数据集之后,在点击对应的SKU属性之后,应该获取当前的SKU,然后和其它的属性对应的SKU进行遍历拼接。即可获得从 SKUResult的商品信息。效果如图:

See the Pen 商品组合SKU实现算法(完整版) by bamboo (@celverbamboo) on CodePen.