2020-10-23 10:06:00
围观(5479)
昨天博主去了一家公司面试,被面试官问到了了一个问题:“看你的简历,在上一家公司有参与商城项目的开发,那商品规格这个表是怎么设计的?”
博主想都没想,就说了:“可以设置两个表,一个是 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号线快车即时到站查询