乐优商城学习笔记五-商品规格管理

0.学习目标

  • 了解商品规格数据结构设计思路
  • 实现商品规格查询
  • 了解SPU和SKU数据结构设计思路
  • 实现商品查询
  • 了解商品新增的页面实现
  • 独立编写商品新增后台功能

1.商品规格数据结构

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

1.1.SPU和SKU

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

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

以图为例来看:
1526085541996

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

可以看出:

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

1.2.数据库设计分析

1.2.1.思考并发现问题

弄清楚了SPU和SKU的概念区分,接下来我们一起思考一下该如何设计数据库表。

首先来看SPU,大家一起思考下SPU应该有哪些字段来描述?

id:主键
title:标题
description:描述
specification:规格
packaging_list:包装
after_service:售后服务
comment:评价
category_id:商品分类
brand_id:品牌

似乎并不复杂,但是大家仔细思考一下,商品的规格字段你如何填写?

1526086539789

不同商品的规格不一定相同,数据库中要如何保存?

再看下SKU,大家觉得应该有什么字段?

id:主键
spu_id:关联的spu
price:价格
images:图片
stock:库存
颜色?
内存?
硬盘?

碰到难题了,不同的商品分类,可能属性是不一样的,比如手机有内存,衣服有尺码,我们是全品类的电商网站,这些不同的商品的不同属性,如何设计到一张表中?

1.2.2.分析规格参数

仔细查看每一种商品的规格你会发现:

虽然商品规格千变万化,但是同一类商品(如手机)的规格是统一的,有图为证:

华为的规格:

1526087063700

三星的规格:

1526087142454

也就是说,商品的规格参数应该是与分类绑定的。每一个分类都有统一的规格参数模板,但不同商品其参数值可能不同

如下图所示:

1526088168565

1.2.3.SKU的特有属性

SPU中会有一些特殊属性,用来区分不同的SKU,我们称为SKU特有属性。如华为META10的颜色、内存属性。

不同种类的商品,一个手机,一个衣服,其SKU属性不相同。

同一种类的商品,比如都是衣服,SKU属性基本是一样的,都是颜色、尺码等。

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

1526088981953

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

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

1526089506566

1.2.4.搜索属性

打开一个搜索页,我们来看看过滤的条件:

1526090072535

你会发现,过滤条件中的屏幕尺寸、运行内存、网路、机身内存、电池容量、CPU核数等,在规格参数中都能找到:

1526090228171

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

1526091216124

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格式

为什么是一个json?我们看下规格参数的格式:

1526092179381

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

  • group:代表组,与商品分类关联
  • param_key:属性名,与组关联,一对多
  • param_value:属性备选值,与属性名关联,一对多

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

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

1.3.2.json结构分析

先整体看一下:

1526092693138

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

接下来是params:

1526093111370

主芯片这一组为例:

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

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

    • k:属性名称
    • searchable:是否作为搜索字段,将来在搜索页面使用,boolean类型
    • global:是否是SPU全局属性,boolean类型。true为全局属性,false为SKU的特有属性
    • options:属性值的可选项,数组结构。起约束作用,不允许填写可选项以外的值,比如CPU核数,有人添10000核岂不是很扯淡
    • numerical:是否为数值,boolean类型,true则为数值,false则不是。为空也代表非数值
    • unit:单位,如:克,毫米。如果是数值类型,那么就需要有单位,否则可以不填。

上面的截图中所有属性都是全局属性,我们来看看内存,应该是特有属性:

1526262641446

总结下:

  • 规格参数分组,每组有多个参数
  • 参数的 k代表属性名称,没有值,具体的SPU才能确定值
  • 参数会有不同的属性:是否可搜索,是否是全局、是否是数值,这些都用boolean值进行标记:
    • SPU下的多个SKU共享的参数称为全局属性,用global标记
    • SPU下的多个SKU特有的参数称为特有属性
    • 如果参数是数值类型,用numerical标记,并且指定单位unit
    • 如果参数可搜索,用searchable标记

2.商品规格参数管理

2.1.1.页面实现

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

1526095548672

可以看出页面分成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则隐藏弹窗

2.1.2.data中定义的属性

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

1526287774316

  • specifications:选中一个商品分类后,需要查询后台获取规格参数信息,保存在这个对象中,Vue会完成页面渲染。
  • oldSpec:当前页兼具了规格的增、改、查等功能,这个对象记录被修改前的规格参数,以防用户撤销修改,用来恢复数据。
  • dialog:是否显示对话框的标记。true则显示,false则不显示
  • currentNode:记录当前选中的商品分类节点
  • isInsert:判断接下来是新增还是修改

2.2.规格参数的查询

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

了解一下:

2.2.1.树节点的点击事件

当我们点击树节点时,要将v-dialog打开,因此必须绑定一个点击事件:

1526095959539

我们来看下handleClick方法:

handleClick(node) {
    // 判断点击的节点是否是父节点(只有点击到叶子节点才会弹窗)
    if (!node.isParent) {
        // 如果是叶子节点,那么就发起ajax请求,去后台查询商品规格数据。
        this.$http.get("/item/spec/" + node.id)
            .then(resp => {
            // 查询成功后,把响应结果赋值给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.后端代码

entity

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

    @Id
    private Long categoryId;
    private String specifications;
}

mapper

public interface SpecificationMapper extends Mapper<Specification> {
}

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

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

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

  • 请求参数:商品分类id

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

    1526104087329

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

代码:

@RestController
@RequestMapping("spec")
public class SpecificationController {

    @Autowired
    private SpecificationService specificationService;

    @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:

@Service
public class SpecificationService {

    @Autowired
    private SpecificationMapper specificationMapper;

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

页面访问测试:

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

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("保存失败!")
            });
        }

2.3.2后台

contoller

    /**
     *添加规格模板
     * @param specification
     * @return
     */
    @PostMapping
    public ResponseEntity<Void> saveSpecification(Specification specification){
        this.specificationService.saveSpecification(specification);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

    /**
     * 更新规格模板
     * @param specification
     * @return
     */
    @PutMapping
    public ResponseEntity<Void> updateSpecification(Specification specification){
        this.specificationService.updateSpecification(specification);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

    /**
     * 删除规格模板
     * @param id
     * @return
     */
    @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

/**
 * @Author smallmartial
 * @Date 2019/4/14
 * @Email smallmarital@qq.com
 */
public interface SpecificationMapper extends Mapper<Specification> {
}

service

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

    public void updateSpecification(Specification specification) {
        /**
         *  updateByPrimaryKeySelective会对字段进行判断再更新(如果为Null就忽略更新),
         *  如果你只想更新某一字段,可以用这个方法。
         *
         * updateByPrimaryKey对你注入的字段全部更新,
         * 如果为字段不更新,数据库的值就为null。
         */
        this.specificationMapper.updateByPrimaryKeySelective(specification);
    }

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

2.4测试


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值