文章目录

(一)搭建搜索微服务
(二)结合页面设计Goods数据模型
(三)商品微服务提供接口
(四)根据spu构建Goods
(五)完成数据导入功能
(六)完成基本查询

(一)搭建搜索微服务


maven依赖如下:

    <dependencies><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!-- elasticsearch --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency><!-- eureka --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- feign --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--        想要调用item的接口,要使用到实体类--><dependency><groupId>com.leyou.item</groupId><artifactId>leyou-item-interface</artifactId><version>1.0.0-SNAPSHOT</version></dependency><!--        可能还会用到通用的一些类--><dependency><groupId>com.leyou.common</groupId><artifactId>leyou-common</artifactId><version>1.0.0-SNAPSHOT</version></dependency></dependencies>

注意:leyou-search不能直接操作数据库,需要借助leyou-item来操作
也就是要使用feign组件来请求leyou-item的接口,间接地获取数据库的数据

application.yml配置如下:

server:port: 8083
spring:application:name: search-servicedata:elasticsearch:cluster-name: elasticsearchcluster-nodes: 192.168.28.233:9300
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eurekaregistry-fetch-interval-seconds: 10instance:lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期

引导类:

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

(二)结合页面设计Goods数据模型

以结果为导向


可以看到,每一个搜索结果都有至少1个商品,当我们选择大图下方的小图,商品会跟着变化。
因此,搜索的结果是SPU,即多个SKU的集合
既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。

需要什么数据

再来看看页面中有什么数据:

直观能看到的:图片、价格、标题、副标题
暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件:

这些过滤条件也都需要存储到索引库中,包括:
商品分类、品牌、可用来搜索的规格参数等
综上所述,我们需要的数据格式有:
spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数

最终的数据结构

我们创建一个类,封装要保存到索引库的数据,并设置映射属性:

@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {@Idprivate Long id; // spuId@Field(type = FieldType.Text, analyzer = "ik_max_word")private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌@Field(type = FieldType.Keyword, index = false)private String subTitle;// 卖点private Long brandId;// 品牌idprivate Long cid1;// 1级分类idprivate Long cid2;// 2级分类idprivate Long cid3;// 3级分类idprivate Date createTime;// 创建时间private List<Long> price;// 价格@Field(type = FieldType.Keyword, index = false)private String skus;// List<sku>信息的json结构private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
}

一些特殊字段解释:

  • all:用来进行全文检索的字段,里面包含标题、商品分类信息

  • price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤

  • skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段

  • specs:所有规格参数的集合。key是参数名,是参数值

    例如:我们在specs中存储 内存:4G,6G,颜色为红色,转为json就是:

    {"specs":{"内存":[4G,6G],"颜色":"红色"}
    }
    

    当存储到索引库时,elasticsearch会处理为两个字段:

    • specs.内存:[4G,6G]
    • specs.颜色:红色

    另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合

    • specs.颜色.keyword:红色

(三)商品微服务提供接口

先思考我们需要的数据:

  • SPU信息

  • SKU信息

  • SPU的详情

  • 商品分类名称(拼接all字段)

  • 品牌名称

  • 规格参数

再思考我们需要哪些服务:

  • 第一:分批查询spu的服务,已经写过。
  • 第二:根据spuId查询sku的服务,已经写过
  • 第三:根据spuId查询SpuDetail的服务,已经写过
  • 第四:根据商品分类id,查询商品分类名称,没写过
  • 第五:根据商品品牌id,查询商品的品牌,没写过
  • 第六:规格参数接口

因此我们需要额外提供一个查询商品分类名称的接口。

在CategoryController中添加接口:

 /*** 根据分类id查询商品分类名称* @param ids* @return*/@GetMapping("names")public ResponseEntity<List<String>> queryNamesByIds(@RequestParam("ids")List<Long> ids){List<String> names = this.categoryService.queryNamesByIds(ids);if (CollectionUtils.isEmpty(names)) {return ResponseEntity.notFound().build();}return ResponseEntity.ok(names);}
    /*** 根据商品品牌id查询商品的品牌* @param id* @return*/@GetMapping("{id}")public ResponseEntity<Brand> queryBrandById(@PathVariable("id") Long id) {Brand brand = brandService.queryBrandById(id);if (brand == null) {return ResponseEntity.notFound().build();}return ResponseEntity.ok(brand);}

第一步:服务的提供方在leyou-item-interface中提供API接口,并编写接口声明:

以商品分类服务接口为例,返回值不再使用ResponseEntity:

@RequestMapping("category")
public interface CategoryApi {@GetMapping("names")ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids") List<Long> ids);
}

第二步:在调用方leyou-search中编写FeignClient,但不要写方法声明了
直接继承leyou-item-interface提供的api接口:

以商品分类服务接口为例,直接继承接口就可以:

@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {}

测试如下:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class CategoryClientTest {@Autowiredprivate CategoryClient categoryClient;@Testpublic void testQueryCategories() {List<String> names = categoryClient.queryNameByIds(Arrays.asList(1L, 2L, 3L));names.forEach(System.out::println);}
}

(四)根据spu构建Goods

导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存
因此我们先编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods

@Service
public class SearchService {@Autowiredprivate BrandClient brandClient;@Autowiredprivate CategoryClient categoryClient;@Autowiredprivate GoodsClient goodsClient;@Autowiredprivate SpecificationClient specificationClient;private static final ObjectMapper MAPPER = new ObjectMapper();public Goods buildGoods(Spu spu) throws IOException {// 创建goods对象Goods goods = new Goods();// 查询品牌Brand brand = this.brandClient.queryBrandById(spu.getBrandId());// 查询分类名称List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));// 查询spu下的所有skuList<Sku> skus = this.goodsClient.querySkuBySpuId(spu.getId());List<Long> prices = new ArrayList<>();List<Map<String, Object>> skuMapList = new ArrayList<>();// 遍历skus,获取价格集合skus.forEach(sku -> {prices.add(sku.getPrice());Map<String, Object> skuMap = new HashMap<>();skuMap.put("id", sku.getId());skuMap.put("title", sku.getTitle());skuMap.put("price", sku.getPrice());skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : "");skuMapList.add(skuMap);});// 查询出所有的搜索规格参数List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);// 查询spuDetail。获取规格参数值SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId());// 获取通用的规格参数Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() {});// 获取特殊的规格参数Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() {});// 定义map接收{规格参数名,规格参数值}Map<String, Object> specs = new HashMap<>();params.forEach(param -> {// 判断是否通用规格参数if (param.getGeneric()) {// 获取通用规格参数值String value = genericSpecMap.get(param.getId()).toString();// 判断是否是数值类型if (param.getNumeric()) {// 如果是数值的话,判断该数值落在那个区间value = chooseSegment(value, param);}// 把参数名和值放入结果集中specs.put(param.getName(), value);} else {specs.put(param.getName(), specialSpecMap.get(param.getId()));}});// 设置参数goods.setId(spu.getId());goods.setCid1(spu.getCid1());goods.setCid2(spu.getCid2());goods.setCid3(spu.getCid3());goods.setBrandId(spu.getBrandId());goods.setCreateTime(spu.getCreateTime());goods.setSubTitle(spu.getSubTitle());// 用空格隔开,方便分词goods.setAll(spu.getTitle() + " " + brand.getName() + " " + StringUtils.join(names, " "));// 获取spu下的所有sku的价格goods.setPrice(prices);// 获取spu下的所有sku,并转化成json字符串goods.setSkus(MAPPER.writeValueAsString(skuMapList));// 获取所有查询的规格参数{name:value}goods.setSpecs(specs);return goods;}private String chooseSegment(String value, SpecParam p) {double val = NumberUtils.toDouble(value);String result = "其它";// 保存数值段for (String segment : p.getSegments().split(",")) {String[] segs = segment.split("-");// 获取数值范围double begin = NumberUtils.toDouble(segs[0]);double end = Double.MAX_VALUE;if (segs.length == 2) {end = NumberUtils.toDouble(segs[1]);}// 判断是否在范围内if (val >= begin && val < end) {if (segs.length == 1) {result = segs[0] + p.getUnit() + "以上";} else if (begin == 0) {result = segs[1] + p.getUnit() + "以下";} else {result = segment + p.getUnit();}break;}}return result;}
}

(五)完成数据导入功能

创建GoodsRepository,用法类似于通用mapper,如下:

public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {}

我们新建一个测试类,在里面进行数据的操作:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouSearchApplication.class)
public class ElasticsearchTest {@Autowiredprivate GoodsReponsitory goodsReponsitory;@Autowiredprivate ElasticsearchTemplate elasticsearchTemplate;@Testpublic void createIndex(){// 创建索引库,以及映射this.elasticsearchTemplate.createIndex(Goods.class);this.elasticsearchTemplate.putMapping(Goods.class);}
}

效果如下:

