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>

    结果:

     

     

  • 相关阅读:
    201671010119 2016-2017-2《Java程序设计》第十四周学习心得
    201671010119 2016-2017-2《Java程序设计》第十三周学习心得
    201671010119 2016-2017-2《Java程序设计》第十二周学习心得
    201671010119 2016-2017-2《Java程序设计》第十一周学习心得
    201671010119 2016-2017-2《Java程序设计》第十周学习心得
    201671010119 2016-2017-2《Java程序设计》第九周学习心得
    201671010118 2016-2017-2《Java程序设计》 面向对象程序设计课程学习进度条
    201671010118 2016-2017-2《Java程序设计》 第十八周学习心得
    201671010118 2016-2017-2《Java程序设计》 第十七周学习心得
    201671010118 2016-2017-2《Java程序设计》 第十六周学习心得
  • 原文地址:https://www.cnblogs.com/luckyplj/p/11609169.html
Copyright © 2011-2022 走看看