java电商项目(五)

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 本文介绍了如何构建一个基于Elasticsearch的商品搜索微服务,主要包括以下几个部分:1. **数据导入ES**: - 搭建搜索工程,创建`legou-search`项目,提供搜索服务和索引数据更新操作。 - 配置`pom.xml`文件,引入必要的依赖。 - 创建启动器和配置文件,配置Elasticsearch连接信息。 - 分析索引库数据格式,确定需要存储的数据字段。 - 实现商品微服务接口,调用其他微服务获取所需数据。 - 创建索引并导入数据,将SPU和SKU数据转换为索引库中的Goods对象。2. **商品搜索*

[TOC]

1. 数据导入ES

创建搜索微服务工程,legou-search,该工程主要提供搜索服务以及索引数据的更新操作。

1.1 搭建搜索工程

legou-search/pom.xml

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

    <artifactId>legou-search</artifactId>

    <packaging>pom</packaging>
    <modules>
        <module>legou-search-interface</module>
        <module>legou-search-service</module>
    </modules>


</project>

legou-search/legou-search-interface/pom.xml

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

    <artifactId>legou-search-interface</artifactId>

    <dependencies>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!--elasticsearch-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

        <!--商品微服务-->
        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-item-interface</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.core.Starter</mainClass>
                    <layout>ZIP</layout>
                    <classifier>exec</classifier>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

legou-search/legou-search-service/pom.xml

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

    <artifactId>legou-search-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-search-interface</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <!--elasticsearch-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

        <!--商品微服务-->
        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-item-interface</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.lxs</groupId>
            <artifactId>legou-common</artifactId>
            <version>${project.version}</version>
        </dependency>


    </dependencies>


</project>

1.2启动器和配置文件

legou-search/legou-search-service/src/main/resources/bootstrap.yml

spring:
  application:
    name: search-service
  # 多个接口上的@FeignClient(“相同服务名”)会报错,overriding is disabled。
  # 设置 为true ,即 允许 同名
  main:
    allow-bean-definition-overriding: true

config-repo/search-service.yml

server:
  port: 9006

logging:
  #file: demo.log
  pattern:
    console: "%d - %msg%n"
  level:
    org.springframework.web: debug
    com.lxs: debug

security:
  oauth2:
    resource:
      jwt:
        key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签

spring:
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: 192.168.220.110:9300
  elasticsearch:
    rest:
      uris: 192.168.220.110:9200

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

legou-search/legou-search-service/src/main/java/com/lxs/legou/search/SearchApplication.java

package com.lxs.legou.search;

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

@SpringBootApplication //spring boot
@EnableDiscoveryClient //将微服务注册到注册中心
@EnableFeignClients //通过feign调用其他微服务
@EnableCircuitBreaker //开启熔断,微服务容错保护
public class SearchApplication {
   

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

}

1.3.索引库数据格式分析

我们需要商品数据导入索引库,便于用户搜索。我们有SPU和SKU,到底如何保存到索引库?

1.3.1.以结果为导向

大家来看下搜索结果页:

1532180648745

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

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

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

1.3.2.需要什么数据

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

1526607712207

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

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

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

1526608095471

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

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

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

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

1.3.3.最终的数据结构

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

package com.lxs.legou.search.po;

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

@Data
@Document(indexName = "goods_legou", type = "docs_legou", 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;// 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:红色
  • ES5.0及以后的版本取消了string类型,将原先的string类型拆分为textkeyword两种类型。它们的区别在于text会对字段进行分词处理而keyword则不会。

  • 当你没有以IndexTemplate等形式为你的索引字段预先指定mapping的话,ES就会使用Dynamic Mapping,通过推断你传入的文档中字段的值对字段进行动态映射。例如传入的文档中字段price的值为12,那么price将被映射为long类型;字段addr的值为"192.168.0.1",那么addr将被映射为ip类型。然而对于不满足ip和date格式的普通字符串来说,情况有些不同:ES会将它们映射为text类型,但为了保留对这些字段做精确查询以及聚合的能力,又同时对它们做了keyword类型的映射,作为该字段的fields属性写到_mapping中。例如,当ES遇到一个新的字段"foobar": "some string"时,会对它做如下的Dynamic Mapping:

{
    "foobar": {
        "type" "text",
        "fields": {
            "keyword": {
                "type": "keyword",
                "ignore_above": 256
            }
        }
    }
}

又比如商场中的CPU品牌

"specs" : {
  "properties" : {
    "CPU品牌" : {
      "type" : "text",
      "fields" : {
        "keyword" : {
          "type" : "keyword",
          "ignore_above" : 256
        }
      }
    }
  • 在之后的查询中使用specs.CPU品牌是将specs.CPU品牌作为text类型查询,而使用specs.CPU品牌.keyword则是将specs.CPU品牌作为keyword类型查询。前者会对查询内容做分词处理之后再匹配,而后者则是直接对查询结果做精确匹配。

  • ES的term query做的是精确匹配而不是分词查询,因此对text类型的字段做term查询将是查不到结果的(除非字段本身经过分词器处理后不变,未被转换或分词)。此时,必须使用specs.CPU品牌.keyword来对specs.CPU品牌字段以keyword类型进行精确匹配。

1.4 商品微服务提供接口

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

先思考我们需要的数据:

  • SPU信息

  • SKU信息

  • SPU的详情

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

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

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

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

1.4.1 查询分类

使用OpenFeign调用流程

legou-search/legou-search-service/src/main/java/com/lxs/legou/search/client/CategoryClient.java

package com.lxs.legou.search.client;

import com.lxs.legou.item.api.CategoryApi;
import com.lxs.legou.item.po.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@FeignClient(name = "item-service", fallback = CategoryClient.CategoryClientFallback.class)
public interface CategoryClient extends CategoryApi {

    @Component
    @RequestMapping("/category-fallback") //这个可以避免容器中requestMapping重复
    class CategoryClientFallback implements CategoryClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(CategoryClientFallback.class);

        @Override
        public List<String> queryNameByIds(List<Long> ids) {
            LOGGER.info("异常发生,进入fallback方法");
            return null;
        }

        @Override
        public List<Category> list(Category category) {
            LOGGER.info("异常发生,进入fallback方法");
            return null;
        }

        @Override
        public Category edit(Long id) {
            LOGGER.info("异常发生,进入fallback方法");
            return null;
        }
    }

}

CategoryApi

package com.lxs.legou.item.api;

import com.lxs.legou.core.json.JSON;
import com.lxs.legou.core.po.BaseEntity;
import com.lxs.legou.item.po.Category;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequestMapping("/item/category")
public interface CategoryApi {

    @ApiOperation(value="根据ids查询names", notes = "根据分类id查询名称列表")
    @GetMapping("/names")
    public List<String> queryNameByIds(@RequestParam("ids") List<Long> ids);

    @ApiOperation(value="查询", notes="根据实体条件查询")
    @RequestMapping(value = "/list")
    public List<Category> list(Category category);

    @ApiOperation(value="加载", notes="根据ID加载")
    @GetMapping("/edit/{id}")
    public Category edit(@PathVariable Long id);



}

CategoryController

package com.lxs.legou.item.controller;

import com.lxs.legou.core.controller.BaseController;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.service.ICategoryService;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @Title: 分类控制器
 */
@RestController
@RequestMapping("/category")
public class CategoryController extends BaseController<ICategoryService, Category> {

    @ApiOperation(value="根据ids查询names", notes = "根据分类id查询名称列表")
    @GetMapping("/names")
    public ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids") List<Long> ids) {
        List<String> names = service.selectNamesByIds(ids);

        if (null == names || names.size() == 0) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }

        return ResponseEntity.ok(names);
    }

}

CategoryServiceImpl

@Service
public class CategoryServiceImpl extends CrudServiceImpl<Category> implements ICategoryService {

    @Override
    public List<String> selectNamesByIds(List<Long> ids) {
        QueryWrapper<Category> queryWrapper = Wrappers.<Category>query().in("id_", ids);
        return getBaseMapper().selectList(queryWrapper).stream().map(item -> item.getTitle()).collect(Collectors.toList());
    }
}

CategoryService接口

public interface ICategoryService extends ICrudService<Category> {

    public List<String> selectNamesByIds(List<Long> ids);

}

1.4.2 查询品牌

