乐优商城(七)商品规格管理

目录

一、商品规格数据结构

1.1 SPU和SKU

1.2 数据库设计分析

1.2.1 规格参数分析

1.2.2 SKU特有属性

1.2.3 搜索属性

1.3 规格参数表

1.3.1 表结构

1.3.2 JSON结构分析

二、商品规格参数管理

2.1 页面分析

2.2 规格参数查询

2.2.1 树节点的点击事件

2.2.2 后端接口

2.3 规格参数增加

2.3.1 前端代码

2.3.2 后台接口

2.4 规格参数修改

2.5 规格参数删除

2.6 总结

2.7 测试


商品规格管理

一、商品规格数据结构

乐优商城是一个全品类的电商网站,因此商品的种类繁多,每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU和SKU。

1.1 SPU和SKU

SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集

SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品

  • 本页的 vivo x21就是一个商品集(SPU)
  • 因为颜色、内存等不同,而细分出不同的x21,如亮冰钻黑6+128G版。(SKU)

可以看出:

  • SPU是一个抽象的商品集概念,为了方便后台的管理。
  • SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU

1.2 数据库设计分析

1.2.1 规格参数分析

当仔细查看每一种规格后就会发现,虽然商品规格千变万化,但是同一类商品(手机)的规格是统一的,如下图所示:

vivo的规格

oppo的规格

所以,商品的规格参数应该是与分类绑定的,每个分类都有统一的规格参数模板,dan'shi 不同商品的参数值可能不同。

如下图所示:

1.2.2 SKU特有属性

SPU中会有一些特殊属性,用来区分不同的SKU,我们称为SKU特有属性。如vivo x21的颜色、内存属性。不同种类的商品,一个手机,一个衣服,其SKU属性不相同。同一种类的商品,比如都是衣服,SKU属性基本是一样的,都是颜色、尺码等。

这样说起来,似乎SKU的特有属性也是与分类相关的?事实上,仔细观察会发现,SKU的特有属性是商品规格参数的一部分

也就是说,没必要单独对SKU的特有属性进行设计,它可以看做是规格参数中的一部分。这样规格参数中的属性可以标记成两部分:

  •  所有sku共享的规格属性(称为全局属性)
  •  每个sku不同的规格属性(称为特有属性)

1.2.3 搜索属性

打开搜索页,查看过滤条件:

过滤条件中的屏幕尺寸、运行内存、机身内存、CPU核数等,在规格参数中都能找到:

也就是说,规格参数中的数据,将来会有一部分作为搜索条件来使用。所以在设计时,将这部分属性标记出来,将来做搜索的时候,作为过滤条件。要注意的是,无论是SPU的全局属性,还是SKU的特有属性,都有可能作为搜索过滤条件的,并不冲突,而是有一个交集:

1.3 规格参数表

1.3.1 表结构

