乐优商城学习Day08:
注意:此次代码都是在第七天的基础上
第七天的链接如下:
https://blog.csdn.net/zcylxzyh/article/details/100637514
此次笔记内容主要为:
1.给出完整的search.html代码
2.实现过滤分类和品牌和实现过滤规格参数(后台)
下面开始第八天的学习:
1.给出完整的search.html代码
此次页面为search.html,本次所有操作均在此页面进行,但我们主要关注后台就直接给出完整的search.html。
包括了过滤,分类等的页面渲染。
<!DOCTYPE html>
<html xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>
<title>乐优商城--商品搜索结果页</title>
<link rel="icon" href="assets/img/favicon.ico">
<link href='./css/material.css' rel="stylesheet">
<link href="./css/vuetify.min.css" rel="stylesheet">
<script src="./js/vue/vue.js"></script>
<script src="./js/vue/vuetify.js"></script>
<script src="./js/axios.min.js"></script>
<script src="./js/common.js"></script>
<link rel="stylesheet" type="text/css" href="css/webbase.css"/>
<link rel="stylesheet" type="text/css" href="css/pages-list.css"/>
<link rel="stylesheet" type="text/css" href="css/widget-cartPanelView.css"/>
<style type="text/css">
* {
box-sizing: unset;
}
.btn-arrow, .btn-arrow:visited, .btn-arrow:link, .btn-arrow:active {
width: 46px;
height: 23px;
border: 1px solid #DDD;
background: #FFF;
line-height: 23px;
font-family: "\5b8b\4f53";
text-align: center;
font-size: 16px;
color: #AAA;
text-decoration: none;
out-line: none
}
.btn-arrow:hover {
background-color: #1299ec;
color: whitesmoke;
}
.top-pagination {
display: block;
padding: 3px 15px;
font-size: 11px;
font-weight: 700;
line-height: 18px;
color: #999;
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
text-transform: uppercase;
float: right;
margin-top: 6px
}
.top-pagination span {
margin-right: 10px;
}
.logo-list li {
padding: 8px;
}
.logo-list li:hover {
background-color: #f3f3f3;
}
.type-list a:hover {
color: #1299ec;
}
.skus {
list-style: none;
}
.skus li {
list-style: none;
display: inline-block;
float: left;
margin-left: 2px;
border: 2px solid #f3f3f3;
}
.skus li.selected {
border: 2px solid #dd1144;
}
.skus img {
width: 25px;
height: 25px;
}
</style>
<script type="text/javascript" src="plugins/jquery/jquery.min.js"></script>
</head>
<body>
<div id="searchApp">
<div id="nav-bottom">
<ly-top/>
</div>
<!--list-content-->
<div class="main">
<div class="py-container">
<div class="bread">
<!--面包屑-->
<ul class="fl sui-breadcrumb">
<li><span>全部结果:</span></li>
<li><a href="#">手机</a></li>
<li><span>手机通讯</span></li>
</ul>
<!--已选择过滤项-->
<ul class="tags-choose">
<li class="tag" v-for="(v,k) in search.filter" :key="k">
{
{k === 'brandId' ? '品牌' : k}}:<span style="color: red" v-text="findValue(k,v)">apple</span>
<i class="sui-icon icon-tb-close" @click="deleteFilter(k)"></i>
</li>
</ul>
<div class="clearfix"></div>
</div>
<!--selector-->
<div class="clearfix selector">
<div class="type-wrap" v-for="(f,i) in remainFilter" v-show="i <= 5 || showMore" :key="f.k"
v-if="f.k !== 'brandId'">
<div class="fl key" v-text="f.k === 'cid3' ? '分类' : f.k"></div>
<div class="fl value">
<ul class="type-list">
<li v-for="(o,j) in f.options" :key="j" v-if="o" @click="selectFilter(f.k,o.id || o)">
<a v-text="o.name || o"></a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap logo" v-else>
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list" v-for="(o,j) in f.options" :key="j">
<li v-if="o.image"><img :src="o.image" @click="selectFilter(f.k,o.id || o)"/></li>
<li v-else><a href="#" v-text="o.name" @click="selectFilter(f.k,o.id || o)"></a></li>
</ul>
</div>
<div class="fl ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
</div>
</div>
<div class="type-wrap" style="text-align: center">
<v-btn small flat v-show="!showMore" @click="showMore=true">
更多
<v-icon>arrow_drop_down</v-icon>
</v-btn>
<v-btn small="" flat v-show="showMore" @click="showMore=false">
收起
<v-icon>arrow_drop_up</v-icon>
</v-btn>
</div>
</div>
<!--details-->
<div class="details">
<div class="sui-navbar">
<div class="navbar-inner filter">
<ul class="sui-nav">
<li class="active">
<a href="#">综合</a>
</li>
<li>
<a href="#">销量</a>
</li>
<li>
<a href="#">新品</a>
</li>
<li>
<a href="#">评价</a>
</li>
<li>
<a href="#">价格</a>
</li>
</ul>
<div class="top-pagination">
<span>共 <i style="color: #222;">{
{total}}</i> 商品</span>
<span><i style="color: red;">{
{search.page}}</i>/{
{totalPage}}</span>
<a class="btn-arrow" href="#" @click.prevent="prePage"
style="display: inline-block"><</a>
<a class="btn-arrow" href="#" @click.prevent="nextPage"
style="display: inline-block">></a>
</div>
</div>
</div>
<div class="goods-list">
<ul class="yui3-g">
<li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
<div class="list-wrap">
<div class="p-img">
<a :href="'/item/'+goods.id+'.html'" target="_blank"><img :src="goods.selectedSku.image"
height="200"/></a>
<ul class="skus">
<li :class="{selected: goods.selectedSku.id === sku.id}"
@mouseenter="goods.selectedSku=sku"
v-for="sku in goods.skus" :key="sku.id">
<img :src="sku.image">
</li>
</ul>
</div>
<div class="clearfix"></div>
<div class="price">
<strong>
<em>¥</em>
<i v-text="ly.formatPrice(goods.selectedSku.price)"></i>
</strong>
</div>
<div class="attr">
<em v-text="goods.selectedSku.title.substring(0,21) + '...' "></em>
</div>
<div class="cu">
<em><span>促</span>{
{goods.subTitle.substring(0,15) + '...'}}</em>
</div>
<div class="commit">
<i class="command">已有2000人评价</i>
</div>
<div class="operate">
<a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">对比</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">关注</a>
</div>
</div>
</li>
</ul>
</div>
<div class="fr">
<div class="sui-pagination pagination-large">
<ul>
<li :class="{prev:true, disabled: search.page === 1}">
<a href="#" @click.prevent="prePage">«上一页</a>
</li>
<li :class="{active: index(i) === search.page}" v-for="i in Math.min(5,totalPage)" :key="i">
<a href="#" v-text="index(i)"></a>
</li>
<li class="dotted" v-show="search.page+2<totalPage && totalPage > 5"><span>...</span></li>
<li :class="{next:true, disabled: search.page === totalPage}">
<a href="#" @click.prevent="nextPage">下一页»</a>
</li>
</ul>
<div><span>共{
{totalPage}}页 </span><span>
到第
<input type="text" class="page-num">
页 <button class="page-confirm" onclick="alert(1)">确定</button></span></div>
</div>
</div>
</div>
<!--hotsale-->
<div class="clearfix hot-sale">
<h4 class="title">热卖商品</h4>
<div class="hot-list">
<ul class="yui3-g">
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_01.png"/>
</div>
<div class="attr">
<em>Apple苹果iPhone 6s (A1699)</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4088.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有700人评价</i>
</div>
</div>
</li>
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_03.png"/>
</div>
<div class="attr">
<em>金属A面,360°翻转,APP下单省300!</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4088.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有700人评价</i>
</div>
</div>
</li>
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_04.png"/>
</div>
<div class="attr">
<em>256SSD商务大咖,完爆职场,APP下单立减200</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4068.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有20人评价</i>
</div>
</div>
</li>
<li class="yui3-u-1-4">
<div class="list-wrap">
<div class="p-img">
<img src="img/like_02.png"/>
</div>
<div class="attr">
<em>Apple苹果iPhone 6s (A1699)</em>
</div>
<div class="price">
<strong>
<em>¥</em>
<i>4088.00</i>
</strong>
</div>
<div class="commit">
<i class="command">已有700人评价</i>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var vm = new Vue({
el: "#searchApp",
data: {
ly: ly,
search: {
},
goodsList: [],
total: 0,
totalPage: 0,
selectedSku: {
},
filters: [],
showMore: false //是否显示更多
},
created() {
//获取请求参数
const search = ly.parse(location.search.substring(1));
search.page = parseInt(search.page) || 1;
search.filter = search.filter ? search.filter : {
};
this.search = search;
//发送后台
this.loadData();
},
watch: {
search: {
deep: true,
handler(val, old) {
if (!old || !old.key) {
// 如果旧的search值为空,或者search中的key为空,证明是第一次
return;
}
// 把search对象变成请求参数,拼接在url路径
window.location.href = "http://www.leyou.com/search.html?" + ly.stringify(val);
}
}
},
methods: {
loadData() {
//发送到后台
ly.http.post("/search/page", this.search).then(resp => {
console.log(resp);
//保存分页结果
this.total = resp.data.total;
this.totalPage = resp.data.totalPage;
//保存当前页商品
resp.data.items.forEach(goods => {
goods.skus = JSON.parse(goods.skus);
goods.selectedSku = goods.skus[0];
});
this.goodsList = resp.data.items;
//获取聚合结果并形成过滤项
this.filters.push({
k: "cid3",
options: resp.data.categories,
});
this.filters.push({
k: "brandId",
options: resp.data.brands,
});
console.log(resp);
resp.data.specs.forEach(spec => this.filters.push(spec));
}).catch(error => {
})
},
index(i) {
if (this.search.page <= 3 || this.totalPage < 5) {
return i;
} else if (this.search.page >= this.totalPage - 2) {
return this.totalPage - 5 + i;
} else {
return this.search.page - 3 + i;
}
},
prePage() {
if (this.search.page > 1) {
this.search.page--;
}
},
nextPage() {
if (this.search.page < this.totalPage) {
this.search.page++;
}
},
selectFilter(key, option) {
const {
...obj} = this.search.filter;
obj[key] = option;
this.search.filter = obj;
},
findValue(k, v) {
if (!this.filters)
return
if (k !== 'brandId')
return v;
return this.filters.find(f => f.k === 'brandId').options[0].name;
},
deleteFilter(k) {
const {
...obj} = this.search.filter;
delete obj[k];
this.search.filter = obj;
}
},
computed: {
remainFilter() {
const keys = Object.keys(this.search.filter);
return this.filters.filter(f => !keys.includes(f.k) && f.options.length > 1);
}
},
components: {
lyTop: () => import("./js/pages/top.js")
}
});
</script>
<!-- 底部栏位 -->
<!--页面底部,由js动态加载-->
<div class="clearfix footer"></div>
<script type="text/javascript">$(".footer").load("foot.html");</script>
<!--页面底部END-->
</body>
<!--购物车单元格 模板-->
<script type="text/template" id="tbar-cart-item-template">
<div class="tbar-cart-item">
<div class="jtc-item-promo">
<em class="promo-tag promo-mz">满赠<i class="arrow"></i></em>
<div class="promo-text">已购满600元,您可领赠品</div>
</div>
<div class="jtc-item-goods">
<span class="p-img"><a href="#" target="_blank"><img src="{2}" alt="{1}" height="50" width="50"/></a></span>
<div class="p-name">
<a href="#">{
1}</a>
</div>
<div class="p-price"><strong>¥{
3}</strong>×{
4}</div>
<a href="#none" class="p-del J-del">删除</a>
</div>
</div>
</script>
<!--侧栏面板结束-->
<script type="text/javascript" src="js/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript">
$(function () {
$("#service").hover(function () {
$(".service").show();
}, function () {
$(".service").hide();
});
$("#shopcar").hover(function () {
$("#shopcarlist").show();
}, function () {
$("#shopcarlist").hide();
});
})
</script>
<script type="text/javascript" src="js/model/cartModel.js"></script>
<script type="text/javascript" src="js/czFunction.js"></script>
<script type="text/javascript" src="js/plugins/jquery.easing/jquery.easing.min.js"></script>
<script type="text/javascript" src="js/plugins/sui/sui.min.js"></script>
<script type="text/javascript" src="js/widget/cartPanelView.js"></script>
</html>
此部分结束。
2.实现过滤分类和品牌和实现过滤规格参数(后台)
2.1 过滤分类和品牌分析
先来看分类和品牌。在我们的数据库中已经有所有的分类和品牌信息。在这个位置,是不是把所有的分类和品牌信息都展示出来呢?
显然不是,用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。
无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。
2.2 扩展返回的结果
原来,我们返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,我们需要对返回的结果进行扩展,添加分类和品牌的数据。
那么问题来了:以什么格式返回呢?
看页面:
分类:页面显示了分类名称,但背后肯定要保存id信息。所以至少要有id和name
品牌:页面展示的有logo,有文字,当然肯定有id,基本上是品牌的完整数据
我们新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合:
package com.leyou.search.pojo;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
public class SearchResult extends PageResult<Goods> {
private List<Category> categories;// 分类待选项
private List<Brand> brands;// 品牌待选项
private List<Map<String,Object>> specs; //规格参数 key 及 待选项
public SearchResult(){
}
public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Category> categories, List<Brand> brands, List<Map<String, Object>> specs) {
super(total, totalPage, items);
this.categories = categories;
this.brands = brands;
this.specs = specs;
}
}
2.3 过滤规格参数分析
有四个问题需要先思考清楚:
- 什么时候显示规格参数过滤?
- 如何知道哪些规格需要过滤?
- 要过滤的参数,其可选值是如何获取的?
- 规格过滤的可选值,其数据格式怎样的?
什么情况下显示有关规格参数的过滤?
如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。
因此,我们在后台需要对聚合得到的商品分类数量进行判断,如果等于1,我们才继续进行规格参数的聚合。
如何知道哪些规格需要过滤?
我们不能把数据库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。
值的庆幸的是,我们在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。
因此,一旦商品分类确定,我们就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。
要过滤的参数,其可选值是如何获取的?
虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。
与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值。
规格过滤的可选值,其数据格式怎样的?
我们直接看页面效果:
我们之前存储时已经将数据分段,恰好符合这里的需求
接下来,我们就用代码实现刚才的思路。
总结一下,应该是以下几步:
- 1)用户搜索得到商品,并聚合出商品分类
- 2)判断分类数量是否等于1,如果是则进行规格参数聚合
- 3)先根据分类,查找可以用来搜索的规格
- 4)对规格参数进行聚合
- 5)将规格参数聚合结果整理后返回
2.4 修改SearchService:
package com.leyou.search.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.JsonUtils;
import com.leyou.common.utils.NumberUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
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.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
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 org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specClient;
@Autowired
private GoodsRepository repository;
@Autowired
private ElasticsearchTemplate template;
public Goods buildGoods(Spu spu) {
Long spuId = spu.getId();
//查询分类
List<Category> categories = categoryClient.queryCategoryByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
if (CollectionUtils.isEmpty(categories)) {
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
}
//从List集合获取到名字
List<String> names = categories.stream().map(Category::getName).collect(Collectors.toList());
//查询品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
if (brand == null) {
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
// 搜索字段
String all = spu.getTitle() + StringUtils.join(names, " ") + brand.getName();
//查询sku
List<Sku> skuList = goodsClient.querySkuBySpuId(spuId);
if (CollectionUtils.isEmpty(skuList)) {
throw new LyException(ExceptionEnum.GOODS_SKU_NOT_FOUND);
}
//对sku进行处理
List<Map<String, Object>> skus = new ArrayList<>();
//价格集合
Set<Long> priceList = new HashSet<>();
for (Sku sku : skuList) {
Map<String, Object> map = new HashMap<>();
map.put("id", sku.getId());
map.put("title", sku.getTitle());
map.put("price", sku.getPrice());
map.put("image", StringUtils.substringBefore(sku.getImages(), ","));
skus.add(map);
//处理价格
priceList.add(sku.getPrice());
}
//查询规格参数
List<SpecParam> params = specClient.queryParamList(null, spu.getCid3(), true);
if (CollectionUtils.isEmpty(params)) {
throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
}
//查询商品详情
SpuDetail spuDetail = goodsClient.queryDetailById(spuId);
//获取通用规格参数
Map<Long, String> generSpec = JsonUtils.toMap(spuDetail.getGenericSpec(), Long.class, String.class);
//获取特有规格参数
Map<Long, List<String>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
});
//规格参数
Map<String, Object> specs = new HashMap<>();
for (SpecParam param : params) {
//规格名称
String key = param.getName();
Object value = "";
//判断是否是通用规格
if(param.getGeneric()){
value = generSpec.get(param.getId());
//判断是否是数值类型
if(param.getNumeric()){
//处理成段
value = chooseSegment(value.toString(),param);
}
}else {
value = specialSpec.get(param.getId());
}
//存入map
specs.put(key,value);
}
//构建goods对象
Goods goods = new Goods();
goods.setBrandId(spu.getBrandId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(spu.getCreateTime());
goods.setId(spuId);
goods.setAll(all);// 搜索字段,包含标题,分类,品牌,规格等
goods.setPrice(priceList);//所有sku的价格集合
goods.setSkus(JsonUtils.toString(skus));//所有sku的集合的json格式
goods.setSpecs(specs);//所有的可搜索的规格参数
goods.setSubTitle(spu.getSubTitle());
return goods;
}
//处理成段
private String chooseSegment(String value, SpecParam p) {
double val = NumberUtils.toDouble(value);
String result = "其它";
// 保存数值段
for (String segment : p.getSegments().split(",")) {
String[] segs = segment.split("-");
// 获取数值范围
double begin = NumberUtils.toDouble(segs[0]);
double end = Double.MAX_VALUE;
if(segs.length == 2){
end = NumberUtils.toDouble(segs[1]);
}
// 判断是否在范围内
if(val >= begin && val < end){
if(segs.length == 1){
result = segs[0] + p.getUnit() + "以上";
}else if(begin == 0){
result = segs[1] + p.getUnit() + "以下";
}else{
result = segment + p.getUnit();
}
break;
}
}
return result;
}
//搜索
public PageResult<Goods> search(SearchRequest request) {
int page = request.getPage() - 1;
int size = request.getSize();
//创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//0 结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id","subTitle","skus"},null));
//1 分页
queryBuilder.withPageable(PageRequest.of(page,size));
//2 搜索条件
QueryBuilder basicQuery = buildBasicQuery(request);
queryBuilder.withQuery(basicQuery);
// 3 聚合分类和品牌
// 3.1 聚合分类
String categoryAggName = "category_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
// 3.2 聚合品牌
String brandAggName = "brand_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
//4 查询
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
//5 解析结果
// 5.1 解析分页结果
long total = result.getTotalElements();
int totalPage = result.getTotalPages();
List<Goods> goodsList = result.getContent();
// 5.2 解析聚合结果
Aggregations aggs = result.getAggregations();
List<Category> categories = parseCategoryAgg(aggs.get(categoryAggName));
List<Brand> brands = parseBrandAgg(aggs.get(brandAggName));
//6 完成规格参数聚合
List<Map<String,Object>> specs = null;
if(categories != null && categories.size() == 1){
// 商品分类存在,并且数量为1,可以聚合规格参数
specs = buildSpecificationAgg(categories.get(0).getId(), basicQuery);
}
return new SearchResult(total,totalPage,goodsList,categories,brands,specs);
}
//基础查询
private QueryBuilder buildBasicQuery(SearchRequest request) {
//创建布尔查询
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
//查询条件
queryBuilder.must(QueryBuilders.matchQuery("all",request.getKey()));
//过滤条件
Map<String, String> map = request.getFilter();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
//处理key
if(!"cid3".equals(key) && !"brandId".equals(key)){
key = "specs."+ key + ".keyword";
}
queryBuilder.filter(QueryBuilders.termQuery(key,entry.getValue()));
}
return queryBuilder;
}
//规格参数聚合
private List<Map<String, Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) {
List<Map<String, Object>> specs = new ArrayList<>();
//1 查询需要聚合的规格参数
List<SpecParam> params = specClient.queryParamList(null, cid, true);
//2 聚合
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//2.1 带上查询条件
queryBuilder.withQuery(basicQuery);
// 2.2 聚合
for (SpecParam param : params) {
String name = param.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs."+name+".keyword"));
}
// 3 获取结果
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
// 4 解析结果
Aggregations aggs = result.getAggregations();
for (SpecParam param : params) {
//规格参数名
String name = param.getName();
StringTerms terms = aggs.get(name);
//准备map
Map<String, Object> map = new HashMap<>();
map.put("k",name);
map.put("options",terms.getBuckets().stream().map(b -> b.getKeyAsString()).collect(Collectors.toList()));
specs.add(map);
}
return specs;
}
//品牌聚合
private List<Brand> parseBrandAgg(LongTerms terms) {
try{
List<Long> ids = terms.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());
List<Brand> brands = brandClient.queryBrandByIds(ids);
return brands;
}catch (Exception e) {
return null;
}
}
//分类聚合
private List<Category> parseCategoryAgg(LongTerms terms) {
try {
List<Long> ids = terms.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());
List<Category> categories = categoryClient.queryCategoryByIds(ids);
return categories;
}catch (Exception e){
return null;
}
}
}
至此今天学习结束。