欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第十九章:过滤条件的筛选和商品详情后台实现

程序员文章站 2022-03-25 14:55:13
...

此博客用于个人学习,来源于网上,对知识点进行一个整理。

1. 过滤条件的筛选:

当我们点击页面的过滤项,要做哪些事情?

  • 把过滤条件保存在 search 对象中(watch 监控到 search 变化后就会发送到后台)
  • 在页面顶部展示已选择的过滤项
  • 把商品分类展示到顶部面包屑

1.1 保存过滤项:

1)定义属性:

我们把已选择的过滤项保存在 search 中:

第十九章:过滤条件的筛选和商品详情后台实现
要注意,在 created 构造函数中会对 search 进行初始化,所以要在构造函数中对 filter 进行初始化:

第十九章:过滤条件的筛选和商品详情后台实现
search.filter 是一个对象,结构:

{
    "过滤项名":"过滤项值"
}

2) 绑定点击事件:

给所有的过滤项绑定点击事件:

第十九章:过滤条件的筛选和商品详情后台实现
要注意,点击事件传2个参数:

  • k:过滤项的 key
  • option:当前过滤项对象

在点击事件中,保存过滤项到 selectedFilter :

selectFilter(k, o){
    const obj = {};
    Object.assign(obj, this.search);
    if(k === '分类' || k === '品牌'){
        o = o.id;
    }
    obj.filter[k] = o.name || o;
    this.search = obj;
}

另外,这里 search 对象中嵌套了 filter 对象,请求参数格式化时需要进行特殊处理,修改 common.js 中的一段代码:

第十九章:过滤条件的筛选和商品详情后台实现

1.2 后台添加过滤条件:

既然请求已经发送到了后台,那接下来我们就在后台去添加这些条件:

1)拓展请求对象:

我们需要在请求类: SearchRequest 中添加属性,接收过滤属性。过滤属性都是键值对格式,但是 key 不确定,所以用一个 map 来接收即可。

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

    private Integer page;// 当前页

    private Map<String,Object> filter;//过滤条件

    private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
    private static final Integer DEFAULT_PAGE = 1;// 默认页

    public String getKey() {
        return key;
    }

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

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

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

2)添加过滤条件:

目前,我们的基本查询是这样的:

第十九章:过滤条件的筛选和商品详情后台实现
现在,我们要把页面传递的过滤条件也加入进去。

因此不能在使用普通的查询,而是要用到 BooleanQuery,基本结构是这样的:

GET /heima/_search
{
    "query":{
        "bool":{
        	"must":{ "match": { "title": "小米手机",operator:"and"}},
        	"filter":{
                "range":{"price":{"gt":2000.00,"lt":3800.00}}
        	}
        }
    }
}

所以,我们对原来的基本查询进行改造,因为比较复杂,我们将其封装到一个方法中:

/**
 * 构建布尔查询
 * @param request
 * @return
 */
private BoolQueryBuilder buildBoolQueryBuilder(SearchRequest request) {
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    //给布尔值添加基本查询条件
    boolQueryBuilder.must(QueryBuilders.matchQuery("all",request.getKey()).operator(Operator.AND));
    //添加过滤条件
    //获取用户选择的过滤信息
    Map<String,Object> filter = request.getFilter();
    for (Map.Entry<String,Object> entry: filter.entrySet()){
        String key = entry.getKey();
        if (StringUtils.equals("品牌",key)){
            key = "brandId";
        }else if (StringUtils.equals("分类",key)){
            key = "cid3";
        }else {
            key = "specs." + key + ".keyword";
        }
        boolQueryBuilder.filter(QueryBuilders.termQuery(key,entry.getValue()));
    }
    return boolQueryBuilder;
}

2. 商品详情后台实现:

当用户搜索到商品,肯定会点击查看,就会进入商品详情页,接下来我们完成商品详情页的后台部分。

2.1 商品详情页服务:

商品详情浏览量比较大,并发高,我们会独立开启一个微服务,用来展示商品详情。

1)创建 module:商品的详情页服务,命名为: leyou-goods-web。

2)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.goods</groupId>
    <artifactId>leyou-goods-web</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <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.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.item</groupId>
            <artifactId>leyou-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

3)编写启动类:

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

4)application.yml 文件:

server:
  port: 8084
spring:
  application:
    name: goods-web
  thymeleaf:
    cache: false
  rabbitmq:
    host: 192.168.56.101
    virtual-host: /leyou
    username: leyou
    password: leyou
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 5

5)页面模板:

从 leyou-portal 中复制 item.html 模板到当前项目 resource 目录下的 templates 中。

2.2 页面跳转:

1)修改页面跳转路径:

首先我们需要修改搜索结果页的商品地址,目前所有商品的地址都是:http://www.leyou.com/item.html。

第十九章:过滤条件的筛选和商品详情后台实现
我们应该跳转到对应的商品的详情页才对。

通过详情页的预览,我们知道商品详情页是多个 SKU 的集合,即 SPU。所以,页面跳转时,我们应该携带 SPU 的 id 信息。

采用了路径占位符的方式来传递 spu 的 id,我们打开 search.html,修改其中的商品路径:

第十九章:过滤条件的筛选和商品详情后台实现
2)nginx 反向代理:

接下来,我们要把这个地址指向我们刚刚创建的服务:leyou-goods-web,其端口为8084。在 nginx.conf 中添加一段逻辑:

第十九章:过滤条件的筛选和商品详情后台实现
把以 /item 开头的请求,代理到我们的8084端口。

3)编写跳转 controller:

在 leyou-goods-web 中编写 controller,接收请求,并跳转到商品详情页:

@Controller
@RequestMapping("item")
public class GoodsController {

    /**
     * 跳转到商品详情页
     * @param model
     * @param id
     * @return
     */
    @GetMapping("{id}.html")
    public String toItemPage(Model model, @PathVariable("id")Long id){

        return "item";
    }
}

2.3 封装模型数据:

我们已知的条件是传递来的 spu 的 id,我们需要根据 spu 的 id 查询到下面的数据:

  • spu 信息
  • spu 的详情
  • spu 下的所有 sku
  • 品牌
  • 商品三级分类
  • 商品规格参数、规格参数组

1)商品微服务提供接口:

以上所需数据中,根据 id 查询 spu 的接口目前还没有,我们需要在商品微服务中提供这个接口:

GoodsApi:

/**
 * 根据spu的id查询spu
 * @param id
 * @return
 */
@GetMapping("spu/{id}")
public Spu querySpuById(@PathVariable("id") Long id);

GoodsController:

@GetMapping("spu/{id}")
public ResponseEntity<Spu> querySpuById(@PathVariable("id") Long id){
    Spu spu = this.goodsService.querySpuById(id);
    if(spu == null){
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(spu);
}

GoodsService:

public Spu querySpuById(Long id) {
    return this.spuMapper.selectByPrimaryKey(id);
}

我们在页面展示规格时,需要按组展示:

第十九章:过滤条件的筛选和商品详情后台实现
组内有多个参数,为了方便展示。我们在 leyou-item-service 中提供一个接口,查询规格组,同时在规格组内的所有参数。

拓展 SpecGroup 类:我们在 SpecGroup 中添加一个 SpecParam 的集合,保存该组下所有规格参数。

@Table(name = "tb_spec_group")
public class SpecGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long cid;

    private String name;

    //忽略该字段
    @Transient
    private List<SpecParam> params;// 该组下的所有规格参数集合

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCid() {
        return cid;
    }

    public void setCid(Long cid) {
        this.cid = cid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<SpecParam> getParams() {
        return params;
    }

    public void setParams(List<SpecParam> params) {
        this.params = params;
    }
}

然后提供查询接口:

SpecificationAPI:

@RequestMapping("spec")
public interface SpecificationApi {

    /**
     * 根据条件查询规格参数
     * @param gid
     * @return
     */
    @GetMapping("params")
    public List<SpecParam> queryParams(
            @RequestParam(value = "gid",required = false)Long gid,
            @RequestParam(value = "cid",required = false)Long cid,
            @RequestParam(value = "generic",required = false)Boolean generic,
            @RequestParam(value = "searching",required = false)Boolean searching
    );

    @GetMapping("group/param/{cid}")
    public List<SpecGroup> queryGroupsWithParam(@PathVariable("cid")Long cid);
}

SpecificationController:

@GetMapping("group/param/{cid}")
public ResponseEntity<List<SpecGroup>> queryGroupsWithParam(@PathVariable("cid")Long cid){
    List<SpecGroup> groups = this.specificationService.queryGroupsWithParam(cid);
    if (CollectionUtils.isEmpty(groups)){
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(groups);
}

SpecificationService:

public List<SpecGroup> queryGroupsWithParam(Long cid) {
    List<SpecGroup> groups = queryGroupsByCid(cid);
    groups.forEach(group->{
        List<SpecParam> params = this.queryParamsByGid(group.getId(), null, null, null);
        group.setParams(params);
    });
    return groups;
}

在 service 中,我们调用之前编写过的方法,查询规格组,和规格参数,然后封装返回。

2) 创建 FeignClient:

我们在 leyou-goods-web 服务中,创建 FeignClient:

BrandClient:

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

CategoryClient:

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

GoodsClient:

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

SpecificationClient:

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

3)封装数据模型:

