先来看分类和品牌。在我们的数据库中已经有所有的分类和品牌信息。在这个位置,是不是把所有的分类和品牌信息都展示出来呢?
显然不是,用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。
无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。
1.扩展返回的结果
原来,我们返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,我们需要对返回的结果进行扩展,添加分类和品牌的数据。
那么问题来了:以什么格式返回呢?
看页面:
分类:页面显示了分类名称,但背后肯定要保存id信息。所以至少要有id和name
品牌:页面展示的有logo,有文字,当然肯定有id,基本上是品牌的完整数据
我们新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合
package lucky.leyou.domain; import lucky.leyou.common.domain.PageResult; import lucky.leyou.item.domain.Brand; import java.util.List; import java.util.Map; public class SearchResult extends PageResult<Goods> { private List<Map<String, Object>> categories; private List<Brand> brands; public SearchResult(List<Map<String, Object>> categories, List<Brand> brands) { this.categories = categories; this.brands = brands; } public SearchResult(Long total, List<Goods> items, List<Map<String, Object>> categories, List<Brand> brands) { super(total, items); this.categories = categories; this.brands = brands; } public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Map<String, Object>> categories, List<Brand> brands) { super(total, totalPage, items); this.categories = categories; this.brands = brands; } public List<Map<String, Object>> getCategories() { return categories; } public void setCategories(List<Map<String, Object>> categories) { this.categories = categories; } public List<Brand> getBrands() { return brands; } public void setBrands(List<Brand> brands) { this.brands = brands; } }
2.聚合商品分类和品牌
我们修改搜索的业务逻辑,对分类和品牌聚合。
因为索引库中只有id,所以我们根据id聚合,然后再根据id去查询完整数据。
所以,商品微服务需要提供一个接口:根据品牌id集合,批量查询品牌。
修改controller:
/** * 搜索商品 * * @param request * @return */ @PostMapping("page") public ResponseEntity<SearchResult> search(@RequestBody SearchRequest request) { SearchResult result = this.searchService.search(request); if (result == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } return ResponseEntity.ok(result); }
修改SearchService:
package lucky.leyou.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lucky.leyou.client.BrandClient; import lucky.leyou.client.CategoryClient; import lucky.leyou.client.GoodsClient; import lucky.leyou.client.SpecificationClient; import lucky.leyou.common.domain.PageResult; import lucky.leyou.domain.Goods; import lucky.leyou.domain.SearchRequest; import lucky.leyou.domain.SearchResult; import lucky.leyou.item.domain.*; import lucky.leyou.reponsitory.GoodsRepository; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.elasticsearch.index.query.Operator; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; 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.io.IOException; import java.util.*; /** * 搜索服务 */ @Service public class SearchService { @Autowired private BrandClient brandClient; @Autowired private CategoryClient categoryClient; @Autowired private GoodsClient goodsClient; @Autowired private SpecificationClient specificationClient; @Autowired private GoodsRepository goodsRepository; private static final ObjectMapper MAPPER = new ObjectMapper(); /** * 把Spu转为Goods * @param spu * @return * @throws IOException */ public Goods buildGoods(Spu spu) throws IOException { // 创建goods对象 Goods goods = new Goods(); // 根据品牌id查询品牌 Brand brand = this.brandClient.queryBrandById(spu.getBrandId()); // 查询分类名称,Arrays.asList该方法能将方法所传参数转为List集合 List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3())); // 根据spuid查询spu下的所有sku List<Sku> skus = this.goodsClient.querySkuBySpuId(spu.getId()); //初始化一个价格集合,收集所有的sku的价格 List<Long> prices = new ArrayList<>(); //收集sku的必要的字段信息 List<Map<String, Object>> skuMapList = new ArrayList<>(); // 遍历skus,获取价格集合 skus.forEach(sku ->{ prices.add(sku.getPrice()); Map<String, Object> skuMap = new HashMap<>(); skuMap.put("id", sku.getId()); skuMap.put("title", sku.getTitle()); skuMap.put("price", sku.getPrice()); //获取sku中的图片,数据库中的图片可能是多张,多张是以,分隔,所以也以逗号进行切割返回图片数组,获取第一张图片 skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : ""); skuMapList.add(skuMap); }); // 以tb_spec_param表中的分类cid字段和searching字段为查询条件查询出tb_spec_param表中所有的搜索规格参数 //将每一个查询结果封装成SpecParam这个bean对象中,将bean对象放入map中构成查询结果集 List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true); // 根据spuid查询spuDetail(即数据库表tb_spu_detail中的一行数据)。获取规格参数值 SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId()); // 获取通用的规格参数,利用jackson工具类json转换为object对象(反序列化),参数1:要转化的json数据,参数2:要转换的数据类型格式 Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() { }); // 获取特殊的规格参数 Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() { }); // 定义map接收{规格参数名,规格参数值} Map<String, Object> paramMap = new HashMap<>(); params.forEach(param -> { // 判断是否通用规格参数 if (param.getGeneric()) { // 获取通用规格参数值 String value = genericSpecMap.get(param.getId()).toString(); // 判断是否是数值类型 if (param.getNumeric()){ // 如果是数值的话,判断该数值落在那个区间 value = chooseSegment(value, param); } // 把参数名和值放入结果集中 paramMap.put(param.getName(), value); } else { paramMap.put(param.getName(), specialSpecMap.get(param.getId())); } }); // 设置参数 goods.setId(spu.getId()); goods.setCid1(spu.getCid1()); goods.setCid2(spu.getCid2()); goods.setCid3(spu.getCid3()); goods.setBrandId(spu.getBrandId()); goods.setCreateTime(spu.getCreateTime()); goods.setSubTitle(spu.getSubTitle()); goods.setAll(spu.getTitle() +" "+ StringUtils.join(names, " ")+" "+brand.getName()); //获取spu下的所有sku的价格 goods.setPrice(prices); //获取spu下的所有sku,并使用jackson包下ObjectMapper工具类,将任意的Object对象转化为json字符串 goods.setSkus(MAPPER.writeValueAsString(skuMapList)); //获取所有的规格参数{name:value} goods.setSpecs(paramMap); return goods; } /** * 判断value值所在的区间 * 范例:value=5.2 Segments:0-4.0,4.0-5.0,5.0-5.5,5.5-6.0,6.0- * @param value * @param p * @return */ 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 SearchResult search(SearchRequest request) { String key = request.getKey(); // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品 if (StringUtils.isBlank(key)) { return null; } // 自定义查询构建器,构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 1、对key进行全文检索查询 queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND)); // 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle queryBuilder.withSourceFilter(new FetchSourceFilter( new String[]{"id","skus","subTitle"}, null)); // 3、分页 // 准备分页参数 int page = request.getPage(); int size = request.getSize(); queryBuilder.withPageable(PageRequest.of(page - 1, size)); //添加分类和品牌聚合 String categoryAggName = "categories"; String brandAggName = "brands"; queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 4、查询,获取结果 //Page<Goods> pageInfo = this.goodsRepository.search(queryBuilder.build()); // 执行搜索,获取搜索的结果集 AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build()); // 解析聚合结果集 List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName)); List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName)); // 封装结果并返回 return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(),categories,brands); } /** * 解析品牌聚合结果集 * @param aggregation * @return */ private List<Brand> getBrandAggResult(Aggregation aggregation) { // 处理聚合结果集 LongTerms terms = (LongTerms)aggregation; // 获取所有的品牌id桶 List<LongTerms.Bucket> buckets = terms.getBuckets(); // 定义一个品牌集合,搜集所有的品牌对象 List<Brand> brands = new ArrayList<>(); // 解析所有的id桶,查询品牌 buckets.forEach(bucket -> { Brand brand = this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue()); brands.add(brand); }); return brands; // 解析聚合结果集中的桶,把桶的集合转化成id的集合 // List<Long> brandIds = terms.getBuckets().stream().map(bucket -> bucket.getKeyAsNumber().longValue()).collect(Collectors.toList()); // 根据ids查询品牌 //return brandIds.stream().map(id -> this.brandClient.queryBrandById(id)).collect(Collectors.toList()); // return terms.getBuckets().stream().map(bucket -> this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue())).collect(Collectors.toList()); } /** * 解析分类 * @param aggregation * @return */ private List<Map<String,Object>> getCategoryAggResult(Aggregation aggregation) { // 处理聚合结果集 LongTerms terms = (LongTerms)aggregation; // 获取所有的分类id桶 List<LongTerms.Bucket> buckets = terms.getBuckets(); // 定义一个品牌集合,搜集所有的品牌对象 List<Map<String, Object>> categories = new ArrayList<>(); List<Long> cids = new ArrayList<>(); // 解析所有的id桶,查询品牌 buckets.forEach(bucket -> { cids.add(bucket.getKeyAsNumber().longValue()); }); List<String> names = this.categoryClient.queryNameByIds(cids); 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); } return categories; } }
测试:
3.页面渲染数据
(1)过滤参数数据结构
首先看页面原来的代码:
我们可以把所有的过滤条件放入一个数组
中,然后在页面利用v-for
遍历一次生成。
其基本结构是这样的:
[ { k:"过滤字段名", options:[{/*过滤字段值对象*/},{/*过滤字段值对象*/}] } ]
我们先在data中定义数组:filters,等待组装过滤参数:
data: { ly, search:{ key: "", page: 1 }, goodsList:[], // 接收搜索得到的结果 total: 0, // 总条数 totalPage: 0, // 总页数 filters:[] // 过滤参数集合 },
然后在查询搜索结果的回调函数中,对过滤参数进行封装:
loadData(){ // ly.http.post("/search/page", ly.stringify(this.search)).then(resp=>{ //注意:http在common.js文件定义的,实际上就是axios //resp表示后台响应的数据对象,resp.data为数据 ly.http.post("/search/page", this.search).then(resp=>{ if(resp.data.items.length===0){ return } this.total=resp.data.total; this.totalPage=resp.data.totalPage; //遍历goodsList集合 resp.data.items.forEach(goods=>{ //将skus字段这个json字符串转换为json对象 goods.skus=JSON.parse(goods.skus); //扩展一个selected属性 goods.selected=goods.skus[0]; }); this.goodsList=resp.data.items; //初始化分类过滤项 this.filters.push({ k:"分类", options:data.categories }); //初始化品牌过滤项 this.filters.push({ k:"品牌", options:data.brands }); }); },
测试:
(2)页面渲染数据
<!--selector--> <div class="clearfix selector"> <div class="type-wrap" v-for="(f,i) in filters" :key="i" v-if="f.k !== '品牌'"> <div class="fl key">{{f.k}}</div> <div class="fl value"> <ul class="type-list"> <li v-for="(option, j) in f.options" :key="j"> <a>{{option.name}}</a> </li> </ul> </div> <div class="fl ext"></div> </div> <div class="type-wrap logo" v-else> <div class="fl key brand">{{f.k}}</div> <div class="value logos"> <ul class="logo-list"> <li v-for="(option, j) in f.options" v-if="option.image"><img :src="option.image" /></li> <li style="text-align: center" v-else><a style="line-height: 30px; font-size: 12px" href="#">{{option.name}}</a></li> </ul> </div> <div class="fl ext"> <a href="javascript:void(0);" class="sui-btn">多选</a> </div> </div>
结果: