5.1基本搜索

1.索引库数据导入

昨天我们学习了Elasticsearch的基本应用。今天就学以致用,搭建搜索微服务,实现搜索功能。

1.1.创建module

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

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

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>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- elasticsearch -->
        <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.17.131:9300
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    fetch-registry-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;

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 11:26
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(LySearchApplication.class);
    }
}

1.2.索引库数据格式分析

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

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

1.2.1.以结果为导向

大家来看下搜索结果页:

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

可以看到,每一个搜索结果都有至少1个商品,当我们选择大图下方的小图,商品会跟着变化。

因此,搜索的结果是SPU,即多个SKU的集合

既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。

1.2.2.需要什么数据

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

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

直观能看到的:图片、价格、标题、副标题

暗藏的数据:spu的id,sku的id

另外,页面还有过滤条件:

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

这些过滤条件也都需要存储到索引库中,包括:

商品分类、品牌、可用来搜索的规格参数等

综上所述,我们需要的数据格式有:

spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数

1.2.3.最终的数据结构

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

package com.leyou.search.pojo;

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.List;
import java.util.Map;

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 11:37
 */
@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 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:红色

1.3.商品微服务提供接口

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

先思考我们需要的数据:

  • SPU信息

  • SKU信息

  • SPU的详情

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

  • 品牌名称

  • 规格参数

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

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

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

在CategoryController中添加接口:

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

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7AagA7ep-1589298964476)(assets/测试分类和品牌查询的接口.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Ddf46bt-1589298964478)(assets/测试分类和品牌查询的接口2.png)]

1.4.FeignClient调用服务接口

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

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 23:14
 */
@FeignClient("item-service")
public interface CategoryClient {
    @GetMapping("category/list/ids")
    List<Category> queryCategoryByIds(@RequestParam("ids") List<Long> ids);
}

项目解构:

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

测试类:

生成测试类方法:鼠标移动至CategoryClient类名上,点击小黄灯,点击create Test

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

测试结果:

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

1.5.借用GoodsFeignClient思考问题

我们还是按照上面的方法编写GoodsFeignClient

@FeignClient(value = "item-service")
public interface GoodsClient {

    /**
     * 分页查询商品
     * @param page
     * @param rows
     * @param saleable
     * @param key
     * @return
     */
    @GetMapping("/spu/page")
    PageResult<SpuBo> querySpuByPage(
            @RequestParam(value = "page", defaultValue = "1") Integer page,
            @RequestParam(value = "rows", defaultValue = "5") Integer rows,
            @RequestParam(value = "saleable", defaultValue = "true") Boolean saleable,
            @RequestParam(value = "key", required = false) String key);

    /**
     * 根据spu商品id查询详情
     * @param id
     * @return
     */
    @GetMapping("/spu/detail/{id}")
    SpuDetail querySpuDetailById(@PathVariable("id") Long id);

    /**
     * 根据spu的id查询sku
     * @param id
     * @return
     */
    @GetMapping("sku/list")
    List<Sku> querySkuBySpuId(@RequestParam("id") Long id);
}

以上的这些代码直接从商品微服务中拷贝而来,完全一致。差别就是没有方法的具体实现。大家觉得这样有没有问题?

而FeignClient代码遵循SpringMVC的风格,因此与商品微服务的Controller完全一致。这样就存在一定的问题:

  • 代码冗余。尽管不用写实现,只是写接口,但服务调用方要写与服务controller一致的代码,有几个消费者就要写几次。
  • 增加开发成本。调用方还得清楚知道接口的路径,才能编写正确的FeignClient。

1.6.解决方案

因此,一种比较友好的实践是这样的:

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

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

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

商品分类服务接口:

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

商品服务接口,返回值不再使用ResponseEntity:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 23:50
 */
public interface GoodsApi {
    /**
     * 根据spu的id查询详情detail
     *
     * @param spuId
     * @return
     */
    @GetMapping("/spu/detail/{id}")
    SpuDetail queryDetailById(@PathVariable("id") Long spuId);

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

    /**
     * 分页查询spu
     *
     * @param page
     * @param rows
     * @param saleable
     * @param key
     * @return
     */
    @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  //搜索
    );
}

品牌的接口:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 23:59
 */
public interface BrandApi {
    /**
     * 根据id查询品牌
     * @param id
     * @return
     */
    @GetMapping("brand/{id}")
    public ResponseEntity<Brand> queryBrandById(@PathVariable("id") Long id);
}

规格参数的接口:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/11 0:01
 */
public interface SpecificationApi {
    /**
     * 查询参数集合
     * @param gid 组id
     * @param cid 分类id
     * @param searching 是否搜索
     * @return
     */
    @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
    ) ;
}

需要引入springMVC及leyou-common的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>leyou-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

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

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

商品的FeignClient:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 23:38
 */
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {

}

商品分类的FeignClient:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/10 23:14
 */
@FeignClient("item-service")
public interface CategoryClient extends CategoryApi {
}

品牌的FeignClient:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/11 0:03
 */
@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}

规格参数的FeignClient:

/**
 * @description
 * @Author: mty
 * @Date:2020/5/11 0:03
 */
@FeignClient("item-service")
public interface SpeccificationClient extends SpecificationApi {
}

是不是简单多了?

继续测试:

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

1.7.导入数据

1.7.1.创建GoodsRepository

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

java代码:

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

1.7.2.创建索引

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

/**
 * @description
 * @Author: mty
 * @Date:2020/5/11 0:22
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest {
    @Autowired
    private GoodsRepository goodsReponsitory;

    @Autowired
    private ElasticsearchTemplate template;

    @Test
    public void createIndex(){
        // 创建索引库,以及映射
        this.template.createIndex(Goods.class);
        this.template.putMapping(Goods.class);
    }
}

通过kibana查看:乐优项目创建goods索引库

{
  "goods": {
    "aliases": {},
    "mappings": {
      "docs": {
        "properties": {
          "all": {
            "type": "text",
            "analyzer": "ik_max_word"
          },
          "skus": {
            "type": "keyword",
            "index": false
          },
          "subTitle": {
            "type": "keyword",
            "index": false
          }
        }
      }
    },
    "settings": {
      "index": {
        "refresh_interval": "1s",
        "number_of_shards": "1",
        "provided_name": "goods",
        "creation_date": "1589128624360",
        "store": {
          "type": "fs"
        },
        "number_of_replicas": "0",
        "uuid": "I7a16Q6_RFyOBUKeGZuv4A",
        "version": {
          "created": "6020499"
        }
      }
    }
  }
}

1.7.3.导入数据

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

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.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.SpeccificationClient;
import com.leyou.search.pojo.Goods;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

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

/**
 * @description
 * @Author: mty
 * @Date:2020/5/11 10:07
 */
@Service
public class SearchService {
    @Autowired
    private CategoryClient categoryClient;
    @Autowired
    private BrandClient brandClient;
    @Autowired
    private GoodsClient goodsClient;
    @Autowired
    private SpeccificationClient specClient;

    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);
        }
        //将categories中的cid转化成字符串
        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_FOND);
        }
        //对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());
        }
//        Set<Long> priceList = skuList.stream().map(Sku::getPrice).collect(Collectors.toSet());

        //查询规格参数
        List<SpecParam> params = specClient.queryParamList(null, spu.getCid3(), true);
        if (CollectionUtils.isEmpty(params)) {
            throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOND);
        }
        //查询商品详情
        SpuDetail detail = goodsClient.queryDetailById(spuId);
        //获取通用规格参数
        Map<Long, String> genericSpec = JsonUtils.parseMap(detail.getGenericSpec(), Long.class, String.class);
        //获取特有规格参数
        Map<Long, List<String>> specialSpec = JsonUtils
                .nativeRead(detail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
                });
        //规格参数,key是规格参数的名称,值是规格参数的值
        Map<String, Object> specs = new HashMap<>();
        for (SpecParam param : params) {
            //规格名称
            String key = param.getName();
            Object value = "";
            //判断是否是通用规格
            if (param.getGeneric()) {
                value = genericSpec.get(param.getId());
                //判断是否是数值类型
                if (param.getNumeric()) {
                    value = chooseSegment(value.toString(), param);

                }
            } else {
                value = specialSpec.get(param.getId());
            }
            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.serialize(skuList));// 所有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;
    }
}

因为过滤参数中有一类比较特殊,就是数值区间:

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

所以我们在存入时要进行处理:

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

然后编写一个测试类,循环查询Spu,然后调用IndexService中的方法,把SPU变为Goods,然后写入索引库:

@Test
    public void loadData() {
        int page = 1;
        int rows = 100;
        int size = 0;
        do {
            //查询spu信息
            PageResult<Spu> result = goodsClients.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());

            //存入索引库
            goodsReponsitory.saveAll(goodsList);

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

通过kibana查询, 可以看到数据成功导入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQBpXtnO-1589298964491)(assets/goods导入数据.png)]

2.实现基本搜索

2.1.页面分析

2.1.1.页面跳转

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

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

当我们输入任何文本,点击搜索,就会跳转到搜索页search.html了:

并且将搜索关键字以请求参数携带过来:

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

我们打开search.html,在最下面会有提前定义好的Vue实例:

<script type="text/javascript">
    var vm = new Vue({
        el: "#searchApp",
        data: {
            search:{}
        },
        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 => {
                    console.log(resp);
                }).catch(error => {

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

这个Vue实例中,通过import导入的方式,加载了另外一个js:top.js并作为一个局部组件。top其实是页面顶部导航组件,我们暂时不管

2.1.2.发起异步请求

要想在页面加载后,就展示出搜索结果。我们应该在页面加载时,获取地址栏请求参数,并发起异步请求,查询后台数据,然后在页面渲染。

我们在data中定义一个对象,记录请求的参数:

data: {
        search:{}
    }

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

 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 => {
                    console.log(resp);
                }).catch(error => {

                })
            }
        }
  • 我们这里使用ly是common.js中定义的工具对象。
  • 这里使用的是post请求,这样可以携带更多参数,并且以json格式发送

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wYWqyTEA-1589298964496)(assets/添加允许信任域名.png)]

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

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

刷新页面试试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfV7gFfN-1589298964499)(assets/异步请求接口测试页面.png)]

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

2.2.后台提供搜索接口

2.2.1.controller

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

首先分析几个问题:

  • 请求方式:Post

  • 请求路径:/search/page,不过前面的/search应该是网关的映射路径,因此真实映射路径page,代表分页查询

  • 请求参数:json格式,目前只有一个属性:key-搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此我们可以用一个对象来接收请求的json数据:

    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;
            }
            // 获取页码时做一些校验,不能小于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;

/**
 * @description
 * @Author: mty
 * @Date:2020/5/11 17:11
 */
@RestController
public class SearchController {
    @Autowired
    private SearchService searchService;
    /**
     * 搜索功能
     * @param request
     * @return
     */
    @PostMapping("page")
    public ResponseEntity<PageResult<Goods>> search(@RequestBody SearchRequest request){
        return ResponseEntity.ok(searchService.search(request));
    }
}

2.2.2.service

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

添加search方法

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 = goodsRepository.search(queryBuilder.build());
        //4解析结果
        long totalElements = result.getTotalElements();
        int totalPages = result.getTotalPages();
        List<Goods> goodsList = result.getContent();
        return new PageResult<>(totalElements, totalPages, goodsList);
}

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

2.2.3.测试

刷新页面测试:

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

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

数据是查到了,但是因为我们只查询部分字段,所以结果json 数据中有很多null,这很不优雅。

解决办法很简单,在leyou-search的application.yml中添加一行配置,json处理时忽略空值:

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

结果:

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

2.3.页面渲染

页面已经拿到了结果,接下来就要渲染样式了。

2.3.1.保存搜索结果

首先,在data中定义属性,保存搜索的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tx7qLADH-1589298964507)(assets/搜索结果属性定义.png)]

loadData的异步查询中,将结果赋值给

goodsList:[],
total: 0,
totalPage: 0,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EiEoCDP1-1589298964509)(assets/给所搜结果赋值.png)]

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MlRopGIB-1589298964510)(assets/测试保存的数据.png)]

2.3.2.循环展示商品

在search.html的中部,有一个div,用来展示所有搜索到的商品:

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

可以看到,div中有一个无序列表ul,内部的每一个li就是一个商品spu了。

我们删除多余的,只保留一个li,然后利用vue的循环来展示搜索到的结果:

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

2.3.3.多sku展示

2.3.3.1.分析

接下来展示具体的商品信息,来看图:

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

这里我们可以发现,一个商品位置,是多个sku的信息集合。当用户鼠标选择某个sku,对应的图片、价格、标题会随之改变!

我们先来实现sku的选择,才能去展示不同sku的数据。

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

可以看到,在列表中默认第一个是被选中的,那我们就需要做两件事情:

  • 在搜索到数据时,先默认把第一个sku作为被选中的,记录下来

  • 记录当前被选中的是哪一个sku,记录在哪里比较合适呢?显然是遍历到的goods对象自己内部,因为每一个goods都会有自己的sku信息。

2.3.3.2.初始化sku

查询出的结果集skus是一个json类型的字符串,不是js对象

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

我们在查询成功的回调函数中,对goods进行遍历,把skus转化成json对象集合,并添加一个selected属性保存被选中的sku:

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

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

2.3.3.3.多sku图片列表

接下来,我们看看多个sku的图片列表位置:

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

看到又是一个无序列表,这里我们也一样删掉多余的,保留一个li,需要注意选中的项有一个样式类:selected

我们的代码:

<!--多sku图片列表-->
<ul class="skus">
    <li :class="{selected: sku.id == goods.selected.id}" v-for="sku in goods.skus" :key="sku.id"
        @mouseOver="goods.selected=sku">
        <img :src="sku.image">
    </li>
</ul>

注意:

  • class样式通过 goods.selected的id是否与当前sku的id一致来判断
  • 绑定了鼠标事件,鼠标进入后把当前sku赋值到goods.selected

2.3.4.展示sku其它属性

现在,我们已经可以通过goods.selected获取用户选中的sku,那么我们就可以在页面展示了:

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

刷新页面:

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

看起来很完美是吧!

但其实有一些瑕疵

2.3.5.几个问题

2.3.5.1.价格显示的是分

首先价格显示就不正确,我们数据库中存放的是以分为单位,所以这里要格式化。

好在我们之前common.js中定义了工具类,可以帮我们转换。

改造:

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

结果报错:

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

为啥?

因为在Vue范围内使用任何变量,都会默认去Vue实例中寻找,我们使用ly,但是Vue实例中没有这个变量。所以解决办法就是把ly记录到Vue实例:

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

然后刷新页面:

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

2.3.5.2.标题过长

标题内容太长了,已经无法完全显示,怎么办?

截取一下:

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

最好在加个悬停展示所有内容的效果

2.3.5.3.sku点击不切换

还有一个错误比较隐蔽,不容易被发现。我们点击sku 的图片列表,发现没有任何变化。

这不科学啊,为什么?

这是因为Vue的自动渲染是基于对象的属性变化的。比如页面使用GoodsList进行渲染,如果GoodsList变化,或者其内部的任何子对象变化,都会Vue感知,从而从新渲染页面。

然而,这一切有一个前提,那就是当你第一次渲染时,对象中有哪些属性,Vue就只监视这些属性,后来添加的属性发生改变,是不会被监视到的。

而我们的goods对象中,本身是没有selected属性的,是我们后来才添加进去的:

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

这段代码稍微改造一下,即可:

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

也就是说,我们先把selected属性初始化完毕,然后才把整个对象赋值给goodsList,这样,goodsList已初始化时就有selected属性,以后就会被正常监控了。

.3.4.展示sku其它属性

现在,我们已经可以通过goods.selected获取用户选中的sku,那么我们就可以在页面展示了:

[外链图片转存中…(img-IgYFOy6F-1589298964526)]

刷新页面:

[外链图片转存中…(img-5kSo4LBa-1589298964527)]

看起来很完美是吧!

但其实有一些瑕疵

2.3.5.几个问题

2.3.5.1.价格显示的是分

首先价格显示就不正确,我们数据库中存放的是以分为单位,所以这里要格式化。

好在我们之前common.js中定义了工具类,可以帮我们转换。

改造:

[外链图片转存中…(img-ccrb1cdk-1589298964530)]

结果报错:

[外链图片转存中…(img-XSwkEL2A-1589298964532)]

为啥?

因为在Vue范围内使用任何变量,都会默认去Vue实例中寻找,我们使用ly,但是Vue实例中没有这个变量。所以解决办法就是把ly记录到Vue实例:

[外链图片转存中…(img-tWYmtY6f-1589298964534)]

然后刷新页面:

[外链图片转存中…(img-81WodUOQ-1589298964536)]

2.3.5.2.标题过长

标题内容太长了,已经无法完全显示,怎么办?

截取一下:

[外链图片转存中…(img-xGGI4vpO-1589298964538)]

最好在加个悬停展示所有内容的效果

2.3.5.3.sku点击不切换

还有一个错误比较隐蔽,不容易被发现。我们点击sku 的图片列表,发现没有任何变化。

这不科学啊,为什么?

这是因为Vue的自动渲染是基于对象的属性变化的。比如页面使用GoodsList进行渲染,如果GoodsList变化,或者其内部的任何子对象变化,都会Vue感知,从而从新渲染页面。

然而,这一切有一个前提,那就是当你第一次渲染时,对象中有哪些属性,Vue就只监视这些属性,后来添加的属性发生改变,是不会被监视到的。

而我们的goods对象中,本身是没有selected属性的,是我们后来才添加进去的:

[外链图片转存中…(img-tOlG0NQ2-1589298964540)]

这段代码稍微改造一下,即可:

[外链图片转存中…(img-OpAB3lGT-1589298964542)]

也就是说,我们先把selected属性初始化完毕,然后才把整个对象赋值给goodsList,这样,goodsList已初始化时就有selected属性,以后就会被正常监控了。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是 C 语言代码实现: ```c #include <stdio.h> #include <stdlib.h> // 定义二叉树结点结构体 typedef struct TreeNode { int data; struct TreeNode *left; // 左子树指针 struct TreeNode *right; // 右子树指针 } TreeNode, *TreeNodePtr; // 二叉树建立函数 void createBinaryTree(TreeNodePtr *root) { int data; scanf("%d", &data); if (data == -1) { // 输入-1表示该节点为空 *root = NULL; } else { *root = (TreeNodePtr) malloc(sizeof(TreeNode)); (*root)->data = data; createBinaryTree(&((*root)->left)); createBinaryTree(&((*root)->right)); } } // 中序遍历函数 void inorderTraversal(TreeNodePtr root) { if (root != NULL) { inorderTraversal(root->left); printf("%d ", root->data); inorderTraversal(root->right); } } // 前序遍历函数 void preorderTraversal(TreeNodePtr root) { if (root != NULL) { printf("%d ", root->data); preorderTraversal(root->left); preorderTraversal(root->right); } } // 后序遍历函数 void postorderTraversal(TreeNodePtr root) { if (root != NULL) { postorderTraversal(root->left); postorderTraversal(root->right); printf("%d ", root->data); } } // 计算树的深度 int treeDepth(TreeNodePtr root) { if (root == NULL) { return 0; } else { int leftDepth = treeDepth(root->left); int rightDepth = treeDepth(root->right); return (leftDepth > rightDepth) ? (leftDepth + 1) : (rightDepth + 1); } } // 统计结点个数 int countNodes(TreeNodePtr root) { if (root == NULL) { return 0; } else { return (countNodes(root->left) + countNodes(root->right) + 1); } } // 统计叶子结点个数 int countLeaves(TreeNodePtr root) { if (root == NULL) { return 0; } else if (root->left == NULL && root->right == NULL) { return 1; } else { return (countLeaves(root->left) + countLeaves(root->right)); } } // 统计度为 1 的结点个数 int countDegreeOneNodes(TreeNodePtr root) { if (root == NULL) { return 0; } else if ((root->left != NULL && root->right == NULL) || (root->left == NULL && root->right != NULL)) { return (countDegreeOneNodes(root->left) + countDegreeOneNodes(root->right) + 1); } else { return (countDegreeOneNodes(root->left) + countDegreeOneNodes(root->right)); } } // 输出从叶子结点到根结点的路径 void printPathFromLeafToRoot(TreeNodePtr root, int path[], int pathLen) { if (root == NULL) { return; } path[pathLen] = root->data; pathLen++; if (root->left == NULL && root->right == NULL) { // 如果是叶子结点,输出路径 printf("%d: ", path[0]); for (int i = 1; i < pathLen; i++) { printf("%d ", path[i]); } printf("\n"); } else { printPathFromLeafToRoot(root->left, path, pathLen); printPathFromLeafToRoot(root->right, path, pathLen); } } int main() { TreeNodePtr root; printf("请输入二叉树中各节点的值,-1表示该节点为空:\n"); createBinaryTree(&root); printf("中序遍历结果:"); inorderTraversal(root); printf("\n"); printf("前序遍历结果:"); preorderTraversal(root); printf("\n"); printf("后序遍历结果:"); postorderTraversal(root); printf("\n"); printf("树的深度为:%d\n", treeDepth(root)); printf("树的结点个数为:%d\n", countNodes(root)); printf("树的叶子结点个数为:%d\n", countLeaves(root)); printf("树的度为 1 的结点个数为:%d\n", countDegreeOneNodes(root)); int path[100]; printf("从每个叶子结点到根结点的路径:\n"); printPathFromLeafToRoot(root, path, 0); return 0; } ``` 注意:以上实现是二叉树的基本操作,但二叉树的操作还有很多,例如:搜索树、平衡树、堆、哈夫曼树等等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值