我们创建一个 GoodsService,在里面来封装数据模型。

这里要查询的数据:

  • SPU

  • SpuDetail

  • SKU 集合

  • 商品分类

    • 这里值需要分类的 id 和 name 就够了,因此我们查询到以后自己需要封装数据
  • 品牌对象

  • 规格组

    • 查询规格组的时候,把规格组下所有的参数也一并查出,上面提供的接口中已经实现该功能,我们直接调用
  • sku 的特有规格参数

    • 在页面渲染时,需要知道参数的名称

我们就需要把 id 和 name一一对应起来,因此需要额外查询 sku 的特有规格参数,然后变成一个 id:name 的键值对格式,也就是一个 Map,方便将来根据 id 查找。

GoodsService:

@Service
public class GoodsService {

    @Autowired
    private BrandClient brandClient;
    @Autowired
    private CategoryClient categoryClient;
    @Autowired
    private GoodsClient goodsClient;
    @Autowired
    private SpecificationClient specificationClient;

    public Map<String,Object> loadData(Long spuId){

        Map<String,Object> model = new HashMap<>();

        //根据spuId查询spu
        Spu spu = this.goodsClient.querySpuById(spuId);

        //查询spuDetail
        SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spuId);

        //查询分类,Map<String,Object>
        List<Long> cids = Arrays.asList(spu.getCid1(),spu.getCid2(),spu.getCid3());
        List<String> names = this.categoryClient.queryNamesByIds(cids);
        //初始化一个分类的map
        List<Map<String,Object>> categories = new ArrayList<>();
        for (int i = 0;i < cids.size();i++){
            Map<String,Object> map = new HashMap<>();
            map.put("id",cids.get(i));
            map.put("name",names.get(i));
            categories.add(map);
        }

        //查询品牌
        Brand brand = this.brandClient.queryBrandById(spuId);

        //查询skus
        List<Sku> skus = this.goodsClient.querySkusBySpuId(spuId);

        //查询规格参数组
        List<SpecGroup> groups = this.specificationClient.queryGroupsWithParam(spu.getCid3());

        //查询特殊的规格参数
        List<SpecParam> params = this.specificationClient.queryParams(null,spu.getCid3(),null,null);
        //初始化特殊规格参数的map
        Map<Long,String> paramMap = new HashMap<>();
        params.forEach(param->{
            paramMap.put(param.getId(),param.getName());
        });

        model.put("spu",spu);
        model.put("spuDetail",spuDetail);
        model.put("categories",categories);
        model.put("brand",brand);
        model.put("skus",skus);
        model.put("groups",groups);
        model.put("paramMap",paramMap);

        return model;
    }
}

然后在 controller 中把数据放入 model:

@Controller
@RequestMapping("item")
public class GoodsController {

    @Autowired
    private GoodsService goodsService;
    
    /**
     * 跳转到商品详情页
     * @param model
     * @param id
     * @return
     */
    @GetMapping("{id}.html")
    public String toItemPage(Model model, @PathVariable("id")Long id){
        // 加载所需的数据
        Map<String, Object> modelMap = this.goodsService.loadModel(id);
        // 放入模型
        model.addAllAttributes(modelMap);
        return "item";
    }
}