    @Testpublic void importData() {Integer page = 1;Integer rows = 100;do {// 分批查询spuBoPageResult<SpuBo> pageResult = this.goodsClient.querySpuByPage(null, true, page, rows);// 遍历spubo集合转化为List<Goods>List<Goods> goodsList = pageResult.getItems().stream().map(spuBo -> {try {return this.searchService.buildGoods((Spu) spuBo);} catch (IOException e) {e.printStackTrace();}return null;}).collect(Collectors.toList());this.goodsReponsitory.saveAll(goodsList);// 获取当前页的数据条数,如果是最后一页,没有100条rows = pageResult.getItems().size();// 每次循环页码加1page++;} while (rows == 100);}

效果如下:

(六)完成基本查询

在首页的顶部,有一个输入框:

当我们输入任何文本,点击搜索,就会跳转到搜索页search.html了,并且将搜索关键字携带过来:

我们应该在页面加载时,获取地址栏请求参数,并发起异步请求,查询后台数据,然后在页面渲染
我们在data中定义search对象,记录请求的参数;定义goodsList对象,记录商品列表:

data: {search: {key: "" // 搜索页面的关键字},goodsList: [] //商品列表
}

我们通过钩子函数created(),在页面加载时获取请求参数,并记录下来

created(){// 判断是否有请求参数if(!location.search){return;}// 将请求参数转为对象const search = ly.parse(location.search.substring(1));// 记录在data的search对象中this.search = search;// 发起请求,根据条件搜索this.loadData();
}

然后发起请求,搜索数据

methods: {loadData(){// ly.http.post("/search/page", ly.stringify(this.search)).then(resp=>{ly.http.post("/search/page", this.search).then(resp=>{console.log(resp);});}
}
  • 这里使用POST是因为后期可能会携带很多参数(关键词、分页、过滤等等)
  • 我们这里使用ly是common.js中定义的工具对象
  • 这里使用的是post请求,这样可以携带更多参数,并且以json格式发送

leyou-gateway中的CORS配置类中,添加允许信任域名:

并在leyou-gateway工程的Application.yml中添加网关映射:

刷新页面试试:

因为后台没有提供接口,所以无法访问,接下来我们实现后台接口


首先分析几个问题:

  • 请求方式:POST
  • 请求路径:/search/page,前面的/search是网关的映射路径,真实映射路径page代表分页查询
  • 请求参数:json格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此我们可以用一个对象来接收请求的json数据:

    package com.leyou.search.pojo;public class SearchRequest {private String key;// 搜索条件private Integer page;// 当前页private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小private static final Integer DEFAULT_PAGE = 1;// 默认页public String getKey() {return key;}public void setKey(String key) {this.key = key;}public Integer getPage() {if (page == null) {return DEFAULT_PAGE;}// 获取页码时做一些校验,不能小于1return Math.max(DEFAULT_PAGE, page);}public void setPage(Integer page) {this.page = page;}public Integer getSize() {return DEFAULT_SIZE;}
    }
    
  • 返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息
    我们可以使用之前定义的PageResult类

代码如下:

    /*** 搜索商品** @param request* @return*/@PostMapping("page")public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest request) {PageResult<Goods> result = this.searchService.search(request);if (result == null || CollectionUtils.isEmpty(result.getItems())) {return new ResponseEntity<>(HttpStatus.NOT_FOUND);}return ResponseEntity.ok(result);}
    public PageResult<Goods> search(SearchRequest request) {String key = request.getKey();// 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品if (StringUtils.isBlank(key)) {return null;}// 构建查询条件NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 1、对key进行全文检索查询queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND));// 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitlequeryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));// 3、分页// 准备分页参数int page = request.getPage();int size = request.getSize();queryBuilder.withPageable(PageRequest.of(page - 1, size));// 4、查询,获取结果Page<Goods> pageInfo = this.goodsRepository.search(queryBuilder.build());// 封装结果并返回return new PageResult<>(pageInfo.getTotalElements(), pageInfo.getTotalPages(), pageInfo.getContent());}

注意:我们要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。

测试结果如下:


数据是查到了,但是因为我们只查询部分字段,所以结果json 数据中有很多null,这很不优雅
解决办法很简单,在leyou-search的application.yml中添加一行配置,json处理时忽略空值:

spring:jackson:default-property-inclusion: non_null # 配置json处理时忽略空值

乐优商城之基本搜索(十四)相关推荐

  1. 乐优商城之项目搭建(四)

    文章目录 (一)项目分类 (二)电商行业 (三)专业术语 (四)项目介绍 (五)技术选型 (六)开发环境 (七)搭建后台环境:父工程 (八)搭建后台环境:eureka (九)搭建后台环境:zuul ( ...

  2. 乐优商城(十)用户注册

    文章目录 1. 搭建用户微服务 1.1 用户微服务的结构 1.2 创建 leyou-user 1.3 创建 leyou-user-interface 1.4 创建 leyou-user-service ...

  3. 乐优商城(四)商品规格管理

    文章目录 1. 商品规格 1.1 SPU 和 SKU 1.2 分析商品规格的关系 1.3 数据库设计 1.3.1 商品规格组表 1.3.2 商品规格参数表 2. 商品规格组 2.1 商品规格组前端 2 ...

  4. 乐优商城源码/数据库及笔记总结

    文章目录 1 源码 2 笔记 2.1 项目概述 2.2 微服务 3 项目优化 4 项目或学习过程中涉及到的设计模式 5 安全问题 6 高内聚低耦合的体现 7 项目中待优化的地方 1 源码 Github ...

  5. 乐优商城之分类查询品牌查询(八)

    文章目录 (一)编写分类查询 (二)跨域问题 (三)cors跨域原理 (四)解决跨域问题 (五)品牌查询页面分析 (六)品牌查询后台代码 (七)分页查询排序的原理 (八)axios (一)编写分类查询 ...

  6. 乐优商城(10)--数据同步

    乐优商城(10)–数据同步 一.RabbitMQ 1.1.问题分析 目前已经完成了商品详情和搜索系统的开发.思考一下,是否存在问题? 商品的原始数据保存在数据库中,增删改查都在数据库中完成. 搜索服务 ...

  7. 【javaWeb微服务架构项目——乐优商城day03】——(搭建后台管理前端,Vuetify框架,使用域名访问本地项目,实现商品分类查询,cors解决跨域,品牌的查询)

    乐优商城day03 0.学习目标 1.搭建后台管理前端 1.1.导入已有资源 1.2.安装依赖 1.3.运行一下看看 1.4.目录结构 1.5.调用关系 2.Vuetify框架 2.1.为什么要学习U ...

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

    0.学习目标 了解商品规格数据结构设计思路 实现商品规格查询 了解SPU和SKU数据结构设计思路 实现商品查询 了解商品新增的页面实现 独立编写商品新增后台功能 1.商品规格数据结构 乐优商城是一个全 ...

  9. 【javaWeb微服务架构项目——乐优商城day15】——会调用订单系统接口,实现订单结算功能,实现微信支付功能

    0.学习目标 会调用订单系统接口 实现订单结算功能 实现微信支付功能 源码笔记及资料: 链接:https://pan.baidu.com/s/1_opfL63P1pzH3rzLnbFiNw 提取码:v ...

最新文章

  1. 以Dapper、Zipkin和LightStep [x]PM为例阐述分布式跟踪的过去、现在和未来
  2. 希捷撤离 硬盘的那些风花雪月记忆
  3. 如何解决编程的误差问题_柏威机械丨高精密零件加工是如何解决误差精度问题的?...
  4. Vue2.x源码学习笔记-Vue实例的属性和方法整理
  5. javaweb调用python算法_请教怎么用java远程调用python? 多谢
  6. ffmpeg,rtmpdump和nginx rtmp实现录屏,直播和录制
  7. sprintf()--字串格式化命令
  8. Java程序员从笨鸟到菜鸟之(四十八)细谈struts2(十)ognl概念和原理详解
  9. Excel:仅选择可见的单元格
  10. 韩语在线翻译图片识别_Text Scanner for Mac(ocr文字识别工具)
  11. 苹果手机账号验证失败连接不上服务器,Apple ID登录连接服务器验证失败怎么解决?...
  12. HTML 标签 (HTML超文本标记语言)
  13. Android获取手机中外置内存卡、内置内存卡、手机内存路径
  14. 【考研英语语法】一般现在时练习题
  15. 三菱FX3UFX2NFX1N PLC 模拟器模拟通信功能
  16. oracle连接超时是什么意思,oracle连接超时自动断开问题
  17. 论文“Structure-from-Motion Revisited” 对ISFM改进的理解
  18. Android Snackbar使用方法及小技巧-design
  19. 长篇幅详解辐射定标、大气校正、监督分类、掩膜统计、植被覆盖度操作
  20. Tomcat服务器安装、配置教程

热门文章

  1. Python快速入门——Day3
  2. mybatis分页未明确定义列
  3. C语言算法题 合并两个数组并排序
  4. python闭包实现原理_Python 闭包详解
  5. 让生成式 AI 安全、值得信赖且更相关 Making Generative AI Safe, Trustworthy, and More Relevant
  6. 隐藏式摄像机探测器金(futureapps Pro 14.0)
  7. 函数计算乘积python multi_实现multi()函数,参数个数不限,返回所有参数的乘积。_学小易找答案...
  8. linux如何压缩zip文件格式,linux下常用压缩格式的压缩与解压方法
  9. React+fetch通过修改配置文件解决跨域问题
  10. 2017 年了,这么多前端框架,你会怎样选择?