CREATE TABLE `tb_specification` (
  `category_id` bigint(20) NOT NULL COMMENT '规格模板所属商品分类id',
  `specifications` varchar(3000) NOT NULL DEFAULT '' COMMENT '规格参数模板,json格式',
  PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品规格参数模板,json格式。';

specificatons:规格参数模板,json格式

如果按照传统数据库设计,这里至少需要3张表:

  • group:代表组,与商品分类关联

  • param_key:属性名,与组关联,一对多

  • param_value:属性备选值,与属性名关联,一对多

这样程序的复杂度大大增加,但是提高了数据的复用性。

所以解决方案是,采用json来保存整个规格参数模板,不需要额外的表,一个字符串就够了。

1.3.2 JSON结构分析

整体

  • 因为规格参数分为很多组,所以json最外层是一个数组。
  • 数组中是对象类型,每个对象代表一个组的数据,对象的属性包括:

  group:组的名称

  params:该组的所有属性

params

主芯片这一组为例:

  • group:注明,这里是主芯片

  • params:该组的所有规格属性,因为不止一个,所以是一个数组。这里包含四个规格属性:CPU品牌,CPU型号,CPU频率,CPU核数。每个规格属性都是一个对象,包含以下信息:

    • k:属性名称

    • searchable:是否作为搜索字段,将来在搜索页面使用,boolean类型

    • global:是否是SPU全局属性,boolean类型。true为全局属性,false为SKU的特有属性

    • options:属性值的可选项,数组结构。起约束作用,不允许填写可选项以外的值,比如CPU核数,有人添10000核岂不是很扯淡

    • numerical:是否为数值,boolean类型,true则为数值,false则不是。为空也代表非数值

    • unit:单位,如:克,毫米。如果是数值类型,那么就需要有单位,否则可以不填。

以上属性都是全局属性,下面是特殊属性:

总结下:

  • 规格参数分组,每组有多个参数

  • 参数的 k代表属性名称,没有值,具体的SPU才能确定值

  • 参数会有不同的属性:是否可搜索,是否是全局、是否是数值,这些都用boolean值进行标记:

    • SPU下的多个SKU共享的参数称为全局属性,用global=true标记

    • SPU下的多个SKU特有的参数称为特有属性,用global=false标记

    • 如果参数是数值类型,用numerical标记,并且指定单位unit

    • 如果参数可搜索,用searchable标记

二、商品规格参数管理

2.1 页面分析

因为规格是跟商品分类绑定的,因此首先会展现商品分类树,并且提示你要选择商品分类,才能看到规格参数的模板。

可以看出页面分成3个部分:

  • v-card-title:标题部分,这里是提示信息,告诉用户要先选择分类,才能看到模板

  • v-tree:这里用到的是我们之前讲过的树组件,展示商品分类树,不过现在是假数据,我们只要把treeData属性删除,它就会走url属性指定的路径去查询真实的商品分类树了。

    <v-tree url="/item/category/list" :isEdit="false"  @handleClick="handleClick" />
  • v-dialog:Vuetify提供的对话框组件,v-model绑定的dialog属性是boolean类型:

    • true则显示弹窗

    • false则隐藏弹窗

再看一下Vue实例中data定义了哪些属性,对页面会产生怎样的影响:

  • specifications:选中一个商品分类后,需要查询后台获取规格参数信息,保存在这个对象中,Vue会完成页面渲染。

  • oldSpec:当前页兼具了规格的增、改、查等功能,这个对象记录被修改前的规格参数,以防用户撤销修改,用来恢复数据。

  • dialog:是否显示对话框的标记。true则显示,false则不显示

  • currentNode:记录当前选中的商品分类节点

  • units:数值类型的可选单位

2.2 规格参数查询

点击树叶子节点后要显示规格参数,因此查询功能应该编写在点击事件中。

2.2.1 树节点的点击事件

      handleClick(node) {
        // 把当前点击IDE节点记录下来
        this.currentNode = node;
        // 判断点击的节点是否是父节点(只有点击到叶子节点才会弹窗)
        if (!node.isParent) {
          // 如果是叶子节点,那么就发起ajax请求,去后台查询商品规格数据。
          this.$http.get("/item/spec/" + node.id)
            .then(resp => {
              console.log(resp.data)
              // 查询成功后,把响应结果赋值给specifications属性,Vue会进行自动渲染。
              this.specifications = resp.data;
              // 记录下此时的规格数据,当页面撤销修改时,用来恢复原始数据
              this.oldSpec = resp.data;
              // 打开弹窗
              this.dialog = true;
              // 标记此时要进行修改操作
              this.isInsert = false;
            })
            .catch(() => {
              // 如果没有查询成功,那么询问是否添加规格
              this.$message.confirm('该分类还没有规格参数,是否添加?')
                .then(() => {
                  // 如果要添加,则将specifications初始化为空
                  this.specifications = [{
                    group: '',
                    params: []
                  }];
                  // 打开弹窗
                  this.dialog = true;
                  // 标记为新增
                  this.isInsert = true;
                })
            })
        }
      }

重点是编写后台接口,返回数据即可。

2.2.2 后端接口

Pojo

package com.leyou.item.pojo;

import javax.persistence.Id;
import javax.persistence.Table;

@Table(name = "tb_specification")
public class Specification {

    @Id
    private Long categoryId;
    private String specifications;

    public Long getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Long categoryId) {
        this.categoryId = categoryId;
    }

    public String getSpecifications() {
        return specifications;
    }

    public void setSpecifications(String specifications) {
        this.specifications = specifications;
    }

    @Override
    public String toString() {
        return "Specification{" +
                "categoryId=" + categoryId +
                ", specifications='" + specifications + '\'' +
                '}';
    }
}

