zoukankan      html  css  js  c++  java
  • 036 搭建搜索微服务04----分类和品牌的过滤

    先来看分类和品牌。在我们的数据库中已经有所有的分类和品牌信息。在这个位置,是不是把所有的分类和品牌信息都展示出来呢?

    显然不是,用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。

    无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。

    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>

    结果:

     

     

  • 相关阅读:
    UVa 116 单向TSP(多段图最短路)
    POJ 1328 Radar Installation(贪心)
    POJ 1260 Pearls
    POJ 1836 Alignment
    POJ 3267 The Cow Lexicon
    UVa 1620 懒惰的苏珊(逆序数)
    POJ 1018 Communication System(DP)
    UVa 1347 旅行
    UVa 437 巴比伦塔
    UVa 1025 城市里的间谍
  • 原文地址:https://www.cnblogs.com/luckyplj/p/11609169.html
Copyright © 2011-2022 走看看