电商最小存货单位SKU——详细代码实现

3,231 阅读7分钟

1、什么是SKU?

2、什么是笛卡尔积?

3、SKU组合代码实现思路

4、SKU选择代码实现思路

什么是SKU?

SKU(Stock Keeping Unit)最小存货单位。

例如我们在某购物APP上选购一款Mac电脑,可以选择颜色、内存、以及其他的一些销售属性。

这些销售属性的组合,比如深空灰16G+512G官方标配版,就可以作为这个商品库存及发货的标准,也就是我们说的一个SKU 如果你明白了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选择代码实现思路

屏幕录制2022-02-15 下午2.38.22的副本.gif

前面我们实现了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列表,去判断每一个选项是否可选。

判断SKU是否可选.png

  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的功能基本都已实现,有问题欢迎一起探讨