我的商品库经历

之前在外贸行业做过SaaS,对接过阿里国际站的商品库。在没有对接阿里国际站商品库之前,公司早已有自己的商品库设计【跟阿里的设计还是有很大出入的,包括但不限于:商品维度以及数据结构、预置skuId(数据量过大)和组合的时候生成skuId,新建商品组合sku的时候设计的不一样,自然在选择商品的时候也不一样】,实际上处理起来复杂的多,不单单要处理数据结构上的不一致,还要考虑业务需求上的特殊性(比如:下单的时候选择了一批不同的商品,然后再次选择商品,再次回到订单页面的时候相同的商品要进行组合展示等等)
运用 集合、 矩阵、排列组合、 sku组合算法设计(json key的唯一性)等,具体代码实现等。有很多 编程思想 在解决实际问题非常有效,在没有系统学习的时候,已经可以写出复合某些算法的程序了。
当时的商品库改造不涉及库存问题

原有的商品字段

  1. {
  2. "code": "0",
  3. "data": {
  4. "BTR": "0",
  5. "spuName": "小碎花",
  6. "unitVol": "0",
  7. "displayDetails": "url链接",
  8. "quotationMethod": "1",
  9. "auditState": "2",
  10. "attrdesc": {},
  11. "VATRT": "13.00",
  12. "unitGW": "0",
  13. "cont40R": "0",
  14. "cont20R": "0",
  15. "displayDesc": "无",
  16. "delState": "0",
  17. "spuCode": "1100",
  18. "offFlag": "1",
  19. "createCtId": "260921713",
  20. "purPrice": "0",
  21. "spec1": [
  22. {
  23. "imagesId": "",
  24. "itemName": "白色",
  25. "inUse": 1,
  26. "dictItemCode": "16777215",
  27. "remarks": ""
  28. },
  29. {
  30. "imagesId": "",
  31. "itemName": "红色",
  32. "inUse": 1,
  33. "dictItemCode": "16711680",
  34. "remarks": ""
  35. },
  36. {
  37. "imagesId": "",
  38. "itemName": "黄色",
  39. "inUse": 1,
  40. "dictItemCode": "16776960",
  41. "remarks": ""
  42. }
  43. ],
  44. "cId": "1785567",
  45. "createDate": "2021-12-15 17:02:02",
  46. "spec2": [
  47. {
  48. "dictItem": [],
  49. "itemName": "XS",
  50. "itemValue": "",
  51. "inUse": 1,
  52. "dictItemCode": 1,
  53. "id": 26011,
  54. "remarks": ""
  55. },
  56. {
  57. "dictItem": [],
  58. "itemName": "S",
  59. "itemValue": "",
  60. "inUse": 1,
  61. "dictItemCode": 2,
  62. "id": 26012,
  63. "remarks": ""
  64. }
  65. ],
  66. "recommendFlag": "0",
  67. "modifyDate": "2021-12-15 17:02:02",
  68. "strucId_2": [
  69. {
  70. "modifyDate": "2021-12-15 17:02:03",
  71. "modifyCtId": "260921713",
  72. "parSpuId": 10964842,
  73. "createCtId": "260921713",
  74. "skuId": 1735667,
  75. "skuCode": "SK21121500001",
  76. "spec1": "16777215",
  77. "cId": "1785567",
  78. "spec2": 1,
  79. "createDate": "2021-12-15 17:02:03"
  80. },
  81. {
  82. "modifyDate": "2021-12-15 17:02:03",
  83. "modifyCtId": "260921713",
  84. "parSpuId": 10964842,
  85. "createCtId": "260921713",
  86. "skuId": 1735776,
  87. "skuCode": "SK21121500002",
  88. "spec1": "16777215",
  89. "cId": "1785567",
  90. "spec2": 2,
  91. "createDate": "2021-12-15 17:02:03"
  92. },
  93. {
  94. "modifyDate": "2021-12-15 17:02:03",
  95. "modifyCtId": "260921713",
  96. "parSpuId": 10964842,
  97. "createCtId": "260921713",
  98. "skuId": 1735838,
  99. "skuCode": "SK21121500003",
  100. "spec1": "16711680",
  101. "cId": "1785567",
  102. "spec2": 1,
  103. "createDate": "2021-12-15 17:02:03"
  104. },
  105. {
  106. "modifyDate": "2021-12-15 17:02:03",
  107. "modifyCtId": "260921713",
  108. "parSpuId": 10964842,
  109. "createCtId": "260921713",
  110. "skuId": 1735984,
  111. "skuCode": "SK21121500004",
  112. "spec1": "16711680",
  113. "cId": "1785567",
  114. "spec2": 2,
  115. "createDate": "2021-12-15 17:02:03"
  116. },
  117. {
  118. "modifyDate": "2021-12-15 17:02:03",
  119. "modifyCtId": "260921713",
  120. "parSpuId": 10964842,
  121. "createCtId": "260921713",
  122. "skuId": 1736033,
  123. "skuCode": "SK21121500005",
  124. "spec1": "16776960",
  125. "cId": "1785567",
  126. "spec2": 1,
  127. "createDate": "2021-12-15 17:02:03"
  128. },
  129. {
  130. "modifyDate": "2021-12-15 17:02:03",
  131. "modifyCtId": "260921713",
  132. "parSpuId": 10964842,
  133. "createCtId": "260921713",
  134. "skuId": 1736192,
  135. "skuCode": "SK21121500006",
  136. "spec1": "16776960",
  137. "cId": "1785567",
  138. "spec2": 2,
  139. "createDate": "2021-12-15 17:02:03"
  140. }
  141. ],
  142. "salePrice": "0",
  143. "strucId_4": [
  144. {
  145. "quantity": "10",
  146. "modifyDate": "2021-12-15 17:02:03",
  147. "salePrice": "10.000",
  148. "modifyCtId": "260921713",
  149. "parSpuId": 10964842,
  150. "createCtId": "260921713",
  151. "cId": "1785567",
  152. "createDate": "2021-12-15 17:02:03"
  153. },
  154. {
  155. "quantity": "20",
  156. "modifyDate": "2021-12-15 17:02:03",
  157. "salePrice": "9.000",
  158. "modifyCtId": "260921713",
  159. "parSpuId": 10964842,
  160. "createCtId": "260921713",
  161. "cId": "1785567",
  162. "createDate": "2021-12-15 17:02:03"
  163. }
  164. ],
  165. "payMode": "2",
  166. "qualityDesc": "无",
  167. "saleCur": "USD",
  168. "modifyCtId": "260921713",
  169. "unitNW": "0",
  170. "ownerDeptKey": "1746772",
  171. "purCur": "CNY",
  172. "unitQty": "0",
  173. "spuId": 10964842,
  174. "tradeMode": "4",
  175. "ownerCtId": "260921713",
  176. "category": "100005719",
  177. "productionDesc": "无",
  178. "packDesc": "无"
  179. },
  180. "lMsg": {
  181. "data": {},
  182. "key": "api.1528870186768"
  183. },
  184. "msg": "添加成功",
  185. "ApiTime": 1.967,
  186. "version": "92b7f772eff07c7a0409c69080939c8e"
  187. }

