第8章-全文检索(上).md 31 KB

第8章-全文检索-站内搜索(上)

学习目标:

  • 能够说出商品检索业务功能
  • 搭建搜索微服务/创建商品索引库
  • 握"nested"类型的应用
  • 完成商品上下架功能
  • 完成及时的更新商品热度

img

1、商品检索功能介绍

根据用户输入的检索条件,查询出对用的商品

1.1. 检索两个入口

首页的分类

img

搜索栏

img

1.2. 检索列表展示页面

img

1.3 根据业务搭建数据结构

1.3.1 建立映射

这时我们要思考三个问题:

  1. 哪些字段需要分词

    • 例如:商品名称
  2. 我们用哪些字段进行过滤

    • 平台属性值

    • 分类Id

    • 品牌Id

  3. 哪些字段我们需要通过搜索查询出来。

    • 商品名称
    • 价格
    • 图片

以上分析的所有显示,以及分词,过滤的字段都应该在es中出现。Es中如何保存这些数据呢?

根据上述的字段描述,应该建立一个mappings对应的存上上述字段描述的信息!

根据以上制定出如下结构:mappings

Index:goods

document: properties

field: id,price,title…

ES中index默认是true。

注意:ik_max_word 中文词库必须有!

attrs:平台属性值的集合,主要用于平台属性值过滤。

1.3.2 nested 介绍

nested:类型是一种特殊的对象object数据类型(specialised version of the object datatype ),允许对象数组彼此独立地进行索引和查询。

demo: 建立一个普通的index

如果linux 中有这个my_comment_index 先删除!DELETE /my_comment_index

步骤1:建立一个索引( 存储博客文章及其所有评论)

PUT my_comment_index/_doc/1
{
  "title": "狂人日记",
  "body": "《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国国民精神的麻木愚昧颇感痛切。",
  "comments": [
    {
      "name": "张三",
      "age": 34,
      "rating": 8,
      "comment": "非常棒的文章",
      "commented_on": "30 Nov 2023"
    },
    {
      "name": "李四",
      "age": 38,
      "rating": 9,
      "comment": "文章非常好",
      "commented_on": "25 Nov 2022"
    },
    {
      "name": "王五",
      "age": 33,
      "rating": 7,
      "comment": "手动点赞",
      "commented_on": "20 Nov 2021"
    }
  ]
}

如上所示,所以我们有一个文档描述了一个帖子和一个包含帖子上所有评论的内部对象评论。 但是Elasticsearch搜索中的内部对象并不像我们期望的那样工作。

步骤2 : 执行查询

GET /my_comment_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "comments.name": "李四"
          }
        },
        {
          "match": {
            "comments.age": 34
          }
        }
      ]
    }
  }
}

查询结果:居然正常的响应结果了

image-20221205232357129

原因分析:comments字段默认的数据类型是Object,故我们的文档内部存储为:

{ "title": [ 狂人日记], "body": [ 《狂人日记》是一篇象征性和寓意很强的小说,当时... ], "comments.name": [ 张三, 李四, 王五 ], "comments.comment": [ 非常棒的文章,文章非常好,王五,... ], "comments.age": [ 33, 34, 38 ], "comments.rating": [ 7, 8, 9 ] }

我们可以清楚地看到,comments.name和comments.age之间的关系已丢失。这就是为什么我们的文档匹配李四和34的查询。

步骤3:删除当前索引

DELETE /my_comment_index

步骤4:建立一个nested 类型的(comments字段映射为nested类型,而不是默认的object类型)

PUT my_comment_index
{
  "mappings": {
      "properties": {
        "comments": {
          "type": "nested" 
        }
    }
  }
}


PUT my_comment_index/_doc/1
{
  "title": "狂人日记",
  "body": "《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国国民精神的麻木愚昧颇感痛切。",
  "comments": [
    {
      "name": "张三",
      "age": 34,
      "rating": 8,
      "comment": "非常棒的文章",
      "commented_on": "30 Nov 2023"
    },
    {
      "name": "李四",
      "age": 38,
      "rating": 9,
      "comment": "文章非常好",
      "commented_on": "25 Nov 2022"
    },
    {
      "name": "王五",
      "age": 33,
      "rating": 7,
      "comment": "手动点赞",
      "commented_on": "20 Nov 2021"
    }
  ]
}

重新执行步骤1,使用nested 查询

GET /my_comment_index/_search
{
  "query": {
    "nested": {
      "path": "comments",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "comments.name": "李四"
              }
            },
            {
              "match": {
                "comments.age": 34
              }
            }
          ]
        }
      }
    }
  }
}

结果发现没有返回任何的文档,这是何故?

当将字段设置为nested 嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。文档的内部表示:

{ {

"comments.name":    [ 张三],
"comments.comment": [ 非常棒的文章 ],
"comments.age":     [ 34 ],
"comments.rating":  [ 9 ]

}, {

"comments.name":    [ 李四],
"comments.comment": [ 文章非常好 ],
"comments.age":     [ 38 ],
"comments.rating":   [ 8 ]

}, {

"comments.name":    [ 王五],
"comments.comment": [手动点赞],
"comments.age":     [ 33 ],
"comments.rating":   [ 7 ]

}, {

"title":            [ 狂人日记 ],
"body":             [ 《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国... ]

} }

每个内部对象都在内部存储为单独的隐藏文档。 这保持了他们的领域之间的关系。

2、搭建service-list服务

gmall-service模块下搭建搜索模块:service-list

image-20221214212506025

2.1 配置pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>gmall-service</artifactId>
        <groupId>com.atguigu.gmall</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>service-list</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.atguigu.gmall</groupId>
            <artifactId>service-product-client</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>
</project>

说明:

  1. 引入service-product-client模块

  2. 引入spring-boot-starter-data-elasticsearch依赖

  3. 在父工程中gmall-parent模块pom.xml中properties节点中指定client版本跟ES服务端版本一致

    <elasticsearch.version>7.8.0</elasticsearch.version>
    

2.2 启动类

package com.atguigu.gmall;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class ListApp {
    public static void main(String[] args) {
        SpringApplication.run(ListApp.class, args);
    }
}

2.3 添加配置文件

bootstrap.properties

spring.application.name=service-list
spring.profiles.active=dev
spring.cloud.nacos.discovery.server-addr=192.168.200.128:8848
spring.cloud.nacos.config.server-addr=192.168.200.128:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml

说明:Nacos配置文件中添加es配置信息

2.4 实体类

说明:在gmall-model模块中已有商品文档实体类,以及平台属性实体类跟ES索引库mapping简历映射

商品文档实体类

package com.atguigu.gmall.list.model;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

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

// Index = goods , Type = info  es 7.8.0 逐渐淡化type!  修改!
//  es 的分片,副本是为了保证高可用!
@Data
@Document(indexName = "goods" , shards = 3,replicas = 2)
public class Goods {
    // 商品Id skuId
    @Id
    private Long id;

    @Field(type = FieldType.Keyword, index = false)
    private String defaultImg;

    //  es 中能分词的字段,这个字段数据类型必须是 text!
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title;

    @Field(type = FieldType.Double)
    private Double price;

    //  @Field(type = FieldType.Date)   6.8.1
    @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime; // 新品

    @Field(type = FieldType.Long)
    private Long tmId;

    @Field(type = FieldType.Keyword)
    private String tmName;

    @Field(type = FieldType.Keyword)
    private String tmLogoUrl;

    @Field(type = FieldType.Long)
    private Long category1Id;

    @Field(type = FieldType.Keyword)
    private String category1Name;

    @Field(type = FieldType.Long)
    private Long category2Id;

    @Field(type = FieldType.Keyword)
    private String category2Name;

    @Field(type = FieldType.Long)
    private Long category3Id;

    @Field(type = FieldType.Keyword)
    private String category3Name;

    //  商品的热度! 我们将商品被用户点查看的次数越多,则说明热度就越高!
    @Field(type = FieldType.Long)
    private Long hotScore = 0L;

    // 平台属性集合对象
    // Nested 支持嵌套查询 允许对象数组彼此独立检索和查询
    @Field(type = FieldType.Nested)
    private List<SearchAttr> attrs;

}

销售属性实体类

package com.atguigu.gmall.model.list;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Data
public class SearchAttr {
    // 平台属性Id
    @Field(type = FieldType.Long)
    private Long attrId;
    // 平台属性值名称
    @Field(type = FieldType.Keyword)
    private String attrValue;
    // 平台属性名
    @Field(type = FieldType.Keyword)
    private String attrName;
}

2.5 创建索引库

package com.atguigu.gmall.list.controller;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.model.Goods;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: atguigu
 * @create: 2023-03-03 10:10
 */
@RestController
@RequestMapping("api/list")
public class ListApiController {


    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;


    /**
     * 创建索引库
     *
     * @return
     */
    @GetMapping("inner/createIndex")
    public Result createIndex() {
        //调用工具类创建索引
        elasticsearchRestTemplate.createIndex(Goods.class);
        //设置索引库映射信息
        elasticsearchRestTemplate.putMapping(Goods.class);
        return Result.ok();
    }


    /**
     * 删除索引库
     * @return
     */
    @GetMapping("inner/deleteIndex")
    public Result deleteIndex() {
        elasticsearchRestTemplate.deleteIndex("goods");
        return Result.ok();
    }

}

在浏览器运行:

http://localhost:8203/api/list/inner/createIndex

通过kibana查看mapping

img

重点:attrs 数据类型必须是nested !

3、商品上架/下架

构建goods数据模型分析

Sku基本信息(详情业务已封装了接口)

Sku分类信息(详情业务已封装了接口)

Sku的品牌信息(无)

Sku对应的平台属性(详情业务已封装了接口)

3.1 在service-product封装接口

3.1.1 Sku的品牌接口

YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/643

service-product商品微服务模块中增加查询品牌RestFul接口实现

控制器ProductApiController

@Autowired
private BaseTrademarkService baseTrademarkService;

/**
 * 根据品牌ID查询品牌信息
 *
 * @param tmId 品牌ID
 * @return
 */
@GetMapping("/inner/getTrademark/{tmId}")
public BaseTrademark getTrademarkById(@PathVariable("tmId") Long tmId) {
    BaseTrademark trademark = baseTrademarkService.getById(tmId);
    return trademark;
}

3.2 在service-product-client添加接口

service-product-client中ProductFeignClient提供Feign API接口

/**
 * 根据品牌ID查询品牌信息
 *
 * @param tmId 品牌ID
 * @return
 */
@GetMapping("/api/product/inner/getTrademark/{tmId}")
public BaseTrademark getTrademarkById(@PathVariable("tmId") Long tmId);

服务降级类

@Override
public BaseTrademark getTrademark(Long tmId) {
    return null;
}

3.3 实现商品上架/下架功能

YAPI接口文档:

3.3.1 控制器

service-list模块ListApiController 处理上下架请求

@Autowired
private SearchService searchService;


/**
 * 测试接口,商品文档对象录入索引
 * @param skuId
 * @return
 */
@GetMapping("/inner/upperGoods/{skuId}")
public Result upperGoods(@PathVariable("skuId") Long skuId){
    searchService.upperGoods(skuId);
    return Result.ok();
}


/**
 * 测试接口,商品文档删除
 * @param skuId
 * @return
 */
@GetMapping("/inner/lowerGoods/{skuId}")
public Result lowerGoods(@PathVariable("skuId") Long skuId){
    searchService.lowerGoods(skuId);
    return Result.ok();
}

3.3.2 业务接口

SearchService

package com.atguigu.gmall.list.service;

public interface SearchService {

    /**
     * 构建商品索引库文档对象Goods,将文档存入索引库
     * @param skuId
     */
    void upperGoods(Long skuId);

    /**
     * 删除索引库文档对象
     * @param skuId
     */
    void lowerGoods(Long skuId);
}

3.3.3. 业务实现类

SearchServiceImpl

package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.Goods;
import com.atguigu.gmall.list.model.SearchAttr;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.weaver.ast.Var;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-03-03 10:39
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ProductFeignClient productFeignClient;

    //索引库名称
    private static final String index_name = "goods";

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * 构建商品索引库文档对象Goods,将文档存入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.构建索引库文档对象-远程调用商品微服务获取各种信息
            Goods goods = new Goods();
            //1.1 远程调用商品微服务:根据skuId查询商品基本信息
            goods.setId(skuId);
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfoAndImages(skuId);
                if (skuInfo != null) {
                    goods.setTitle(skuInfo.getSkuName());
                    goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                    goods.setCreateTime(skuInfo.getCreateTime());
                    goods.setCreatedDate(skuInfo.getCreateTime());
                }
                return skuInfo;
            });

            //获取价格
            CompletableFuture<Void> priceVoidCompletableFuture = CompletableFuture.runAsync(() -> {
                goods.setPrice(productFeignClient.getSkuPrice(skuId).doubleValue());
            });

            //1.2 远程调用商品微服务:根据分类ID查询分类信息
            CompletableFuture<Void> baseCategoryViewCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());
                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());
                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            });

            //1.3 远程调用商品微服务:根据品牌ID查询品牌信息
            CompletableFuture<Void> trademarkCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            });

            //1.4 远程调用商品微服务:根据skuId查询平台属性
            CompletableFuture<Void> atrrCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    List<SearchAttr> searchAttrs = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrs);
                }
            });

            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceVoidCompletableFuture,
                    baseCategoryViewCompletableFuture,
                    atrrCompletableFuture
            ).join();

            //2.采用Java client方式将索引库文档对象存入索引库(查询官方文档)
            //2.1 构建创建文档IndexRequest封装操作的索引库
            IndexRequest request = new IndexRequest(index_name);
            request.id(skuId.toString());
            request.source(JSON.toJSONString(goods), XContentType.JSON);

            //2.2 调用ES客户端对象 执行保存
            restHighLevelClient.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("[搜索服务]-上架商品异常:{}", e);
            throw new RuntimeException("上架商品异常:"+e.getMessage());
        }
    }


    /**
     * 删除索引库文档对象
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            DeleteRequest request = new DeleteRequest(
                    index_name,
                    skuId.toString());
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("[搜索服务]-删除商品异常:{}", e);
            throw new RuntimeException("删除商品异常:"+e.getMessage());
        }
    }
}

添加数据

通过kibana查看数据

说明:后期学习了MQ,我们可以根据后台系统添加和修改等操作,发送mq消息自动上下架商品

http://localhost:8203/api/list/inner/upperGoods/21

http://localhost:8203/api/list/inner/lowerGoods/21

4、商品热度排名设值

搜索商品时,后面我们会根据热点排序,何时更新热点?我们在获取商品详情时调用更新

4.1 封装接口与实现类与控制器

YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/739

4.1.1 控制器

ListApiController

package com.atguigu.gmall.list.api;

import com.atguigu.gmall.list.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: atguigu
 * @create: 2023-03-03 14:07
 */
@RestController
@RequestMapping("/api/list")
public class ListApiController {


    @Autowired
    private SearchService searchService;

    /**
     * 提供给详情服务调用:更新商品热度分值
     * @param skuId
     */
    @GetMapping("/inner/incrHotScore/{skuId}")
    public void incrHotScore(@PathVariable("skuId") Long skuId){
        searchService.incrHotScore(skuId);
    }
}

4.1.2 业务层

SearchService

/**
 * 提供给详情服务调用:更新商品热度分值
 * @param skuId
 */
void incrHotScore(Long skuId);

SearchServiceImpl

/**
 * 提供给详情服务调用:更新商品热度分值
 *
 * @param skuId
 */
@Override
public void incrHotScore(Long skuId) {
    try {
        //1.根据SkuID获取缓存中商品热度 分值 对结果进行自增+1
        Double score = redisTemplate.opsForZSet().incrementScore("hotScore", skuId.toString(), 1);

        //2.根据SkuID更新索引库中当前文档的排名分值
        if (score % 10 == 0) {
            //2.1 根据索引库主键ID查询商品文档
            GetRequest getRequest = new GetRequest(index_name, skuId.toString());
            GetResponse response = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
            String sourceAsString = response.getSourceAsString();
            Goods goods = JSON.parseObject(sourceAsString, Goods.class);
            goods.setHotScore(score.longValue());

            //2.2 修改索引库文档
            UpdateRequest updateRequest = new UpdateRequest(index_name, skuId.toString());
            updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);
            restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
        }
    } catch (IOException e) {
        e.printStackTrace();
        log.error("[更新文档热度分值失败:{}]", e);
    }
}

4.2 在service-list-client封装接口

4.2.1 搭建service-list-client

gmall-client模块下搭建:service-list-client模块。搭建方式如service-item-client

4.2.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>gmall-client</artifactId>
        <groupId>com.atguigu.gmall</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-list-client</artifactId>


</project>

4.2.3 添加接口

提供远程调用的Feign接口

package com.atguigu.gmall.list.client;


import com.atguigu.gmall.list.client.impl.ListDegradeFeignClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "service-list", fallback = ListDegradeFeignClient.class)
public interface ListFeignClient {

    /**
     * 提供给详情服务调用:更新商品热度分值
     * @param skuId
     */
    @GetMapping("/api/list/inner/incrHotScore/{skuId}")
    public void incrHotScore(@PathVariable("skuId") Long skuId);

}

服务降级类

package com.atguigu.gmall.list.client.impl;

import com.atguigu.gmall.list.client.ListFeignClient;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2023-03-03 14:36
 */
@Component
public class ListDegradeFeignClient implements ListFeignClient {
    @Override
    public void incrHotScore(Long skuId) {

    }
}

4.3 在service-item模块调用接口

service-item模块pom.xml中引入依赖

<dependency>
    <groupId>com.atguigu.gmall</groupId>
    <artifactId>service-list-client</artifactId>
    <version>1.0</version>
</dependency>

接口调用,更新service-item模块中ItemServiceImpl汇总商品信息方法:getBySkuId

@Autowired
private ListFeignClient listFeignClient;

/**
 * 汇总商品详情页所需数据
 *
 * @param skuId - **skuInfo**:当前商品SKU信息包含SKU图片列表
 *              - **categoryView**:当前商品所属的分类信息(包含三级)
 *              - **price**:当前商品最新价格
 *              - **spuPosterList**:当前商品海报图片集合
 *              - **skuAttrList**:当前商品平台属性及属性值集合--- 规格与参数
 *              - **spuSaleAttrList**:当前商品销售属性集合选中效果
 *              - **valuesSkuJson**:切换SKU转换SKU商品json字符串信息
 * @param skuId
 * @return
 */
@Override
public Map<String, Object> getItemAllData(Long skuId) {
    HashMap<String, Object> data = new HashMap<>();
    //0.判断用户要查询的商品是否不存在,如果不存在直接返回null TODO 开发阶段为了方便测试,暂时注释,测试阶段再放开
    //RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
    //if (!bloomFilter.contains(skuId)) {
    //    return data;
    //}

    //0.supplyAsync构建有返回值异步操作对象
    CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
        //1.远程调用商品服务-根据skuID查询商品sku信息
        SkuInfo skuInfo = productFeignClient.getSkuInfoAndImages(skuId);
        if (skuInfo != null) {
            data.put("skuInfo", skuInfo);
        }
        return skuInfo;
    }, executor);

    //2.根据商品Sku三家分类ID查询分类信息
    CompletableFuture<Void> categoryViewCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync((skuInfo -> {
        BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
        if (categoryView != null) {
            data.put("categoryView", categoryView);
        }
    }), executor);

    //3.根据SKuID查询价格
    CompletableFuture<Void> priceCompletableFuture = CompletableFuture.runAsync(() -> {
        BigDecimal price = productFeignClient.getSkuPrice(skuId);
        if (price != null) {
            data.put("price", price);
        }
    }, executor);


    //4.根据Sku所属的SpuID查询海报图片列表
    CompletableFuture<Void> spuPosterListCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
        List<SpuPoster> spuPosterList = productFeignClient.getSpuPosterBySpuId(skuInfo.getSpuId());
        if (!CollectionUtils.isEmpty(spuPosterList)) {
            data.put("spuPosterList", spuPosterList);
        }
    }, executor);

    //5.根据SkuID查询商品平台属性列表
    CompletableFuture<Void> skuAttrListCompletableFuture = CompletableFuture.runAsync(() -> {
        List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
        if (!CollectionUtils.isEmpty(attrList)) {
            data.put("skuAttrList", attrList);
        }
    }, executor);

    //6.根据spuId,skuId查询当前商品销售属性(带选中效果)
    CompletableFuture<Void> spuSaleAttrListCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
        List<SpuSaleAttr> listCheckBySku = productFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
        if (!CollectionUtils.isEmpty(listCheckBySku)) {
            data.put("spuSaleAttrList", listCheckBySku);
        }
    }, executor);

    //7.切换SKU转换SKU商品json字符串信息
    CompletableFuture<Void> valuesSkuJsonCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
        String valuesSkuJson = productFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
        if (StringUtils.isNotBlank(valuesSkuJson)) {
            data.put("valuesSkuJson", valuesSkuJson);
        }
    }, executor);

    //8. 远程调用搜索微服务-更新商品得分分值
    CompletableFuture<Void> incrScoreCompletableFuture = CompletableFuture.runAsync(() -> {
        listFeignClient.incrHotScore(skuId);
    });

    //8.组合多个异步任务对象 ,必须等待所有任务执行完毕
    CompletableFuture.allOf(
            skuInfoCompletableFuture,
            categoryViewCompletableFuture,
            spuPosterListCompletableFuture,
            spuSaleAttrListCompletableFuture,
            valuesSkuJsonCompletableFuture,
            priceCompletableFuture,
            skuAttrListCompletableFuture,
            incrScoreCompletableFuture
    ).join();
    return data;
}