谷粒商城之高级篇

谷粒商城之高级篇

前言

高级篇正式开始啦!

PS 第一章 ElasticSearch 参见 另外一篇文章 谷粒商城之高级篇知识补充

2 商城业务

2.1 商品上架

2.1.1 商品Mapping

ES是将数据存储在内存中,所以在检索中优于mysql。ES也支持集群,数据分片存储。

需求:

上架的商品才可以在网站上展示,没有上架的商品存储在数据库中。
上架的商品需要可以被检索。

分析sku在es中如何存储:

商品mapping

分析:商品上架在es中是存sku还是spu?

1)检索的时候输入名字,是需要按照sku的title进行全文检索的
2)检索使用商品规格,规格是spu的公共属性,每个spu是一样的
3)按照分类id进去的都是直接列出spu的,还可以切换。
4〕我们如果将sku的全量信息保存到es中(包括spu属性〕就太多字段了

方案1:方便检索   
{
   
    skuId:1
    spuId:11
    skyTitile:华为xx
    price:999
    saleCount:99
    attr:[
        {
   尺寸:5},
        {
   CPU:高通945},
        {
   分辨率:全高清}
	]
}
缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu对应的sku的规格参数都一样
冗余:
举例:100*20=2000MB=2G

方案2:分布式   
sku索引
{
   
    spuId:1
    skuId:11
    xxx
}
attr索引
{
   
    skuId:11
    attr:[
        {
   尺寸:5},
        {
   CPU:高通945},
        {
   分辨率:全高清}
	]
}

举例:
先找到4000个符合要求的spu,再根据4000个spu查询对应的属性,封装了4000个id,long 8B*4000=32000B=32KB
1K个人检索,就是32MB

结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络拥堵
因此选用方案1,以空间换时间

1669197427060

建立product索引

最终选用的数据模型:
PUT product
{
   
    "mappings":{
   
        "properties": {
   
            "skuId":{
    "type": "long" },
            "spuId":{
    "type": "keyword" },  # 不可分词
            "skuTitle": {
   
                "type": "text",
                "analyzer": "ik_smart"  # 中文分词器
            },
            "skuPrice": {
    "type": "keyword" },
            "skuImg"  : {
    "type": "keyword" },
            "saleCount":{
    "type":"long" },
            "hasStock": {
    "type": "boolean" },
            "hotScore": {
    "type": "long"  },
            "brandId":  {
    "type": "long" },
            "catalogId": {
    "type": "long"  },
            "brandName": {
   "type": "keyword"},
            "brandImg":{
   
                "type": "keyword",
                "index": false,  # 不可被检索,不生成index
                "doc_values": false # 不可被聚合
            },
            "catalogName": {
   "type": "keyword" },
            "attrs": {
    # attrs:当前sku的属性规格
                "type": "nested",
                "properties": {
   
                    "attrId": {
   "type": "long"  },
                    "attrName": {
   
                        "type": "keyword",
                        "index": false,
                        "doc_values": false
                    },
                    "attrValue": {
   "type": "keyword" }
                }
            }
        }
    }
}

其中

“type”: “keyword” 保持数据精度问题,可以检索,但不分词

“index”:false 代表不可被检索
“doc_values”: false 不可被聚合,es就不会维护一些聚合的信息
冗余存储的字段:不用来检索,也不用来分析,节省空间

库存是bool。

检索品牌id,但是不检索品牌名字、图片

用skuTitle检索

nested嵌入式对象
属性是"type": “nested”,因为是内部的属性进行检索

数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)
user.name=[“aaa”,“bbb”]
user.addr=[“ccc”,“ddd”]

这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的

数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)

1669204852792

nested阅读:https://blog.csdn.net/weixin_40341116/article/details/80778599

参考使用聚合:https://blog.csdn.net/kabike/article/details/101460578

课件内容:

分析:商品上架在es 中是存sku 还是spu?
1)、检索的时候输入名字,是需要按照sku 的title 进行全文检索的
2)、检索使用商品规格,规格是spu 的公共属性,每个spu 是一样的
3)、按照分类id 进去的都是直接列出spu 的,还可以切换。
4)、我们如果将sku 的全量信息保存到es 中(包括spu 属性)就太多量字段了。
5)、我们如果将spu 以及他包含的sku 信息保存到es 中,也可以方便检索。但是sku 属于
spu 的级联对象,在es 中需要nested 模型,这种性能差点。
6)、但是存储与检索我们必须性能折中。
7)、如果我们分拆存储,spu 和attr 一个索引,sku 单独一个索引可能涉及的问题。
检索商品的名字,如“手机”,对应的spu 有很多,我们要分析出这些spu 的所有关联属性,
再做一次查询,就必须将所有spu_id 都发出去。假设有1 万个数据,数据传输一次就
10000*4=4MB;并发情况下假设1000 检索请求,那就是4GB 的数据,,传输阻塞时间会很
长,业务更加无法继续。
所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数
据库范式。

index:
默认true,如果为false,表示该字段不会被索引,但是检索结果里面有,但字段本身不能
当做检索条件。
doc_values:
默认true,设置为false,表示不可以做排序、聚合以及脚本操作,这样更节省磁盘空间。
还可以通过设定doc_values 为true,index 为false 来让字段不能被搜索但可以用于排序、聚
合以及脚本操作:

2.1.2 上架细节

上架是将后台的商品放在es 中可以提供检索和查询功能。
1)、hasStock:代表是否有库存。默认上架的商品都有库存。如果库存无货的时候才需要
更新一下es
2)、库存补上以后,也需要重新更新一下es
3)、hotScore 是热度值,我们只模拟使用点击率更新热度。点击率增加到一定程度才更新
热度值。
4)、下架就是从es 中移除检索项,以及修改mysql 状态

商品上架步骤:
1)、先在es 中按照之前的mapping 信息,建立product 索引。
2)、点击上架,查询出所有sku 的信息,保存到es 中
3)、es 保存成功返回,更新数据库的上架状态信息。

2.1.3 数据一致性

1)、商品无库存的时候需要更新es 的库存信息
2)、商品有库存也要更新es 的信息

2.1.4 代码实现

POST /product/spuinfo/{spuId}/up

  • SpuInfoController:
 /**
     * /product/spuinfo/{spuId}/up
     * 商品上架功能
     */
    @PostMapping("/{spuId}/up")
    public R spuUp(@PathVariable("spuId") Long spuId){
   
        spuInfoService.up(spuId);

        return R.ok();
    }

product里组装好,search里保存到es中,进行商品上架

  • 商品上架entity

商品上架需要在es中保存spu信息并更新spu的状态信息,由于SpuInfoEntity与索引的数据模型并不对应,所以我们要建立专门的vo进行数据传输。

//商品在 es中保存的数据模型
@Data
public class SkuEsModel {
   

    private Long skuId;

    private Long spuId;

    private String skuTitle;

    private BigDecimal skuPrice;

    private String skuImg;

    private Long saleCount;

    private Boolean hasStock;

    private Long hotScore;

    private Long brandId;

    private Long catalogId;

    private String brandName;

    private String brandImg;

    private String catalogName;

    private List<Attrs> attrs;


    @Data
    public static class Attrs {
   

        private Long attrId;
        private String attrName;
        private String attrValue;
    }

}
  • 商品上架service

sku的规格参数相同,因此我们要将查询规格参数提前,只查询一次

1)在ware微服务里添加"查询sku是否有库存"的controller

WareSkuController

 	//查询sku 是否有库存
    @PostMapping("/hasstock")
    public R getSkuHasStock(@RequestBody  List<Long> skuIds){
   

        //sku_id,stock
        List<SkuHasStockVo> vos =  wareSkuService.getSkuHasStock(skuIds);

        return R.ok().setData(vos);

    }

WareSkuServiceImpl

  @Override
    public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
   

        List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
   
            SkuHasStockVo vo = new SkuHasStockVo();

            //查询当前 sku的总库存量
            //SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = 1
            Long count = baseMapper.getSkuStock(skuId);

            vo.setSkuId(skuId);
            vo.setHasStock(count==null?false:count>0);
            return vo;
        }).collect(Collectors.toList());


        return collect;


    }

WareSkuDao

   Long getSkuStock(Long skuId);//一个参数的话,可以不用写@Param,多个参数一定要写,方便区分

WareSkuDao.xml

    </update>
    <select id="getSkuStock" resultType="java.lang.Long">

        SELECT SUM(stock-stock_locked) FROM `wms_ware_sku` WHERE sku_id = #{skuId}

    </select>

SkuHasStockVo

@Data
public class SkuHasStockVo {
   

    private Long skuId;

    private Boolean hasStock;

}

然后用feign调用

在 package com.atguigu.gulimall.product.feign下:

@FeignClient("gulimall-ware") //说明调用哪一个 远程服务
public interface WareFeignService {
   

    /**
     * 1、R设计的时候可以加上泛型
     * 2、直接返回我们想要的结果
     * 3、自己封装解析结果
     * @param skuIds
     * @return
     */
    @PostMapping("/ware/waresku/hasstock")//注意路径复制完全
    R getSkuHasStock(@RequestBody List<Long> skuIds);
}

2)将 R 工具类进行改装

public class R extends HashMap<String, Object> {
   
	private static final long serialVersionUID = 1L;


	//利用 阿里巴巴提供的fastjson 进行逆转
	public <T> T getData(TypeReference<T> typeReference){
   
		Object data = get("data");//默认是map
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, typeReference);
		return t;
	}

	public R setData(Object data){
   
		put("data",data);
		return this;
	}
...

3)收集成map的时候,toMap()参数为两个方法,如 SkyHasStockVo::getSkyId,item->item.getHasStock()

将封装好的SkuInfoEntity,调用search的feign,保存到es中

1669219523241

ElasticSaveController

@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSaveController {
   

    @Autowired
    ProductSaveService productSaveService;

    //上架商品
    // 添加@RequestBody 将 请求体中的 List<SkuEsModel> 集合转换为json数据,因此请求方式必须为  @PostMapping
    @PostMapping("/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){
   


        // 如果返回的是 boolean 类型的false,说明我们的 sku数据有问题
        //如果返回的是 catch里面的内容,可能是 es 客户端连接不上了
        boolean b = false;
        try {
   
            b = productSaveService.productStatusUp(skuEsModels);
        }catch (Exception e){
   
            log.error("ElasticSaveController商品上架错误: {}",e);
            return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
        }

        if (!b){
   
            return R.ok();
        }else {
   
            return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
        }

    }
}

ProductSaveServiceImpl

@Slf4j
@Service
public class ProductSaveServiceImpl implements ProductSaveService {
   

    @Autowired
    RestHighLevelClient restHighLevelClient;

    @Override
    public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
   

        //保存到es
        //1.给 es 中建立索引。product,建立好映射关系。

        //2.给 es 中保存这些数据
        BulkRequest bulkRequest = new BulkRequest();
        for (SkuEsModel model : skuEsModels) {
   
            //1.构造保存请求
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
            indexRequest.id(model.getSkuId().toString());
            String s = JSON.toJSONString(model);
            indexRequest.source(s, XContentType.JSON);


            bulkRequest.add(indexRequest);
        }
        //BulkRequest bulkRequest, RequestOptions options
        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

        //TODO 1、如果批量错误
        boolean b = bulk.hasFailures();
        List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
   
            return item.getId();
        }).collect(Collectors.toList());

        log.info("商品上架完成:{},返回数据:{}",collect,bulk.toString());

        return b;
    }
}

EsConstant

public class EsConstant {
   

    public static final String PRODUCT_INDEX = "product"; //sku数据在 es中的索引

}

fenign 调用: gulimall-product 调用 gulimall-search

SearchFeignService

@FeignClient("gulimall-search")
public interface SearchFeignService {
   

    @PostMapping("/search/save/product")
    R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}

4)上架失败返回R.error(错误码,消息)

此时再定义一个错误码枚举。在接收端获取他返回的状态码

BizCodeEnume

 PRODUCT_UP_EXCEPTION(11000,"商品上架异常");

5)上架后再让数据库中变为上架状态

这里在 gulimall-common 包下的 ProductConstant 创建一个新的枚举类

public class ProductConstant {
   
    ...
	public enum StatusEnum {
   
        NEW_SPU(0,"新建"), SPU_UP(1,"商品上架"),SPU_DOWN(2,"商品下架");
        private int code;
        private String msg;

        StatusEnum(int code, String msg) {
   
            this.code = code;
            this.msg = msg;
        }

        public int getCode() {
   
            return code;
        }

        public String getMsg() {
   
            return msg;
        }
    }
}

6)mybatis为了能兼容接收null类型,要把long改为Long

debug时很容易远程调用异常,因为超时了

商品上架代码

SpuInfoController

 /**
     * /product/spuinfo/{spuId}/up
     * 商品上架功能
     */
    @PostMapping("/{spuId}/up")
    public R spuUp(@PathVariable("spuId") Long spuId){
   
        spuInfoService.up(spuId);

        return R.ok();
    }

SpuInfoServiceImpl

 @Override
    public void up(Long spuId) {
   

        //1.查出当前 spuid 对应的所有 sku信息、品牌的名字
        List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
        List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());

        //TODO  4、查询当前sku的所有可以用来被检索的规格属性,
        List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
        List<Long> attrIds = baseAttrs.stream().map(attr -> {
    //返回所有属性的id
            return attr.getAttrId();
        }).collect(Collectors.toList());

        List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);

        Set<Long> idSet = new HashSet<>(searchAttrIds);//因为是kv 键值对,转换成 set 集合比较方便

        // 从  baseAttrs 集合中 过滤 出  attrValueEntities 集合
        List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
   
            return idSet.contains(item.getAttrId());
        }).map(item -> {
    //将 set集合 映射 成  map集合
            SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
            BeanUtils.copyProperties(item, attrs);//属性对拷:item 是数据库中查出来的数据
            return attrs;
        }).collect(Collectors.toList());

        //TODO 1、发送远程调用,库存系统查询是否有库存
        //由于远程调用可能出现网络问题,所以需要进行try  - catch处理一下
        Map<Long, Boolean> stockMap = null;
        try {
   
            R r = wareFeignService.getSkuHasStock(skuIdList);

            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>(){
   

            };
            stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
        }catch (Exception e){
   
            log.error("库存服务查询异常:原因{}",e);
        }


        //2.封装每个sku的信息
        Map<Long, Boolean> finalStockMap = stockMap;
        List<SkuEsModel> upProducts = skus.stream().map(sku -> {
   //通过  stream API 将 skus中的 数据遍历
            //组装我们需要的数据
            SkuEsModel esModel = new SkuEsModel();
            BeanUtils.copyProperties(sku, esModel);//属性对拷,将 sku中的属性 拷贝到 esmodel中

            //需要单独处理的数据 ,SkuInfoEntity 和 SkuEsModel中相比少的数据。
            //skuPrice,skuImg

            esModel.setSkuPrice(sku.getPrice());
            esModel.setSkuImg(sku.getSkuDefaultImg());

            //hotScore(热度评分)  hasStock(库存)

            //设置库存信息
            //如果远程调用出现问题,默认给 true值;如果没有问题,那就赋真正的值
            if (finalStockMap == null){
   
                esModel.setHasStock(true);
            }else {
   
                esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
            }

            //TODO 2、热度评分。0
            esModel.setHotScore(0L);//这里的热度评分应该是一个比较复杂的操作,这里简单处理一下

            //TODO 3、查询品牌和分类的名字信息
            //品牌
            BrandEntity brand = brandService.getById(esModel.getBrandId());
            esModel.setBrandName(brand.getName());
            esModel.setBrandImg(brand.getLogo());

            //分类
            CategoryEntity category = categoryService.getById(esModel.getCatalogId());
            esModel.setCatalogName(category.getName());

            //设置检索属性
            esModel.setAttrs(attrsList);

            return esModel;
        }).collect(Collectors.toList());


        //TODO 5、将数据发送给 es 进行保存,gulimall-search
        R r = searchFeignService.productStatusUp(upProducts);
        if (r.getCode() == 0){
   
            //远程调用成功
            //TODO 6、修改当前spu的状态
            baseMapper.updataSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
        }else {
   
            //远程调用失败
            //TODO  7、重复调用?接口幂等性;重试机制? xxx
            
            //Feign 调用流程
            /**
             * 1.构造请求数据,将对象转为json;
             * RequestTemplate template = buildTemplateFromArgs.create(argv);
             * 2.发送请求进行执行(执行成功会解码响应数据);
             * executeAndDecode(template)'
             * 3.执行请求会有重试机制
             * while(true){
             *    try{
             *      executeAndDecode(template);
             *    }catch(){
             *       try{ retryer.continueOrPropagate(e);}catch(){throw ex;
             *       continue;
             *          }
             *    }
             *
             */

        }

    }

1669257801032

Feign

1669219830792

这里再次 将 feign 接口代码展示出来:

gulimall-product 调用 gulimall-search 将 商品上架内容保存在 ElasticSearch中,方便全文检索:

SearchFeignService

@FeignClient("gulimall-search")
public interface SearchFeignService {
   

    @PostMapping("/search/save/product")
    R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}

gulimall-product 调用 gulimall-ware 将 查询 商品库存:

WareFeignService

@FeignClient("gulimall-ware") //说明调用哪一个 远程服务
public interface WareFeignService {
   

    /**
     * 1、R设计的时候可以加上泛型
     * 2、直接返回我们想要的结果
     * 3、自己封装解析结果
     * @param skuIds
     * @return
     */
    @PostMapping("/ware/waresku/hasstock")//注意路径复制完全
    R getSkuHasStock(@RequestBody List<Long> skuIds);
}

ps:

这里可以用到的idea 快捷键:

  1. ctrl + e 可以快速调出最近使用的(打开最近修改的文件)

1669258047712

  1. 快速从 controller 跳转 到 实现类

    ctrl + shift + 鼠标左键

  2. 从 controller 跳转到 接口

    ctrl + 鼠标左键

  3. 生成 try-catch等(surround with)

    alt + shift +z

  4. 生成构造器/get/set/toString

    alt + shift + s

7)效果展示

商品成功上架,显示状态 为 已上架

1669277105482

2.2 商城系统首页

1669277707859

不使用前后端分离开发了,管理后台用vue
nginx发给网关集群,网关再路由到微服务

静态资源放到nginx中

2.2.1 渲染首页

  1. 依赖
    导入thymeleaf依赖
  <!--模板引擎:thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
  1. html\首页资源\index 放到 gulimall-product 下的static文件夹

​ index.html 放到 templates中

1669279333669

  1. 关闭thymeleaf缓存,方便开发实时看到更新
thymeleaf:
    cache: false
  1. web开发放到web包下,原来的controller是前后分离对接手机等访问的,所以可
    以改成app,对接app应用。

web 包:存放专门进行页面跳转的controller

rest 接口对接的使我们分离的项目(比如手机的一些 app ):将controller 改名为 app

  1. 效果展示:访问首页

1669279445355

2.2.2 渲染一级分类数据

编写 处理首页的controller

gulimall-product的 web 包下新建 IndexController

@Controller
public class IndexController {
   


    @Autowired
    CategoryService categoryService;

    @GetMapping({
   "/","/index.html"})
    public String indexPage(Model model){
   


        //TODO 1.查出所有的1级分类
       List<CategoryEntity> categoryEntities =  categoryService.getLevel1Categorys();



       //spring mvc提供了一个  model  接口
        // 给 model 中放的数据,就会默认放到页面的请求域中,因为是转发。所以使用addAttribute
        //给首页 放一个属性 ,属性名: categorys   属性值:categoryEntities------以后来到 index页面,就可以直接取出  属性。
       model.addAttribute("categorys",categoryEntities);

        // 如果返回的 是  逻辑视图(也就是页面地址) ,就会进行拼串
        //视图解析器进行拼串:
        //classpath:/ 表示类路径下 :resources下:文件夹右下角 有一个小图标
        //默认规则:默认前缀:public static final String DEFAULT_PREFIX = "classpath:/templates/";
        //          默认后缀:public static final String DEFAULT_SUFFIX = ".html";
        // classpath:/templates/ + 返回值 +   .html
        return "index";
    }

}

编写 获取 1级分类的实现

CategoryServiceImpl

	 /**
     *  查找 1级分类
     *  parent_cid = 0  或者  cat_level = 1
     * @return
     */
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
   

        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;

    }

引入 热部署依赖devtools使页面实时生效

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

首页遍历一级分类菜单数据

修改 index.html

<!--轮播主体内容-->
    <div class="header_main">
		....
        <div class="header_main_left">
          <ul>
            <li th:each="category : ${categorys}">
              <a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}" ><b th:text="${category.name}">家用电器</b></a>
            </li>
          </ul>
        </div>
          ......

thymeleaf 知识小补充(复习):

thymeleaf官网:https://www.thymeleaf.org/

  1. ${}:动态取值
th:text="${category.name}"
  1. th:each:遍历
<tr th:each="prod : ${prods}">

prod : 当前元素

${prods}:要遍历的对象

th:each="category : ${categorys}"
  1. 自定义属性:我们需要获得 分类的 id
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>

value:属性名叫什么

#{subscribe.submit}:属性值叫什么

 th:attr="ctg-data=${category.catId}"

原生属性:

th:value="#{subscribe.submit}"

效果展示:

1669283016658

2.2.3 渲染二级三级分类数据

当 鼠标滑到 1级分类时,展示 它的二级分类数据及三级分类数据。

利用 catalogLoader.js来获取请求,解析展示数据。

1669302197638

按照 此json 数据方式

1669302065051

新建 Catelog2Vo封装 数据

/**
 * 2级分类 vo
 *
 * @author wystart
 * @create 2022-11-24 21:53
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Catelog2Vo {
   
    private String catalog1Id;//1级分类id
    private List<Catelog3Vo> catalog3List; //三级子分类
    private String id;
    private String name;


    /**
     * 三级分类 vo
     * "catalog2Id":"61",
     * "id":"610",
     * "name":"商务休闲鞋"
     */
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Catelog3Vo {
   
        private String catalog2Id; //父分类,2级分类 id
        private String id;
        private String name;

    }


}

IndexController

  //index/catalog.json
    @ResponseBody
    @GetMapping("/index/catalog.json")
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
   

        Map<String, List<Catelog2Vo>> catalogJson = categoryService.getCatalogJson();

        return catalogJson;
    }

CategoryServiceImpl

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
   

        //1.查出所有1级分类
        List<CategoryEntity> level1Categorys = getLevel1Categorys();

        //2.封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
   
            //1.每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getParentCid()));
            //2.封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
   
                catelog2Vos = categoryEntities.stream().map(l2 -> {
   
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1.找到当前二级分类的三级分类,封装成 vo
                    List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
                    // 三级分类有数据的情况下
                    if (level3Catelog != null){
   
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
   
                            //2.封装成指定格式
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;

                }).collect(Collectors.toList());

            }
            return catelog2Vos;
        }));

        return parent_cid;
    }

效果展示:

访问 http://localhost:10000/index/catalog.json

得到 json 数据

1669301819816

首页展示效果:http://localhost:10000

1669301854810

模板引擎总结

* 5.模板引擎
* 1)、thymeleaf-starter: 关闭缓存
* 2)、静态资源都放在static 文件夹下就可以按照路径直接访问
* 3)、页面放在 templates下,直接访问
*   SpringBoot,访问项目的时候,默认会找 index
* 4)、页面修改不重启服务器实时更新
*   1)、引入 dev-tools
*   2)、修改完页面 ctrl + shift + f9  或者  ctrl + f9,重新自动编译下页面(注意:如果代码配置等修改,建议重启)
*

2.2.4 nginx 搭建域名访问环境

1669306447448

我们利用反向代理:让 Nginx 配合网关 搭建我们的访问环境,将我们的各个微服务放在内网中,避免端口直接暴露带来的危险。

利用 SwitchHosts软件可以快速修改hosts文件,注意需要以管理员身份运行

1669303542850

原理:

1669306617881

查看本机localhost对应的IP地址:

ipconfig:查看本机IP:ipconfig:

192.168.1.103 (windows的localhost地址)

192.168.56.1(linux虚拟机的localhost地址)

两者都可以,都算本机

1669304892211

Nginx的配置文件详解:

1669306432874

1669306804896

可以加载 外部配置文件的配置,这样可以避免Nginx的配置文件过大。(总配置文件)

  • Nginx 反向代理配置

接下来我们配置 server块:

先复制一份,留作备份:

1669306701061

修改配置文件:

1669305063974

proxy_pass:代理通过:相当于代理给谁(转交给谁)

gulimall下的所有请求都代理给 192.168.56.1下的10000端口。

1669305318783

Nginx的所有配置都以 ; 结尾,否则报错。

通过域名访问:gulimall.com

1669305370580

原理解析:
1.首先浏览器访问 gulimall.com----我们在windows里面指定了 gulimall.com 映射的是虚拟机IP:192.168.56.10,所以浏览器访问 gulimall.com 先会来到我们的虚拟机;
2.虚拟机里面的 Nginx又监听了80端口,在Nginx的配置文件中,它监听了来自80端口的所有请求,而且域名是 gulimall.com;所以符合以上条件,Nginx就会帮我们代理到我们本机:proxy_pass http://192.168.56.1:10000;
3.最后我们就又回到了本机
4.最后总结就是:域名来到 Nginx,Nginx 配置了gulimall.com ,代理到10000端口服务;

分布式情况下:商城系统有很多,不止一个,那需要每次修改 Nginx的代理配置?
太麻烦!!!
让Nginx 将请求代理给网关,由网关自动转发给我们各个服务;网关就能动态发现哪些服务上线,哪些服务下线;而且网关还具有负载均衡功能。

Nginx将请求交给网关,由网关从注册中心动态发现商品服务都在那,进而由网关负载均衡到商品服务;

网关也会部署多个,Nginx可以将请求负载均衡到某一个网关,然后由网关在进行转发。

  • Nginx 搭配网关 实现 负载均衡到网关

    • Nginx

      修改 总配置 nginx.conf 在 http 块内:

      1669341418073

      在server 块内:

      修改 server配置:gulimall.conf:相当于 是 负载均衡的配置,直接路由到上游服务器网关,由网关进行转发

      1669341538959

      效果就是:访问 gulimall.com ,代理 给 Nginx ,Nginx 转交 给网关 ,网关再转给商品服务。

    • 网关配置:

              - id: gulimall_host_route
                uri: lb://gulimall-product
                predicates:
                  - Host=**.gulimall.com,gulimall.com # 只要是 gulimall下的所有请求都转给  gulimall-product
      

      注意这个配置 一定要放在 最后:因为如果放在前面 ,它会禁用下面其他的网关配置:比如,http://gulimall.com//product/attrattrgrouprelation/list 这个api 接口访问,它会首先到 gulimall.com,然后因为没有进行 截串 设置(截取 /api前缀),出现 404 访问不到。

  • 测试效果

    这里出现 404 问题:原因:Nginx 转发给网关的时候,会丢失很多请求头信息,这里就缺失了 host 地址,这里我们暂时只配置 上 host 地址,以后缺啥补啥。

1669342458806

重启测试:

直接访问域名成功:gulimall.com

1669343410733

访问接口 也成功。http://gulimall.com//product/attrattrgrouprelation/list

1669343483954

最后总结:

最终原理:

  1. 首先浏览器访问 gulimall.com
    因为我们在Windows配置了host映射:gulimall.com 映射IP 192.168.56.10(虚拟机Ip)
    所以会直接来到虚拟机

  2. 又因为 浏览器访问 默认不带端口,那就是访问80端口,所以会来到 Nginx,我们又配置 了 80端口监听 gulimall.com 这个域名;此外由于 **location/**下的配置:代理转发:

    Nginx 又代理给网关,这里注意一个细节:由于Nginx 转发会丢失 一些请求头信息,所以我们要加上请求头的配置,这里暂时只配置 host地址,之后的其他请求头配置我们用到的时候在进行添加;

  3. 网关发现 域名 是gulimall.com,进而就会找到 对应的配置:路由到商品服务,进而就转给了商品服务,这处网关配置一定要放在最后面,避免放在前面禁用后面的其他截串配置。

  • 域名映射效果:
    • 请求接口 gulimall.com
    • 请求页面 gulimall.com
    • nginx 直接代理给网关,网关判断
      • 如果是/api/***,转交给对应的服务器
      • 如果是满足域名,转交给对应的服务

重要!!!!

关于 第3章 性能与压力测试 和 第4章 缓存与分布式锁单独写在另外一篇文档:谷粒商城之高级篇知识补充。


2.3 检索业务

2.3.1 页面环境搭建

①秉承动静分离的原则,我们将 静态资源放到 Nginx下:

在Nginx新建一个 文件夹search,用来存放相关静态资源。

1669625868815

②修改 index页面下的静态资源前缀

静态资源

1669625610230

1669625695767

加上 thymeleaf 的名称空间

1669627210900

③域名映射

1669626589774

④ *.gulimall.com 表示所有请求Nginx都处理,最后的结果就是Nginx转发给 网关

1669626693068

最终的转发效果就是:

1669627748923

⑤网关配置

        - id: gulimall_host_route
          uri: lb://gulimall-product
          predicates:
            - Host=gulimall.com #这里和之前的相比有修改

        - id: gulimall_search_route
          uri: lb://gulimall-search
          predicates:
            - Host=search.gulimall.com

⑥访问 http://search.gulimall.com/

1669627011493

2.3.2 调整页面跳转

为了以后开发方便,我们加上 热部署依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

关闭thymeleaf 的缓存

spring.thymeleaf.cache=false

我们可以通过检索页面的以下两个地方跳转会商城首页

一个是超链接,一个是图标。

1669638677570

首先是超链接修改:

search服务里面的index页面,改为 http://gulimall.com

1669638937119

接着是图标处:改为 http://gulimall.com

1669639060928

修改域名映射:让gulimall.com和带其子域名的都转发给网关。

1669628933564

测试:成功跳转回首页。

1669639260858

接下来,我们在首页上可以有这么两个地方,可以跳转到我们的检索页面:

①关键字搜索:搜索按钮1669639381807

②点击分类,跳转到检索页面。1669639442078

③修改配置:

  1. 通过分类点击到检索页面

    • 将 检索页面 重命名为 list.html

      1669639624407

    • 创建SearchController

    @Controller
    public class SearchController {
         
    
    
        @GetMapping("/list.html")
        public String listPage() {
         
    
            return "list";
        }
    
    }
    
    
    • 避坑:

      如果点击 分类跳转 到 检索页面,报错,然后控制台域名是:search.gmall.com开头,那么我们需要去

      Nginx 下的 html/static/index/js,在 catelogLoader中搜索gmall,替换为 gulimall

1669637851626

  1. 通过首页的搜索图标跳转到检索页面

    修改 gulimall-product下的index.html页面:

    搜索 search:

    search方法应该是这样:之前修改前缀的时候多加了/static,所以一直访问不到,下面这个是正确的。

    1669640002551

    另外图标处修改为:

1669640089958

ps: 注意一定要把product商品服务中的application.yaml配置文件中 thymeleaf 的页面缓存设置为false,之前测试缓存的时候给设为 开启了,开发中我们关闭。

1669640254206

  1. 测试,都成功跳转到检索页面。

ps:测试的时候,注意浏览器缓存问题,不然有时候测试不成功。

2.3.3 检索返回结果模型分析抽取

1、检索业务分析
商品检索三个入口:
1)、选择分类进入商品检索

1669643862464

2)、输入检索关键字展示检索页1669643878433

3)、选择筛选条件进入

1669643894216

检索条件&排序条件

  • 全文检索:skuTitle
  • 排序: saleCount、hotScore、skuPrice
  • 过滤:hasStock、skuPrice 区间、brandId、catalogId、attrs
  • 聚合:attrs

完整的url 参数

keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1
&catalogId=1&attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏

修改 SearchController

@Controller
public class SearchController {
   


    @Autowired
    MallSearchService mallSearchService;

    /**
     * 创建SearchParam:避免controller 方法参数位置接收太多的请求参数
     * 自动将页面提交过来的所有请求查询参数封装成指定的对象
     * @param param
     * @return
     */
    @GetMapping("/list.html")
    public String listPage(SearchParam param, Model model) {
   

      //1、根据传递过来的页面的查询参数,去es中检索商品
      SearchResult result =   mallSearchService.search(param);
      //放到 model 中,方便页面取值
      model.addAttribute("result",result);

        return "list";
    }

}

创建 SearchParam类(vo包下):封装页面所有可能传递过来的查询条件:请求参数模型

/**
 * 封装页面所有可能传递过来的查询条件
 *
 * catalog3Id=225&keyword=小米&sort=saleCount_asc&hasStock=0/1&brandId=1&brandId=2&attrs=1_5寸:6寸&attrs=2_16G:8G
 */
@Data
public class SearchParam {
   


    private String keyword;//页面传递过来的全文匹配关键字
    private Long catalog3Id;//页面传递过来的三级分类id


    /**
     * sort=saleCount_asc/desc
     * sort=skuPrice_asc/desc
     * sort=hostScore_asc/desc
     *
     */
    private String sort;//排序条件


    /**
     * 好多的过滤条件
     * hasStock(是否有货)、skuPrice 区间、brandId、catalogId、attrs
     * hasStock=0/1 :0有货;1无货
     * skuPrice=1_500/500_/_500
     * brandId=1
     * attrs=2_5寸:6寸
     *
     */
    private Integer hasStock = 1;//是否只显示有货
    private String skuPrice;//价格区间查询
    private List<Long> brandId;//按照品牌进行查询,可以多选
    private List<String> attrs;//按照属性进行筛选
    private Integer pageNum = 1;//页码

}

创建 SearchResult :封装页面所有可能返回的结果:响应数据模型

/**
 * 封装页面所有可能返回的结果
 */
@Data
public class SearchResult {
   

    //查询到的所有商品信息
    private List<SkuEsModel> products;


    /**
     * 以下是分页信息
     */
    private Integer pageNum;//当前页码
    private Long total;//总记录数
    private Integer totalPages;//总页码

    private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
    private List<CatalogVo> catalogs;//当前查询到的结果,所有涉及到的所有分类
    private List<AttrVo> attrs;//当前查询到的结果,所有涉及到的所有属性

    //============================以上是返回给页面的所有信息============================

    @Data
    public static class BrandVo{
   
        private Long brandId;

        private String brandName;

        private String brandImg;

    }

    @Data
    public static class CatalogVo{
   
        private Long catalogId;

        private String catalogName;

    }

    @Data
    public static class AttrVo{
   
        private Long attrId;

        private String attrName;

        private List<String> attrValue;

    }
}

创建 MallSearchService 及其实现

MallSearchService

public interface MallSearchService {
   


    /**
     *
     * @param param  检索的所有参数
     * @return   返回检索的结果,里面包含页面所需要的所有信息
     */
    SearchResult search(SearchParam param);
}

MallSearchServiceImpl

@Service
public class MallSearchServiceImpl implements MallSearchService {
   
    @Override
    public Object search(SearchParam param) {
   
        return null;
    }
}

分析结果 见 上面的 SearchParam类及SearchResult类。

2.3.4 检索DSL语句

在 Kibana中进行检索DSL语句测试。

  • 查询部分

最终检索语句:

GET product/_search
{
   
  "query": {
   
    "bool": {
   
      "must": [
        {
   
          "match": {
     #模糊匹配-全文检索
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [ #过滤条件 
        {
   
          "term": {
   
            "catalogId": "225"
          }
        },
        {
   
          "terms": {
   
            "brandId": [
              "1",
              "2",
              "9"
            ]
          }
        },
        {
   
          "nested": {
    #嵌套查询
            "path": "attrs",
            "query": {
   
              "bool": {
   
                "must": [
                  {
   
                    "term": {
   
                      "attrs.attrId": {
   
                        "value": "15"
                      }
                    }
                  },
                  {
   
                    "terms": {
   
                      "attrs.attrValue": [
                      "海思(Hisilicon)",
                      "以官网信息为准"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
   
          "term": {
   
            "hasStock": {
   
              "value": "true"
            }
          }
        },
        {
   
          "range": {
   
            "skuPrice": {
   
              "gte": 0,
              "lte": 6000
            }
          }
        }
      ]
    }
  },
  "sort": [ #排序
    {
   
      "skuPrice": {
   
        "order": "desc"
      }
    }
  ],
  "from": 0, #分页
  "size": 1,
  "highlight": {
   #高亮
    "fields": {
   
      "skuTitle": {
   }
    },
    "pre_tags": "<b style='color:red'>",
    "post_tags": "</b>"
  }
}

整个查询条件:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析。

  • 接下来就是聚合分析部分。

这里我们希望可以通过品牌属性等也可以检索到商品。

所以加上 品牌属性等检索条件。

报错:

1669691892973

修改映射,让他们都可以进行聚合分析。

1669691237526

创建新的映射

PUT gulimall_product
{
   
  "mappings": {
   
    "properties": {
   
      "attrs": {
   
        "type": "nested",
        "properties": {
   
          "attrId": {
   
            "type": "long"
          },
          "attrName": {
   
            "type": "keyword"
          },
          "attrValue": {
   
            "type": "keyword"
          }
        }
      },
      "brandId": {
   
        "type": "long"
      },
      "brandImg": {
   
        "type": "keyword"
      },
      "brandName": {
   
        "type": "keyword"
      },
      "catalogId": {
   
        "type": "long"
      },
      "catalogName": {
   
        "type": "keyword"
      },
      "hasStock": {
   
        "type": "boolean"
      },
      "hotScore": {
   
        "type": "long"
      },
      "saleCount": {
   
        "type": "long"
      },
      "skuId": {
   
        "type": "long"
      },
      "skuImg": {
   
        "type": "keyword"
      },
      "skuPrice": {
   
        "type": "keyword"
      },
      "skuTitle": {
   
        "type": "text",
        "analyzer": "ik_smart"
      },
      "spuId": {
   
        "type": "keyword"
      }
    }
  }
}

数据迁移

# 数据迁移
POST _reindex
{
   
  "source":{
   
      "index":"product"
   },
  "dest":{
   
      "index":"gulimall_product"
   }
}

查询

GET gulimall_product/_search

迁移成功。

修改 EsConstant 代码

1669691730027

最终聚合分析语句:

 "aggs": {
   
    "brand_agg": {
   
      "terms": {
   
        "field": "brandId",
        "size": 10
      },
      "aggs": {
   
        "brand_name_agg": {
   
          "terms": {
   
            "field": "brandName",
            "size": 10
          }
        },
        "brand_img_agg":{
   
          "terms": {
   
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalog_agg":{
   
      "terms": {
   
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
   
        "catalog_name_agg": {
   
          "terms": {
   
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg":{
   
      "nested": {
   
        "path": "attrs"
      },
      "aggs": {
   
        "attr_id_agg": {
   
          "terms": {
   
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
   
            "attr_name_agg": {
   
              "terms": {
   
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attr_value_agg":{
   
              "terms": {
   
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
  • 整个查询的检索DSL语句:
GET product/_search
{
   
  "query": {
   
    "bool": {
   
      "must": [
        {
   
          "match": {
   
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
   
          "term": {
   
            "catalogId": "225"
          }
        },
        {
   
          "terms": {
   
            "brandId": [
              "1",
              "2",
              "9"
            ]
          }
        },
        {
   
          "nested": {
   
            "path": "attrs",
            "query": {
   
              "bool": {
   
                "must": [
                  {
   
                    "term": {
   
                      "attrs.attrId": {
   
                        "value": "15"
                      }
                    }
                  },
                  {
   
                    "terms": {
   
                      "attrs.attrValue": [
                      "海思(Hisilicon)",
                      "以官网信息为准"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
   
          "term": {
   
            "hasStock": {
   
              "value": "true"
            }
          }
        },
        {
   
          "range": {
   
            "skuPrice": {
   
              "gte": 0,
              "lte": 6000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
   
      "skuPrice": {
   
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 1,
  "highlight": 
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值