Mapper

package com.leyou.item.mapper;

import com.leyou.item.pojo.Specification;
import tk.mybatis.mapper.common.Mapper;

/**
 * @author li
 */

@org.apache.ibatis.annotations.Mapper
public interface SpecificationMapper extends Mapper<Specification> {
}

Controller

先分析下需要的东西,在页面的ajax请求中可以看出:

  • 请求方式:查询,肯定是get

  • 请求路径:/spec/{cid} ,这里通过路径占位符传递商品分类的id

  • 请求参数:商品分类id

  • 返回结果:页面是直接把resp.data赋值给了specifications:

那么返回的应该是规格参数的字符串

package com.leyou.item.controller;

import com.leyou.item.pojo.Specification;
import com.leyou.item.service.SpecificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

/**
 * @author li
 */
@RestController
@RequestMapping("spec")
public class SpecificationController {

    @Autowired
    private SpecificationService specificationService;

    /**
     * 查询商品分类对应的规格参数模板
     * @param id
     * @return
     */
    @GetMapping("{id}")
    public ResponseEntity<String> querySpecificationByCategoryId(@PathVariable("id") Long id){
        Specification spec = this.specificationService.queryById(id);
        if (spec == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(spec.getSpecifications());
    }
}

Service

接口

package com.leyou.item.service;

import com.leyou.item.pojo.Specification;

/**
 * @Author: 98050
 * Time: 2018-08-14 15:26
 * Feature:
 */
public interface SpecificationService {
    /**
     * 根据category id查询规格参数模板
     * @param id
     * @return
     */
    Specification queryById(Long id);
}

实现类

package com.leyou.item.service.serviceimpl;

import com.leyou.item.mapper.SpecificationMapper;
import com.leyou.item.pojo.Specification;
import com.leyou.item.service.SpecificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author: 98050
 * Time: 2018-08-14 15:26
 * Feature:
 */
@Service
public class SpecificationServiceImpl implements SpecificationService {

    @Autowired
    private SpecificationMapper specificationMapper;

    @Override
    public Specification queryById(Long id) {
        return this.specificationMapper.selectByPrimaryKey(id);
    }
}

测试访问

数据库中由3条完整模板信息

先访问:http://api.leyou.com/api/item/spec/76

页面访问:

规格参数的增加、删除和修改全部在这个对话框中完成,所以删除和修改就可以合并。

注:每一个分组的全部数据后期都以JSON的方式存入

2.3 规格参数增加

2.3.1 前端代码

