sku算法详解及Demo~接上篇
前言
做过电商项目前端售卖的应该都遇见过不同规格产品库存的计算问题,业界名词叫做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下的单个元素
首先定义变量
skuStock
(库存对象),skuPartNameStock
(用于缓存非全名库存,如{'小': 4})将规格列表下的已选属性集合作为入参
selected
,如果在当前规格未选择相关属性则传入空字符串,即最开始时selected === ['', '']
判断当前已选属性
selected
是否已有缓存库存,有则直接返回缓存库存判断当前是否已全选,如果全选则返回从 skuStock 读取的库存,并在此之前及时缓存库存
定义库存变量 remainStock,将选属性数组 willSelected
遍历库存规格,判断当前规格属性是否已选,已选则将当前属性推入 willSelected
未选则遍历属性数组,将属性数组和已选数组 selected 组合,递归取得当前组合库存,并将库存进行累加
最后返回累加的库存作为已选属性为 selected 时对应的库存,并及时缓存于 skuPartNameStock 对象中
// sku库存列表转对象
const skuStock = skuList.forEach(sku => {
this.skuStock[sku.skuGroup && sku.skuGroup.join("-")] = sku.remainStock;
});
// 用于缓存库存信息
const skuPartNameStock = {};
/**
* 获取库存
* @param {Array} selected 已选属性数组
* @return {Object} skuInfo
*
*/
function getRemainByKey(selected) {
const selectedJoin = selected.join("-");
// 如果已有缓存则返回
if (typeof skuPartNameStock[selectedJoin] !== "undefined") {
return skuPartNameStock[selectedJoin];
}
// 返回skuStock的库存,并及时缓存
if (selected.length === skuNameList.length) {
skuPartNameStock[selectedJoin] = skuStock[selectedJoin]
? skuStock[selectedJoin]
: 0;
return skuPartNameStock[selectedJoin];
}
let remainStock = 0;
const willSelected = [];
for (let i = 0; i < skuNameList.length; i += 1) {
// 对应规格的sku是否已选择
const exist = skuNameList[i].skuValues.find(
name => name === selected[0]
);
if (exist && selected.length > 0) {
willSelected.push(selected.shift());
} else {
// 对应sku未选择,则遍历该规格所有sku
for (let j = 0; j < skuNameList[i].skuValues.length; j += 1) {
remainStock += this.getRemainByKey(
willSelected.concat(skuNameList[i].skuValues[j], selected)
);
}
break;
}
}
// 返回前缓存
skuPartNameStock[selectedJoin] = remainStock;
return skuPartNameStock[selectedJoin];
}
demo演示
利用此算法写了个 skuModal
的 vue demo,在此贴下代码,大家可以作为组件引用看看效果方便理解
<template>
<div v-if="visible" class="modal">
<div class="content">
<div class="title">
{{ skuInfo.specName }}
<span class="close" @click="close">
<svg
t="1590840102842"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1264"
width="32"
height="32"
>
<path
d="M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z"
p-id="1265"
fill="#666666"
></path>
</svg>
</span>
</div>
<div class="info">
<img :src="skuInfo.pic" class="pic" />
<div class="sku-info">
<span class="price">
¥{{
skuInfo.minPrice === skuInfo.maxPrice
? skuInfo.minPrice
: skuInfo.minPrice + "-" + skuInfo.maxPrice
}}
</span>
<span class="selected">{{ skuInfo.selectedTip }}</span>
<span class="stock">剩余{{ skuInfo.remainStock }}件</span>
</div>
</div>
<div v-for="(sku, index) in skuStatusGroup" :key="index" class="spec">
<span class="name">{{ sku.name }}</span>
<div class="group">
<span
v-for="(keyInfo, idx) in sku.list"
:key="idx"
class="spec-name"
:class="{
active: keyInfo.status === 1,
disabled: keyInfo.status === -1
}"
@click="selectSku(index, idx)"
>{{ keyInfo.key }}</span
>
</div>
</div>
<div class="footer">
<button
class="btn"
:class="skuInfo.isSelectedAll ? 'active' : ''"
type="button"
@click="confirm"
>
确认
</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
visible: Boolean
},
data() {
return {
skuInfo: {
// 当前选择的sku信息
minPrice: 0,
maxPrice: 0,
pic: "",
selected: [], // 已选sku 未选择用 '' 占位
realSelectd: [],
selectedTip: "",
specName: "",
stock: 0,
isSelectedAll: false
},
skuStatusGroup: [], // 当前sku状态数组
skuStock: {}, // sku对应库存 红-大
skuPartNameStock: {}, // sku对应库存(不完全名) 红
skuList: [], // 接口返回的sku列表
skuInfoCache: {} // 缓存不同sku的skuInfo
};
},
methods: {
initSku(data) {
const { skuList, skuNameList } = data;
// 清空旧的sku数据
this.clearOldSku();
skuNameList.forEach(({ skuName, skuValues }) => {
this.skuStatusGroup.push({
name: skuName,
list: skuValues.map(value => ({
key: value,
status: 0 // 0 可选 -1 不可选 1 已选
}))
});
});
this.skuNameList = skuNameList;
// 规格文案
this.skuInfo.specName = skuNameList.map(item => item.skuName).join(" | ");
// sku 初始库存
skuList.forEach(sku => {
this.skuStock[sku.skuGroup && sku.skuGroup.join("-")] = sku.remainStock;
});
// sku原始列表
this.skuList = skuList || [];
// 首次过滤sku库存
this.filterSkuKey();
},
// 清空旧sku数据
clearOldSku() {
this.skuStatusGroup = [];
this.skuStock = {};
this.skuPartNameStock = {};
this.skuList = [];
this.skuInfoCache = {};
},
close() {
this.$emit("update:visible", false);
},
// 更新skuInfo
updateSkuInfo(selected) {
const { skuStatusGroup } = this;
const realSelectd = selected.filter(item => item);
const priceInfo = this.getskuInfoByKey(selected);
const stock = this.getRemainByKey(realSelectd);
const isSelectedAll = realSelectd.length === selected.length;
const selectedTip = isSelectedAll
? `已选择 ${realSelectd.join("、")}`
: `请选择 ${selected
.map((item, idx) => {
if (!item) {
return skuStatusGroup[idx].name;
}
return null;
})
.filter(item => item)
.join("、")}`;
this.skuInfo = Object.assign({}, this.skuInfo, priceInfo, {
selected,
stock,
realSelectd,
isSelectedAll,
selectedTip
});
},
// 根据已选sku及库存更新sku列表状态
filterSkuKey() {
const { skuStatusGroup } = this;
const selected = [];
// 通过sku状态获取已选数组
skuStatusGroup.forEach(sku => {
let pos = 0;
const isInSelected = sku.list.some((skuInfo, idx) => {
pos = idx;
return skuInfo.status === 1;
});
selected.push(isInSelected ? sku.list[pos].key : "");
});
// 更新skuInfo
this.updateSkuInfo(selected);
// 根据已选择的sku来筛选库存
skuStatusGroup.forEach((sku, skuIdx) => {
const curSelected = selected.slice();
// 已选的不用更新
sku.list.forEach(skuInfo => {
if (skuInfo.status === 1) {
return;
}
// 将不同sku代入计算库存
const cacheKey = curSelected[skuIdx];
curSelected[skuIdx] = skuInfo.key;
const stock = this.getRemainByKey(curSelected.filter(item => item));
curSelected[skuIdx] = cacheKey;
// 更新sku状态
if (stock <= 0) {
// eslint-disable-next-line no-param-reassign
skuInfo.status = -1;
} else {
// eslint-disable-next-line no-param-reassign
skuInfo.status = 0;
}
});
});
},
// sku按钮点击 选择sku
selectSku(listIdx, keyIdx) {
const { list } = this.skuStatusGroup[listIdx];
const { status } = list[keyIdx];
// status -1 无库存 0 未选择 1 已选择
if (status === -1) {
return;
}
// 更新该规格下sku选择状态
list.forEach((keyInfo, idx) => {
if (keyInfo.status !== -1) {
if (idx === keyIdx) {
// eslint-disable-next-line no-param-reassign
keyInfo.status = 1 - status;
} else {
// eslint-disable-next-line no-param-reassign
keyInfo.status = 0;
}
}
});
// 根据库存更新可选sku
this.filterSkuKey();
},
/**
* 获取已选择的sku匹配的商品信息
* @param {Array} selected 已选sku数组
*/
getskuInfoByKey(selected = []) {
const { skuList } = this;
const cacheInfo = this.skuInfoCache[
selected.filter(item => item).join("-")
];
// 如果已有缓存信息则直接返回
if (cacheInfo) {
return cacheInfo;
}
const info = {
minPrice: -1,
maxPrice: -1,
pic: ""
};
skuList.forEach(sku => {
const group = sku.skuGroup;
// 通过已选的 key => key 来确定是否匹配
const isInclude = selected.every(
(name, index) => name === "" || name === group[index]
);
if (isInclude) {
const { minPrice, maxPrice } = info;
// 排除首次 -1
info.minPrice =
minPrice === -1 ? sku.price : Math.min(minPrice, sku.price);
info.maxPrice =
maxPrice === -1 ? sku.price : Math.max(maxPrice, sku.price);
info.pic = sku.picUrl;
}
});
// 如果主sku未选择,则默认使用第一张图
if (selected[0] === "") info.pic = skuList[0].picUrl;
this.skuInfoCache[selected.filter(item => item).join("-")] = info;
return info;
},
/**
* sku算法 获取已选择sku的库存数
* @param {Array} selected 已选择的sku数组
*/
getRemainByKey(selected = []) {
const { skuStock, skuPartNameStock, skuNameList } = this;
const selectedJoin = selected.join("-");
// 如果已有缓存则返回
if (typeof skuPartNameStock[selectedJoin] !== "undefined") {
return skuPartNameStock[selectedJoin];
}
// 所有sku已选择 及时缓存
if (selected.length === skuNameList.length) {
skuPartNameStock[selectedJoin] = skuStock[selectedJoin]
? skuStock[selectedJoin]
: 0;
return skuPartNameStock[selectedJoin];
}
let remainStock = 0;
const willSelected = [];
for (let i = 0; i < skuNameList.length; i += 1) {
// 对应规格的sku是否已选择
const exist = skuNameList[i].skuValues.find(
_item => _item === selected[0]
);
if (exist && selected.length > 0) {
willSelected.push(selected.shift());
} else {
// 对应sku未选择,则遍历该规格所有sku
for (let j = 0; j < skuNameList[i].skuValues.length; j += 1) {
remainStock += this.getRemainByKey(
willSelected.concat(skuNameList[i].skuValues[j], selected)
);
}
break;
}
}
// 返回前缓存
skuPartNameStock[selectedJoin] = remainStock;
return skuPartNameStock[selectedJoin];
},
// 确认订单
confirm() {
const { skuList } = this;
if (skuList.length > 1 && !this.skuInfo.isSelectedAll) {
return;
}
const { skuId } = this.skuList.filter(item => {
if (item.skuGroup.join("-") === this.skuInfo.realSelectd.join("-")) {
return true;
}
return false;
})[0];
this.$emit("confirm", skuId);
}
}
};
</script>
<style lang="less" scoped>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.2);
}
.content {
position: absolute;
top: 50%;
left: 50%;
max-height: 900px;
padding: 0 20px 20px;
overflow: auto;
background: #fff;
border-radius: 12px;
transform: translate(-50%, -50%);
z-index: 1;
.title {
display: flex;
justify-content: space-between;
color: #666;
font-size: 32px;
line-height: 60px;
text-align: left;
border-bottom: 1px solid #eee;
.close {
display: flex;
align-items: center;
}
}
.info {
display: flex;
margin-top: 10px;
.pic {
width: 180px;
height: 180px;
border-radius: 4px;
}
.sku-info {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 30px;
color: #999;
font-size: 26px;
span {
margin-bottom: 20px;
}
.price {
color: #333;
}
}
}
.spec {
display: flex;
padding: 20px;
.name {
color: #999;
font-size: 24px;
line-height: 54px;
}
.group {
margin-left: 20px;
.spec-name {
display: inline-block;
height: 54px;
margin: 0 30px 10px 0;
padding: 0 40px;
line-height: 54px;
color: #333;
font-size: 28px;
background: rgba(245, 245, 245, 1);
border-radius: 28px;
border: 1px solid rgba(204, 204, 204, 1);
&.active {
color: #ff981a;
background: #ffeeeb;
border: 1px solid #ff981a;
}
&.disabled {
color: #cccccc;
background: #f5f5f5;
border: 1px solid transparent;
}
}
}
}
.btn {
width: 690px;
height: 80px;
color: rgba(255, 255, 255, 1);
font-size: 32px;
background: rgba(204, 204, 204, 1);
border-radius: 44px;
outline: none;
&.active {
color: #fff;
background: #ff981a;
}
}
}
}
</style>
总结
做过电商项目的应该都处理或者听说过 sku,学习相关概念和真正理解如何计算 sku 可以帮助我们更加熟悉业务,提升自己对于相关业务的处理能力。以后在面试中遇到面试官的提问也能更稳一些。第一种 sku 算法可以参考上一篇博客。
参考
欢迎到前端学习打卡群一起学习~ 516913974
sku算法详解及Demo~接上篇的更多相关文章
- FloodFill算法详解及应用
啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色. 这种算法思想还在许多其他地方有应用.比如说扫雷 ...
- BM算法 Boyer-Moore高质量实现代码详解与算法详解
Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...
- kmp算法详解
转自:http://blog.csdn.net/ddupd/article/details/19899263 KMP算法详解 KMP算法简介: KMP算法是一种高效的字符串匹配算法,关于字符串匹配最简 ...
- 机器学习经典算法详解及Python实现--基于SMO的SVM分类器
原文:http://blog.csdn.net/suipingsp/article/details/41645779 支持向量机基本上是最好的有监督学习算法,因其英文名为support vector ...
- [转] KMP算法详解
转载自:http://www.matrix67.com/blog/archives/115 KMP算法详解 如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段. 我们这里说的K ...
- 【转】AC算法详解
原文转自:http://blog.csdn.net/joylnwang/article/details/6793192 AC算法是Alfred V.Aho(<编译原理>(龙书)的作者),和 ...
- KMP算法详解(转自中学生OI写的。。ORZ!)
KMP算法详解 如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段. 我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法.KMP算法是拿来处理字符串匹配的.换句 ...
- EM算法详解
EM算法详解 1 极大似然估计 假设有如图1的X所示的抽取的n个学生某门课程的成绩,又知学生的成绩符合高斯分布f(x|μ,σ2),求学生的成绩最符合哪种高斯分布,即μ和σ2最优值是什么? 图1 学生成 ...
- Tarjan算法详解
Tarjan算法详解 今天偶然发现了这个算法,看了好久,终于明白了一些表层的知识....在这里和大家分享一下... Tarjan算法是一个求解极大强联通子图的算法,相信这些东西大家都在网络上百度过了, ...
随机推荐
- VMware15.5.0安装MacOS10.15.0系统 安装步骤(上)
VMware15.5.0安装MacOS10.15.0系统安装步骤(上)超详细! 说明: 本文是目前最新的安装和调配教程且MacOS10.15和10.16版本搭建方法相同,我也会在一些细节地方加上小技巧 ...
- C. Game with Chips(陷阱暴力题)
\(为什么说这是个陷阱呢??\) \(因为不管你脑洞多大,数学多好,都发现会束手无策\) \(每移动一次不知道往哪个方向,不知道先访问哪个点,同时要记录所有点的坐标,记录每个点是否访问过目标点.... ...
- python学习之列表的定义以及增删改查
列表定义: >>> name['lily','lucy','tom'] >>> nums = [11,22,33,'100','lily'] #python中的列表 ...
- 【漫画】JAVA并发编程之并发模拟工具
原创声明:本文来源于公众号[胖滚猪学编程],转载请注明出处. 上一节[漫画]JAVA并发编程三大Bug源头(可见性.原子性.有序性)我们聊了聊并发编程的三个bug源头,这还没开始进入并发世界,胖滚猪就 ...
- [hdu3572]最大流(dinic)
题意:有m台机器,n个任务,每个任务需要在第si~ei天之间,且需要pi天才能完成,每台机器每天只能做一个任务,不同机器每天不能做相同任务,判断所有任务是否可以做完. 思路: 把影响答案的对象提取出来 ...
- [hdu5312]数的拆分,数学推导
题意:给定一个序列,a[n]=3n(n-1)+1,n>=1,求给定的m(m<=1e9)最少可以用几个a里面的数表示(可以重复) 思路:对答案分类 (1)假定答案为1,则m必定是a中的某一个 ...
- [hdu5247]rmq+预处理
题意:有一个无序数组,求有多少个长度为k的区间满足把区间内的数排序后是连续的. 思路:长度为k的区间排序后是 连续的数等价于maxval-minval等于k-1并且不同的数有k个(或者说没有相同的数) ...
- 关于fromdata的上传文件问题
<div <label>上传pdf</label> <input id="fileId" type="file" accep ...
- springmvc 文件上传异步处理
springmvc3提供了文件上传异步处理功能,当文件上传时,controller不需要一直等到文件上传成功后再返回视图,而是先返回到servlet容器,待异步处理的线程完成后转向指定视图! 首先要在 ...
- ClickHouse基本操作(二)
一.先来说一下,ClickHouse为啥快 MySQL单条SQL是单线程的,只能跑满一个core,ClickHouse相反,有多少CPU,吃多少资源,所以飞快: ClickHouse不支持事务,不存在 ...