我的商品库经历
之前在外贸行业做过SaaS,对接过阿里国际站的商品库。在没有对接阿里国际站商品库之前,公司早已有自己的商品库设计【跟阿里的设计还是有很大出入的,包括但不限于:商品维度以及数据结构、预置skuId(数据量过大)和组合的时候生成skuId,新建商品组合sku的时候设计的不一样,自然在选择商品的时候也不一样】,实际上处理起来复杂的多,不单单要处理数据结构上的不一致,还要考虑业务需求上的特殊性(比如:下单的时候选择了一批不同的商品,然后再次选择商品,再次回到订单页面的时候相同的商品要进行组合展示等等)
运用 集合、 矩阵、排列组合、 sku组合算法设计(json key的唯一性)等,具体代码实现等。有很多 编程思想 在解决实际问题非常有效,在没有系统学习的时候,已经可以写出复合某些算法的程序了。
当时的商品库改造不涉及库存问题
原有的商品字段
{"code": "0","data": {"BTR": "0","spuName": "小碎花","unitVol": "0","displayDetails": "url链接","quotationMethod": "1","auditState": "2","attrdesc": {},"VATRT": "13.00","unitGW": "0","cont40R": "0","cont20R": "0","displayDesc": "无","delState": "0","spuCode": "1100","offFlag": "1","createCtId": "260921713","purPrice": "0","spec1": [{"imagesId": "","itemName": "白色","inUse": 1,"dictItemCode": "16777215","remarks": ""},{"imagesId": "","itemName": "红色","inUse": 1,"dictItemCode": "16711680","remarks": ""},{"imagesId": "","itemName": "黄色","inUse": 1,"dictItemCode": "16776960","remarks": ""}],"cId": "1785567","createDate": "2021-12-15 17:02:02","spec2": [{"dictItem": [],"itemName": "XS","itemValue": "","inUse": 1,"dictItemCode": 1,"id": 26011,"remarks": ""},{"dictItem": [],"itemName": "S","itemValue": "","inUse": 1,"dictItemCode": 2,"id": 26012,"remarks": ""}],"recommendFlag": "0","modifyDate": "2021-12-15 17:02:02","strucId_2": [{"modifyDate": "2021-12-15 17:02:03","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","skuId": 1735667,"skuCode": "SK21121500001","spec1": "16777215","cId": "1785567","spec2": 1,"createDate": "2021-12-15 17:02:03"},{"modifyDate": "2021-12-15 17:02:03","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","skuId": 1735776,"skuCode": "SK21121500002","spec1": "16777215","cId": "1785567","spec2": 2,"createDate": "2021-12-15 17:02:03"},{"modifyDate": "2021-12-15 17:02:03","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","skuId": 1735838,"skuCode": "SK21121500003","spec1": "16711680","cId": "1785567","spec2": 1,"createDate": "2021-12-15 17:02:03"},{"modifyDate": "2021-12-15 17:02:03","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","skuId": 1735984,"skuCode": "SK21121500004","spec1": "16711680","cId": "1785567","spec2": 2,"createDate": "2021-12-15 17:02:03"},{"modifyDate": "2021-12-15 17:02:03","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","skuId": 1736033,"skuCode": "SK21121500005","spec1": "16776960","cId": "1785567","spec2": 1,"createDate": "2021-12-15 17:02:03"},{"modifyDate": "2021-12-15 17:02:03","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","skuId": 1736192,"skuCode": "SK21121500006","spec1": "16776960","cId": "1785567","spec2": 2,"createDate": "2021-12-15 17:02:03"}],"salePrice": "0","strucId_4": [{"quantity": "10","modifyDate": "2021-12-15 17:02:03","salePrice": "10.000","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","cId": "1785567","createDate": "2021-12-15 17:02:03"},{"quantity": "20","modifyDate": "2021-12-15 17:02:03","salePrice": "9.000","modifyCtId": "260921713","parSpuId": 10964842,"createCtId": "260921713","cId": "1785567","createDate": "2021-12-15 17:02:03"}],"payMode": "2","qualityDesc": "无","saleCur": "USD","modifyCtId": "260921713","unitNW": "0","ownerDeptKey": "1746772","purCur": "CNY","unitQty": "0","spuId": 10964842,"tradeMode": "4","ownerCtId": "260921713","category": "100005719","productionDesc": "无","packDesc": "无"},"lMsg": {"data": {},"key": "api.1528870186768"},"msg": "添加成功","ApiTime": 1.967,"version": "92b7f772eff07c7a0409c69080939c8e"}
阿里国际站商品字段

以下内容来源于政采云前端团队,记录一下他们是怎么做的,学习学习
前言
在我们实际开发过程中,商品创建页会先进行规格组装,商品购买页会对规格选择做处理。规格组装通过规格组合成 SKU 集合,规格选择根据规格内容获取库存数据量,计算 SKU 是否可被选择,两者功能在电商流程中缺一不可。
组装 SKU 实践
属性描述 SKU
最小存货单位( Stock Keeping Unit )在连锁零售门店中有时称单品为一个 SKU,定义为保存库存控制的最小可用单位,例如纺织品中一个 SKU 通常表示规格、颜色、款式。
业务场景
只要是做电商类相关的产品,比如购物 APP、购物网站等等,都会遇到这么一个场景,每个商品对应着多个规格,用户可以根据不同的规格组合,选择出自己想要的产品。我们自己在生活中也会经常用到这个功能。
现有规格
const type = ["男裤", "女裤"]const color = ["黑色", "白色"]const size = ["S","L"]
那么根据现有规格,可以得到所有的 SKU 为:
[["男裤", "黑色", "S"],["男裤", "黑色", "L"],["男裤", "白色", "S"],["男裤", "白色", "L"],["女裤", "黑色", "S"],["女裤", "黑色", "L"],["女裤", "白色", "S"],["女裤", "白色", "L"],]
SKU 组合实现思路
笛卡尔积
首先让我们来看看笛卡尔积的描述
笛卡尔乘积是指在数学中,两个[集合] X 和 Y 的笛卡尔积(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。
那么让我们来进行代码实现,看看代码如何实现笛卡尔积。
实现代码
/*** 笛卡尔积组装* @param {Array} list* @returns []*/function descartes(list) {// parent 上一级索引;count 指针计数let point = {}; // 准备移动指针let result = []; // 准备返回数据let pIndex = null; // 准备父级指针let tempCount = 0; // 每层指针坐标let temp = []; // 组装当个 sku 结果// 一:根据参数列生成指针对象for (let index in list) {if (typeof list[index] === 'object') {point[index] = { parent: pIndex, count: 0 };pIndex = index;}}// 单维度数据结构直接返回if (pIndex === null) {return list;}// 动态生成笛卡尔积while (true) {// 二:生成结果let index;for (index in list) {tempCount = point[index].count;temp.push(list[index][tempCount]);}// 压入结果数组result.push(temp);temp = [];// 三:检查指针最大值问题,移动指针while (true) {if (point[index].count + 1 >= list[index].length) {point[index].count = 0;pIndex = point[index].parent;if (pIndex === null) {return result;}// 赋值 parent 进行再次检查index = pIndex;} else {point[index].count++;break;}}}}
让我们看看实际的输入输出和调用结果。
那么这个经典的排列组合问题就这样解决啦。接下来,让我们再看看,如何在商品购买中,去处理商品多规格选择。
商品多规格选择
开始前回顾下使用场景
这个图片已经能很明确的展示业务需求了。结合上述动图可知,在用户每次选择了某一规格后,需要通过程序的计算去处理其他规格情况,以便给用户提供当前情况下可供选择的其他规格。
那么让我们来看看实现思路,首先在初始化中,提供可选择的 SKU,从可选择的 SKU 中去剔除不包含的规格内容,在剔除后,提供可以进行下一步选择的规格,后续在每次用户点击情况下,处理可能选中的 SKU,最终在全部规格选择完成后,得到选中的 SKU。
商品多规格选择实现思路
邻接矩阵
首先,看下什么是邻接矩阵,来自百度百科的解释
- 用一个二维数组存放顶点间关系(边或弧)的数据,这个二维数组称为邻接矩阵。
- 逻辑结构分为两部分:V 和 E 集合,其中,V 是顶点,E 是边。因此,用一个一维数组存放图中所有顶点数据。
字面描述可能比较晦涩难懂,那么让我们来看看图片帮助理解,如果两个顶点互通(有连线),那么它们对应下标的值则为 1,否则为 0。
让我们继续前面的🌰 数据来看
规格
const type = ["男裤", "女裤"]const color = ["黑色", "白色"]const size = ["S","L"]
假设总 SKU 的库存值为下面示例,可选为有库存,不可选为某项规格无库存
[["男裤", "黑色", "S"], // S 无号["男裤", "黑色", "L"],["男裤", "白色", "S"], // S 无号["男裤", "白色", "L"],["女裤", "黑色", "S"], // S 无号["女裤", "黑色", "L"],["女裤", "白色", "S"], // S 无号["女裤", "白色", "L"],]
那么根据邻接矩阵思想,可以得到结果图:
从图中可以看出,SKU 中每两规格都可选择,那么相对的标志值为 1,否则为 0,当整条规格选中都是 1,才会使整条 SKU 链路可选。
思路是有了,但是如何通过代码去实现呢,想必大家也有各种方式去实现,那么我就介绍下自己的实现方式:集合。
计算思路
集合
想起集合,那么计算思路算是有了,这边我们需要用集合相等的情况,去处理 SKU 和规格值的计算。
实现思维导图
核心代码
计算质数方法:
/*** 准备质数* @param {Int} num 质数范围* @returns*/getPrime: function (num) {// 从第一个质数 2 开始let i = 2;const arr = [];/*** 检查是否是质数* @param {Int} number* @returns*/const isPrime = (number) => {for (let ii = 2; ii < number / 2; ++ii) {if (number % ii === 0) {return false;}}return true;};// 循环判断,质数数量够完成返回for (i; arr.length < total; ++i) {if (isPrime(i)) {arr.push(i);}}// 返回需要的质数return arr;}// 上述动图入参以及返回结果展示:// getPrime(500) return==>// 0: (8) [2, 3, 5, 7, 11, 13, 17, 19]// 1: (8) [23, 29, 31, 37, 41, 43, 47, 53]// 2: (8) [59, 61, 67, 71, 73, 79, 83, 89]// 3: (8) [97, 101, 103, 107, 109, 113, 127, 131]// 4: (8) [137, 139, 149, 151, 157, 163, 167, 173]// 5: (8) [179, 181, 191, 193, 197, 199, 211, 223]// 6: (8) [227, 229, 233, 239, 241, 251, 257, 263]
初始化处理,得到第一批邻接矩阵结果:
/*** 初始化,格式需要对比数据,并进行初始化是否可选计算*/init: function () {this.light = util.cloneTwo(this.maps, true);var light = this.light;// 默认每个规则都可以选中,即赋值为 1for (var i = 0; i < light.length; i++) {var l = light[i];for (var j = 0; j < l.length; j++) {this._way[l[j]] = [i, j];l[j] = 1;}}// 对应结果值,此处将数据处理的方法对应邻接矩阵的思维导图// 0: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 1: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 2: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 3: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 4: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 5: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 6: (8) [1, 1, 1, 1, 1, 1, 1, 1]// 得到每个可操作的 SKU 质数的集合for (i = 0; i < this.openway.length; i++) {// 计算结果单行示例:// this.openway[i].join('*') ==> eval(2*3*5*7*11*13*17*19)this.openway[i] = eval(this.openway[i].join('*'));}// return 初始化得到规格位置,规格默认可选处理,可选 SKU 的规格对应的质数合集this._check();}
计算是否可选方法:
/*** 检查是否可以选择,更新邻接矩阵对应结果值* @param {Boolean} isAdd 是否新增状态* @returns*/_check: function (isAdd) {var light = this.light;var maps = this.maps;for (var i = 0; i < light.length; i++) {var li = light[i];var selected = this._getSelected(i);for (var j = 0; j < li.length; j++) {if (li[j] !== 2) {//如果是加一个条件,只在是 light 值为 1 的点进行选择if (isAdd) {if (li[j]) {light[i][j] = this._checkItem(maps[i][j], selected);}} else {light[i][j] = this._checkItem(maps[i][j], selected);}}}}return this.light;},/*** 检查是否可选内容,更新邻接矩阵对应结果值* @param {Int} item 当前规格质数* @param {Array} selected* @returns*/_checkItem: function (item, selected) {// 拿到可以选择的 SKU 内容集合var openway = this.openway;var val;// 拿到已经选中规格集合*此规格集合值val = item * selected;// 可选 SKU 集合反除,查询是否可选for (var i = 0; i < openway.length; i++) {this.count++;if (openway[i] % val === 0) {return 1;}}return 0;}
添加规格方法:
/** 选择可选规格后处理* @param {array} point [x, y]*/add: function (point) {point = point instanceof Array ? point : this._way[point];// 得到选中规格对应的质数内容var val = this.maps[point[0]][point[1]];// 检查是否可选中if (!this.light[point[0]][point[1]]) {throw new Error('this point [' + point + '] is no availabe, place choose an other');}// 判断是否选中内容已经存在已经选择内容中if (val in this.selected) return;var isAdd = this._dealChange(point, val);this.selected.push(val);// 选择后邻接矩阵对应数据修改为 2,以做是否可选区分this.light[point[0]][point[1]] = 2;this._check(!isAdd);}
移除已选规格方法:
/*** 移除已选规格* @param {Array} point*/remove: function (point) {point = point instanceof Array ? point : this._way[point];// 容错处理try {var val = this.maps[point[0]][point[1]];} catch (e) {}if (val) {// 在选中内容中,定位取出需要移除规格质数for (var i = 0; i < this.selected.length; i++) {if (this.selected[i] == val) {var line = this._way[this.selected[i]];// 对应邻接矩阵内容更新为可选this.light[line[0]][line[1]] = 1;// 从已选内容中移除this.selected.splice(i, 1);}}// 进行重新计算this._check();}}
参考文献
1.上述集合计算思路借鉴文献, 详情见链接 (http://git.shepherdwind.com/sku-search-algorithm.html)。
2.另一种正则匹配实现思路文献借鉴,详情见链接 (https://gist.github.com/shepherdwind/2141756)。
3.邻接矩阵思路借鉴文献,详情见链接 (https://gist.github.com/shepherdwind/2141756)。
