1、什么是SKU?
2、什么是笛卡尔积?
3、SKU组合代码实现思路
4、SKU选择代码实现思路
什么是SKU?
SKU(Stock Keeping Unit)最小存货单位。
例如我们在某购物APP上选购一款Mac电脑,可以选择颜色、内存、以及其他的一些销售属性。
这些销售属性的组合,比如深空灰16G+512G官方标配版,就可以作为这个商品库存及发货的标准,也就是我们说的一个SKU
![图片[1]-电商最小存货单位SKU——详细代码实现-一一网](https://www.proyy.com/skycj/data/images/2022-03-24/fed124d0eff66a4dff3d650b02d1caac.jpg)
如果你明白了SKU的概念,那接下来看看这个例子,这个商品一共有几个SKU?

我是一条分割线
一共是10个SKU,你答对了么?
根据这个商品的尺码和颜色,我们可以得到10种[尺码X颜色]的组合,分别是
[XS黑色,XS军绿色,S黑色,S军绿色,M黑色,M军绿色,L黑色,L军绿色,XL黑色,XL军绿色]
这些SKU组合,我们可以通过笛卡尔积来得到。那么什么是笛卡尔积?
笛卡尔积
在数学中,笛卡尔积是一种对集合的运算。
假设有集合A, 集合B,用A中的元素作为第一个元素,用B中的元素作为第二个元素构成的有序对,所有这样的有序对的集合叫做A和B的笛卡尔积,记作AxB
用符号来表示记为:A×B={(x,y)|x∈A∧y∈B}
例如:A={a, b}, B={1,2,3}
用集合A中的a作为第一个元素,集合B中的1作为第二个元素,得到(a,1)
用集合A中的a作为第一个元素,集合B中的2作为第二个元素,得到(a,2)
用集合A中的a作为第一个元素,集合B中的3作为第二个元素,得到(a,3)
用集合A中的b作为第一个元素,集合B中的1作为第二个元素,得到(b,1)
用集合A中的b作为第一个元素,集合B中的2作为第二个元素,得到(b,2)
用集合A中的b作为第一个元素,集合B中的3作为第二个元素,得到(b,3)
最后我们得到AxB={(a,1),(a,2),(a,3),(b,1),(b,2),(b,3)}
是不是和我们上面计算SKU组合是一个意思呢?
SKU组合代码实现思路
现在基本的概念和组合方法我们都已经Get了,看下如何转变成代码,让计算机帮我们实现笛卡尔积
就以一个实际的例子来引入吧
现在 你是一个服装电商卖家,要上架一款大衣,这款大衣颜色有黑色,白色,灰色;长度有长款,短款;尺码有S,M,L,那么可能的SKU组合我们通过树状图画出来

我们先来用最简单粗暴的循环实现看看
const colors = ["黑色", "白色", "灰色"];
const length = ["长款", "短款"];
const size = ["S", "M", "L"];
// 获取SKU组合的方法
const getSkuList = () => {
  const result = [];
  colors.forEach((c) => {
    length.forEach((l) => {
      size.forEach((s) => {
        result.push([c, l, s]);
      });
    });
  });
  return result; // [["黑色","长款","S"],["黑色","长款","M"],["黑色","长款","L"],["黑色","短款","S"],["黑色","短款","M"],["黑色","短款","L"],["白色","长款","S"],["白色","长款","M"],["白色","长款","L"],["白色","短款","S"],["白色","短款","M"],["白色","短款","L"],["灰色","长款","S"],["灰色","长款","M"],["灰色","长款","L"],["灰色","短款","S"],["灰色","短款","M"],["灰色","短款","L"]]
};
复制代码确实结果是没问题的,但这种暴力循环没法应用到实际业务当中去。如果一个商品有10个销售属性,总不能写10层循环吧。况且我们会有各种不同的商品,不同的销售属性,不能像这样在for循环里把循环的数组定义死。
我们可以考虑借助reduce函数,将数组的每一项进行拼接操作,最终整合成一个结果
const colors = ["黑色", "白色", "灰色"];
const length = ["长款", "短款"];
const size = ["S", "M", "L"];
// 方法一
const getSkuList = (attrList) => {
  if (attrList.length < 2) return attrList[0] || [];
  return attrList.reduce((total, current) => {
    const res = [];
    total.forEach((t) => {
      current.forEach((c) => {
        const temp = Array.isArray(t) ? [...t] : [t];
        temp.push(c);
        res.push(temp);
      });
    });
    return res;
  });
}
// 方法二
const getSkuList2 = (attrList) => {
  return attrList.reduce(
    (total, current) => total.flatMap((t) => current.map((c) => [...t, c])),
    [[]]
  );
};
getSkuList([colors,length,size]); // [["黑色","长款","S"],["黑色","长款","M"],["黑色","长款","L"],["黑色","短款","S"],["黑色","短款","M"],["黑色","短款","L"],["白色","长款","S"],["白色","长款","M"],["白色","长款","L"],["白色","短款","S"],["白色","短款","M"],["白色","短款","L"],["灰色","长款","S"],["灰色","长款","M"],["灰色","长款","L"],["灰色","短款","S"],["灰色","短款","M"],["灰色","短款","L"]]
复制代码从树状图我们能看出来,从上往下到叶子结点的每一条路径,代表着一个SKU组合。其实还有点类似经典的回溯算法组合问题。
那我们再尝试使用回溯算法来实现一下。回溯算法的基本思想我在这里就不多做解释了,感兴趣的同学可以自行了解。
对于我们这个场景来说,回溯的“终止条件”就是当路径长度等于销售属性个数的时候,说明已经访问到叶子结点了,一个SKU组合成功,可以存放到结果中去。每一层的结点就是每一个销售属性的选项,例如第一层就是颜色,第二层是长度,第三层是尺码。我们看下代码如何实现的:
const colors = ["黑色", "白色", "灰色"];
const length = ["长款", "短款"];
const size = ["S", "M", "L"];
// 方法三
const getSkuList = (attrList) => {
  const result = [];
  const backTracking = (path, level) => {
    if (path.length === attrList.length) {
      result.push([...path]);
      return;
    }
    attrList[level].forEach((item) => {
      path.push(item);
      backTracking(path, level + 1);
      path.pop();
    });
  };
  backTracking([], 0);
  return result; 
};
getSkuList([colors,length,size]); // [["黑色","长款","S"],["黑色","长款","M"],["黑色","长款","L"],["黑色","短款","S"],["黑色","短款","M"],["黑色","短款","L"],["白色","长款","S"],["白色","长款","M"],["白色","长款","L"],["白色","短款","S"],["白色","短款","M"],["白色","短款","L"],["灰色","长款","S"],["灰色","长款","M"],["灰色","长款","L"],["灰色","短款","S"],["灰色","短款","M"],["灰色","短款","L"]]
复制代码SKU选择代码实现思路

前面我们实现了SKU组合的方法,主要适用于卖家后台系统,在创建商品时,编辑各SKU的信息。
在买家端,买家会选择自己需要的SKU,系统需要根据买家的选择,实时反馈对应SKU的价格、库存等信息。
服务端给到前端的信息是一个完整的SKU列表,数据结构类似这样, 数组里的每一条数据即为一个SKU,包含了SKU的唯一标识skuId, SKU的属性组成,SKU的库存和价格
[
  {
    skuId: "111",
    skuInfo: {
      color: "黑色",
      length: "长款",
      size: "S",
    },
    stock: 10,
    price: 10,
  },
  {
    skuId: "112",
    skuInfo: {
      color: "黑色",
      length: "长款",
      size: "M",
    },
    stock: 9,
    price: 9,
  },
  {
    skuId: "113",
    skuInfo: {
      color: "黑色",
      length: "长款",
      size: "L",
    },
    stock: 8,
    price: 8,
  },
  ...
]
复制代码以及一个完整的销售属性列表,数据结构类似这样:
[
  {
    label: "颜色",
    name: "color",
    options: [
      {
        value: "黑色",
      },
      {
        value: "白色",
      },
      {
        value: "灰色",
      },
    ],
  },
  {
    label: "长度",
    name: "length",
    options: [
      {
        value: "长款",
      },
      {
        value: "短款",
      },
    ],
  },
  {
    label: "尺码",
    name: "size",
    options: [
      {
        value: "S",
      },
      {
        value: "M",
      },
      {
        value: "L",
      },
    ],
  },
]
复制代码根据这个销售属性列表,我们很容易可以渲染出SKU选择的组件(Demo为React+antd实现)
<div className="main-content">
      <Form name="basic">
        {attrs.map((attr) => {
          return (
            <Form.Item key={attr.name} label={attr.label} name={attr.name}>
              <div className="form-content">
                {attr.options.map((opt, index) => (
                  <Button
                    key={index}
                    type={
                      opt.value === selectedSku[attr.name]
                        ? "primary"
                        : "default"
                    }
                    className="opt"
                    disabled={!isSkuValid(attr.name, opt.value)}
                    onClick={() => onClick(attr.name, opt.value)}
                  >
                    {opt.value}
                  </Button>
                ))}
              </div>
            </Form.Item>
          );
        })}
      </Form>
    </div>
复制代码我们用一个对象来记录已选择的选项, 当点击选项时,更新selectedSku对象的值
  const [selectedSku, setSelectedSku] = useState({});
  const onClick = (attrKey, optValue) => {
    setSelectedSku({
      ...selectedSku,
      [attrKey]: selectedSku[attrKey] === optValue ? "" : optValue,
    });
  };
复制代码实际情况下,某些SKU存在无货的场景,那对应的按钮是置灰无法选择的。但是目前SKU组件是依靠销售属性列表渲染的,没有库存信息,也就是说,我们需要结合销售属性数组和SKU列表,去判断每一个选项是否可选。

  const isSkuValid = (attrKey, optValue) => {
    // 先假设当前属性值已选中,拼入已选对象里
    const tempSelectedSku = {
      ...selectedSku,
      [attrKey]: optValue,
    };
    // 过滤出已选对象中属性值不为空的
    const skuToBeChecked = Object.keys(tempSelectedSku).filter(
      (key) => tempSelectedSku[key] !== ""
    );
    // 在skuList里找到所有包含已选择属性的sku且库存>0的sku
    const filteredSkuList = skuList.filter((sku) =>
      skuToBeChecked.every(
        (skuKey) =>
          tempSelectedSku[skuKey] === sku.skuInfo[skuKey] && sku.stock > 0
      )
    );
    return filteredSkuList.length > 0;
  };
复制代码这样我们就实现了买家端SKU选择的功能。
总结
至此,卖家后台创建商品SKU及买家界面选择SKU的功能基本都已实现,有问题欢迎一起探讨





















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
