14

sku算法详解及Demo~接上篇

 4 years ago
source link: http://www.cnblogs.com/formercoding/p/12995715.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

做过电商项目前端售卖的应该都遇见过不同规格产品库存的计算问题,业界名词叫做 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: ["大", "小"],
  },
];

算法演示

在前端用户选择单个规格或多个规格后,我们需要动态计算出此时其他按钮是否还能点击(组合有库存),以及当前状态对应的总库存量,封面图和价格区间。

以上面的数据举个 :chestnut:

Z3IbErB.gif

开始时什么都没有选择,展示默认图片,规格列表中的第一项组合(['红色-大'])对应的图片,库存为商品总库存,价格为商品的价格区间。然后在用户选择某个属性或几个属性的时候实时计算对应的图片,库存,价格区间。

同时根据当前已选属性,置灰不可选择的属性。在本例中, 蓝色 大 的产品对应的库存为 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>

使用方式

<!-- 引用组件 -->
<skuModal ref="sku" :visible.sync="visible" @confirm="confirm"></skuModal>
// 初始化sku
this.$refs.sku.initSku({
  skuNameList, // 格式参考上文
  skuList // 格式参考上文
});

总结

做过电商项目的应该都处理或者听说过 sku,学习相关概念和真正理解如何计算 sku 可以帮助我们更加熟悉业务,提升自己对于相关业务的处理能力。以后在面试中遇到面试官的提问也能更稳一些。第一种 sku 算法可以参考上一篇博客。

参考

欢迎到前端学习打卡群一起学习~ 516913974


Recommend

  • 36
    • 掘金 juejin.im 5 years ago
    • Cache

    前端SKU算法的实现

    本文将提供一种前端SKU算法的实现思路,这可能并不是最佳的实现方式,但可以为没有思路的小伙伴提供一种解决方案。 对于SPU与SKU概念还不了解的小伙伴,请先移步认识SKU与SPU了解一下大致概念,本文便不再详述。 实现效果预览 实现思路 对于前端来说,如果没

  • 9

    如何设置速卖通的SKU?速卖通设置SKU提高SEO排名方法 2021年6月27日只要是在网上开店的商家,对于专业术语SKU应该不陌生。无论你在哪...

  • 9

    编辑导语:在做电商时,SKU产品库是大多数运营者会接触的系统,而在SKU产品库中,又有两种不同的设计方案,这二者又有何区别?

  • 5

    SKU的几种管理粒度:单品、产品、套装和箱产品 vitamin 2021-08-24 0 评论...

  • 13

    文章目录 组装 SKU 实践SKU 组合实现思路商品多规格选择商品多规格选择实现思路让我们继续前面的🌰 数据来看

  • 8

    什么是SKUSKU,Stock Keeping Unit,库存单元,是商品库存的最小单位。通俗的讲,一种商品可能有各种规格的货,每一种货就是一个SKU。比如,iphone6有白色16G、金色16G、白色64G、金色64G、等多种SKU;再比如商家售卖的某款T恤有白色S码、黑色S码、白...

  • 5
    • www.zoo.team 3 years ago
    • Cache

    SKU 开源了

    文章目录 🎉 它来了它来了,SKU 它开源了! 🎉 SKU 开源了 ...

  • 3
    • segmentfault.com 3 years ago
    • Cache

    前端电商 sku 的全排列算法

    前端电商 sku 的全排列算法需求需求描述起来很简单,有这样三个数组:let names = ["iPhone",'iPhone xs']let colors = ['黑色','白色']

  • 6
    • www.biaodianfu.com 3 years ago
    • Cache

    电商网站的SPU与SKU

    术→技巧, 研发 电商网站的SPU与SKU 钱魏Way · 2021-11-04 · 2 次...

  • 4
    • keelii.com 3 years ago
    • Cache

    sku 多维属性状态判断算法

    sku 多维属性状态判断算法 2016-12-22 15:55 PM · 首页

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK