乐优商城 Day07 (非教程,搜索功能实现)

乐优商城学习Day07:

注意:此次代码都是在第六天的基础上
第六天的链接如下:
https://blog.csdn.net/zcylxzyh/article/details/99943700

此次笔记内容主要为:
1.elasticsearch复杂查询,聚合
2.搭建搜索微服务
3.搜索的页面渲染

下面开始第七天的学习:


1.elasticsearch复杂查询,聚合

复杂玩法:
Elasticsearch的原生查询:

在昨天的es-demo中的EsTest中加入:

@Test
    public void testQuery(){
    
    
        //创建查询构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //结果过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
    
    "id","title","price"},null));
        //添加查询条件
        queryBuilder.withQuery(QueryBuilders.matchQuery("title","小米手机"));
        //排序
        queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
        //分页
        queryBuilder.withPageable(PageRequest.of(0,2));

        Page<Item> result = repository.search(queryBuilder.build());

        long total = result.getTotalElements();
        System.out.println("total = " + total);
        int totalPages = result.getTotalPages();
        System.out.println("totalPages = " + totalPages);
        List<Item> list = result.getContent();
        for (Item item : list) {
    
    
            System.out.println("item = " + item);
        }
    }

结果:

在这里插入图片描述

聚合:
继续加入:

 //聚合
    @Test
    public void testAgg(){
    
    
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        String aggName = "popularBrand";
        //聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(aggName).field("brand"));

        //查询并返回待聚合结果
        AggregatedPage<Item> result = template.queryForPage(queryBuilder.build(), Item.class);

        //解析聚合
        Aggregations aggs = result.getAggregations();

        //获取指定名称的聚合
        StringTerms terms = aggs.get(aggName);

        //获取桶
        List<StringTerms.Bucket> buckets = terms.getBuckets();

        for (StringTerms.Bucket bucket : buckets) {
    
    
            System.out.println("bucket.getKeyAsString() = " + bucket.getKeyAsString());
            System.out.println("bucket.getDocCount() = " + bucket.getDocCount());
        }

    }

结果:
在这里插入图片描述
第一部分结束。


2.搭建搜索微服务

2.1 搜索微服务

首先创建出搜索微服务:
在这里插入图片描述
pom文件:

<?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>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-search</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.service</groupId>
            <artifactId>ly-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

配置文件:
在这里插入图片描述

server:
  port: 8083
spring:
  application:
    name: search-service
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: 192.168.133.128:9300
  jackson:
    default-property-inclusion: non_null
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 5
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1

启动类:
在这里插入图片描述

package com.leyou;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(LySearchApplication.class, args);
    }
}

2.2 索引库

接下来,我们需要商品数据导入索引库,便于用户搜索。

那么问题来了,我们有SPU和SKU,到底如何保存到索引库?

在这里插入图片描述
再来看看页面中有什么数据:
在这里插入图片描述
在这里插入图片描述
最终的数据结构:
我们创建一个类,封装要保存到索引库的数据,并设置映射属性
在这里插入图片描述
Goods:


package com.leyou.search.pojo;

import lombok.Data;
import org.springframework.data.annotation.Id;
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.Map;
import java.util.Set;

@Data
@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
    
    
    @Id
    private 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;// 品牌id
    private Long cid1;// 1级分类id
    private Long cid2;// 2级分类id
    private Long cid3;// 3级分类id
    private Date createTime;// 创建时间
    private Set<Long> price;// 价格
    @Field(type = FieldType.Keyword, index = false)
    private String skus;// sku信息的json结构
    private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
}

索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。

先思考我们需要的数据:

  • SPU信息
  • SKU信息
  • SPU的详情
  • 商品分类名称(拼接all字段)

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

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

因此我们需要额外提供一个查询商品分类名称的接口。
在这里插入图片描述
在CategoryController中添加:

 //根据id查询商品分类
    @GetMapping("list/ids")
    public ResponseEntity<List<Category>> queryCategoryByIds(@RequestParam("ids")List<Long> ids){
    
    
        return ResponseEntity.ok(categoryService.queryByIds(ids));
    }

对应的service:

   public List<Category> queryByIds(List<Long> ids){
    
    
        List<Category> list = categoryMapper.selectByIdList(ids);
        if (CollectionUtils.isEmpty(list)) {
    
    
            //返回404
            throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
        }
        return list;
    }

BrandController中添加:


    //根据id查询品牌
    @GetMapping("{id}")
    public ResponseEntity<Brand> queryBrandById(@PathVariable("id") Long id){
    
    
        return ResponseEntity.ok(brandService.queryById(id));
    }

对应的service

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

然后:操作leyou-search工程

现在,我们要在搜索微服务调用商品微服务的接口。

方法:

  • 我们的服务提供方不仅提供实体类,还要提供api接口声明
  • 调用方不用字自己编写接口方法声明,直接继承提供方给的Api接口即可,

第一步:服务的提供方在leyou-item-interface中提供API接口,并编写接口声明:需要引入springMVC及leyou-common的依赖:
在这里插入图片描述
pom文件:

<?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>ly-item</artifactId>
        <groupId>com.leyou.service</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-item-interface</artifactId>
<dependencies>
    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper-core</artifactId>
        <version>1.0.4</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.6</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.leyou.common</groupId>
        <artifactId>ly-common</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
</dependencies>

</project>

BrandApi:

package com.leyou.item.api;

import com.leyou.item.pojo.Brand;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

public interface BrandApi {
    
    
    @GetMapping("brand/{id}")
    Brand queryBrandById(@PathVariable("id") Long id);
}

CategoryApi:

package com.leyou.item.api;

import com.leyou.item.pojo.Category;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

public interface CategoryApi {
    
    
    @GetMapping("category/list/ids")
    List<Category> queryCategoryByIds(@RequestParam("ids")List<Long> ids);
}

GoodsApi :

package com.leyou.item.api;

import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

public interface GoodsApi {
    
    
    //根据spu查询下面的所有sku
    @GetMapping("sku/list")
   List<Sku> querySkuBySpuId(@RequestParam("id") Long spuId);

    @GetMapping("/spu/page")
    PageResult<Spu> querySpuByPage(
            @RequestParam(value = "page",defaultValue = "1") Integer page,
            @RequestParam(value = "rows",defaultValue = "5") Integer rows,
            @RequestParam(value = "saleable",required = false) Boolean saleable,
            @RequestParam(value = "key",required = false) String key
    );
    //根据spu的id查询详情detail
    @GetMapping("/spu/detail/{id}")
    SpuDetail queryDetailById(@PathVariable("id") Long spuId);
}


SpecificationApi:

package com.leyou.item.api;

import com.leyou.item.pojo.SpecParam;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

public interface SpecificationApi {
    
    
    @GetMapping("spec/params")
    List<SpecParam> queryParamList(
            @RequestParam(value = "gid", required = false) Long gid,
            @RequestParam(value = "cid", required = false) Long cid,
            @RequestParam(value = "searching", required = false) Boolean searching
    );
}

第二步:在调用方leyou-search中编写FeignClient,但不要写方法声明了,直接继承leyou-item-interface提供的api接口:
在这里插入图片描述
BrandClient

package com.leyou.search.client;

import com.leyou.item.api.BrandApi;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient("item-service")
public interface BrandClient extends BrandApi {
    
    
}

CategoryClient

package com.leyou.search.client;

import com.leyou.item.api.CategoryApi;
import com.leyou.item.pojo.Category;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

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

GoodsClient

package com.leyou.search.client;

import com.leyou.item.api.GoodsApi;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
    
    
}

SpecificationClient

package com.leyou.search.client;

import com.leyou.item.api.SpecificationApi;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
    
    
}

导入数据:
导入数据只做一次,以后的更新删除等操作通过消息队列来操作索引库

创建GoodsRepository
在这里插入图片描述
GoodsRepository

package com.leyou.search.repository;

import com.leyou.search.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

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

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

在这里插入图片描述
创建索引库:
在这里插入图片描述
GoodsRepositoryTest:

package com.leyou.search.repository;

import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Spu;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.service.SearchService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.CollectionUtils;

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

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest {
    
    

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private ElasticsearchTemplate template;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SearchService searchService;

    @Test
    public void testCreateIndex(){
    
    
        template.createIndex(Goods.class);
        template.putMapping(Goods.class);
    }

    @Test
    public void loadData(){
    
    
        int page = 1;
        int rows = 100;
        int size = 0;
        do {
    
    
            //查询spu信息
            PageResult<Spu> result = goodsClient.querySpuByPage(page, rows, true, null);

            List<Spu> spuList = result.getItems();
            if(CollectionUtils.isEmpty(spuList)){
    
    
                break;
            }
            //构建成goods
            List<Goods> goodsList = spuList.stream().map(searchService::buildGoods).collect(Collectors.toList());
            //存入索引库
            goodsRepository.saveAll(goodsList);

            //翻页
            page++;
            size = spuList.size();
        }while (size == 100);

    }
}

2.3 导入数据

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

在这里插入图片描述
在这里插入图片描述
SearchService:

package com.leyou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.JsonUtils;
import com.leyou.common.utils.NumberUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class SearchService {
    
    

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specClient;

    @Autowired
    private GoodsRepository repository;

    public Goods buildGoods(Spu spu) {
    
    
        Long spuId = spu.getId();
        //查询分类
        List<Category> categories = categoryClient.queryCategoryByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
        if (CollectionUtils.isEmpty(categories)) {
    
    
            throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
        }
        //从List集合获取到名字
        List<String> names = categories.stream().map(Category::getName).collect(Collectors.toList());
        //查询品牌
        Brand brand = brandClient.queryBrandById(spu.getBrandId());
        if (brand == null) {
    
    
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }

        // 搜索字段
        String all = spu.getTitle() + StringUtils.join(names, " ") + brand.getName();

        //查询sku
        List<Sku> skuList = goodsClient.querySkuBySpuId(spuId);
        if (CollectionUtils.isEmpty(skuList)) {
    
    
            throw new LyException(ExceptionEnum.GOODS_SKU_NOT_FOUND);
        }
        //对sku进行处理
        List<Map<String, Object>> skus = new ArrayList<>();
        //价格集合
        Set<Long> priceList = new HashSet<>();

        for (Sku sku : skuList) {
    
    
            Map<String, Object> map = new HashMap<>();
            map.put("id", sku.getId());
            map.put("title", sku.getTitle());
            map.put("price", sku.getPrice());
            map.put("image", StringUtils.substringBefore(sku.getImages(), ","));
            skus.add(map);
            //处理价格
            priceList.add(sku.getPrice());
        }
        //查询规格参数
        List<SpecParam> params = specClient.queryParamList(null, spu.getCid3(), true);
        if (CollectionUtils.isEmpty(params)) {
    
    
            throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
        }
        //查询商品详情
        SpuDetail spuDetail = goodsClient.queryDetailById(spuId);

        //获取通用规格参数
        Map<Long, String> generSpec = JsonUtils.toMap(spuDetail.getGenericSpec(), Long.class, String.class);
        //获取特有规格参数
        Map<Long, List<String>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
    
    
        });
        //规格参数
        Map<String, Object> specs = new HashMap<>();
        for (SpecParam param : params) {
    
    
            //规格名称
            String key = param.getName();
            Object value = "";
            //判断是否是通用规格
            if(param.getGeneric()){
    
    
                value = generSpec.get(param.getId());
                //判断是否是数值类型
                if(param.getNumeric()){
    
    
                    //处理成段
                   value =  chooseSegment(value.toString(),param);
                }
            }else {
    
    
                value = specialSpec.get(param.getId());
            }
            //存入map
            specs.put(key,value);
        }

        //构建goods对象
        Goods goods = new Goods();

        goods.setBrandId(spu.getBrandId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setCreateTime(spu.getCreateTime());
        goods.setId(spuId);
        goods.setAll(all);// 搜索字段,包含标题,分类,品牌,规格等
        goods.setPrice(priceList);//所有sku的价格集合
        goods.setSkus(JsonUtils.toString(skus));//所有sku的集合的json格式
        goods.setSpecs(specs);//所有的可搜索的规格参数
        goods.setSubTitle(spu.getSubTitle());


        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;
    }

    public PageResult<Goods> search(SearchRequest request) {
    
    
        int page = request.getPage() - 1;
        int size = request.getSize();

        //创建查询构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //0 结果过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
    
    "id","subTitle","skus"},null));
        //1 分页
        queryBuilder.withPageable(PageRequest.of(page,size));
        //2 过滤
        queryBuilder.withQuery(QueryBuilders.matchQuery("all",request.getKey()));
        //3 查询
        Page<Goods> result = repository.search(queryBuilder.build());

        //4 解析结果
        long total = result.getTotalElements();
        int totalPage = result.getTotalPages();
        List<Goods> goodsList = result.getContent();
        return new PageResult<>(total,totalPage,goodsList);

    }
}

然后编写一个测试类,循环查询Spu,然后调用IndexService中的方法,把SPU变为Goods,然后写入索引库:
代码在上面Test中:
在这里插入图片描述
在这里插入图片描述

2.3 实现基本搜索

首先,在leyou-gateway中

添加网关映射:
在这里插入图片描述
在这里插入图片描述

server:
  port: 10010
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
zuul:
  prefix: /api # 添加路由前缀
  routes:
    item-service: /item/**
    search-service: /search/**
    upload-service:
      path: /upload/**
      serviceId: upload-service
      strip-prefix: false
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 5000 # 熔断超时时长:5000ms
ribbon:
  ConnectTimeout: 1000 # 连接超时时间(ms)
  ReadTimeout: 3500 # 通信超时时间(ms)
  MaxAutoRetriesNextServer: 0 # 同一服务不同实例的重试次数
  MaxAutoRetries: 0 # 同一实例的重试次数

添加允许信任域名:
在这里插入图片描述
在这里插入图片描述
然后实现后台接口:

controller

首先分析几个问题:

  • 请求方式: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 int DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
    private static final int 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;
        }
        // 获取页码时做一些校验,不能小于1
        return Math.max(DEFAULT_PAGE, page);
    }

    public void setPage(Integer page) {
    
    
        this.page = page;
    }

    public Integer getSize() {
    
    
        return DEFAULT_SIZE;
    }
}

返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息,我们可以使用之前定义的PageResult类

在这里插入图片描述

package com.leyou.search.web;

import com.leyou.common.vo.PageResult;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SearchController {
    
    
    @Autowired
    private SearchService searchService;

    //搜索功能
    @PostMapping("page")
    public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest request) {
    
    
        return ResponseEntity.ok(searchService.search(request));
    }
}

service:
在这里插入图片描述

 public PageResult<Goods> search(SearchRequest request) {
    
    
        int page = request.getPage() - 1;
        int size = request.getSize();

        //创建查询构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //0 结果过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
    
    "id","subTitle","skus"},null));
        //1 分页
        queryBuilder.withPageable(PageRequest.of(page,size));
        //2 过滤
        queryBuilder.withQuery(QueryBuilders.matchQuery("all",request.getKey()));
        //3 查询
        Page<Goods> result = repository.search(queryBuilder.build());

        //4 解析结果
        long total = result.getTotalElements();
        int totalPage = result.getTotalPages();
        List<Goods> goodsList = result.getContent();
        return new PageResult<>(total,totalPage,goodsList);

    }

注意点:我们要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。
在这里插入图片描述
解决办法很简单,在leyou-search的application.yml中添加一行配置,json处理时忽略空值:

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

在这里插入图片描述
至此,第二部分结束。


3.搜索的页面渲染

在这里插入图片描述
页面渲染部分:

<div class="goods-list">
                <ul class="yui3-g">
                    <li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
                        <div class="list-wrap">
                            <div class="p-img">
                                <a href="item.html" target="_blank"><img :src="goods.selectedSku.image" height="200"/></a>
                                <ul class="skus">
                                    <li :class="{selected: goods.selectedSku.id === sku.id}" @mouseenter="goods.selectedSku=sku"
                                        v-for="sku in goods.skus" :key="sku.id">
                                        <img :src="sku.image">
                                    </li>

                                </ul>
                            </div>
                            <div class="clearfix"></div>
                            <div class="price">
                                <strong>
                                    <em>¥</em>
                                    <i v-text="ly.formatPrice(goods.selectedSku.price)"></i>
                                </strong>
                            </div>
                            <div class="attr">
                                <em v-text="goods.selectedSku.title.substring(0, 21) + '..'"></em>
                            </div>
                            <div class="cu">
                                <em><span></span>{
   
   {goods.subTitle.substring(0,15) + '..'}}</em>
                            </div>
                            <div class="commit">
                                <i class="command">已有2000人评价</i>
                            </div>
                            <div class="operate">
                                <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
                                <a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a>
                                <a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a>
                            </div>
                        </div>
                    </li>

                </ul>
            </div>

提供和后台交互的代码部分:

 var vm = new Vue({
        el: "#searchApp",
        data: {
            ly,
            search:{},
            goodsList:[],
            totalPage: 0,
            total: 0,
            selectedSku:{}
        },
        created(){
            //获取请求参数
           const search = ly.parse(location.search.substring(1));
           this.search = search;
            //发送后台
            this.loadData();
        },
        methods:{
          loadData(){
            //发送到后台
            ly.http.post("/search/page",this.search).then(resp =>{
                //保存分页结果
                this.total = resp.data.total;
                this.totalPage = resp.data.totalPage;
                //保存当前页商品
                resp.data.items.forEach(goods =>{
                    //把json处理成JS对象
                    goods.skus = JSON.parse(goods.skus);
                    //初始化被选中的sku
                    goods.selectedSku = goods.skus[0];
                })
                this.goodsList = resp.data.items;
            }).catch(error =>{

            })
          }
        },
        components:{
            lyTop: () => import("./js/pages/top.js")
        }
    });

整个search的代码:

<!DOCTYPE html>
<html xmlns:v-bind="http://www.w3.org/1999/xhtml">

<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>
    <title>乐优商城--商品搜索结果页</title>
    <link rel="icon" href="assets/img/favicon.ico">
    <link href='./css/material.css' rel="stylesheet">
    <link href="./css/vuetify.min.css" rel="stylesheet">
    <script src="./js/vue/vue.js"></script>
    <script src="./js/vue/vuetify.js"></script>
    <script src="./js/axios.min.js"></script>
    <script src="./js/common.js"></script>
    <link rel="stylesheet" type="text/css" href="css/webbase.css"/>
    <link rel="stylesheet" type="text/css" href="css/pages-list.css"/>
    <link rel="stylesheet" type="text/css" href="css/widget-cartPanelView.css"/>
    <style type="text/css">
* {
     
     
    box-sizing: unset;
}
        .btn-arrow, .btn-arrow:visited, .btn-arrow:link, .btn-arrow:active {
     
     
            width: 46px;
            height: 23px;
            border: 1px solid #DDD;
            background: #FFF;
            line-height: 23px;
            font-family: "\5b8b\4f53";
            text-align: center;
            font-size: 16px;
            color: #AAA;
            text-decoration: none;
            out-line: none
        }

        .btn-arrow:hover {
     
     
            background-color: #1299ec;
            color: whitesmoke;
        }

        .top-pagination {
     
     
            display: block;
            padding: 3px 15px;
            font-size: 11px;
            font-weight: 700;
            line-height: 18px;
            color: #999;
            text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
            text-transform: uppercase;
            float: right;
            margin-top: 6px
        }

        .top-pagination span {
     
     
            margin-right: 10px;
        }
/*        .logo-list li{
            padding:8px;
        }
       .logo-list li:hover{
            background-color: #f3f3f3;
        }*/
        .type-list a:hover{
     
     
            color: #1299ec;
        }
        .skus {
     
     
            list-style: none;
        }
        .skus li{
     
     
            list-style: none;
            display: inline-block;
            float: left;
            margin-left: 2px;
            border: 2px solid #f3f3f3;
        }
        .skus li.selected{
     
     
            border: 2px solid #dd1144;
        }
        .skus img{
     
     
            width: 25px;
            height: 25px;
        }
    </style>
    <script type="text/javascript" src="plugins/jquery/jquery.min.js"></script>
</head>

<body >

<div id="searchApp">
<div id="nav-bottom">
    <ly-top/>
</div>


<!--list-content-->
<div class="main" >
    <div class="py-container">

        <div class="bread">
            <!--面包屑-->
            <ul class="fl sui-breadcrumb">
                <li><span>全部结果:</span></li>
                <li ><a href="#">手机</a></li>
                <li ><span>手机通讯</span></li>
            </ul>
            <!--已选择过滤项-->
            <ul class="tags-choose">
                <li class="tag">
                    品牌:<span style="color: red">apple</span>
                    <i class="sui-icon icon-tb-close"></i>
                </li>
                <li class="tag">
                    内存:<span style="color: red">4G</span>
                    <i class="sui-icon icon-tb-close"></i>
                </li>
            </ul>
            <div class="clearfix"></div>
        </div>
        <!--selector-->
        <div class="clearfix selector">
            <div class="type-wrap">
                <div class="fl key">分类</div>
                <div class="fl value">
                    <ul class="type-list">
                        <li>
                            <a>手机</a>
                        </li>
                        <li>
                            <a>儿童手表</a>
                        </li>
                    </ul>
                </div>
                <div class="fl ext"></div>
            </div>
            <div class="type-wrap logo">
                <div class="fl key brand">品牌</div>
                <div class="value logos">
                    <ul class="logo-list">
                        <li><img src="img/_/phone01.png" /></li>
                        <li><img src="img/_/phone02.png" /></li>
                        <li><img src="img/_/phone03.png" /></li>
                        <li><img src="img/_/phone04.png" /></li>
                        <li><img src="img/_/phone05.png" /></li>
                        <li><img src="img/_/phone06.png" /></li>
                        <li><img src="img/_/phone07.png" /></li>
                        <li><img src="img/_/phone08.png" /></li>
                        <li><img src="img/_/phone09.png" /></li>
                        <li><img src="img/_/phone10.png" /></li>
                        <li><img src="img/_/phone11.png" /></li>
                        <li><img src="img/_/phone12.png" /></li>
                        <li><img src="img/_/phone13.png" /></li>
                        <li><img src="img/_/phone14.png" /></li>
                        <li><img src="img/_/phone05.png" /></li>
                        <li><img src="img/_/phone06.png" /></li>
                        <li><img src="img/_/phone07.png" /></li>
                        <li style="text-align: center"><a style="line-height: 30px; font-size: 12px" href="#">黑马</a></li>
                    </ul>
                </div>
                <div class="fl ext">
                    <a href="javascript:void(0);" class="sui-btn">多选</a>
                </div>
            </div>
            <div class="type-wrap">
                <div class="fl key">网络制式</div>
                <div class="fl value">
                    <ul class="type-list">
                        <li>
                            <a>GSM(移动/联通2G)</a>
                        </li>
                        <li>
                            <a>电信2G</a>
                        </li>
                        <li>
                            <a>电信3G</a>
                        </li>
                        <li>
                            <a>移动3G</a>
                        </li>
                        <li>
                            <a>联通3G</a>
                        </li>
                        <li>
                            <a>联通4G</a>
                        </li>
                        <li>
                            <a>电信3G</a>
                        </li>
                        <li>
                            <a>移动3G</a>
                        </li>
                        <li>
                            <a>联通3G</a>
                        </li>
                        <li>
                            <a>联通4G</a>
                        </li>
                    </ul>
                </div>
                <div class="fl ext"></div>
            </div>
            <div class="type-wrap">
                <div class="fl key">显示屏尺寸</div>
                <div class="fl value">
                    <ul class="type-list">
                        <li>
                            <a>4.0-4.9英寸</a>
                        </li>
                        <li>
                            <a>4.0-4.9英寸</a>
                        </li>
                    </ul>
                </div>
                <div class="fl ext"></div>
            </div>
            <div class="type-wrap">
                <div class="fl key">摄像头像素</div>
                <div class="fl value">
                    <ul class="type-list">
                        <li>
                            <a>1200万以上</a>
                        </li>
                        <li>
                            <a>800-1199万</a>
                        </li>
                        <li>
                            <a>1200-1599万</a>
                        </li>
                        <li>
                            <a>1600万以上</a>
                        </li>
                        <li>
                            <a>无摄像头</a>
                        </li>
                    </ul>
                </div>
                <div class="fl ext"></div>
            </div>
            <div class="type-wrap">
                <div class="fl key">价格</div>
                <div class="fl value">
                    <ul class="type-list">
                        <li>
                            <a>0-500元</a>
                        </li>
                        <li>
                            <a>500-1000元</a>
                        </li>
                        <li>
                            <a>1000-1500元</a>
                        </li>
                        <li>
                            <a>1500-2000元</a>
                        </li>
                        <li>
                            <a>2000-3000元 </a>
                        </li>
                        <li>
                            <a>3000元以上</a>
                        </li>
                    </ul>
                </div>
                <div class="fl ext">
                </div>
            </div>
            <div class="type-wrap" style="text-align: center">
                <v-btn small flat>
                    更多<v-icon>arrow_drop_down</v-icon>
                </v-btn>
                <v-btn small="" flat>
                    收起<v-icon>arrow_drop_up</v-icon>
                </v-btn>
            </div>
        </div>
        <!--details-->
        <div class="details">
            <div class="sui-navbar">
                <div class="navbar-inner filter">
                    <ul class="sui-nav">
                        <li class="active">
                            <a href="#">综合</a>
                        </li>
                        <li>
                            <a href="#">销量</a>
                        </li>
                        <li>
                            <a href="#">新品</a>
                        </li>
                        <li>
                            <a href="#">评价</a>
                        </li>
                        <li>
                            <a href="#">价格</a>
                        </li>
                    </ul>
                    <div class="top-pagination">
                        <span><i style="color: #222;">7600+</i> 商品</span>
                        <span><i style="color: red;">2</i>/123</span>
                        <a class="btn-arrow" href="#" style="display: inline-block">&lt;</a>
                        <a class="btn-arrow" href="#" style="display: inline-block">&gt;</a>
                    </div>
                </div>
            </div>
            <div class="goods-list">
                <ul class="yui3-g">
                    <li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
                        <div class="list-wrap">
                            <div class="p-img">
                                <a href="item.html" target="_blank"><img :src="goods.selectedSku.image" height="200"/></a>
                                <ul class="skus">
                                    <li :class="{selected: goods.selectedSku.id === sku.id}" @mouseenter="goods.selectedSku=sku"
                                        v-for="sku in goods.skus" :key="sku.id">
                                        <img :src="sku.image">
                                    </li>

                                </ul>
                            </div>
                            <div class="clearfix"></div>
                            <div class="price">
                                <strong>
                                    <em>¥</em>
                                    <i v-text="ly.formatPrice(goods.selectedSku.price)"></i>
                                </strong>
                            </div>
                            <div class="attr">
                                <em v-text="goods.selectedSku.title.substring(0, 21) + '..'"></em>
                            </div>
                            <div class="cu">
                                <em><span></span>{
   
   {goods.subTitle.substring(0,15) + '..'}}</em>
                            </div>
                            <div class="commit">
                                <i class="command">已有2000人评价</i>
                            </div>
                            <div class="operate">
                                <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
                                <a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a>
                                <a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a>
                            </div>
                        </div>
                    </li>

                </ul>
            </div>
            <div class="fr">
                <div class="sui-pagination pagination-large">
                    <ul>
                        <li class="prev disabled">
                            <a href="#">«上一页</a>
                        </li>
                        <li class="active">
                            <a href="#">1</a>
                        </li>
                        <li>
                            <a href="#">2</a>
                        </li>
                        <li>
                            <a href="#">3</a>
                        </li>
                        <li>
                            <a href="#">4</a>
                        </li>
                        <li>
                            <a href="#">5</a>
                        </li>
                        <li class="dotted"><span>...</span></li>
                        <li class="next">
                            <a href="#">下一页»</a>
                        </li>
                    </ul>
                    <div><span>共10页&nbsp;</span><span>
      到第
      <input type="text" class="page-num"><button class="page-confirm" onclick="alert(1)">确定</button></span></div>
                </div>
            </div>
        </div>
        <!--hotsale-->
        <div class="clearfix hot-sale">
            <h4 class="title">热卖商品</h4>
            <div class="hot-list">
                <ul class="yui3-g">
                    <li class="yui3-u-1-4">
                        <div class="list-wrap">
                            <div class="p-img">
                                <img src="img/like_01.png"/>
                            </div>
                            <div class="attr">
                                <em>Apple苹果iPhone 6s (A1699)</em>
                            </div>
                            <div class="price">
                                <strong>
                                    <em>¥</em>
                                    <i>4088.00</i>
                                </strong>
                            </div>
                            <div class="commit">
                                <i class="command">已有700人评价</i>
                            </div>
                        </div>
                    </li>
                    <li class="yui3-u-1-4">
                        <div class="list-wrap">
                            <div class="p-img">
                                <img src="img/like_03.png"/>
                            </div>
                            <div class="attr">
                                <em>金属A面,360°翻转,APP下单省300!</em>
                            </div>
                            <div class="price">
                                <strong>
                                    <em>¥</em>
                                    <i>4088.00</i>
                                </strong>
                            </div>
                            <div class="commit">
                                <i class="command">已有700人评价</i>
                            </div>
                        </div>
                    </li>
                    <li class="yui3-u-1-4">
                        <div class="list-wrap">
                            <div class="p-img">
                                <img src="img/like_04.png"/>
                            </div>
                            <div class="attr">
                                <em>256SSD商务大咖,完爆职场,APP下单立减200</em>
                            </div>
                            <div class="price">
                                <strong>
                                    <em>¥</em>
                                    <i>4068.00</i>
                                </strong>
                            </div>
                            <div class="commit">
                                <i class="command">已有20人评价</i>
                            </div>
                        </div>
                    </li>
                    <li class="yui3-u-1-4">
                        <div class="list-wrap">
                            <div class="p-img">
                                <img src="img/like_02.png"/>
                            </div>
                            <div class="attr">
                                <em>Apple苹果iPhone 6s (A1699)</em>
                            </div>
                            <div class="price">
                                <strong>
                                    <em>¥</em>
                                    <i>4088.00</i>
                                </strong>
                            </div>
                            <div class="commit">
                                <i class="command">已有700人评价</i>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>
        </div>
    </div>
</div>

</div>

<script type="text/javascript">
    var vm = new Vue({
     
     
        el: "#searchApp",
        data: {
     
     
            ly,
            search:{
     
     },
            goodsList:[],
            totalPage: 0,
            total: 0,
            selectedSku:{
     
     }
        },
        created(){
     
     
            //获取请求参数
           const search = ly.parse(location.search.substring(1));
           this.search = search;
            //发送后台
            this.loadData();
        },
        methods:{
     
     
          loadData(){
     
     
            //发送到后台
            ly.http.post("/search/page",this.search).then(resp =>{
     
     
                //保存分页结果
                this.total = resp.data.total;
                this.totalPage = resp.data.totalPage;
                //保存当前页商品
                resp.data.items.forEach(goods =>{
     
     
                    //把json处理成JS对象
                    goods.skus = JSON.parse(goods.skus);
                    //初始化被选中的sku
                    goods.selectedSku = goods.skus[0];
                })
                this.goodsList = resp.data.items;
            }).catch(error =>{
     
     

            })
          }
        },
        components:{
     
     
            lyTop: () => import("./js/pages/top.js")
        }
    });
</script>
<!-- 底部栏位 -->
<!--页面底部,由js动态加载-->
<div class="clearfix footer"></div>
<script type="text/javascript">$(".footer").load("foot.html");</script>
<!--页面底部END-->

</body >
<!--购物车单元格 模板-->
<script type="text/template" id="tbar-cart-item-template">
    <div class="tbar-cart-item">
        <div class="jtc-item-promo">
            <em class="promo-tag promo-mz">满赠<i class="arrow"></i></em>
            <div class="promo-text">已购满600元,您可领赠品</div>
        </div>
        <div class="jtc-item-goods">
            <span class="p-img"><a href="#" target="_blank"><img src="{2}" alt="{1}" height="50" width="50"/></a></span>
            <div class="p-name">
                <a href="#">{
     
     1}</a>
            </div>
            <div class="p-price"><strong>¥{
     
     3}</strong>×{
     
     4}</div>
            <a href="#none" class="p-del J-del">删除</a>
        </div>
    </div>
</script>
<!--侧栏面板结束-->
<script type="text/javascript" src="js/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript">
    $(function () {
     
     
        $("#service").hover(function () {
     
     
            $(".service").show();
        }, function () {
     
     
            $(".service").hide();
        });
        $("#shopcar").hover(function () {
     
     
            $("#shopcarlist").show();
        }, function () {
     
     
            $("#shopcarlist").hide();
        });

    })
</script>
<script type="text/javascript" src="js/model/cartModel.js"></script>
<script type="text/javascript" src="js/czFunction.js"></script>
<script type="text/javascript" src="js/plugins/jquery.easing/jquery.easing.min.js"></script>
<script type="text/javascript" src="js/plugins/sui/sui.min.js"></script>
<script type="text/javascript" src="js/widget/cartPanelView.js"></script>


</html>

结果:
在这里插入图片描述
至此,今天的学习结束。

猜你喜欢

转载自blog.csdn.net/zcylxzyh/article/details/100637514
今日推荐