sku算法介绍及实现
前言
做过电商项目前端售卖的应该都遇见过不同规格产品库存的计算问题,业界名词叫做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