如何高效生成商品规格属性组合?——探秘笛卡尔乘积算法

1,399 阅读5分钟

1、前言

只要是做电商类相关的产品,比如购物 APP、购物网站等等,都会遇到这么一个场景,每个商品对应着多个规格,用户可以根据不同的规格组合,选择出自己想要的产品。
我们自己在生活中也会经常用到这个功能,然而就是这样一个看似简单的商品多规格属性组合算法,在电商类业务中却是比较复杂的一块内容了。

我们先看一下使用场景:

GIF 2023-4-18 11-37-17.gif

通过上图我们就能发现。商品多属性的集合通常是通过将各个规格的取值组合在一起,生成一系列 SKU。这些 SKU 可以唯一地标识一个商品,包括其多属性规格信息,例如颜色、尺码、款式等。

2、使用笛卡尔乘积算法

商品多属性SKU集合计算公式通常是:
SKU = 属性1选项值 + 属性2选项值 + ...+ 属性n选项值
例如,假设一个T恤有三个属性:颜色、尺码和材质。颜色有红色、蓝色和绿色可选,尺码有S、M、L可选,材质有棉质和涤纶可选。

const combination = [
  { name: '颜色', id:'color', list: ['红色', '蓝色', '绿色'] },
  { name: '尺码', id:'size', list: ['S', 'M', 'L'] },
  { name: '材质', id:'texture', list: ['棉质', '涤纶'] },
]

通过属性匹配就会存在以下几种SKU:

const skuList = [
  {'红色', 'S', '棉质'}, {'红色', 'S', '涤纶'}, {'红色', 'M', '棉质'}, 
  {'红色', 'M', '涤纶'}, {'红色', 'L', '棉质'}, {'红色', 'L', '涤纶'}, 
  {'蓝色', 'S', '棉质'}, {'蓝色', 'S', '涤纶'}, {'蓝色', 'M', '棉质'}, 
  {'蓝色', 'M', '涤纶'}, {'蓝色', 'L', '棉质'}, {'蓝色', 'L', '涤纶'}, 
  {'绿色', 'S', '棉质'}, {'绿色', 'S', '涤纶'}, {'绿色', 'M', '棉质'}, 
  {'绿色', 'M', '涤纶'}, {'绿色', 'L', '棉质'}, {'绿色', 'L', '涤纶'}
]

那么,我们应该怎么把 combination 通过计算成 skuList 呢?
其实很简单,我们可以使用 笛卡尔乘积算法
在数学中,笛卡尔积可以从多个集合中分别选取一个元素,进行组合的操作,生成一个新的集合。
设A,B为一个集合,将A中的元素作为第一个元素,B中的元素作为第二个元素,形成有序对。所有这些有序对都由一个称为a和B的笛卡尔积的集合组成,并被记录为AxB。
具体的实现步骤如下:

<script setup>
  import { reactive,onMounted } from "vue";
  const combination = reactive([
    { name: '颜色', id:'color', list: ['红色', '蓝色', '绿色'] },
    { name: '尺码', id:'size', list: ['S', 'M', 'L'] },
    { name: '材质', id:'texture', list: ['棉质', '涤纶'] },
  ]);

  let skuList = reactive([])
  
  // let attributesValue = reactive([])

  // const checkInventory = ()=> {}

  const cartesianProduct  = () => {
    const array = combination.map(item => item.list.map(itemVal => ({ name: item.name, val: itemVal })));
    const data = [];
    
    // 使用 笛卡尔乘积+递归 生成变体组合
    /**
    * skuarr:用于存储每次递归生成的组合结果的数组,初始为空数组。
    * i:当前处理的数组 array 的索引。
    * func 函数会递归地将 array 中的每个元素进行组合,生成一个包含所有可能组合的二维数组 data。
    * 在递归过程中,每次都会创建一个新的数组 skuarr,用于存储当前递归层级的组合结果,从而确保不会对上一层级的组合结果造成干扰。
    */
    const func = (skuarr = [], i) => {
      for (let j = 0; j < array[i].length; j++) {
        if (i < array.length - 1) {
          skuarr[i] = array[i][j] // 将当前的变体选项加入 skuarr 数组中
          func(skuarr, i + 1) // 递归调用下一层
        } else {
          const array = [...skuarr, array[i][j]]
          const valueList = array.map(item => item.val)
          data.push(valueList)
        }
      }
    }
    
    func([], 0);
    return data;
  };

  onMounted(()=> {
    const data = cartesianProduct ()
    skuList.push(...data)
    console.log('skuList: ', skuList);
  })
</script>

如此,对于每个属性,只需要列出其所有的选项值,然后基于这些选项值,按照上面的公式来生成所有可能的SKU组合。

3、判断规格是否有存货

在电商系统中,一般会有一个商品的 SKU 列表,每个 SKU 都代表着一种具体的产品规格和库存量。
当用户选择某一个规格时,需要遍历所有可能的 SKU,找到与该规格组合匹配的 SKU,并检查该 SKU 的库存情况。如果有库存,则该规格可以被勾选;否则,该规格应该被禁用或者显示为无法选择的状态。

如上 combination 表可知,当前产品有三个规格:颜色、尺码、材质。
其中,颜色和尺寸和材质之间存在依赖关系,即不同的颜色对应不同的尺寸和材质。
此外,每个 SKU 都会对应着特定的库存量。

当用户选择颜色为红色时,需要遍历所有SKU,找到颜色为红色的SKU,并获取该SKU对应的尺寸。
接下来,需要检查所有库存量大于0的SKU,看看它们中是否存在尺寸与当前选择的尺寸相同的SKU。
如果存在,则该尺寸可以被勾选;否则,该尺寸应该被禁用或者显示为无法选择的状态。

我们把 cartesianProduct 函数稍微改一下,加入库存:

const cartesianProduct = () => {
  const array = combination.map(item => item.list.map(itemVal => ({ name: item.name, val: itemVal })));
  const data = [];

  const func = (skuarr = [], i) => {
    for (let j = 0; j < array[i].length; j++) {
      if (i < array.length - 1) {
        skuarr[i] = array[i][j]
        func(skuarr, i + 1)
      } else {
        data.push([...skuarr, array[i][j]])
      }
    }
    return data
  }
  
  let newList = func([], 0);

  // 开始组装每一项变体属性
  return newList.map(item => {
    const attributesValue = {};
    
    item.forEach(({ id, val }) => {
      attributesValue[id] = val;
    });

    return {
      attributes: item,
      attributesValue,
      SKU: item.map(i => i.val).join(),
      stock: Math.floor(Math.random() * 6), // 生成0-5的随机整数库存
      price: 100,
    };
  });
};

排列好规格组合:

<template>
  <div class="attributes" v-for="item in combination" :key="item.id">
    <div>{{ item.name }}</div>
    <el-radio-group v-model="attributesValue[item.id]" size="large">
      <el-radio-button :label="e" v-for="e in item.list" :disabled="!checkInventory(item.id, e)"/>
    </el-radio-group>
  </div>
</template>
<script setup>
import { reactive } from "vue";
const attributesValue = reactive({
  color:'',
  size:'',
  texture:'',
})
...
</script>

如果需要实现每选中一个规格,其他依赖此规格的组合是否有存货,我们还需要添加一个函数计算是否可选
在js中添加函数 checkInventory :

<script setup>
...
const checkInventory = (id, val) => {
  // 构造新的 attributesValue 对象
  const newAttributesValue = { ...attributesValue, [id]: val };

  for (const sku of skuList) {
    let match = true;
    for (const [key, value] of Object.entries(newAttributesValue)) {
      if (value && sku.attributesValue[key] !== value) {
        match = false;
        break;
      }
    }
    if (match && sku.stock > 0) {
      return true;
    }
  }
  
  return false;
}
</script>

好了,写到这里基本上完成了。再微调一下style,然后看看效果吧!

附上上述代码: vue2 vue3
小伙伴们可以自己去试一试。

4、多属性规格的增删改

接下来我们把完整的商品多规格属性增删改流程补一补 查看详情