leyou商城day5 商品管理

16 篇文章 0 订阅
12 篇文章 0 订阅

01、课程目标

  • 了解SPU与SKU数据结构设计思路

  • 独立实现商品查询

  • 独立实现商品新增后台

02、SPU与SKU:概念说明与表结构

1)概念说明

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

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

以图为例来看:

在这里插入图片描述

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

可以看出:

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

2)SPU与SKU表结构分析

1)思考分析

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

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

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

似乎并不复杂.

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

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

sku的特有属性也是变化的,不同商品,特有属性不一定相同,那么我们的表字段岂不是不确定?

sku的这个特有属性该如何设计呢?

2)SKU的特有属性

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

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

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

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

在这里插入图片描述

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

  • spu下所有sku共享的规格属性(称为通用属性
  • spu下每个sku不同的规格属性(称为特有属性

回忆一下之前我们设计的tb_spec_param表,是不是有一个字段,名为generic,标记通用和特有属性。就是为了这里使用。

这样以来,商品SKU表就只需要设计规格属性以外的其它字段了,规格属性由之前的规格参数表tb_spec_param来保存。

但是,规格属性的值依然是需要与商品相关联的。

03、SPU与SKU:spu与spu_detail表结构分析

1)数据库表切分说明

  • 横向切分【水平拆分】

    一种情况是 数据量太大了【2000万左右】

    一种情况是 历史数据使用的几率很低

  • 纵向切分【垂直拆分】

    一种情况是 表的字段太多了【50个左右】

    一种情况是 表中有一些大字段,比如:blob,clob,增删改的效率低【long】,需要将这些大字段单独分离出去

2)表结构

spu表
CREATE TABLE `tb_spu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
  `name` varchar(256) NOT NULL DEFAULT '' COMMENT '商品名称',
  `sub_title` varchar(256) DEFAULT '' COMMENT '副标题,一般是促销信息',
  `cid1` bigint(20) NOT NULL COMMENT '1级类目id',
  `cid2` bigint(20) NOT NULL COMMENT '2级类目id',
  `cid3` bigint(20) NOT NULL COMMENT '3级类目id',
  `brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
  `saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=183 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象性的商品,比如 iphone8';
spu_detail表

与我们前面分析的基本类似,但是似乎少了一些字段,比如商品描述。

我们做了表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail

CREATE TABLE `tb_spu_detail` (
  `spu_id` bigint(20) NOT NULL,
  `description` text COMMENT '商品描述信息',
  `generic_spec` varchar(2048) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
  `special_spec` varchar(1024) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
  `packing_list` varchar(1024) DEFAULT '' COMMENT '包装清单',
  `after_service` varchar(1024) DEFAULT '' COMMENT '售后服务',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。

需要注意的是这两个字段:generic_spec和special_spec。

3)spu中的规格参数:generic_spec字段

前面讲过规格参数与商品分类绑定,同一分类的商品,会有一套相同的规格参数key(规格参数模板),但是这个分类下每个商品的规格参数值都不相同,因此要满足下面几点:

  • 我们有一个规格参数表,跟分类关联,保存的就是某分类下的规格参数模板。
  • 我们还需要表,跟商品关联,保存某个商品,相关联的规格参数的值。
  • 规格参数因为分成了通用规格参数和特有规格参数,因此规格参数值也需要分别于SPU和SKU关联:
    • 通用的规格参数值与SPU关联。
    • 特有规格参数值与SKU关联。

但是我们并没有增加新的表,来看下我们的 表如何存储这些信息:

generic_spec字段

如果要设计一张表,来表示spu中的通用规格属性的值,至少需要下面的字段:

spu_id:与哪个商品关联
param_id:是商品的哪个规格参数
value:具体的值

我们并没有这么设计。而是把与某个商品相关的规格属性值,直接保存到这个商品spu表中,因此这些规格属性关联的商品就一目了然,那么上述3个属性中的spu_id就无需保存了,而剩下的就是param_id和规格参数值了。两者刚好是一一对应关系,组成一个键值对。我们刚好可以用一个json结构来标示。

是也就是spuDetail表中的generic_spec,其中保存通用规格参数信息的值:

整体来看:

在这里插入图片描述

json结构,其中都是键值对:

  • key:对应的规格参数的spec_param的id
  • value:对应规格参数的值

4)spu中的规格参数:special_spec字段

我们说spu中只保存通用规格参数,那么为什么有多出了一个special_spec字段呢?

以手机为例,品牌、操作系统等肯定是通用规格属性,内存、颜色等肯定是特有属性。

当你确定了一个SPU,比如小米的:红米4X,因为颜色内存等不同,会形成多个sku。如果把每个sku的颜色、内存等信息都整理一下,会形成下面的结果:

颜色:[香槟金, 樱花粉, 磨砂黑]
内存:[2G, 3G]
机身存储:[16GB, 32GB]

也就是说这里把一个spu下的每个sku的特有规格属性值聚合在了一起!这个就是special_spec字段了。

来看数据格式:

在这里插入图片描述

也是json结构:

  • key:规格参数id
  • value:spu属性的数组

那么问题来:为什么要在spu中把所有sku的规格属性聚合起来保存呢?

因为我们有时候需要把所有规格参数都查询出来,而不是只查询1个sku的属性。比如,商品详情页展示可选的规格参数时:

在这里插入图片描述

刚好符号我们的结构,这样页面渲染就非常方便了。

综上所述,spu与商品规格参数模板的关系如图所示:

在这里插入图片描述

04、SPU与SKU:sku表结构分析

1)表结构

CREATE TABLE `tb_sku` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
  `spu_id` bigint(20) NOT NULL COMMENT 'spu id',
  `title` varchar(256) NOT NULL COMMENT '商品标题',
  `images` varchar(1024) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
  `stock` int(8) DEFAULT '9999' COMMENT '库存',
  `price` bigint(16) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
  `indexes` varchar(32) DEFAULT '' COMMENT '特有规格属性在spu属性模板中的对应下标组合',
  `own_spec` varchar(1024) DEFAULT '' COMMENT 'sku的特有规格参数键值对,json格式,反序列化时请使用linkedHashMap,保证有序',
  `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
  PRIMARY KEY (`id`),
  KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27359021554 DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的 64g的iphone 8';

特别需要注意的是sku表中的indexes字段和own_spec字段。sku中应该保存特有规格参数的值,就在这两个字段中。

2)sku中的特有规格参数:indexes字段

在SPU表中,已经对特有规格参数及可选项进行了保存,结构如下:

{
    "4": [
        "香槟金",
        "樱花粉",
        "磨砂黑"
    ],
    "12": [
        "2GB",
        "3GB"
    ],
    "13": [
        "16GB",
        "32GB"
    ]
}

这些特有属性如果排列组合,会产生12个不同的SKU,而不同的SKU,其属性就是上面备选项中的一个。

比如:

  • 红米4X,香槟金,2GB内存,16GB存储
  • 红米4X,磨砂黑,2GB内存,32GB存储

你会发现,每一个属性值,对应于SPUoptions数组的一个选项,如果我们记录下角标,就是这样:

  • 红米4X,0,0,0
  • 红米4X,2,0,1

既然如此,我们是不是可以将不同角标串联起来,作为SPU下不同SKU的标示。这就是我们的indexes字段。

在这里插入图片描述

这个设计在商品详情页会特别有用:

在这里插入图片描述

当用户点击选中一个特有属性,你就能根据 角标快速定位到sku。

3)sku中的特有规格参数:own_spec字段

看结构:

{"4":"香槟金","12":"2GB","13":"16GB"}

保存的是特有属性的键值对。

SPU中保存的是可选项,但不确定具体的值,而SKU中的保存的就是具体的值。

05、商品查询:商品对象与表的关系

商品对象所需表:商品对象对应数据库中多张表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PkkY2e5j-1654593974314)(assets/1575601382376.png)]

商品对象比较复杂,需要多张表组合起来,开发中,复杂的业务是会这样的,大家一定要捋清晰这些表的关系。

商品对象的创建需要六张表的数据。

06、商品查询:商品列表数据来源分析

接下来,我们实现商品管理的页面,先看下我们要实现的效果:

在这里插入图片描述

可以看出整体是一个table,然后有新增按钮。是不是跟之前写品牌管理很像?

点击页面菜单中的商品列表页面:

在这里插入图片描述

页面右侧是一个空白的数据表格:

在这里插入图片描述

看到浏览器发起已经发起了查询商品数据的请求:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tofEWfE1-1654593974316)(assets/1528082212618.png)]

因此接下来,我们编写接口即可。

那么这些列表的数据分别来自那些表?

在这里插入图片描述

在这里插入图片描述

列表中我们只能先展示这几个数据,商品表我们之前分析了来源于六张表,那在哪里展示呢?

我们可以在商品详情页去展示。

07、商品查询:商品模块准备工作

1)SPU的entity

package com.leyou.item.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;

@Data
@TableName("tb_spu")
public class Spu {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long brandId;
    private Long cid1;// 1级类目
    private Long cid2;// 2级类目
    private Long cid3;// 3级类目
    private String name;// 商品名称
    private String subTitle;// 子标题
    private Boolean saleable;// 是否上架
    private Date createTime;// 创建时间
    private Date updateTime;// 最后修改时间
}

2)SpuDetail的entity

package com.leyou.item.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;

@Data
@TableName("tb_spu_detail")
public class SpuDetail {
    @TableId(type = IdType.INPUT)
    private Long spuId;// 对应的SPU的id
    private String description;// 商品描述
    private String specialSpec;// 商品特殊规格的名称及可选值模板
    private String genericSpec;// 商品的全局规格属性
    private String packingList;// 包装清单
    private String afterService;// 售后服务
    private Date createTime;// 创建时间
    private Date updateTime;// 最后修改时间
}

3)Sku的entity

package com.leyou.item.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;

@Data
@TableName("tb_sku")
public class Sku {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long spuId;
    private String title;
    private String images;
    private Long price;
    private Integer stock;
    private String ownSpec;// 商品特殊规格的键值对
    private String indexes;// 商品特殊规格的下标
    private Boolean enable;// 是否有效,逻辑删除用
    private Date createTime;// 创建时间
    private Date updateTime;// 最后修改时间
}

4)SPU的DTO(*)

先分析:

  • 请求方式:GET

  • 请求路径:/spu/page

  • 请求参数:

    • page:当前页
    • rows:每页大小
    • key:过滤条件
    • saleable:上架或下架
  • 返回结果:通过页面能看出来,查询要展示的数据都是SPU数据,而且因为是分页查询,我们可以返回与之前品牌查询一样的PageResult。

    • 要注意,页面展示的中需要是商品分类和品牌名称,而SPU表中中保存的是id,我们需要在DTO中处理这些字段。

      我们可以对Spu拓展categoryName和brandName属性:

package com.leyou.item.dto;

import lombok.Data;

import java.util.Date;

/**
 * 封装Spu的相关数据
 */
@Data
public class SpuDTO {

    private Long id;
    private Long brandId;
    private Long cid1;// 1级类目
    private Long cid2;// 2级类目
    private Long cid3;// 3级类目
    private String name;// 商品名称
    private String subTitle;// 子标题
    private Boolean saleable;// 是否上架
    private Date createTime;// 创建时间
    private Date updateTime;// 最后修改时间

    //分类名称 格式:手机通讯/手机/手机
    private String categoryName;
    //品牌名称
    private String brandName;

}


5)SPU的Mapper

package com.leyou.item.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.entity.Spu;

public interface SpuMapper extends BaseMapper<Spu> {
}

6)SpuDetail的Mapper

package com.leyou.item.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.entity.SpuDetail;

public interface SpuDetailMapper extends BaseMapper<SpuDetail> {
}

7)Sku的Mapper

package com.leyou.item.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.entity.Sku;

public interface SkuMapper extends BaseMapper<Sku> {
}

8)Goods商品的Service

package com.leyou.item.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.leyou.item.mapper.SkuMapper;
import com.leyou.item.mapper.SpuDetailMapper;
import com.leyou.item.mapper.SpuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 商品业务Service
 */
@Service
@Transactional
public class GoodsService{
    @Autowired
    private SpuMapper spuMapper;
    @Autowired
    private SpuDetailMapper spuDetailMapper;
    @Autowired
    private SkuMapper skuMapper;

}

9)Goods商品的Controller

package com.leyou.item.controller;

import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

/**
 * 商品Controller
 */
@RestController
public class GoodsController {
    @Autowired
    private GoodsService goodsService;
}

08、商品查询:品牌和分类名称显示的准备工作

1)根据品牌id查询品牌对象

提供处理器

 /**
     * 根据id查询品牌
     */
    @GetMapping("/brand/{id}")
    public ResponseEntity<Brand> findBrandById(@PathVariable("id") Long id){ // @PathVariable接收URL的参数
           Brand brand = brandService.findBrandById(id);
           return ResponseEntity.ok(brand);
    }

提供service

public Brand findBrandById(Long id) {
        Brand brand = brandMapper.selectById(id);
        if(brand==null){
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }
        return brand;
    }

2)根据分类id的集合查询分类对象的列表

提供处理器

   /**
     * 根据分类ID查询分类集合
     */
    @GetMapping("/category/list")
    public ResponseEntity<List<Category>> findCategoriesByIds(@RequestParam("ids") List<Long> ids){
        List<Category> categories = categoryService.findCategoriesByIds(ids);
        return ResponseEntity.ok(categories);
    }

提供service

public List<Category> findCategoriesByIds(List<Long> ids) {
        List<Category> categories = categoryMapper.selectBatchIds(ids);
        if(CollectionUtils.isEmpty(categories)){
            throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
        }
        return categories;
    }

09、商品查询:Spu分页查询(*)

1)GoodsController添加方法

先分析:

  • 请求方式:GET
  • 请求路径:/spu/page
  • 请求参数:
    • page:当前页
    • rows:每页大小
    • key:过滤条件
    • saleable:上架或下架
  • 返回结果:PageResult
package com.leyou.item.controller;

import com.leyou.common.pojo.PageResult;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 商品
 */
@RestController
public class GoodsController {
    @Autowired
    private GoodsService goodsService;

   package com.leyou.item.controller;

import com.leyou.common.pojo.PageResult;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 商品
 */
@RestController
public class GoodsController {
    @Autowired
    private GoodsService goodsService;

    /**
     * 分页查询商品
     */
    @GetMapping("/spu/page")
    public ResponseEntity<PageResult<SpuDTO>> spuPageQuery(
          @RequestParam(value = "page",defaultValue = "1") Integer page,
          @RequestParam(value = "rows",defaultValue = "5") Integer rows,
          @RequestParam(value = "key",required = false) String key,
          @RequestParam(value = "saleable",required = false) Boolean saleable
    ){
        PageResult<SpuDTO> pageResult = goodsService.spuPageQuery(page,rows,key,saleable);
        return ResponseEntity.ok(pageResult);
    }
}

}


2)GoodsService添加方法

package com.leyou.item.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.leyou.common.pojo.PageResult;
import com.leyou.common.utils.BeanHelper;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.mapper.SkuMapper;
import com.leyou.item.mapper.SpuDetailMapper;
import com.leyou.item.mapper.SpuMapper;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import com.leyou.item.pojo.Spu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 商品业务
 */
@Service
@Transactional
public class GoodsService {

    @Autowired
    private SpuMapper spuMapper;
    @Autowired
    private SpuDetailMapper spuDetailMapper;
    @Autowired
    private SkuMapper skuMapper;
    @Autowired
    private BrandService brandService;
    @Autowired
    private CategoryService categoryService;

    public PageResult<SpuDTO> spuPageQuery(Integer page, Integer rows, String key, Boolean saleable) {
        //1.封装条件
        //1.1 封装分页参数
        IPage<Spu> iPage = new Page<>(page,rows);

        //1.2 封装查询条件
        QueryWrapper<Spu> queryWrapper = Wrappers.query();

        //拼接条件
        //SELECT * FROM tb_spu WHERE (NAME LIKE '%华为%' OR sub_title LIKE '%华为%') AND saleable=1
        //处理key
        if(StringUtils.isNotEmpty(key)){
            // where name like '%xx%' or sub_title like '%xx%'

            //and:该方法可以把一些拼接的条件(sql语句)作为一个整体提高sql执行优先级(给一些条件加括号)
            queryWrapper.and(
                    i->
                    i.like("name",key)
                     .or()
                     .like("sub_title",key)
            );


        }

        //处理saleable
        if(saleable!=null){
            // saleable=true/false
            queryWrapper.eq("saleable",saleable);
        }

        //2.查询数据,获取结果
        iPage = spuMapper.selectPage(iPage,queryWrapper);

        //3.处理并返回结果
        //3.1 取出所有Spu对象
        List<Spu> spuList = iPage.getRecords();
        //3.2 拷贝数据,从Spu对象拷贝到SpuDTO对象
        List<SpuDTO> spuDTOList = BeanHelper.copyWithCollection(spuList,SpuDTO.class);
        //3.3 封装分类名称和品牌名称
        getCategoryNameAndBrandName(spuDTOList);
        //3.4 封装PageResult
        PageResult<SpuDTO> pageResult = new PageResult<>(iPage.getTotal(),iPage.getPages(),spuDTOList);
        return pageResult;
    }

    /**
     * 在spuDTOList集合中添加分类名称和品牌名称两个属性
     * @param spuDTOList
     */
    public void getCategoryNameAndBrandName(List<SpuDTO> spuDTOList) {
        spuDTOList.forEach(spuDTO -> {
            //1.处理品牌
            Brand brand = brandService.findBrandById(spuDTO.getBrandId());
            spuDTO.setBrandName(brand.getName());

            //2.处理分类
            //2.1 根据分类ID集合查询分类对象集合
            List<Category> categoryList = categoryService.findCategoriesByIds(Arrays.asList(spuDTO.getCid1(), spuDTO.getCid2(), spuDTO.getCid3()));
            //2.2 格式: 手机通讯/手机/手机
            String categoryName = categoryList.stream().map(Category::getName).collect(Collectors.joining("/"));
            spuDTO.setCategoryName(categoryName);
        });
    }
}


编写完成后,重启微服务,然后打开页面测试:

在这里插入图片描述

10、商品保存:业务分析

1)页面预览

当我们点击新增商品按钮:

在这里插入图片描述

就会出现一个弹窗:

在这里插入图片描述

里面把商品的数据分为了4部分来填写:

  • 基本信息:主要是一些简单的文本数据,包含了SPU和SpuDetail的部分数据,如
    • 商品分类:是SPU中的cid1cid2cid3属性
    • 品牌:是spu中的brandId属性
    • 标题:是spu中的title属性
    • 子标题:是spu中的subTitle属性
    • 售后服务:是SpuDetail中的afterService属性
    • 包装列表:是SpuDetail中的packingList属性
  • 商品描述:是SpuDetail中的description属性,数据较多,所以单独放一个页面
  • 规格参数:商品规格信息,对应SpuDetail中的genericSpec属性
  • SKU属性:spu下的所有Sku信息

也就是说这个页面包含了商品相关的三张表中的数据:

  • tb_spu
  • tb_spu_detail
  • tb_sku

2)商品分类

商品分类的级联选框我们之前在品牌查询已经做过,是要根据分类的pid查询分类,所以这里的级联选框已经实现完成:

刷新页面,可以看到请求已经发出:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

需要注意的是,这里选中以后会显示3级分类,因为数据库中保存的就是商品的1~3级类目。

注意:当选定了一个分类后,那么其实就已经确定了品牌的范围和规格参数的范围。

在这里插入图片描述

3)品牌选择

品牌也是一个下拉选框,不过其选项是不确定的,只有当用户选择了商品分类,才会把这个分类下的所有品牌展示出来。

所以页面编写了watch函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表。刷新页面,当选中一个分类时,可以看到请求发起:

在这里插入图片描述

接下来,我们只要编写后台接口,根据商品分类id,查询对应品牌即可。

11、商品保存:根据分类查询品牌

页面需要去后台查询品牌信息,我们自然需要提供:

1)controller

 /**
     * 根据分类id查询品牌
     */
    @GetMapping("/brand/of/category")
    public ResponseEntity<List<Brand>> findBrandsByCid(@RequestParam("id") Long id){
        List<Brand> brands = brandService.findBrandsByCid(id);
        return ResponseEntity.ok(brands);
    }

2)service

public List<Brand> findBrandsByCid(Long id) {
        //1.根据分类id查询品牌
        List<Brand> brands = brandMapper.findBrandsByCid(id);
        //2.处理结果
        if(CollectionUtils.isEmpty(brands)){
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }
        return brands;
    }

3)mapper

根据分类查询品牌有中间表,需要自己编写Sql:

package com.leyou.item.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface BrandMapper extends BaseMapper<Brand> {
    public void saveCategoryAndBrand(@Param("bid") Long bid,@Param("cids") List<Long> cids);

    /**
     * 根据分类id查询品牌
     * @param id
     * @return
     */
    @Select("SELECT b.* FROM tb_brand b,tb_category_brand cb WHERE b.`id` = cb.brand_id AND cb.category_id=#{categoryId}")
    List<Brand> findBrandsByCid(Long id);
}


效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJ3GO5eu-1654593974320)(assets/1528102194745.png)]

12、商品保存:商品描述及富文本编辑器

1)富文本编辑器介绍

标题、子标题等都是普通文本框,我们直接填写即可,没有需要特别注意的,商品描述信息比较复杂,而且图文并茂,甚至包括视频。

这样的内容,一般都会使用富文本编辑器。

什么是富文本编辑器?

百度百科:

在这里插入图片描述

通俗来说:富文本,就是比较丰富的文本编辑器。普通的框只能输入文字,而富文本还能给文字加颜色样式等。

富文本编辑器有很多,例如:KindEditor、Ueditor。但并不原生支持vue

但是我们今天要说的,是一款支持Vue的富文本编辑器:vue-quill-editor

2)Vue-Quill-Editor介绍

GitHub的主页:https://github.com/surmon-china/vue-quill-editor

Vue-Quill-Editor是一个基于Quill的富文本编辑器:Quill的官网

在这里插入图片描述

3)Vue-Quill-Editor使用指南

使用非常简单:

第一步:安装,使用npm命令:

npm install vue-quill-editor --save

第二步:加载,在js中引入:

全局使用:

import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'

const options = {}; /* { default global options } */

Vue.use(VueQuillEditor, options); // options可选

局部使用:

import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

import {quillEditor} from 'vue-quill-editor'

var vm = new Vue({
    components:{
        quillEditor
    }
})

第三步:页面引用:

<quill-editor v-model="goods.spuDetail.description" :options="editorOption"/>

4)自定义富文本编辑器

不过这个组件有个小问题,就是图片上传的无法直接上传到后台,因此我们对其进行了封装,支持了图片的上传。

在这里插入图片描述

使用也非常简单:

<v-stepper-content step="2">
    <v-editor v-model="goods.spuDetail.description" url="/upload/signature" needSignature/>
</v-stepper-content>
  • url:是图片上传的路径或者上传阿里OSS时的签名路径,这里输入的是签名路径
  • v-model:双向绑定,将富文本编辑器的内容绑定到goods.spuDetail.description

5)效果

在这里插入图片描述

13、商品保存:规格参数与SKU属性

1)规格参数

规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过watch监控来实现:

页面如下:

在这里插入图片描述

2)SKU属性

Sku属性是SPU下的每个商品的不同特征,如图:

在这里插入图片描述

当我们填写一些属性后,会在页面下方生成一个sku表格,大家可以计算下会生成多少个不同属性的Sku呢?

当你选择了上图中的这些选项时:

  • 颜色共2种:夜空黑,绚丽红
  • 内存共2种:4GB,6GB
  • 机身存储1种:64GB

此时会产生多少种SKU呢? 应该是 2 * 2 * 1 = 4种,这其实就是在求笛卡尔积。

我们会在页面下方生成一个sku的表格:

在这里插入图片描述

这个表格中就包含了以上颜色内存的所有可能组合,剩下的价格等信息就需要用户自己来完成了。

注意,页面中的sku的图片上传,默认是上传到阿里云,当然可以根据自己情况修改上传到本地nginx。

最后我们可以根据填写号的特有规格参数属性,来选定最终我们要保存几个sku:

在这里插入图片描述

14、商品保存:修改SpuDTO对象

当我们发起保存提交的时候,页面向后台提交的数据如下:
在这里插入图片描述

我们可以修改SpuDTO,在里面添加两个属性,来接收当前所有提交的数据:

package com.leyou.item.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import lombok.Data;

import java.util.Date;
import java.util.List;

/**
 * 封装商品展示列表需要的数据
 */
@Data
public class SpuDTO{
    private Long id;
    private Long brandId;
    private Long cid1;// 1级类目
    private Long cid2;// 2级类目
    private Long cid3;// 3级类目
    private String name;// 商品名称
    private String subTitle;// 子标题
    private Boolean saleable;// 是否上架
    private Date createTime;// 创建时间
    private Date updateTime;// 最后修改时间

    private String brandName;//品牌名称
    private String categoryName;//分类名称

    private SpuDetail spuDetail; //用于接收添加商品的SpuDetail数据
    private List<Sku> skus; // 用于接收添加商品的SpuDetail数据
}

15、商品保存:完成商品保存逻辑

1)改造GoodsService

为批量添加Sku做准备

@Service
@Transactional
public class GoodsService extends ServiceImpl<SkuMapper, Sku> {

    ……
    
}

2)提供处理器


    /**
     * 添加商品
     */
    @PostMapping("/goods")
    public ResponseEntity<Void> saveGoods(@RequestBody SpuDTO spuDTO){
        goodsService.saveGoods(spuDTO);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

3)提供service

 public void saveGoods(SpuDTO spuDTO) {
        //1.保存spu表
        //数据拷贝
        try {
            Spu spu = BeanHelper.copyProperties(spuDTO, Spu.class);
            //商品默认下架
            spu.setSaleable(false);
            spuMapper.insert(spu);

            //2.保存spuDetail表
            SpuDetail spuDetail = spuDTO.getSpuDetail();
            //必须记得设置spu的ID
            spuDetail.setSpuId(spu.getId());
            spuDetailMapper.insert(spuDetail);

            //3.保存sku表
            List<Sku> skus = spuDTO.getSkus();

            skus.forEach(sku -> {
                //必须记得设置spu的ID
                sku.setSpuId(spu.getId());
                //保存sku
                //skuMapper.insert(sku);
            });

            //调用MyBatis-Plus的批量保存
            saveBatch(skus);
        } catch (Exception e) {
            e.printStackTrace();
            throw new LyException(ExceptionEnum.INSERT_OPERATION_FAIL);
        }

    }

如果使用批量增加,需要继承ServiceImpl类

在这里插入图片描述

4)重启服务器查看保存商品效果

在这里插入图片描述

16、商品修改:商品上下架操作

在商品详情页,每一个商品后面,都会有一个编辑按钮:

在这里插入图片描述

点击这个按钮,并没有打开商品编辑窗口,而是弹出了一个提示窗口:

在这里插入图片描述

已经上架的商品用户可能正在购买,所以不能修改。必须要先下架才可以。

1)页面请求

此时打开控制台,可以看到请求已经发出了:

在这里插入图片描述

请求方式:PUT

请求路径:/spu/saleable

参数有两个 :

  • id:应该是spu的id
  • saleable:布尔值,代表上架或下架

返回结果:应该是无

2)后台实现:Controller

接下来我们在服务端接收请求,并且修改spu的saleable属性。

需要注意的是,我们在修改spu的上下架状态时,无需修改sku的enable属性,sku的enable属性只与当前sku的库存量有关,如果库存大于0,就是有效,否则是无效。

 /**
     * 商品上下架
     */
    @PutMapping("/spu/saleable")
    public ResponseEntity<Void> updateSaleable(@RequestParam("id") Long id,@RequestParam("saleable") Boolean saleable){
        goodsService.updateSaleable(id,saleable);
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }

3)后台实现:Service

public void updateSaleable(Long id, Boolean saleable) {
        try {
            Spu spu = new Spu();
            spu.setId(id);//必须设置数据库存在ID
            spu.setSaleable(saleable);

            //修改状态
            spuMapper.updateById(spu);  // updateById: MyBatis-Plus底层自动判断非NULL值才更新
        } catch (Exception e) {
            e.printStackTrace();
            throw new LyException(ExceptionEnum.UPDATE_OPERATION_FAIL);
        }
    }

17、门户网站:搭建门户网站

后台系统的内容暂时告一段落,有了商品,接下来我们就要在页面展示商品,给用户提供浏览和购买的入口,那就是我们的门户系统。

门户系统面向的是用户,安全性很重要,而且搜索引擎对于单页应用并不友好。因此我们的门户系统不再采用与后台系统类似的SPA(单页应用)。

依然是前后端分离,不过前端的页面会使用独立的html,在每个页面中使用vue来做页面渲染。

1)静态资源

webpack打包多页应用配置比较繁琐,项目结构也相对复杂。这里为了简化开发(毕竟我们不是专业的前端人员),我们不在使用webpack,而是直接编写原生的静态HTML。

将资料中的leyou-portal解压,并把结果赋值到工作空间的目录

链接:https://pan.baidu.com/s/1S_47cnOdsBrNRlu50cfwJQ
提取码:kp60

解压缩:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SJ7Ladpz-1654593974323)(assets/1575856696888.png)]

然后通过idea打开,可以看到项目结构:

在这里插入图片描述

2)live-server

简介

没有webpack,我们就无法使用webpack-dev-server运行这个项目,实现热部署。

所以,这里我们使用另外一种热部署方式:live-server

地址;https://www.npmjs.com/package/live-server

在这里插入图片描述

这是一款带有热加载功能的小型开发服务器。用它来展示你的HTML / JavaScript / CSS,但不能用于部署最终的网站。

安装和运行参数

安装,使用npm命令即可,这里建议全局安装,以后任意位置可用

npm install -g live-server

运行时,直接输入命令:

live-server

另外,你可以在运行命令后,跟上一些参数以配置:

  • --port=NUMBER - 选择要使用的端口,默认值:PORT env var或8080
  • --host=ADDRESS - 选择要绑定的主机地址,默认值:IP env var或0.0.0.0(“任意地址”)
  • --no-browser - 禁止自动Web浏览器启动
  • --browser=BROWSER - 指定使用浏览器而不是系统默认值
  • --quiet | -q - 禁止记录
  • --verbose | -V - 更多日志记录(记录所有请求,显示所有侦听的IPv4接口等)
  • --open=PATH - 启动浏览器到PATH而不是服务器root
  • --watch=PATH - 用逗号分隔的路径来专门监视变化(默认值:观看所有内容)
  • --ignore=PATH- 要忽略的逗号分隔的路径字符串(anymatch -compatible definition)
  • --ignorePattern=RGXP-文件的正则表达式忽略(即.*\.jade)(不推荐使用赞成--ignore
  • --middleware=PATH - 导出要添加的中间件功能的.js文件的路径; 可以是没有路径的名称,也可以是引用middleware文件夹中捆绑的中间件的扩展名
  • --entry-file=PATH - 提供此文件(服务器根目录)代替丢失的文件(对单页应用程序有用)
  • --mount=ROUTE:PATH - 在定义的路线下提供路径内容(可能有多个定义)
  • --spa - 将请求从/ abc转换为/#/ abc(方便单页应用)
  • --wait=MILLISECONDS - (默认100ms)等待所有更改,然后重新加载
  • --htpasswd=PATH - 启用期待位于PATH的htpasswd文件的http-auth
  • --cors - 为任何来源启用CORS(反映请求源,支持凭证的请求)
  • --https=PATH - 到HTTPS配置模块的路径
  • --proxy=ROUTE:URL - 代理ROUTE到URL的所有请求
  • --help | -h - 显示简洁的使用提示并退出
  • --version | -v - 显示版本并退出
测试

我们进入leyou-portal目录,输入命令:

live-server --port=9002

在这里插入图片描述

3)在package.json中配置启动命令

初始化npm,其中-y表示一路yes安装下去

npm init -y

安装vue

npm install vue --save

配置启动脚本:

进入package.json文件,在script中添加启动脚本:

{
  "name": "leyou-portal",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "serve": "live-server --port=9002"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "vue": "^2.6.10"
  }
}

以后可以用 npm run serve启动

4)域名访问

现在我们访问的是:http://127.0.0.1:9002

之前我们已经配置好了9002端口对应的域名:http://www.leyou.com

所以今后我们可以直接使用域名访问

在这里插入图片描述

5)在网关中给门户网站跨域权限

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins:
              - "http://manage.leyou.com"
              - "http://www.leyou.com"
            allowedHeaders:
              - "*"
            allowCredentials: true
            maxAge: 360000
            allowedMethods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTIONS
              - HEAD
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值