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