阿里国际站商品字段

image.png

以下内容来源于政采云前端团队,记录一下他们是怎么做的,学习学习

前言

在我们实际开发过程中,商品创建页会先进行规格组装,商品购买页会对规格选择做处理。规格组装通过规格组合成 SKU 集合,规格选择根据规格内容获取库存数据量,计算 SKU 是否可被选择,两者功能在电商流程中缺一不可。
微信图片_20211215161140.gif

组装 SKU 实践

属性描述 SKU

最小存货单位( Stock Keeping Unit )在连锁零售门店中有时称单品为一个 SKU,定义为保存库存控制的最小可用单位,例如纺织品中一个 SKU 通常表示规格、颜色、款式。

业务场景

  • 只要是做电商类相关的产品,比如购物 APP、购物网站等等,都会遇到这么一个场景,每个商品对应着多个规格,用户可以根据不同的规格组合,选择出自己想要的产品。我们自己在生活中也会经常用到这个功能。

    现有规格

    1. const type = ["男裤", "女裤"]
    2. const color = ["黑色", "白色"]
    3. const size = ["S","L"]

    那么根据现有规格,可以得到所有的 SKU 为:

    1. [
    2. ["男裤", "黑色", "S"],
    3. ["男裤", "黑色", "L"],
    4. ["男裤", "白色", "S"],
    5. ["男裤", "白色", "L"],
    6. ["女裤", "黑色", "S"],
    7. ["女裤", "黑色", "L"],
    8. ["女裤", "白色", "S"],
    9. ["女裤", "白色", "L"],
    10. ]

    SKU 组合实现思路

    笛卡尔积

    首先让我们来看看笛卡尔积的描述

  • 笛卡尔乘积是指在数学中,两个[集合] XY 的笛卡尔积(Cartesian product),又称 [ 直积 ] ,表示为 X × Y,第一个对象是 X 的成员而第二个对象是 Y 的所有可能 [ 有序对 ] 的其中一个成员

  • 假设集合 A = { a, b },集合 B = { 0, 1, 2 },则两个集合的笛卡尔积为 { ( a, 0 ), ( a, 1 ), ( a, 2), ( b, 0), ( b, 1), ( b, 2) }

看来笛卡尔积满足组合计算的条件,那么下面先来一波思维碰撞,先通过导图,看看怎么实现
商品SKU 和 算法实现 - 图3
通过上面的思维导图,可以看出这种规格组合是一个经典的排列组合,去组合每一个规格值得到最终 SKU。
那么让我们来进行代码实现,看看代码如何实现笛卡尔积。

实现代码

  1. /**
  2. * 笛卡尔积组装
  3. * @param {Array} list
  4. * @returns []
  5. */
  6. function descartes(list) {
  7. // parent 上一级索引;count 指针计数
  8. let point = {}; // 准备移动指针
  9. let result = []; // 准备返回数据
  10. let pIndex = null; // 准备父级指针
  11. let tempCount = 0; // 每层指针坐标
  12. let temp = []; // 组装当个 sku 结果
  13. // 一:根据参数列生成指针对象
  14. for (let index in list) {
  15. if (typeof list[index] === 'object') {
  16. point[index] = { parent: pIndex, count: 0 };
  17. pIndex = index;
  18. }
  19. }
  20. // 单维度数据结构直接返回
  21. if (pIndex === null) {
  22. return list;
  23. }
  24. // 动态生成笛卡尔积
  25. while (true) {
  26. // 二:生成结果
  27. let index;
  28. for (index in list) {
  29. tempCount = point[index].count;
  30. temp.push(list[index][tempCount]);
  31. }
  32. // 压入结果数组
  33. result.push(temp);
  34. temp = [];
  35. // 三:检查指针最大值问题,移动指针
  36. while (true) {
  37. if (point[index].count + 1 >= list[index].length) {
  38. point[index].count = 0;
  39. pIndex = point[index].parent;
  40. if (pIndex === null) {
  41. return result;
  42. }
  43. // 赋值 parent 进行再次检查
  44. index = pIndex;
  45. } else {
  46. point[index].count++;
  47. break;
  48. }
  49. }
  50. }
  51. }

让我们看看实际的输入输出和调用结果。
b922978043cd90d91a959275a63970b8.png
那么这个经典的排列组合问题就这样解决啦。接下来,让我们再看看,如何在商品购买中,去处理商品多规格选择。

商品多规格选择

开始前回顾下使用场景
微信图片_20211215161709.gif
这个图片已经能很明确的展示业务需求了。结合上述动图可知,在用户每次选择了某一规格后,需要通过程序的计算去处理其他规格情况,以便给用户提供当前情况下可供选择的其他规格。

那么让我们来看看实现思路,首先在初始化中,提供可选择的 SKU,从可选择的 SKU 中去剔除不包含的规格内容,在剔除后,提供可以进行下一步选择的规格,后续在每次用户点击情况下,处理可能选中的 SKU,最终在全部规格选择完成后,得到选中的 SKU。
6c8af7d5b7e0a86d62e385a41d47bd8b.png

商品多规格选择实现思路

邻接矩阵

首先,看下什么是邻接矩阵,来自百度百科的解释

  • 用一个二维数组存放顶点间关系(边或弧)的数据,这个二维数组称为邻接矩阵。
  • 逻辑结构分为两部分:V 和 E 集合,其中,V 是顶点,E 是边。因此,用一个一维数组存放图中所有顶点数据。