legou-search/legou-search-service/src/main/java/com/lxs/legou/search/client/BrandClient.java

package com.lxs.legou.search.client;

import com.lxs.legou.item.api.BrandApi;
import com.lxs.legou.item.po.Brand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@FeignClient(name = "item-service", fallback = BrandClient.BrandClientFallback.class)
public interface BrandClient extends BrandApi {

    @Component
    @RequestMapping("/brand-fallback") //这个可以避免容器中requestMapping重复
    class BrandClientFallback implements BrandClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(BrandClientFallback.class);

        @Override
        public List<Brand> selectBrandByIds(List<Long> ids) {
            LOGGER.info("异常发生,进入fallback方法");
            return null;
        }
    }

}

提供方法根据品牌id查询品牌名称,拼接all

package com.lxs.legou.item.api;

import com.lxs.legou.item.po.Brand;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@RequestMapping("/item/brand")
public interface BrandApi {

    @ApiOperation(value="根据ids查询", notes = "根据ids查询")
    @GetMapping("/list-by-ids")
    public List<Brand> selectBrandByIds(@RequestParam("ids") List<Long> ids);

}

BrandController

public class BrandController extends BaseController<IBrandService, Brand> {

   @ApiOperation(value="根据ids查询", notes = "根据ids查询")
   @GetMapping("/list-by-ids")
   public List<Brand> selectBrandByIds(@RequestParam("ids") List<Long> ids) {
      return service.selectBrandByIds(ids);
   }

}

1.4.3 查询SKU

legou-search/legou-search-service/src/main/java/com/lxs/legou/search/client/SkuClient.java

package com.lxs.legou.search.client;

import com.lxs.legou.item.api.SkuApi;
import com.lxs.legou.item.po.Sku;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@FeignClient(name = "item-service", fallback = SkuClient.SkuClientFallback.class)
public interface SkuClient extends SkuApi {

    @Component
    @RequestMapping("/sku-fallback")
            //这个可以避免容器中requestMapping重复
    class SkuClientFallback implements SkuClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(SkuClientFallback.class);

        @Override
        public List<Sku> selectSkusBySpuId(Long spuId) {
            LOGGER.error("异常发生,进入fallback方法");
            return null;
        }
    }

}

SkuApi

package com.lxs.legou.item.api;

import com.lxs.legou.item.po.Sku;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@RequestMapping(value = "/item/sku")
public interface SkuApi {

    @ApiOperation(value="查询spu对应的sku", notes="根据spuId查询sku集合")
    @GetMapping("/select-skus-by-spuid/{id}")
    public List<Sku> selectSkusBySpuId(@PathVariable("id") Long spuId);


}

根据spuID查询sku列表

package com.lxs.legou.item.api;

import com.lxs.legou.item.po.Sku;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@RequestMapping(value = "/sku")
public interface SkuApi {

    @ApiOperation(value="查询spu对应的sku", notes="根据spuId查询sku集合")
    @GetMapping("/select-skus-by-spuid/{id}")
    public List<Sku> selectSkusBySpuId(@PathVariable("id") Long spuId);


}

SkuController

@RestController
@RequestMapping(value = "/sku")
public class SkuController extends BaseController<ISkuService, Sku> {

    @ApiOperation(value="查询spu对应的sku", notes="根据spuId查询sku集合")
    @GetMapping("/select-skus-by-spuid/{id}")
    public List<Sku> selectSkusBySpuId(@PathVariable("id") Long spuId) {
        Sku sku = new Sku();
        sku.setSpuId(spuId);
        return service.list(sku);
    }


}

SkuServiceImpl

@Service
public class SkuServiceImpl extends CrudServiceImpl<Sku> implements ISkuService {

    @Override
    public List<Sku> list(Sku entity) {
        QueryWrapper<Sku> queryWrapper = Wrappers.<Sku>query();
        if (entity.getSpuId() != null) {
            queryWrapper.eq("spu_id_", entity.getSpuId());
        }
        return getBaseMapper().selectList(queryWrapper);
    }

}

1.4.4 查询SPU

package com.lxs.legou.search.client;

import com.lxs.legou.item.api.SpuApi;
import com.lxs.legou.item.po.Spu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@FeignClient(name = "item-service", fallback = SpuClient.SpuClientFallback.class)
public interface SpuClient extends SpuApi {

    @Component
    @RequestMapping("/spu-fallback") //这个可以避免容器中requestMapping重复
    class SpuClientFallback implements SpuClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(SpuClientFallback.class);

        @Override
        public List<Spu> selectAll() {
            LOGGER.error("异常发生,进入fallback方法");
            return null;
        }
    }

}

查询所有spu

package com.lxs.legou.item.api;

import com.lxs.legou.item.po.Spu;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@RequestMapping(value = "/item/spu")
public interface SpuApi {

    @ApiOperation(value="查询所有", notes="查询所有spu")
    @GetMapping("/list-all")
    public List<Spu> selectAll();


}

SpuController

@ApiOperation(value="查询所有", notes="查询所有spu")
@GetMapping("/list-all")
public List<Spu> selectAll() {
    return service.list(new Spu());
}

1.4.5 查询SpuDetail

package com.lxs.legou.search.client;

import com.lxs.legou.item.api.SpuDetailApi;
import com.lxs.legou.item.po.SpuDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient(name = "item-service", fallback = SpuDetailClient.SpuDetailFallback.class)
public interface SpuDetailClient extends SpuDetailApi {

    @Component
    @RequestMapping("/spu-detail-fallback") //这个可以避免容器中requestMapping重复
    class SpuDetailFallback implements SpuDetailClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(SpuDetailFallback.class);

        @Override
        public SpuDetail edit(Long id) {
            System.out.println("异常发生,进入fallback方法");
            return null;
        }
    }

}

根据id 查询SpuDetail

package com.lxs.legou.item.api;

import com.lxs.legou.item.po.SpuDetail;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping(value = "/item/spu-detail")
public interface SpuDetailApi {

    /**
     * 加载
     *
     * @param id
     * @return
     * @throws Exception
     */
    @ApiOperation(value="加载", notes="根据ID加载")
    @GetMapping("/edit/{id}")
    public SpuDetail edit(@PathVariable Long id);

}

1.4.6 查询规格参数

package com.lxs.legou.search.client;

import com.lxs.legou.item.api.SpecParamApi;
import com.lxs.legou.item.po.SpecParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@FeignClient(name = "item-service", fallback = SpecParamClient.SpecParamClientFallback.class)
public interface SpecParamClient extends SpecParamApi {

    @Component
    @RequestMapping("/param-fallback") //这个可以避免容器中requestMapping重复
    class SpecParamClientFallback implements SpecParamClient {

        private static final Logger LOGGER = LoggerFactory.getLogger(SpecParamClientFallback.class);

        @Override
        public List<SpecParam> selectSpecParamApi(SpecParam entity) {
            LOGGER.error("异常发生,进入fallback方法");
            return null;
        }
    }

}

根据实体条件查询规格参数

package com.lxs.legou.item.api;

import com.lxs.legou.item.po.SpecParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@RequestMapping(value = "/item/param")
public interface SpecParamApi {

    @ApiOperation(value="查询", notes="根据实体条件查询参数")
    @PostMapping(value = "/select-param-by-entity", consumes = "application/json")
    public List<SpecParam> selectSpecParamApi(@RequestBody SpecParam entity);


}

SpecParamController

@RestController
@RequestMapping(value = "/param")
public class SpecParamController extends BaseController<ISpecParamService, SpecParam> {

    @ApiOperation(value="查询", notes="根据实体条件查询参数")
    @PostMapping(value = "/select-param-by-entity")
    public List<SpecParam> selectSpecParamApi(@RequestBody SpecParam entity) {
        return service.list(entity);
    }


}

SpecParamServiceImpl

package com.lxs.legou.item.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lxs.legou.core.service.impl.CrudServiceImpl;
import com.lxs.legou.item.po.SpecParam;
import com.lxs.legou.item.service.ISpecParamService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SpecParamServiceImpl extends CrudServiceImpl<SpecParam> implements ISpecParamService {

    @Override
    public List<SpecParam> list(SpecParam entity) {
        QueryWrapper<SpecParam> queryWrapper = Wrappers.<SpecParam>query();
        //根据分类id查询规格参数
        if (null != entity.getCid()) {
            queryWrapper.eq("cid_", entity.getCid());
        }
        if (null != entity.getSearching()) {
            queryWrapper.eq("searching_", entity.getSearching());
        }
        return getBaseMapper().selectList(queryWrapper);
    }
}

1.5 导入数据

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

1.5.1.创建GoodsRepository

java代码:

package com.lxs.legou.search.dao;

import com.lxs.legou.search.po.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface GoodsDao extends ElasticsearchRepository<Goods, Long> {
   

}

1.5.2.创建索引

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

package com.lxs.legou.search;

import com.lxs.legou.search.po.Goods;
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;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SearchApplication.class)
public class ElasticSearchTest {
   

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Test
    public void createIndex() {
   
        //创建索引
        this.elasticsearchTemplate.createIndex(Goods.class);
        //配置映射
        this.elasticsearchTemplate.putMapping(Goods.class);
    }

}

通过kibana查看:

1532217819818

1.5.3.导入数据

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

package com.lxs.legou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.lxs.legou.common.utils.JsonUtils;
import com.lxs.legou.item.po.Sku;
import com.lxs.legou.item.po.SpecParam;
import com.lxs.legou.item.po.Spu;
import com.lxs.legou.item.po.SpuDetail;
import com.lxs.legou.search.client.CategoryClient;
import com.lxs.legou.search.client.SkuClient;
import com.lxs.legou.search.client.SpecParamClient;
import com.lxs.legou.search.client.SpuDetailClient;
import com.lxs.legou.search.dao.GoodsDao;
import com.lxs.legou.search.po.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 java.util.*;

/**
 * @Title: 索引服务类
 */
@Service
public class IndexService {
   

    @Autowired
    private GoodsDao goodsDao;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private SpecParamClient specParamClient;

    @Autowired
    private SkuClient skuClient;

    @Autowired
    private SpuDetailClient spuDetailClient;

    /**
     * 根据spu构建索引类型
     *
     * @param spu
     * @return
     */
    public Goods buildGoods(Spu spu) {
   
        Long id = spu.getId();
        //准备数据

        //商品分类名称
        List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
        String all = spu.getTitle() + " " + StringUtils.join(names, " ");

        //sku集合
        List<Sku> skus = skuClient.selectSkusBySpuId(spu.getId());

        //处理sku
        //把商品价格取出单独存放,便于展示
        List<Long> prices = new ArrayList<>();
        List<Map<String, Object>> skuList = new ArrayList<>();


        for (Sku sku : skus) {
   
            prices.add(sku.getPrice());
            Map<String, Object> skuMap = new HashMap<>();
            skuMap.put("id", sku.getId());
            skuMap.put("title", sku.getTitle());
            skuMap.put("image", StringUtils.isBlank(sku.getImages()) ? "" : sku.getImages().split(",")[0]);
            skuMap.put("price", sku.getPrice());
            skuList.add(skuMap);
        }

        //spec
        Map<String, Object> specs = new HashMap<>();
        //spuDetail
        SpuDetail spuDetail = spuDetailClient.edit(spu.getId());

        //通用规格参数值
        Map<String, String> genericMap = JsonUtils.parseMap(spuDetail.getGenericSpec(), String.class, String.class);
        //特有规格参数的值
        Map<String, List<String>> specialMap = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<String, List<String>>>() {
   
        });

        //查询分类对应的规格参数
        SpecParam specParam = new SpecParam();
        specParam.setCid(spu.getCid3());
        specParam.setSearching(true);
        List<SpecParam> params = specParamClient.selectSpecParamApi(specParam);

        for (SpecParam param : params) {
   
            //今后显示的名称
            String name = param.getName();//品牌,机身颜色
            //通用参数
            Object value = null;
            if (param.getGeneric()) {
   
                //通用参数
                value = genericMap.get(name);

                if (param.getNumeric()) {
   
                    //数值类型需要加分段
                    value = this.chooseSegment(value.toString(), param);
                }
            } else {
   //特有参数
                value = specialMap.get(name);

            }
            if (null == value) {
   
                value = "其他";
            }
            specs.put(name, value);
        }

        Goods goods = new Goods();
        goods.setId(spu.getId());
        //这里如果要加品牌,可以再写个BrandClient,根据id查品牌
        goods.setAll(all);
        goods.setSubTitle(spu.getSubTitle());
        goods.setBrandId(spu.getBrandId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setCreateTime(spu.getCreateTime());
        goods.setPrice(prices);
        goods.setSkus(JsonUtils.serialize(skuList));
        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();//4.5  4-5英寸
                }
                break;
            }
        }
        return result;
    }


    /**
     * 根据商品id删除索引
     *
     * @param id
     */
    public void deleteIndex(Long id) {
   
        goodsDao.deleteById(id);
    }

}

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

1526608095471

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

    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();//4.5  4-5英寸
                }
                break;
            }
        }
        return result;
    }

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

package com.lxs.legou.search;

import com.lxs.legou.item.po.Spu;
import com.lxs.legou.search.client.SpuClient;
import com.lxs.legou.search.dao.GoodsDao;
import com.lxs.legou.search.po.Goods;
import com.lxs.legou.search.service.IndexService;
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.test.context.junit4.SpringRunner;

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

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SearchApplication.class)
public class ESLoadDataTest {
   

    @Autowired
    private IndexService indexService;

    @Autowired
    private SpuClient spuClient;

    @Autowired
    private GoodsDao goodsDao;

    @Test
    public void loadData() {
   
        // 查询spu
//            PageResult<SpuBO> result = this.goodsClient.querySpuByPage(page, rows, true, null);
//            List<SpuBO> spus = result.getItems();
        List<Spu> spus = spuClient.selectAll();

        // spu转为goods
        List<Goods> goods = spus.stream().map(spu -> this.indexService.buildGoods(spu))
                .collect(Collectors.toList());

        // 把goods放入索引库
        goodsDao.saveAll(goods);

    }
}

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

1532228358310

    //这个方法用来构建查询条件以及过滤条件
    private QueryBuilder buildBasicQueryWithFilter(SearchRequest searchRequest) {
        //构造布尔查询
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();

        queryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()));

        //给这个查询加过滤
        // 过滤条件构建器
        BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
        //取出map中的实体
        for (Map.Entry<String, String> entry : searchRequest.getFilter().entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            // 商品分类和品牌不用前后加修饰
            if (key != "cid3" && key != "brandId") {
                key = "specs." + key + ".keyword";
            }
            // 字符串类型,进行term查询
            filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
        }

        return queryBuilder.filter(filterQueryBuilder);
    }

以上代码加.keyword的解释如下:

  • ES5.0及以后的版本取消了string类型,将原先的string类型拆分为textkeyword两种类型。它们的区别在于text会对字段进行分词处理而keyword则不会。

  • 当你没有以IndexTemplate等形式为你的索引字段预先指定mapping的话,ES就会使用Dynamic Mapping,通过推断你传入的文档中字段的值对字段进行动态映射。例如传入的文档中字段price的值为12,那么price将被映射为long类型;字段addr的值为"192.168.0.1",那么addr将被映射为ip类型。然而对于不满足ip和date格式的普通字符串来说,情况有些不同:ES会将它们映射为text类型,但为了保留对这些字段做精确查询以及聚合的能力,又同时对它们做了keyword类型的映射,作为该字段的fields属性写到_mapping中。例如,当ES遇到一个新的字段"foobar": "some string"时,会对它做如下的Dynamic Mapping:

{
    "foobar": {
        "type" "text",
        "fields": {
            "keyword": {
                "type": "keyword",
                "ignore_above": 256
            }
        }
    }
}

又比如商场中的CPU品牌

"specs" : {
  "properties" : {
    "CPU品牌" : {
      "type" : "text",
      "fields" : {
        "keyword" : {
          "type" : "keyword",
          "ignore_above" : 256
        }
      }
    }
  • 在之后的查询中使用specs.CPU品牌是将specs.CPU品牌作为text类型查询,而使用specs.CPU品牌.keyword则是将specs.CPU品牌作为keyword类型查询。前者会对查询内容做分词处理之后再匹配,而后者则是直接对查询结果做精确匹配。

  • ES的term query做的是精确匹配而不是分词查询,因此对text类型的字段做term查询将是查不到结果的(除非字段本身经过分词器处理后不变,未被转换或分词)。此时,必须使用specs.CPU品牌.keyword来对specs.CPU品牌字段以keyword类型进行精确匹配。

2 商品搜索

2.1 基本搜索

2.1.1 前端实现

2.2.1.1 发送请求

前端搜索子组件Search调用searchGoodList方法

<Search @onSearch="searchGoodList"></Search>

searchGoodList方法,根据用户输入修改搜索对象中的key

//搜索输入框搜索
searchGoodList(data) {
  this.search.key = data
}

search对象的结构

      search: {
        key: "", // 搜索页面的关键字
        page:1,
        sortBy:"", //根据谁排序
        descending:false, //升序还是降序
        filter:{} //规律条件
      }

search属性的侦听器

watch:{
  search:{
    deep:true,
    handler(val,old){
      if(!old || !old.key){
        // 如果旧的search值为空,或者search中的key为空,证明是第一次
        return;
      }
      this.searchBy();
    }
  }
}

发送搜索请求的searchBy方法

//搜索
searchBy() {
  instance.post(`/search/query`, this.search, {
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    }
  }).then(response => {
    //初始化skus属性,并且让商品的默认选择选第0个
    response.data.items.forEach(goods => {
      //把之际取到的字符串转换成json
      goods.skus = JSON.parse(goods.skus).sort();
      //表示选中的sku。默认选中第0个
      goods.selected = goods.skus[0];
    });
    //从响应数据中获取总的条目数以及总页数
    this.total = response.data.total;
    this.totalPage = response.data.totalPage;

    this.filters = [];
    this.filters.push({
      k:"cid3",
      options:response.data.categories
    });

    this.filters.push({
      k:"brandId",
      options:response.data.brands
    });

    response.data.specs.forEach(spec=>{
      spec.options = spec.options.map(option=>({name:option}));
      this.filters.push(spec);
    });
    //当前页面上的所有的spu
    this.goodsList = response.data.items;
  }).catch(error => {
    console.log(error)
  })
}

发送给搜索微服务的请求数据

2.2.1.2 处理结果

搜索微服务返回结果分析

vue devtools中监控到的数据结构

搜索结果展示

  <div class="goods-list">
    <!--<div class="goods-show-info" v-for="(item, index) in orderGoodsList" :key="index">-->
    <div class="goods-show-info" v-for="(item, index) in goodsList" :key="index">
      <div class="goods-show-img">
        <router-link to="/goodsDetail"><img :src="item.selected.image" height="200"/></router-link>

        <ul class="skus">
          <li :class="{selected : sku.id === item.selected.id}" v-for="(sku,i) in item.skus" :key="i"
              @click="item.selected = sku">
            <img :src="sku.image">
          </li>
        </ul>


      </div>
      <div class="goods-show-price">
        <span>
          <Icon type="social-yen text-danger"></Icon>
          <span class="seckill-price text-danger">{
  {item.selected.price}}</span>
        </span>
      </div>
      <div class="goods-show-detail">
        <span>{
  {item.selected.title}}</span>
      </div>
      <div class="goods-show-num">
        已有<span>10</span>人评价
      </div>
      <div class="goods-show-seller">
        <span>自营</span>
      </div>
    </div>
  </div>
</div>

2.1.2 后端实现

2.1.2.1 实体类

搜索请求对象

SearchRequest对象对应前端的search搜索对象

legou-search/legou-search-interface/src/main/java/com/lxs/legou/search/po/SearchRequest.java

package com.lxs.legou.search.po;

import java.util.HashMap;
import java.util.Map;

public class SearchRequest {
    private String key;// 搜索条件

    private Integer page;// 当前页

    private String sortBy;//根据谁排序

    private Boolean descending; //升序还是降序

    private Map<String,String> filter = new HashMap<>();

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

    public String getSortBy() {
        return sortBy;
    }

    public void setSortBy(String sortBy) {
        this.sortBy = sortBy;
    }

    public Boolean getDescending() {
        return descending;
    }

    public void setDescending(Boolean descending) {
        this.descending = descending;
    }

    public Map<String, String> getFilter() {
        return filter;
    }

    public void setFilter(Map<String, String> filter) {
        this.filter = filter;
    }
}

搜索结果实体类

SearchResult对象对应前端的goodsList和filter对象,是这个对象的数据来源

legou-search/legou-search-interface/src/main/java/com/lxs/legou/search/po/SearchResult.java

package com.lxs.legou.search.po;

import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import lombok.Data;

import java.util.List;
import java.util.Map;

@Data
public class SearchResult {

    private Long total; //总行数
    private Long totalPage; //总页数
    private List items; //当前页数据

    private List<Category> categories;
    private List<Brand> brands;
    private List<Map<String, Object>> specs;

    public SearchResult() {
    }

    public SearchResult(Long total, Long totalPage, List items, List<Category> categories, List<Brand> brands, List<Map<String, Object>> specs) {
        this.total = total;
        this.totalPage = totalPage;
        this.items = items;
        this.categories = categories;
        this.brands = brands;
        this.specs = specs;
    }
}
2.1.2.2 业务类

创建业务类,对于用户输入的key进行基本的搜索

legou-search/legou-search-service/src/main/java/com/lxs/legou/search/service/SearchService.java

package com.lxs.legou.search.service;

import com.lxs.legou.search.dao.GoodsDao;
import com.lxs.legou.search.po.Goods;
import com.lxs.legou.search.po.SearchRequest;
import com.lxs.legou.search.po.SearchResult;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

@Service
public class SearchService {

    @Autowired
    private GoodsDao goodsDao;

    public SearchResult search(SearchRequest searchRequest) {
        String key = searchRequest.getKey();

        if (key == null) {
            return null;
        }

        //查询构建工具
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //设置过滤字段,只返回那些字段
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[] {"id", "subTitle", "skus" }, null));

        //构建基本的查询条件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));

        //构建过滤条件


        //把查询条件添加进到构建器中
        queryBuilder.withQuery(boolQueryBuilder);

        //得到结果
        Page<Goods> page = goodsDao.search(queryBuilder.build());

        //分页数据
        long total = page.getTotalElements(); //总行数
        long totalPages = page.getTotalPages(); //总页数

        return new SearchResult(total, totalPages, page.getContent(), null, null, null);
    }


}
2.1.2.3 控制器
package com.lxs.legou.search.controller;

import com.lxs.legou.search.po.SearchRequest;
import com.lxs.legou.search.po.SearchResult;
import com.lxs.legou.search.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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("/query")
    public ResponseEntity<SearchResult> queryGoodsByPage(@RequestBody SearchRequest searchRequest) {
        SearchResult result = searchService.search(searchRequest);
        if (result == null) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
        return ResponseEntity.ok(result);
    }

}
2.1.2.4 测试

注意配置请求头Content-Type=application/json;charset=UTF-8

2.2. 品牌统计

1562145335144

用户搜索的时候,除了使用分类搜索外,还有可能使用品牌搜索,所以我们还需要显示品牌数据和规格数据,品牌数据和规格数据的显示比较容易,都可以考虑使用分类统计的方式进行分组实现。

1.1 品牌统计分析

看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据品牌名字分组查看有多少品牌,大概执行了2个步骤就可以获取数据结果以及品牌统计,我们可以发现他们的搜索条件完全一样。

-- 查询所有
SELECT * FROM spu_ WHERE name LIKE '%手机%';
-- 根据品牌名字分组查询
SELECT brand_id_ FROM  spu_ WHERE name LIKE '%手机%' GROUP BY  brand_id_;

我们每次执行搜索的时候,需要显示商品品牌名称,这里要显示的品牌名称其实就是符合搜素条件的所有商品的品牌集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即可实现。

1.2 品牌分组统计实现

修改search微服务的legou-search/legou-search-service/src/main/java/com/lxs/legou/search/service/SearchService.java类,添加一个品牌分组搜索

queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brandId"));

整体代码如下:

    private List<Brand> getBrandAgg(String brandAggsName, AggregatedPage<Goods> goodsResult) {
   
        LongTerms longTerms = (LongTerms) goodsResult.getAggregation(brandAggsName);
        List<Long> brandIds = new ArrayList<>();
        for (LongTerms.Bucket bucket : longTerms.getBuckets()) {
   
            brandIds.add(bucket.getKeyAsNumber().longValue());
        }
        return brandClient.selectBrandByIds(brandIds);
    }

使用kibana查询的DSL语句

GET /goods_legou/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "all": "手机"
          }
        }
      ]
    }
  },
  "aggs": {
    "brands": {
      "terms": {
        "field": "brandId"
      }
    }
  }
}

1.3 测试

使用PostMan请求http://localhost:9006/query

1562146243795

2.3. 分类统计

1562145335144

用户搜索的时候,除了使用分类搜索外,还有可能使用品牌搜索,所以我们还需要显示品牌数据和规格数据,品牌数据和规格数据的显示比较容易,都可以考虑使用分类统计的方式进行分组实现。

1.1 分类统计分析

看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据品牌名字分组查看有多少品牌,大概执行了2个步骤就可以获取数据结果以及品牌统计,我们可以发现他们的搜索条件完全一样。

-- 查询所有
SELECT * FROM spu_ WHERE name LIKE '%手机%';
-- 根据分类名字分组查询
SELECT cid3 FROM  spu_ WHERE name LIKE '%手机%' GROUP BY cid3;

我们每次执行搜索的时候,需要显示商品品牌名称,这里要显示的品牌名称其实就是符合搜素条件的所有商品的品牌集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即可实现。

1.2 分类分组统计实现

修改search微服务的legou-search/legou-search-service/src/main/java/com/lxs/legou/search/service/SearchService.java类,添加一个品牌分组搜索

queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggsName).field("cid3"));

整体代码如下:

    private List<Category> getCategoryAgg(String categoryAggsName, AggregatedPage<Goods> goodsResult) {
   
        LongTerms longTerms = (LongTerms) goodsResult.getAggregation(categoryAggsName);
        List<Long> categoryIds = new ArrayList<>();
        for (LongTerms.Bucket bucket : longTerms.getBuckets()) {
   
            categoryIds.add(bucket.getKeyAsNumber().longValue());
        }
        List<String> names = this.categoryClient.queryNameByIds(categoryIds);

        List<Category> categories = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
   
            Category category =new Category();
            category.setId(categoryIds.get(i));
//            category.setName(names.get(i));
            category.setTitle(names.get(i));
            categories.add(category);
        }
        return categories;
    }

使用kibana执行DSL语句

GET /goods_legou/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "all": "手机"
          }
        }
      ]
    }
  },
  "aggs": {
    "brands": {
      "terms": {
        "field": "brandId"
      }
    },
    "categorys": {
      "terms": {
        "field": "cid3"
      }
    }
  }
}

1.3 测试

使用PostMan请求http://localhost:9006/query

1562146243795

2.4 规格统计

1562145335144

用户搜索的时候,除了使用分类、品牌搜索外,还有可能使用规格搜索,所以我们还需要显示规格数据,规格数据的显示相比上面2种实现略微较难一些,需要对数据进行处理,我们也可以考虑使用分类统计和品牌统计的方式进行分组实现。

2.1 规格统计分析

看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据规格分组查看有多少规格,大概执行了2个步骤就可以获取数据结果以及规格统计,我们可以发现他们的搜索条件完全一样。

-- 查询所有
SELECT * FROM spu_detail_ WHERE name LIKE '%手机%';
-- 根据规格名字分组查询
SELECT spec FROM  spu_detail WHERE name LIKE '%手机%' GROUP BY spec;

2.2 规格统计分组实现

修改search微服务的legou-search/legou-search-service/src/main/java/com/lxs/legou/search/service/SearchService.java类,添加一个规格分组搜索

        List<Map<String,Object>> specs = null;

        /*
            - 当分类聚合结果为1个统计规格参数
            - 根据分类查询当前分类的搜索的规格参数
            - 创建NativeQueryBuilder,使用上面搜索一样的条件
            - 循环上面可搜索规格参数,依次添加聚合
            - 处理结果k:参数名,options:聚合的结果数组
        */
        if (categories.size()==1){
   
            specs = getSpecs(categories.get(0).getId(),basicQuery);
        }

整体代码如下:

    //规格参数的聚合应该和查询关联
    private List<Map<String, Object>> getSpecs(Long cid, QueryBuilder query) {
   

        List<Map<String,Object>> specs = new ArrayList<>();

//        List<SpecParam> specParams = this.specificationClient.querySpecParam(null, cid, true, null);
        SpecParam sp = new SpecParam();
        sp.setCid(cid);
        sp.setSearching(true);
        List<SpecParam> specParams = this.specificationClient.selectSpecParamApi(sp);

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //在做聚合之前先做查询,只有符合条件的规格参数才应该被查出来
        queryBuilder.withQuery(query);

        for (SpecParam specParam : specParams) {
   
            String name = specParam.getName();//内存,产地
            queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs."+name+".keyword"));
        }

        AggregatedPage<Goods> aggs = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
        Map<String, Aggregation> stringAggregationMap = aggs.getAggregations().asMap();

        for (SpecParam specParam : specParams) {
   
            Map<String,Object> spec = new HashMap<>();
            String name = specParam.getName();
            if (stringAggregationMap.get(name) instanceof  StringTerms) {
   
                StringTerms stringTerms = (StringTerms) stringAggregationMap.get(name);
                List<String> val = new ArrayList<>();
                for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
   
                    val.add(bucket.getKeyAsString());
                }
                spec.put("k",name);//内存,存储空间,屏幕尺寸
                spec.put("options",val);

                specs.add(spec);
            }
        }

        return specs;
    }

对应kibana中的DSL查询

GET /goods_legou2/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "all": "手机"
          }
        }
      ]
    }
  },
  "aggs": {
    "brands": {
      "terms": {
        "field": "brandId"
      }
    },
    "categorys": {
      "terms": {
        "field": "cid3"
      }
    },
    "CPU品牌": {
      "terms": {
        "field": "specs.CPU品牌.keyword"
      }
    },
    "CPU核数": {
      "terms": {
        "field": "specs.CPU核数.keyword"
      }
    }
  }

}

2.3 测试

2.5 条件过滤

//这个方法用来构建查询条件以及过滤条件
private QueryBuilder buildBasicQueryWithFilter(SearchRequest searchRequest) {
    //构造布尔查询
    BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();

    queryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()));

    //给这个查询加过滤
    // 过滤条件构建器
    BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
    //取出map中的实体
    for (Map.Entry<String, String> entry : searchRequest.getFilter().entrySet()) {
        String key = entry.getKey();
        String value = entry.getValue();
        // 商品分类和品牌不用前后加修饰
        if (key != "cid3" && key != "brandId") {
            key = "specs." + key + ".keyword";
        }
        // 字符串类型,进行term查询
        filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
    }

    return queryBuilder.filter(filterQueryBuilder);
}

以上代码加.keyword的解释如下:

  • ES5.0及以后的版本取消了string类型,将原先的string类型拆分为textkeyword两种类型。它们的区别在于text会对字段进行分词处理而keyword则不会。

  • 当你没有以IndexTemplate等形式为你的索引字段预先指定mapping的话,ES就会使用Dynamic Mapping,通过推断你传入的文档中字段的值对字段进行动态映射。例如传入的文档中字段price的值为12,那么price将被映射为long类型;字段addr的值为"192.168.0.1",那么addr将被映射为ip类型。然而对于不满足ip和date格式的普通字符串来说,情况有些不同:ES会将它们映射为text类型,但为了保留对这些字段做精确查询以及聚合的能力,又同时对它们做了keyword类型的映射,作为该字段的fields属性写到_mapping中。例如,当ES遇到一个新的字段"foobar": "some string"时,会对它做如下的Dynamic Mapping:

{
    "foobar": {
        "type" "text",
        "fields": {
            "keyword": {
                "type": "keyword",
                "ignore_above": 256
            }
        }
    }
}

又比如商场中的CPU品牌

"specs" : {
  "properties" : {
    "CPU品牌" : {
      "type" : "text",
      "fields" : {
        "keyword" : {
          "type" : "keyword",
          "ignore_above" : 256
        }
      }
    }
  • 在之后的查询中使用specs.CPU品牌是将specs.CPU品牌作为text类型查询,而使用specs.CPU品牌.keyword则是将specs.CPU品牌作为keyword类型查询。前者会对查询内容做分词处理之后再匹配,而后者则是直接对查询结果做精确匹配。

  • ES的term query做的是精确匹配而不是分词查询,因此对text类型的字段做term查询将是查不到结果的(除非字段本身经过分词器处理后不变,未被转换或分词)。此时,必须使用specs.CPU品牌.keyword来对specs.CPU品牌字段以keyword类型进行精确匹配。

使用PostMan测试

2.6 分页实现

后台代码

Integer page = searchRequest.getPage() - 1;// page 从0开始
Integer size = searchRequest.getSize();
//把分页条件条件到构建器中
queryBuilder.withPageable(PageRequest.of(page,size));

前台代码

<Page :total="total" :page-size="20" @on-change="changePage"></Page>
changePage(index) {
    this.search.page = index;
}

2.7 排序

后台代码

//获取排序的条件
String sortBy = searchRequest.getSortBy();
Boolean desc = searchRequest.getDescending();
if (StringUtils.isNotBlank(sortBy)){
    //把排序条件加给构建器
    queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
}

2.8 完整代码

后端

legou-search/legou-search-service/src/main/java/com/lxs/legou/search/service/SearchService.java

package com.lxs.legou.search.service;

import com.lxs.legou.item.po.Brand;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.po.SpecParam;
import com.lxs.legou.search.client.BrandClient;
import com.lxs.legou.search.client.CategoryClient;
import com.lxs.legou.search.client.SpecParamClient;
import com.lxs.legou.search.dao.GoodsDao;
import com.lxs.legou.search.po.Goods;
import com.lxs.legou.search.po.SearchRequest;
import com.lxs.legou.search.po.SearchResult;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@Service
public class SearchService {
    @Autowired
    private GoodsDao goodsRepository;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private SpecParamClient specificationClient;

    private Logger logger = LoggerFactory.getLogger(SearchService.class);

    public SearchResult search(SearchRequest searchRequest) {
        List<Category> categories = null;
        List<Brand> brands = null;

        Integer page = searchRequest.getPage() - 1;// page 从0开始
        Integer size = searchRequest.getSize();

        String key = searchRequest.getKey();

        if (key == null) {
            return null;
        }

        //查询构建工具
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //添加了查询的过滤,只要这些字段
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","subTitle","skus"},null));

        //获取基本的查询条件
        QueryBuilder basicQuery = buildBasicQueryWithFilter(searchRequest);

        //把查询条件添加到构建器中(这里仅仅是我的查询条件)
        queryBuilder.withQuery(basicQuery);

        //把分页条件条件到构建器中
        queryBuilder.withPageable(PageRequest.of(page,size));

        //获取排序的条件
        String sortBy = searchRequest.getSortBy();
        Boolean desc = searchRequest.getDescending();
        if (StringUtils.isNotBlank(sortBy)){
            //把排序条件加给构建器
            queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
        }

        //对品牌以及分类做聚合
        String brandAggsName = "brands";
        String categoryAggsName = "categories";

        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggsName).field("brandId"));
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggsName).field("cid3"));

        AggregatedPage<Goods> goodsResult = (AggregatedPage<Goods>) goodsRepository.search(queryBuilder.build());

        long total = goodsResult.getTotalElements();
        long totalPages = (total + size - 1) / size;

        brands = getBrandAgg(brandAggsName,goodsResult);

        categories = getCategoryAgg(categoryAggsName,goodsResult);


        List<Map<String,Object>> specs = null;

        //只有搜索对应的分类个数是1的时候才能聚合规格参数
        //根据用户的搜索条件对应的产品分类查询产品对应的规格参数
        if (categories.size()==1){
            specs = getSpecs(categories.get(0).getId(),basicQuery);
        }

        return new SearchResult(total,totalPages,goodsResult.getContent(),categories,brands,specs);
    }

    //这个方法用来构建查询条件以及过滤条件
    private QueryBuilder buildBasicQueryWithFilter(SearchRequest searchRequest) {
        //构造布尔查询
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();

        queryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()));

        //给这个查询加过滤
        // 过滤条件构建器
        BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
        //取出map中的实体
        for (Map.Entry<String, String> entry : searchRequest.getFilter().entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            // 商品分类和品牌不用前后加修饰
            if (key != "cid3" && key != "brandId") {
                key = "specs." + key + ".keyword";
            }
            // 字符串类型,进行term查询
            filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
        }

        return queryBuilder.filter(filterQueryBuilder);
    }

    //规格参数的聚合应该和查询关联
    private List<Map<String, Object>> getSpecs(Long cid, QueryBuilder query) {

        List<Map<String,Object>> specs = new ArrayList<>();

//        List<SpecParam> specParams = this.specificationClient.querySpecParam(null, cid, true, null);
        SpecParam sp = new SpecParam();
        sp.setCid(cid);
        sp.setSearching(true);
        List<SpecParam> specParams = this.specificationClient.selectSpecParamApi(sp);

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //在做聚合之前先做查询,只有符合条件的规格参数才应该被查出来
        queryBuilder.withQuery(query);

        for (SpecParam specParam : specParams) {
            String name = specParam.getName();//内存,产地
            queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs."+name+".keyword"));
        }

        AggregatedPage<Goods> aggs = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
        Map<String, Aggregation> stringAggregationMap = aggs.getAggregations().asMap();

        for (SpecParam specParam : specParams) {
            Map<String,Object> spec = new HashMap<>();
            String name = specParam.getName();
            if (stringAggregationMap.get(name) instanceof  StringTerms) {
                StringTerms stringTerms = (StringTerms) stringAggregationMap.get(name);
                List<String> val = new ArrayList<>();
                for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
                    val.add(bucket.getKeyAsString());
                }
                spec.put("k",name);//内存,存储空间,屏幕尺寸
                spec.put("options",val);

                specs.add(spec);
            }
        }

        return specs;
    }

    private List<Category> getCategoryAgg(String categoryAggsName, AggregatedPage<Goods> goodsResult) {
        LongTerms longTerms = (LongTerms) goodsResult.getAggregation(categoryAggsName);
        List<Long> categoryIds = new ArrayList<>();
        for (LongTerms.Bucket bucket : longTerms.getBuckets()) {
            categoryIds.add(bucket.getKeyAsNumber().longValue());
        }
        List<String> names = this.categoryClient.queryNameByIds(categoryIds);

        List<Category> categories = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
            Category category =new Category();
            category.setId(categoryIds.get(i));
//            category.setName(names.get(i));
            category.setTitle(names.get(i));
            categories.add(category);
        }
        return categories;
    }

    private List<Brand> getBrandAgg(String aggName, AggregatedPage<Goods> result){
        try {
            LongTerms longTerms1 = (LongTerms) result.getAggregation(aggName);

            List<Long> brandIds = new ArrayList<>();
            for (LongTerms.Bucket bucket : longTerms1.getBuckets()) {
                brandIds.add(bucket.getKeyAsNumber().longValue());
            }
//            return this.brandClient.queryBrandByIds(brandIds);
            return this.brandClient.selectBrandByIds(brandIds);
        } catch (Exception e) {
            logger.error("解析品牌数据错误:{}",e);
            e.printStackTrace();
        }
        return null;
    }
}

前端

src/components/GoodsList.vue

<template>
  <div>
    <Search @onSearch="searchGoodList"></Search>
    <GoodsListNav></GoodsListNav>
    <div class="container">
      <div class="bread-crumb">
        <Breadcrumb>
          <BreadcrumbItem to="/">
            <Icon type="ios-home-outline"></Icon> 首页
          </BreadcrumbItem>
          <BreadcrumbItem to="/goodsList?sreachData=">
            <Icon type="bag"></Icon> {
  {searchKey}}
          </BreadcrumbItem>
          <Tag v-for="(value, name, index) in search.filter" :key="index" closable @on-close="closeTags(name)"><span
                  @click="selectTags(index)">{
  {name + ":" + value}}</span></Tag>

        </Breadcrumb>

      </div>

      <!--搜索过滤条件-->
      <div class="item-class-show">
        <Row class="item-class-group" v-for="(f, i) in filters" :key="i" v-if="i <= 5">
          <i-col class="item-class-name" span="3" v-if="f.k==='cid3'">
            分类
          </i-col>
          <i-col class="item-class-name" span="3" v-else-if="f.k==='brandId'">
            品牌
          </i-col>
          <i-col class="item-class-name" span="3" v-else>
            {
  {f.k}}
          </i-col>
          <i-col class="item-class-select" span="21" v-if="f.k!=='cid3'">
            <span v-for="(o, j) in f.options" :key="j" @click="selectFilter(f.k,o)">{
  { o.name }}</span>
          </i-col>
          <i-col class="item-class-select" span="21" v-else>
            <span v-for="(o, j) in f.options" :key="j" @click="selectFilter(f.k,o)">{
  { o.title }}</span>
          </i-col>
        </Row>

        <Row class="item-class-group" v-if="filters.length > 5">
          <i-col class="item-class-name" span="3">高级选项 : </i-col>
          <i-col class="item-class-select" span="21">
            <span v-bind:style="foldFilter.k == f.k ? 'color:red': 'color:black'" v-for="(f, i) in filters" :key="i" v-if="i > 5" @click="showFold(f)">{
  { f.k }} <Icon type="ios-arrow-up" v-if="foldFilter.k != f.k" /> <Icon type="ios-arrow-down" v-if="foldFilter.k == f.k" /></span>
          </i-col>
        </Row>
        <Row >
          <i-col class="item-class-select" span="24">
            <span v-for="(o, j) in foldFilter.options" :key="j"  @click="selectFilter(foldFilter.k,o)" >{
  { o.name }}</span>
          </i-col>
        </Row>
      </div>


      <!-- 商品展示容器 -->
      <div class="goods-box">
        <div class="as-box">
          <div class="item-as-title">
            <span>商品精选</span>
            <span>广告</span>
          </div>
          <div class="item-as" v-for="(item,index) in asItems" :key="index">
            <div class="item-as-img">
              <img :src="item.img" alt="">
            </div>
            <div class="item-as-price">
              <span>
                <Icon type="social-yen text-danger"></Icon>
                <span class="seckill-price text-danger">{
  {item.price}}</span>
              </span>
            </div>
            <div class="item-as-intro">
              <span>{
  {item.intro}}</span>
            </div>
            <div class="item-as-selled">
              已有<span>{
  {item.num}}</span>人评价
            </div>
          </div>
        </div>
        <div class="goods-list-box">
          <div class="goods-list-tool">
            <ul>
              <li v-for="(item,index) in goodsTool" :key="index" @click="orderBy(item.en, index)"><span :class="{ 'goods-list-tool-active': isAction[index]}">{
  {item.title}} <Icon :type="icon[index]"></Icon></span></li>
            </ul>
          </div>
          <div class="goods-list">
            <!--<div class="goods-show-info" v-for="(item, index) in orderGoodsList" :key="index">-->
            <div class="goods-show-info" v-for="(item, index) in goodsList" :key="index">
              <div class="goods-show-img">
                <router-link to="/goodsDetail"><img :src="item.selected.image" height="200"/></router-link>

                <ul class="skus">
                  <li :class="{selected : sku.id === item.selected.id}" v-for="(sku,i) in item.skus" :key="i"
                      @click="item.selected = sku">
                    <img :src="sku.image">
                  </li>
                </ul>


              </div>
              <div class="goods-show-price">
                <span>
                  <Icon type="social-yen text-danger"></Icon>
                  <span class="seckill-price text-danger">{
  {item.selected.price}}</span>
                </span>
              </div>
              <div class="goods-show-detail">
                <span>{
  {item.selected.title}}</span>
              </div>
              <div class="goods-show-num">
                已有<span>10</span>人评价
              </div>
              <div class="goods-show-seller">
                <span>自营</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="goods-page">
        <Page :total="total" :page-size="20" @on-change="changePage"></Page>
      </div>
    </div>
    <Spin size="large" fix v-if="isLoading"></Spin>
  </div>
</template>

<script>
import Search from '@/components/Search';
import GoodsListNav from '@/components/nav/GoodsListNav';
import GoodsClassNav from '@/components/nav/GoodsClassNav';
import store from '@/vuex/store';
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex';

import instance from '@/libs/api/index'
import Qs from 'qs'

export default {
  name: 'GoodsList',
  beforeRouteEnter (to, from, next) {
    window.scrollTo(0, 0);
    next();
  },
  data () {
    return {
      isAction: [ true, false, false ],
      icon: [ 'arrow-up-a', 'arrow-down-a', 'arrow-down-a' ],
      goodsTool: [
        {title: '综合', en: 'sale'},
        {title: '评论数', en: 'remarks'},
        {title: '价格', en: 'price'}
      ],
      goodsList: [],
      filters:[],
      total:20,
      totalPage:1,
      foldFilter: {}, //折叠显示的过滤条件
      search: {
        key: "", // 搜索页面的关键字
        page:1,
        sortBy:"", //根据谁排序
        descending:false, //升序还是降序
        filter:{} //规律条件
      }
    };
  },
  computed: {
    ...mapState(['asItems', 'isLoading', 'searchKey']),
    ...mapGetters(['orderGoodsList'])
  },
  methods: {
    ...mapActions(['loadGoodsList']),
    ...mapMutations(['SET_GOODS_ORDER_BY']),
    orderBy (data, index) {
      this.search.sortBy = data
      this.search.descending = !this.search.descending
    },
    //搜索输入框搜索
    searchGoodList(data) {
      this.search.key = data
    },
    //搜索
    searchBy() {
      instance.post(`/search/query`, this.search, {
        headers: {
          'Content-Type': 'application/json;charset=UTF-8'
        }
      }).then(response => {
        //初始化skus属性,并且让商品的默认选择选第0个
        response.data.items.forEach(goods => {
          //把之际取到的字符串转换成json
          goods.skus = JSON.parse(goods.skus).sort();
          //表示选中的sku。默认选中第0个
          goods.selected = goods.skus[0];
        });
        //从响应数据中获取总的条目数以及总页数
        this.total = response.data.total;
        this.totalPage = response.data.totalPage;

        this.filters = [];
        this.filters.push({
          k:"cid3",
          options:response.data.categories
        });

        this.filters.push({
          k:"brandId",
          options:response.data.brands
        });

        response.data.specs.forEach(spec=>{
          spec.options = spec.options.map(option=>({name:option}));
          this.filters.push(spec);
        });
        //当前页面上的所有的spu
        this.goodsList = response.data.items;
      }).catch(error => {
        console.log(error)
      })
    },
    showFold(f) {
      this.foldFilter = f;
    },
    selectFilter(k, o) {
      const obj = {};
      Object.assign(obj, this.search);
      if (k === 'cid3' || k === 'brandId') {
        o = o.id;
        obj.filter[k] = o;
        this.search = obj;
      } else {
        obj.filter[k] = o.name;
        this.search = obj;
      }
    },
    closeTags(name) {
      delete this.search.filter[name];
      this.$forceUpdate();
      this.searchBy();
    },
    changePage(index) {
      this.search.page = index;
    }
  },
  watch:{
    search:{
      deep:true,
      handler(val,old){
        if(!old || !old.key){
          // 如果旧的search值为空,或者search中的key为空,证明是第一次
          return;
        }
        this.searchBy();
      }
    }
  },
  created () {
    this.loadGoodsList();
  },
  components: {
    Search,
    GoodsListNav,
    GoodsClassNav
  },
  store
};
</script>

<style scoped>
.container {
  margin: 15px auto;
  width: 93%;
  min-width: 1000px;
}
.text-danger {
  color: #A94442;
}
.seckill-price{
  margin-right: 5px;
  font-size: 25px;
  font-weight: bold;
}
.goods-box {
  display: flex;
}
/* ---------------侧边广告栏开始------------------- */
.as-box {
  width: 200px;
  border: 1px solid #ccc;
}
.item-as-title{
  width: 100%;
  height: 36px;
  color: #B1191A;
  line-height: 36px;
  font-size: 18px;
}
.item-as-title span:first-child{
  margin-left: 20px;
}
.item-as-title span:last-child{
  float: right;
  margin-right: 15px;
  font-size: 10px;
  color: #ccc;
}
.item-as{
  width: 160px;
  margin: 18px auto;
}
.item-as-img{
  width: 160px;
  height: 160px;
  margin: 0px auto;
}
.item-as-price span{
  font-size: 18px;
}
.item-as-intro{
  margin-top: 5px;
  font-size: 12px;
}
.item-as-selled{
  margin-top: 5px;
  font-size: 12px;
}
.item-as-selled span{
  color: #005AA0;
}
/* ---------------侧边广告栏结束------------------- */

/* ---------------商品栏开始------------------- */
.goods-list-box {
  margin-left: 15px;
  width: calc(100% - 215px);
}
.goods-list-tool{
  width: 100%;
  height: 38px;
  border: 1px solid #ccc;
  background-color: #F1F1F1;
}
.goods-list-tool ul{
  padding-left: 15px;
  list-style: none;
}
.goods-list-tool li{
  cursor: pointer;
  float: left;
}
.goods-list-tool span{
  padding: 5px 8px;
  border: 1px solid #ccc;
  border-left: none;
  line-height: 36px;
  background-color: #fff;
}
.goods-list-tool span:hover{
  border: 1px solid #E4393C;
}
.goods-list-tool i:hover{
  color: #E4393C;
}
.goods-list-tool-active {
  color: #fff;
  border-left: 1px solid #ccc;
  background-color: #E4393C !important;
}

.goods-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}
.goods-show-info{
  width: 240px;
  padding: 10px;
  margin: 15px 0px;
  border: 1px solid #fff;
  cursor: pointer;
}
.goods-show-info:hover{
  border: 1px solid #ccc;
  box-shadow: 0px 0px 15px #ccc;
}
.goods-show-price{
  margin-top: 6px;
}
.goods-show-detail{
  font-size: 12px;
  margin: 6px 0px;
}
.goods-show-num{
  font-size: 12px;
  margin-bottom: 6px;
  color: #009688;
}
.goods-show-num span{
  color: #005AA0;
  font-weight: bold;
}
.goods-show-seller{
  font-size: 12px;
  color:#E4393C;
}
.goods-page {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
/* ---------------商品栏结束------------------- */
/* ---------------商品收缩导航------------------*/
.item-class-show {
  margin: 15px auto;
  width: 100%;
}
.item-class-group {
  margin-top: 1px;
  height: 45px;
  border-bottom: 1px solid #ccc;
}
.item-class-group:first-child {
  border-top: 1px solid #ccc;
}
.item-class-name {
  padding-left: 15px;
  line-height: 44px;
  color: #666;
  font-weight: bold;
  background-color: #f3f3f3;
}
.item-class-name:first-child {
  line-height: 43px;
}
.item-class-select span {
  margin-left: 15px;
  width: 160px;
  color: #005aa0;
  line-height: 45px;
  cursor: pointer;
}
.redCls {
  color: red;
}
/* ---------------------搜索导航结束-------------------- */

.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>
相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
目录
相关文章
|
2天前
|
安全 NoSQL Java
java电商项目(十)
本文介绍了电商系统中订单结算和下单流程的实现。主要包括: 1. **订单结页**: - **收件地址分析**:用户从购物车页面跳转到订单结算页,加载用户收件地址。地址信息存储在 `address_` 表中。 - **实现用户收件地址查询**:通过用户登录名查询收件地址,涉及实体类、DAO、Service 和 Controller 的实现。 2. **下单**: - **业务分析**:下单时创建订单数据,包括订单表 `order_` 和订单明细表 `order_item_`,同时修改商品库存、增加用户积分并删除购物车数据。
13 3
|
2天前
|
消息中间件 安全 Java
java电商项目(十一)
本文接续前几个文章的项目进行讲解!
8 1
|
2天前
|
缓存 NoSQL Java
java电商项目(十二)
本文接续前几个文章的项目进行讲解
11 1
|
2天前
|
存储 NoSQL Java
java电商项目(九)
本文介绍了购物车功能的实现过程,包括用户登录后将商品添加至购物车、购物车列表展示及微服务之间的认证机制。具体步骤如下: 1. **购物车功能**: - 用户选择商品并点击“加入购物车”,系统将商品信息存储到Redis中。 2. **微服务之间认证**: - **传递管理员令牌**:在授权中心微服务调用用户微服务时,生成管理员令牌并通过Header传递。 - **传递当前用户令牌**:用户登录后,通过Feign拦截器将用户令牌传递到其他微服务。 - **获取用户数据**:通过`SecurityContextHolder`获取用户信息,并使用公钥解密令牌以验证用户
9 1
|
2天前
|
监控 算法 Java
java电商项目(七)
微服务网关作为系统唯一对外的入口,位于客户端和服务端之间,处理非业务功能,如路由请求、鉴权、监控、缓存、限流等。它解决了客户端直接调用多个微服务带来的复杂性、跨域请求、认证复杂、难以重构等问题。常用的微服务网关技术有Nginx、Zuul和Spring Cloud Gateway。Spring Cloud Gateway因其集成断路器、路径重写和较好的性能而被广泛使用。本文介绍了如何使用Spring Cloud Gateway搭建后台网关系统,包括引入依赖、配置文件、跨域配置、路由过滤配置、负载均衡、限流等。此外,还详细讲解了RBAC权限数据管理、组织机构管理单点登录(SSO)及JWT鉴权的实现
8 1
|
2天前
|
canal 监控 JavaScript
java电商项目(六)
Thymeleaf 是一个类似于 FreeMarker 的模板引擎,能够完全替代 JSP。它支持动静结合,无网络时显示静态内容,有网络时用后台数据替换静态内容,并且与 Spring Boot 完美整合。本文介绍了如何使用 Thymeleaf 生成商品详情页的静态页面。具体步骤包括创建商品静态化微服务、配置项目依赖、创建 Controller 和 Service、生成静态页面、模板填充、静态资源过滤以及启动测试。此外,还介绍了如何通过 Canal 监听商品数据变化,自动触发静态页面的生成或删除。
8 1
|
2天前
|
canal NoSQL 关系型数据库
java电商项目(四)
本章介绍了如何通过Lua、OpenResty、Nginx限流及Canal的使用,实现电商门户首页的高并发解决方案。主要内容包括: 1. **商城门户搭建**:使用Vue和iView构建前端门户项目,介绍如何展示商品分类和广告数据,并通过Redis缓存提升访问速度。 2. **Lua基础**:介绍Lua的基本概念、特性、应用场景及安装步骤,并通过示例展示了Lua的基本语法和常用功能。 3. **OpenResty介绍**:详细说明OpenResty的特性和优势,包括如何安装OpenResty和配置Nginx,以及如何使用Lua脚本操作Nginx缓存和数据库。
8 1
|
2天前
|
存储 前端开发 JavaScript
java电商项目(二)
本文档详细介绍了商品分类和规格参数的实现过程。商品分类分为三级管理,主要用于首页商品导航和后台商品管理,采用树状结构存储。规格参数则用于描述商品的具体属性,包括SPU和SKU的定义,规格参数与分类绑定,支持搜索过滤。文档涵盖了表结构设计、实体类、持久层、业务层、控制层的实现,并提供了前端组件的示例代码,确保前后端无缝对接。
10 1
|
2天前
|
存储 安全 Java
java电商项目(八)
OAuth 2.0 是一种开放标准,允许用户授权第三方应用访问其在某一网站上的私密资源,而无需提供用户名和密码。它通过提供一个令牌(token)来实现这一功能。OAuth 2.0 主要包括四种授权模式:授权码模式、简化模式、密码模式和客户端模式。授权码模式是最常用的一种,适用于第三方平台登录功能。Spring Security OAuth 2.0 提供了强大的工具来实现授权服务器和资源服务器的集成,支持多种授权模式和令牌存储方式,如内存、数据库、JWT 和
10 0
|
2天前
|
前端开发 算法 JavaScript
java电商项目(三)
本文介绍了乐购商城的商品数据分析和管理功能。首先解释了SPU(标准产品单位)和SKU(库存量单位)的概念,以及它们在商品管理和销售中的作用。接着详细分析了SPU、SPU详情和SKU三个表的结构及其关系。文章还介绍了商品管理的需求分析、实现思路和后台代码,包括实体类、持久层、业务层和控制层的实现。最后,文章讲解了前端组件的设计和实现,包括列表组件、添加修改组件、商品描述、通用规格、SKU特有规格和SKU列表的处理。通过这些内容,读者可以全面了解乐购商城的商品管理和数据分析系统。
7 0