      //添加分组
      addGroup() {
        this.specifications.push({
          group: '',
          params: []
        })
      },
      // 添加新属性
      addParam(i) {
        this.specifications[i].params.push({
          k: "",
          searchable: false,
          global: true,
          numerical:false,
          unit:"",
          options: []
        })
      },
      // 为属性添加默认值
      addOption(i, j) {
        this.specifications[i].params[j].options.push("")
      },

上面这三个函数主要是用来添加分组、属性、属性默认值,演示如下所示:

重点就是saveTemplate函数了,包含对规格模板的增加、修改和删除等功能。

// 保存、修改、删除模板
      saveTemplate() {
        this.dialog = true;

        //模板删除
        if (this.specifications.length === 0){
          //console.log("删除:"+this.currentNode.id);
          this.$http.delete("/item/spec/"+this.currentNode.id).then(() => {
            this.dialog = false;
            this.$message.success("删除成功!");
            this.oldSpec = [];
          }).catch(() => {
            this.$message.error("删除失败");
          });
        }else {
          this.$http({
            method: this.oldSpec.length === 0 ? 'post' : 'put',
            url: '/item/spec',
            data: this.$qs.stringify({
              categoryId: this.currentNode.id,
              specifications: JSON.stringify(this.specifications)
            })
          })
            .then(() => {
              this.dialog = false;
              this.$message.success("保存成功!")
              this.oldSpec = [];
            })
            .catch(() => {
              this.$message.error("保存失败!")
            });
        }
      }

注:

如何判断是否进行删除?通过specifications的长度来判断是否进行修改,长度为0表示模板内容为空。

如何判断修改还是新增? 通过oldSpec的长度来判断,因为在点击叶子节点时会查询对应的规格模板,查询到的话就将数据保存一份到oldSpec中,用来进行修改;所以当oldSpec的长度为0时,那么说明没有查到模板,即为新增。(oldSpec是个数组)

2.3.2 后台接口

Controller

    /**
     * 保存一个规格参数模板
     * @param specification
     * @return
     */
    @PostMapping
    public ResponseEntity<Void> saveSpecification(Specification specification){
        this.specificationService.saveSpecification(specification);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

Mapper

package com.leyou.item.mapper;

import com.leyou.item.pojo.Specification;
import tk.mybatis.mapper.common.Mapper;

/**
 * @author li
 */

@org.apache.ibatis.annotations.Mapper
public interface SpecificationMapper extends Mapper<Specification> {
}

Service

接口

    /**
     * 添加规格参数模板
     * @param specification
     */
    void saveSpecification(Specification specification);

实现类

    @Override
    public void saveSpecification(Specification specification) {
        this.specificationMapper.insert(specification);
    }

2.4 规格参数修改

Controller

    /**
     * 修改一个规格参数模板
     * @param specification
     * @return
     */
    @PutMapping
    public ResponseEntity<Void> updateSpecification(Specification specification){
        this.specificationService.updateSpecification(specification);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

Mapper

package com.leyou.item.mapper;

import com.leyou.item.pojo.Specification;
import tk.mybatis.mapper.common.Mapper;

/**
 * @author li
 */

@org.apache.ibatis.annotations.Mapper
public interface SpecificationMapper extends Mapper<Specification> {
}

Service

接口


    /**
     * 修改规格参数模板
     * @param specification
     */
    void updateSpecification(Specification specification);

实现类

    @Override
    public void updateSpecification(Specification specification) {
        this.specificationMapper.updateByPrimaryKeySelective(specification);
    }

2.5 规格参数删除

Controller

    @DeleteMapping("{id}")
    public ResponseEntity<Void> deleteSpecification(@PathVariable("id") Long id){
        Specification specification = new Specification();
        specification.setCategoryId(id);
        this.specificationService.deleteSpecification(specification);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

Mapper

package com.leyou.item.mapper;

import com.leyou.item.pojo.Specification;
import tk.mybatis.mapper.common.Mapper;

/**
 * @author li
 */

@org.apache.ibatis.annotations.Mapper
public interface SpecificationMapper extends Mapper<Specification> {
}

Service

接口

    /**
     * 删除规格参数模板
     * @param specification
     */
    void deleteSpecification(Specification specification);

实现类

    @Override
    public void deleteSpecification(Specification specification) {
        this.specificationMapper.deleteByPrimaryKey(specification);
    }

2.6 总结

对规格模板的操作都是放在一个对话框中进行的,因为specifications中保存的是全部的分组信息,每个分组的信息都是JSON格式。新增和修改就是直接把specifications中的内容传到后台进行相应的操作,删除则是通过判断specifications的长度来决定的,当长度为0时,说明用户已经把所有分组全部删除,所以相应的在数据库中要清除对应id的数据。删除是通过给后台传入id进行的,那么这个id从何而来,通过this.currentNode.id来获取。

2.7 测试

 

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值