字面描述可能比较晦涩难懂,那么让我们来看看图片帮助理解,如果两个顶点互通(有连线),那么它们对应下标的值则为 1,否则为 0。
商品SKU 和 算法实现 - 图7
让我们继续前面的🌰 数据来看
规格

  1. const type = ["男裤", "女裤"]
  2. const color = ["黑色", "白色"]
  3. const size = ["S","L"]

假设总 SKU 的库存值为下面示例,可选为有库存,不可选为某项规格无库存

  1. [
  2. ["男裤", "黑色", "S"], // S 无号
  3. ["男裤", "黑色", "L"],
  4. ["男裤", "白色", "S"], // S 无号
  5. ["男裤", "白色", "L"],
  6. ["女裤", "黑色", "S"], // S 无号
  7. ["女裤", "黑色", "L"],
  8. ["女裤", "白色", "S"], // S 无号
  9. ["女裤", "白色", "L"],
  10. ]

那么根据邻接矩阵思想,可以得到结果图:
商品SKU 和 算法实现 - 图8
从图中可以看出,SKU 中每两规格都可选择,那么相对的标志值为 1,否则为 0,当整条规格选中都是 1,才会使整条 SKU 链路可选。
思路是有了,但是如何通过代码去实现呢,想必大家也有各种方式去实现,那么我就介绍下自己的实现方式:集合。

计算思路

集合

想起集合,那么计算思路算是有了,这边我们需要用集合相等的情况,去处理 SKU 和规格值的计算。

实现思维导图

f1ceed3a45f22c165ba8fe20ad51f735.jpg

核心代码

计算质数方法:

  1. /**
  2. * 准备质数
  3. * @param {Int} num 质数范围
  4. * @returns
  5. */
  6. getPrime: function (num) {
  7. // 从第一个质数 2 开始
  8. let i = 2;
  9. const arr = [];
  10. /**
  11. * 检查是否是质数
  12. * @param {Int} number
  13. * @returns
  14. */
  15. const isPrime = (number) => {
  16. for (let ii = 2; ii < number / 2; ++ii) {
  17. if (number % ii === 0) {
  18. return false;
  19. }
  20. }
  21. return true;
  22. };
  23. // 循环判断,质数数量够完成返回
  24. for (i; arr.length < total; ++i) {
  25. if (isPrime(i)) {
  26. arr.push(i);
  27. }
  28. }
  29. // 返回需要的质数
  30. return arr;
  31. }
  32. // 上述动图入参以及返回结果展示:
  33. // getPrime(500) return==>
  34. // 0: (8) [2, 3, 5, 7, 11, 13, 17, 19]
  35. // 1: (8) [23, 29, 31, 37, 41, 43, 47, 53]
  36. // 2: (8) [59, 61, 67, 71, 73, 79, 83, 89]
  37. // 3: (8) [97, 101, 103, 107, 109, 113, 127, 131]
  38. // 4: (8) [137, 139, 149, 151, 157, 163, 167, 173]
  39. // 5: (8) [179, 181, 191, 193, 197, 199, 211, 223]
  40. // 6: (8) [227, 229, 233, 239, 241, 251, 257, 263]

初始化处理,得到第一批邻接矩阵结果:

  1. /**
  2. * 初始化,格式需要对比数据,并进行初始化是否可选计算
  3. */
  4. init: function () {
  5. this.light = util.cloneTwo(this.maps, true);
  6. var light = this.light;
  7. // 默认每个规则都可以选中,即赋值为 1
  8. for (var i = 0; i < light.length; i++) {
  9. var l = light[i];
  10. for (var j = 0; j < l.length; j++) {
  11. this._way[l[j]] = [i, j];
  12. l[j] = 1;
  13. }
  14. }
  15. // 对应结果值,此处将数据处理的方法对应邻接矩阵的思维导图
  16. // 0: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  17. // 1: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  18. // 2: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  19. // 3: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  20. // 4: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  21. // 5: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  22. // 6: (8) [1, 1, 1, 1, 1, 1, 1, 1]
  23. // 得到每个可操作的 SKU 质数的集合
  24. for (i = 0; i < this.openway.length; i++) {
  25. // 计算结果单行示例:
  26. // this.openway[i].join('*') ==> eval(2*3*5*7*11*13*17*19)
  27. this.openway[i] = eval(this.openway[i].join('*'));
  28. }
  29. // return 初始化得到规格位置,规格默认可选处理,可选 SKU 的规格对应的质数合集
  30. this._check();
  31. }

