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算法是一个求解极大强联通子图的算法,相信这些东西大家都在网络上百度过了, ...
随机推荐
- jacoco 生成单测覆盖率报告
一.jacoco 简介 jacoco 是一个开源的覆盖率工具,它针对的开发语言是 java.其使用方法很灵活,可以嵌入到 ant.maven 中:可以作为 Eclipse 插件:可以作为 javaAg ...
- Spring官网阅读(二)(依赖注入及方法注入)
上篇文章我们学习了官网中的1.2,1.3两小节,主要是涉及了容器,以及Spring实例化对象的一些知识.这篇文章我们继续学习Spring官网,主要是针对1.4小节,主要涉及到Spring的依赖注入.虽 ...
- xpath加PHP对网站相关数据的截取
首先了解一串代码 <?php $url = 'http://www.baidu.com';$ch = curl_init();curl_setopt($ch, CURLOPT_FILE, fo ...
- 基于C语言的Q格式使用详解
用过DSP的应该都知道Q格式吧: 目录 1 前言 2 Q数据的表示 2.1 范围和精度 2.2 推导 3 Q数据的运算 3.1 0x7FFF 3.2 0x8000 3.3 加法 3.4 减法 3.5 ...
- [C#] StringFormat详解之文本方向、对齐
在使用GDI方式处理文本时,往往会用到StringFormat.里面的某些点有点反直觉,不够直观,所以本篇就通过图文的方式去讲解一下. 本篇内容仅涉及到文本方向.对齐的相关内容. 如有错误.不妥之处, ...
- 内容安全策略(CSP)详解
1.背景 1.1.同源策略 网站的安全模式源于"同源策略",web浏览器允许第一个web页面中的脚本访问页面中的数据,但前提是两个web页面具有相同的源.此策略防止一个页面的恶意脚 ...
- DevOps vs. Agile:它们有什么共同点?
DevOps与Agile有很多不同,但它们之间仍可发现很多共同点,这篇文章为读者揭晓. DevOps和Agile之间有着明显的关系.Agile是方法论,Scrum是框架,并DevOps随着看板也落在了 ...
- php_rce
0x01 PHP_RCE RCE(remote command/code execute):远程命令/代码执行 此题为ThinkPHP V5远程代码执行漏洞 0x02 命令执行 http://124. ...
- python实现登录密码重置简易操作
需求: 1.用户输入密码正确登录 2.用户输入密码错误退出并调用函数继续输入 3.用户输入密码符合原先给定的一个值时,允许用户重置密码,并且可以用新密码登录 4.输入三次后禁止输入 虽然贴别的简单,但 ...
- elasticsearch 小总结
elasticsearch 小总结 0. 起因 距离初次写关于es的文章 https://blog.csdn.net/aca_jingru/article/details/44488703 已经过去4 ...