2020-10-23 10:06:00
围观(5116)
昨天博主去了一家公司面试,被面试官问到了了一个问题:“看你的简历,在上一家公司有参与商城项目的开发,那商品规格这个表是怎么设计的?”
博主想都没想,就说了:“可以设置两个表,一个是 product 商品信息表 存放一些例如商品标题,商品主图,之类的信息。另外设置一个 sku 商品规格表用于存放商品对应的规格 例如有 规格名称 规格键值对 库存 价格 等字段”。
面试官听完后一脸疑惑(面试官觉得一个规格表并不能完成规格功能的实现),继续追问“两个表就能实现这个规格了吗?”。
博主继续说也可以用四张表,sku 存商品ID和库存还有价格,还要一个 attr 表存商品ID和规格的键,例如 颜色 尺码 这两个。
有了规格键的表自然就要存规格值的表 attr_value 用于存 attr_id(也就是规格键的ID) 和规格值。
最后还要一个 sku_attr 表用于存放关联的规格键与值的ID。
例如:
sku 表:
+----+------------+-------+-------+ | id | product_id | stock | price | +----+------------+-------+-------+ | 1 | 1 | 10 | 100 | | 2 | 1 | 5 | 50 | | 3 | 1 | 20 | 200 | +----+------------+-------+-------+
attr 表:
+----+------------+------+ | id | product_id | name | +----+------------+------+ | 1 | 1 | 颜色 | | 2 | 1 | 尺码 | +----+------------+------+
attr_value 表(attr_id 对应 attr 表):
+----+---------+-------+ | id | attr_id | value | +----+---------+-------+ | 1 | 1 | 红色 | | 2 | 1 | 黄色 | | 3 | 2 | S | | 4 | 2 | M | +----+---------+-------+
sku_attr 表(sku_id 对应 sku 表。 attr_value_id 对应 attr_value 表。):
+----+--------+---------------+ | id | sku_id | attr_value_id | +----+--------+---------------+ | 1 | 1 | 1 | | 2 | 1 | 4 | | 3 | 2 | 1 | | 4 | 2 | 3 | | 5 | 3 | 2 | | 6 | 3 | 4 | +----+--------+---------------+
获取商品对应的规格信息时,先根据商品的 id 查询 sku 表获取到商品对应的规格的库存与价格。
根据 sku 的 id 可以查询 sku_attr 表的 attr_value_id,根据 attr_value_id 查询 attr_value 表对应的 id 就能获取到规格值,再根据 attr_value 表的 attr_id 可以获取规格键信息。
也就是,商品 id 为 1 这个商品有三个规格,第一个规格是:颜色为红色 尺码为M。 第二个规格是:颜色为红色 尺码为S。 第三个规格是:颜色为黄色 尺码为M。
其实博主觉得用一个规格表更容易理解也更简单,例如博主之前写的这篇文章:使用Layui和Laravel开发商品多规格录入 (这里讲个小插曲,面试官问会不会使用 Layui 博主楞了一下 心里在想类ui是啥? 缓了几秒才反应过来... 平时真的不多说这个词...)
根据博主上面那篇文章写的,可以在数据库存储这样的数据:
+----+------------+----------------------------------+-----------+-------+-------+---------------------+---------------------+ | id | product_id | attrs_name | name | stock | price | created_at | updated_at | +----+------------+----------------------------------+-----------+-------+-------+---------------------+---------------------+ | 1 | 1 | {"颜色":"红色","尺码":"S"} | 红色 S | 10 | 10 | 2020-10-23 10:00:00 | 2020-10-23 10:00:00 | | 2 | 1 | {"颜色":"黄色","尺码":"S"} | 黄色 S | 10 | 10 | 2020-10-23 10:00:00 | 2020-10-23 10:00:00 | | 3 | 1 | {"颜色":"红色","尺码":"M"} | 红色 M | 10 | 10 | 2020-10-23 10:00:00 | 2020-10-23 10:00:00 | | 4 | 1 | {"颜色":"黄色","尺码":"M"} | 黄色 M | 10 | 10 | 2020-10-23 10:00:00 | 2020-10-23 10:00:00 | +----+------------+----------------------------------+-----------+-------+-------+---------------------+---------------------+
然后获取规格信息的时候就不用查询多个表了。
前端输入规格信息存储的时候其实可以使用 “笛卡尔积算法”。
例如颜色 = [黄色 红色]。 尺码 = [S M]
使用笛卡尔积也就是 D = 颜色 × 尺码(2 × 2):
[ [黄色 S] [黄色 M] [红色 S] [红色 M] ]
然后将这四个规格填写上规格与价格,没有的规格就将库存填为 0 即可:
[ [黄色 S stock10 price100] [黄色 M stock5 price200] [红色 S stock20 price100] [红色 M stock10 price100] ]
这样存储即可,上面的 stock 和 price 只是容易理解实际并不需要只需要数字就好。
这样存储之后就剩下最后一个问题,就是前端要怎么渲染显示并选择规格?(这才是本文的重点)
博主对前端并不是非常熟悉,所以找到了一个开源的项目:商品多规格属性前后端实现 Laravel6.0 + Jquery
博主将这个开源项目的前端页面拿了出来:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Sku 多维属性状态判断</title> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <style> body { font-size: 12px; } dt { width: 100px; text-align: right; } dl { clear: both; overflow: hidden; } dl.hl { background: #ddd; } dt, dd { float: left; height: 40px; line-height: 40px; margin-left: 10px; } button { font-size: 14px; font-weight: bold; padding: 4px 4px; } .disabled { color: #999; border: 1px dashed #666; } .active { color: red; } </style> </head> <body> <p><textarea id="data_area" cols="100" rows="10">[{"颜色":"金色","内存":"16G","skuId":1},{"颜色":"金色","内存":"32G","skuId":2},{"颜色":"红色","内存":"16G","skuId":3}]</textarea></p> <p><input onclick="updateData()" type="button" value="更新数据"></p> <hr> <div id="app"></div> <hr> <div id="msg"></div> <script> // 接收数据 var data = JSON.parse($('#data_area').val()) // 所有子集 var res = {} // 属性值分割符“⊙” var spliter = '\u2299' // 组合数据对象 var r = {} // 所有的属性名 var keys = [] // 默认选中(第一个Sku对象) var selectedCache = [] /** * 计算组合数据 * 功能: * 1、按属性分组:将相同属性的不同属性值,放在一起, * 2、将属性值以“⊙”拼接,并同SKU ID绑定 */ function combineAttr(data, keys) { var allKeys = [] var result = {} for (var i = 0; i < data.length; i++) { var item = data[i] var values = [] // 循环属性值以“⊙”拼接 for (var j = 0; j < keys.length; j++) { var key = keys[j] if (!result[key]) result[key] = [] if (result[key].indexOf(item[key]) < 0) result[key].push(item[key]) // 按属性分组:将相同属性的不同属性值,放在一起 values.push(item[key]) } allKeys.push({ path: values.join(spliter), sku: item['skuId'] }) } return { result: result, // 按属性名分组,eg:{颜色:["金色","红色"],"内存":["16G", "32G"],"保修期": ["首月", "半年"]} items: allKeys // 按SkuId把属性值拼接起来,eg:[{path: "金色⊙16G⊙首月", sku: 1},{path: "金色⊙32G⊙半年", sku: 2},{path: "红色⊙16G⊙半年", sku: 3}] } } /** * 渲染 DOM 结构(渲染规格) * 功能: * 1、渲染规格 * 2、设置默认选中 */ function render(data) { var output = '' for (var i = 0; i < keys.length; i++) { var key = keys[i]; var items = data[key] output += '<dl data-type="' + key + '" data-idx="' + i + '">' output += '<dt>' + key + ':</dt>' output += '<dd>' for (var j = 0; j < items.length; j++) { var item = items[j] var cName = j === 0 ? 'active' : '' if (j === 0) { selectedCache.push(item) } output += '<button data-title="' + item + '" class="' + cName + '" value="' + item + '">' + item + '</button> ' } output += '</dd>' output += '</dl>' } $('#app').html(output) } /** * 获取所有已拼接的属性值数组 */ function getAllKeys(arr) { var result = [] for (var i = 0; i < arr.length; i++) { result.push(arr[i].path) } return result // ["金色⊙16G⊙首月", "金色⊙32G⊙半年", "红色⊙16G⊙半年"] } /** * 取得集合的所有子集「幂集」(所有可能性) */ function powerset(arr) { var ps = [[]]; for (var i = 0; i < arr.length; i++) { for (var j = 0, len = ps.length; j < len; j++) { ps.push(ps[j].concat(arr[i])); } } return ps; } /** * 生成所有子集是否可选、库存状态 map(核心) */ function buildResult(items) { var allKeys = getAllKeys(items) for (var i = 0; i < allKeys.length; i++) { var curr = allKeys[i] var sku = items[i].sku var values = curr.split(spliter) var allSets = powerset(values) // 每个组合的子集 for (var j = 0; j < allSets.length; j++) { var set = allSets[j] var key = set.join(spliter) if (res[key]) { res[key].skus.push(sku) } else { res[key] = { skus: [sku] } } } } } function trimSpliter(str, spliter) { var reLeft = new RegExp('^' + spliter + '+', 'g'); var reRight = new RegExp(spliter + '+$', 'g'); var reSpliter = new RegExp(spliter + '+', 'g'); return str.replace(reLeft, '') .replace(reRight, '') .replace(reSpliter, spliter) } /** * 获取当前选中的属性 */ function getSelectedItem() { var result = [] $('dl[data-type]').each(function () { var $selected = $(this).find('.active') if ($selected.length) { result.push($selected.val()) } else { result.push('') } }) return result } /** * 更新所有属性状态 */ function updateStatus(selected) { for (var i = 0; i < keys.length; i++) { var key = keys[i]; var data = r.result[key] var hasActive = !!selected[i] var copy = selected.slice() for (var j = 0; j < data.length; j++) { var item = data[j] if (selected[i] === item) continue copy[i] = item var curr = trimSpliter(copy.join(spliter), spliter) var $item = $('dl').filter('[data-type="' + key + '"]').find('[value="' + item + '"]') var titleStr = '[' + copy.join('-') + ']' if (res[curr]) { $item.removeClass('disabled') setTitle($item.get(0)) } else { $item.addClass('disabled').attr('title', titleStr + ' 无此属性搭配') } } } } /** * 正常属性点击 */ function handleNormalClick($this) { $this.siblings().removeClass('active') $this.addClass('active') } /** * 无效属性点击 */ function handleDisableClick($this) { var $currAttr = $this.parents('dl').eq(0) var idx = $currAttr.data('idx') var type = $currAttr.data('type') var value = $this.val() $this.removeClass('disabled') selectedCache[idx] = value console.log(selectedCache) // 清空高亮行的已选属性状态(因为更新的时候默认会跳过已选状态) $('dl').not($currAttr).find('button').removeClass('active') updateStatus(getSelectedItem()) /** * 恢复原来已选属性 * 遍历所有非当前属性行 * 1. 与 selectedCache 对比 * 2. 如果要恢复的属性存在(非 disable)且 和当前*未高亮行*已选择属性的*可组合*),高亮原来已选择的属性且更新 * 3. 否则什么也不做 */ for (var i = 0; i < keys.length; i++) { var item = keys[i] var $curr = $('dl[data-type="' + item + '"]') if (item == type) continue var $lastSelected = $curr.find('button[value="' + selectedCache[i] + '"]') // 缓存的已选属性没有 disabled (可以被选择) if (!$lastSelected.hasClass('disabled')) { $lastSelected.addClass('active') updateStatus(getSelectedItem()) } } } /** * 高亮当前属性区 */ function highLighAttr() { for (var i = 0; i < keys.length; i++) { var key = keys[i] var $curr = $('dl[data-type="' + key + '"]') if ($curr.find('.active').length < 1) { $curr.addClass('hl') } else { $curr.removeClass('hl') } } } /** * 绑定规格按钮事件 */ function bindEvent() { $('#app').undelegate().delegate('button', 'click', function (e) { var $this = $(this) var isActive = $this.hasClass('.active') var isDisable = $this.hasClass('disabled') if (!isActive) { handleNormalClick($this) if (isDisable) { handleDisableClick($this) } else { selectedCache[$this.parents('dl').eq(0).data('idx')] = $this.val() } updateStatus(getSelectedItem()) highLighAttr() showResult() } }) $('button').each(function () { var value = $(this).val() if (!res[value] && !$(this).hasClass('active')) { $(this).addClass('disabled') } }) } /** * 展示已选择结果 */ function showResult() { var result = getSelectedItem() var s = [] for (var i = 0; i < result.length; i++) { var item = result[i]; if (!!item) { s.push(item) } } if (s.length == keys.length) { var curr = res[s.join(spliter)] if (curr) { s = s.concat(curr.skus) } $('#msg').html('已选择:' + s.join('\u3000-\u3000')) } } /** * 更新初始化数据 */ function updateData() { data = JSON.parse($('#data_area').val()) init(data) } function setTitle(el) { var title = $(el).data('title'); if (title) $(el).attr('title', title); } function setAllTitle() { $('#app').find('button').each(setTitle) } // 初始化函数 function init(data) { // 初始化将下列参数全部置为空 res = {} r = {} keys = [] selectedCache = [] // 保存所有的key值 for (var attr_key in data[0]) { if (!data[0].hasOwnProperty(attr_key)) continue; // 判断自身属性是否存在,即确定是否至少一个Sku对象 if (attr_key !== 'skuId') keys.push(attr_key) // 将第一个Sku对象的Key(除了skuId)放置keyes数组中 } // 组合数组:属性值与SKU ID绑定;属性分组 r = combineAttr(data, keys) // 渲染规格 render(r.result) // 生成所有可选子集 buildResult(r.items) // 根据选中的Item更新所有Item状态 updateStatus(getSelectedItem()) // 展示已选择的结果 showResult() // 绑定规格按钮事件 bindEvent() } /** * 首次加载执行 */ init(data) </script> </body> </html>
这个前端实现了选择规格的问题,那么就在 Laravel 中查询出博主存储在数据库中的规格信息,并放入到这个前端试试,直接在路由上这样写:
Route::get('get_sku', function () { $model_sku = new \App\Sku(); $rows_sku = $model_sku->where('product_id', 1)->get(); $lst_sku = []; foreach ($rows_sku as $row_sku) { $sku = json_decode($row_sku['attrs_name'], true); $sku['skuId'] = $row_sku['id']; $lst_sku[] = $sku; } return json_encode($lst_sku); });
访问这个页面,获取到了这样的一段 JSON:
[ { "颜色": "红色", "尺码": "S", "skuId": 1 }, { "颜色": "黄色", "尺码": "S", "skuId": 2 }, { "颜色": "红色", "尺码": "M", "skuId": 3 }, { "颜色": "黄色", "尺码": "M", "skuId": 4 } ]
直接将这段 JSON 放入前端:
可以完美的实现规格功能。
博主之前参与开发的项目有像上面这样的多个表的设计,也有单独一个规格表的设计,两种设计都各有优势。
本文地址 : bubaijun.com/page.php?id=215
版权声明 : 未经允许禁止转载!
上一篇文章: PHP实现冒泡与快速排序算法
下一篇文章: 广州地铁21号线快车即时到站查询