计算是否可选方法:

  1. /**
  2. * 检查是否可以选择,更新邻接矩阵对应结果值
  3. * @param {Boolean} isAdd 是否新增状态
  4. * @returns
  5. */
  6. _check: function (isAdd) {
  7. var light = this.light;
  8. var maps = this.maps;
  9. for (var i = 0; i < light.length; i++) {
  10. var li = light[i];
  11. var selected = this._getSelected(i);
  12. for (var j = 0; j < li.length; j++) {
  13. if (li[j] !== 2) {
  14. //如果是加一个条件,只在是 light 值为 1 的点进行选择
  15. if (isAdd) {
  16. if (li[j]) {
  17. light[i][j] = this._checkItem(maps[i][j], selected);
  18. }
  19. } else {
  20. light[i][j] = this._checkItem(maps[i][j], selected);
  21. }
  22. }
  23. }
  24. }
  25. return this.light;
  26. },
  27. /**
  28. * 检查是否可选内容,更新邻接矩阵对应结果值
  29. * @param {Int} item 当前规格质数
  30. * @param {Array} selected
  31. * @returns
  32. */
  33. _checkItem: function (item, selected) {
  34. // 拿到可以选择的 SKU 内容集合
  35. var openway = this.openway;
  36. var val;
  37. // 拿到已经选中规格集合*此规格集合值
  38. val = item * selected;
  39. // 可选 SKU 集合反除,查询是否可选
  40. for (var i = 0; i < openway.length; i++) {
  41. this.count++;
  42. if (openway[i] % val === 0) {
  43. return 1;
  44. }
  45. }
  46. return 0;
  47. }

添加规格方法:

  1. /** 选择可选规格后处理
  2. * @param {array} point [x, y]
  3. */
  4. add: function (point) {
  5. point = point instanceof Array ? point : this._way[point];
  6. // 得到选中规格对应的质数内容
  7. var val = this.maps[point[0]][point[1]];
  8. // 检查是否可选中
  9. if (!this.light[point[0]][point[1]]) {
  10. throw new Error(
  11. 'this point [' + point + '] is no availabe, place choose an other'
  12. );
  13. }
  14. // 判断是否选中内容已经存在已经选择内容中
  15. if (val in this.selected) return;
  16. var isAdd = this._dealChange(point, val);
  17. this.selected.push(val);
  18. // 选择后邻接矩阵对应数据修改为 2,以做是否可选区分
  19. this.light[point[0]][point[1]] = 2;
  20. this._check(!isAdd);
  21. }

移除已选规格方法:

  1. /**
  2. * 移除已选规格
  3. * @param {Array} point
  4. */
  5. remove: function (point) {
  6. point = point instanceof Array ? point : this._way[point];
  7. // 容错处理
  8. try {
  9. var val = this.maps[point[0]][point[1]];
  10. } catch (e) {}
  11. if (val) {
  12. // 在选中内容中,定位取出需要移除规格质数
  13. for (var i = 0; i < this.selected.length; i++) {
  14. if (this.selected[i] == val) {
  15. var line = this._way[this.selected[i]];
  16. // 对应邻接矩阵内容更新为可选
  17. this.light[line[0]][line[1]] = 1;
  18. // 从已选内容中移除
  19. this.selected.splice(i, 1);
  20. }
  21. }
  22. // 进行重新计算
  23. this._check();
  24. }
  25. }

参考文献

1.上述集合计算思路借鉴文献, 详情见链接 (http://git.shepherdwind.com/sku-search-algorithm.html)。
2.另一种正则匹配实现思路文献借鉴,详情见链接 (https://gist.github.com/shepherdwind/2141756)。
3.邻接矩阵思路借鉴文献,详情见链接 (https://gist.github.com/shepherdwind/2141756)。