子丹商城——检索服务

根据之前设计的页面,效果如下:

33.jpg

搜索功能相对比较繁杂,因为有各种各样的搜索项,返回的结果也乱;所有,我们需要专门为查询条件、查询结果封装成类;


一、检索查询参数的模型抽取SearchParam.java

条件封装类:

@Data
public class SearchParam {
    private String keyword;     //页面传递过来的全文匹配关键字
    private Long catalog3Id;    //三级分类id

    //排序条件
    //saleCount(销量): saleCount_asc/saleCount_desc
    //hotScore(热度分): hotScore_asc/hotScore_desc
    //skuPrice(价格): skuPrice_asc/skuPrice_desc
    private String sort;

    //过滤条件
    //hasStock(仅显示有货):
    //skuPrice区间
    //brandId(品牌Id)
    //catalog3Id
    //attrs
    private Integer hasStock;   //hasStock=0/1(0表示无库存,1表示有库存)
    private String skuPrice;    //1_500/_500/500_
    private List<Long> brandId; //允许多选

    private List<String> attrs;  //允许多选,属性attrs=name_其它:安卓 ——属性名name,值为其它、安卓等多个值的

    //页码
    private Integer pageNum = 1;   //只有页码,不需要size,使用默认
}

二、检索返回结果模型抽取SearchResult.java

@Data
public class SearchResult {
    //查询到的所有商品信息
    private List<SkuEsModel> products;

    //分页信息
    private Integer pageNum;  //当前页码
    private Long total;   //总记录数
    private Integer totalPages;  //总页数


    //查询到的所有品牌信息
    private List<BrandVo> brands;   //当前查询到的结果,所有涉及到的品牌——用来缩小查询范围

    //查询所涉及到的所有属性
    private List<AttrVo> attrs;     //当前查询到的结果,所有涉及到的属性

    //查询所涉及到的所有分类信息
    private List<CatelogVo> catelogs;   //当前查询到的结果,所有涉及到的分类
}

其中:

结果涉及的所有品牌:BrandVo.java:

@Data
public class BrandVo {
    private Long brandId;
    private String brandName;
    private String brandImg;
}

结果涉及的所有分类:CatalogVo.java:

@Data
public class CatelogVo {
    private Long catelogId;
    private String catelogName;
}

结果涉及的所有属性:AttrVo.java:

@Data
public class AttrVo {
    private Long attrId;

    private String attrName;

    private List<String> attrValue;
}

三、接口调用链路

SearchController.java:

@Controller
public class SearchController {
    @Autowired
    MallSearchService mallSearchService;

    /**
     * 自动将页面提交过来的所有查询参数封装成指定的SearchParam对象
     * @param param
     * @return
     */
    @GetMapping("list.html")
    public String listPage(SearchParam param, Model model){
        SearchResult result = mallSearchService.search(param);
        model.addAttribute("result", result);
        return "list";
    }
}

MallSearchServiceImpl.java:

@Service
public class MallSearchServiceImpl implements MallSearchService {
    @Override
    public SearchResult search(SearchParam param) {
        return null;
    }
}

四、检索功能DSL语句测试

# must:模糊查询,要打分,性能略低,适合text全文检索字段
# filter:过滤,不需要打分,性能高
# 聚合分析(难)
GET product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "11",
              "14"
            ]
          }
        },
        {
          "term": {
            "hasStock": false
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "11"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "海思",
                        "高通"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 5500,
              "lte": 6500
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 100,
  "highlight": {
    "fields": {
      "skuTitle": {}
    },
    "pre_tags": "<b style='color:red'>",
    "post_tags": "</b>"
  },
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": {
        "brand_name_agg": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
        "brand_img_agg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalog_agg": {
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalog_name_agg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attr_id_agg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attr_name_agg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attr_value_agg": {
              "terms": {
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

执行上面的DSL语句,查看结果,是否可用:

34.jpg

结果显示,命中6个目标,并对这六个目标进行了多种聚合分析!


五、Java编写MallSearchServiceImpl.java中的search()方法

1、首先明确方法结构体search():

@Service
@Slf4j
public class MallSearchServiceImpl implements MallSearchService {
    @Autowired
    RestHighLevelClient client;

    //去es中进行检索
    @Override
    public SearchResult search(SearchParam param) {
        //动态构建出查询需要的dsl(记录在http://www.jiguiquan.com)

        SearchResult result=null;
        //1. 创建检索请求
        SearchRequest searchRequest =buildSearchRequest(param);
        try {
            //2.执行检索请求
            SearchResponse response = client.search(searchRequest, ZidanElasticSearchConfig.COMMON_OPTIONS);
            //3.分析响应数据封装成为我们想要的格式
            result=buildSearchResult(param,response);
        } catch (IOException e) {

        }

        return result;
    }
    
    private SearchRequest buildSearchRequest(SearchParam param) {
        return null;
    }

    private SearchResult buildSearchResult(SearchParam param, SearchResponse response) {
        return null;
    }
}

2、完成私有方法buildSearchRequest(SearchParam param):

private SearchRequest buildSearchRequest(SearchParam param) {
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

    /**
     * TODO 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
     */
    //1. 构建bool-query
    BoolQueryBuilder boolQueryBuilder=QueryBuilders.boolQuery();

    //1.1 bool-must: 支持模糊匹配的text类型
    if(StringUtils.isNotBlank(param.getKeyword())){
        boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
    }

    //1.2 bool-fiter: 其它不需要模糊匹配的keyword类型
    //1.2.1 catelogId
    if(null != param.getCatalog3Id()){
        boolQueryBuilder.filter(QueryBuilders.termQuery("catelogId",param.getCatalog3Id()));
    }

    //1.2.2 brandId
    if(null != param.getBrandId() && param.getBrandId().size() >0){
        boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
    }

    //1.2.3 attrs ———— 按照所有指定的属性进行查询 ———— attrs=1_5寸:8寸&2_16G:8G
    if(!CollectionUtils.isEmpty(param.getAttrs())){
        param.getAttrs().forEach(item -> {
            //一组: 1_5寸:8寸
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            String[] s = item.split("_");
            String attrId=s[0];
            String[] attrValues = s[1].split(":");//这个属性检索用的值
            boolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
            boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));
            //每一种组合都得生成一个嵌入式nested的Query条件
            NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs",boolQuery, ScoreMode.None);
            boolQueryBuilder.filter(nestedQueryBuilder);
        });
    }

    //1.2.4 hasStock
    if(null != param.getHasStock()){
        boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1));
    }

    //1.2.5 skuPrice ——按照价格区间1_500/_500/500_
    if(!StringUtils.isEmpty(param.getSkuPrice())){
        //skuPrice形式为:1_500或_500或500_
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
        String[] price = param.getSkuPrice().split("_");
        if(price.length==2){
            rangeQueryBuilder.gte(price[0]).lte(price[1]);
        }else if(price.length == 1){
            if(param.getSkuPrice().startsWith("_")){
                rangeQueryBuilder.lte(price[1]);
            }
            if(param.getSkuPrice().endsWith("_")){
                rangeQueryBuilder.gte(price[0]);
            }
        }
        boolQueryBuilder.filter(rangeQueryBuilder);
    }

    //封装所有的bool查询条件
    searchSourceBuilder.query(boolQueryBuilder);

    /**
     * TODO 排序,分页,高亮
     */
    //排序:形式为sort=hotScore_asc/desc
    if(!StringUtils.isEmpty(param.getSort())){
        String sort = param.getSort();
        String[] sortFileds = sort.split("_");
        SortOrder sortOrder="asc".equalsIgnoreCase(sortFileds[1])?SortOrder.ASC:SortOrder.DESC;
        searchSourceBuilder.sort(sortFileds[0],sortOrder);
    }

    //分页
    searchSourceBuilder.from((param.getPageNum()-1)* EsConstant.PRODUCT_PAGESIZE);
    searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

    //高亮
    if(!StringUtils.isEmpty(param.getKeyword())){
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("skuTitle");
        highlightBuilder.preTags("<b style='color:red'>");
        highlightBuilder.postTags("</b>");
        searchSourceBuilder.highlighter(highlightBuilder);
    }

    /**
     * TODO 聚合分析
     */
    //TODO 1. 品牌聚合
    TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
    brand_agg.field("brandId").size(50);
    //1.1 品牌的子聚合-品牌名聚合
    brand_agg.subAggregation(AggregationBuilders.terms("brand_Name_agg").field("brandName").size(1));
    //1.2 品牌的子聚合-品牌图片聚合
    brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
    searchSourceBuilder.aggregation(brand_agg);

    //TODO 2. 分类聚合
    TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
    catalog_agg.field("catalogId").size(20);
    //2.1 分类聚合的子聚合——分类名称聚合
    catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
    searchSourceBuilder.aggregation(catalog_agg);

    //TODO 3. 按照属性信息进行聚合——嵌入式聚合
    NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
    //3.1 按照属性ID进行聚合————聚合出当前所有的attrId
    TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
    attr_agg.subAggregation(attr_id_agg);
    //3.1.1 在每个attrId下,按照属性名进行聚合
    attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
    //3.1.1 在每个attrId下,按照属性值进行聚合
    attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
    searchSourceBuilder.aggregation(attr_agg);

    log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());
    SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);

    return searchRequest;
}

3、完成私有方法buildSearchResult(SearchParam param, SearchResponse response):

/**
 * 构建结果数据
 * @param response
 * @return
 */
private SearchResult buildSearchResult(SearchParam param,SearchResponse response) {
    SearchResult result = new SearchResult();
    SearchHits hits = response.getHits();

    SearchHit[] subHits = hits.getHits();
    List<SkuEsModel> skuEsModels=null;
    if(subHits != null && subHits.length > 0){
        skuEsModels = Arrays.asList(subHits).stream().map(subHit -> {
            String sourceAsString = subHit.getSourceAsString();
            SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
            if (!StringUtils.isEmpty(param.getKeyword())) {   //带模糊搜索keyword,我们就进行高亮显示
                HighlightField skuTitle = subHit.getHighlightFields().get("skuTitle");
                String skuTitleHighLight = skuTitle.getFragments()[0].string();
                skuEsModel.setSkuTitle(skuTitleHighLight);
            }
            return skuEsModel;
        }).collect(Collectors.toList());

    }

    //1.返回所查询到的所有商品
    result.setProducts(skuEsModels);

    //2.当前所有商品所涉及到的所有属性信息
    ParsedNested attr_agg = response.getAggregations().get("attr_agg");
    ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
    List<AttrVo> attrVos = attr_id_agg.getBuckets().stream().map(item -> {
        AttrVo attrVo = new AttrVo();
        //1.获取属性的id
        long attrId = item.getKeyAsNumber().longValue();
        //2.获取属性名
        String attrName = ((ParsedStringTerms) item.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
        //3.获取属性的所有值
        List<String> attrValues = ((ParsedStringTerms) item.getAggregations().get("attr_value_agg")).getBuckets()
                .stream().map(bucket ->bucket.getKeyAsString()).collect(Collectors.toList());
        attrVo.setAttrId(attrId);
        attrVo.setAttrName(attrName);
        attrVo.setAttrValue(attrValues);
        return attrVo;
    }).collect(Collectors.toList());
    result.setAttrs(attrVos);

    //3.当前所有商品所涉及到的所有品牌信息
    ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
    List<BrandVo> brandVos = brand_agg.getBuckets().stream().map(item -> {
        BrandVo brandVo = new BrandVo();
        //1.获取id
        long brandId = item.getKeyAsNumber().longValue();
        //2.获取品牌名
        String brandName = ((ParsedStringTerms) item.getAggregations().get("brand_Name_agg")).getBuckets().get(0).getKeyAsString();
        //3.获取品牌图片
        String brandImag = ((ParsedStringTerms) item.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
        brandVo.setBrandId(brandId);
        brandVo.setBrandName(brandName);
        brandVo.setBrandImg(brandImag);
        return brandVo;
    }).collect(Collectors.toList());
    result.setBrands(brandVos);

    //4.当前所有商品所涉及到的所有分类信息
    ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
    List<CatalogVo> catalogVos = catalog_agg.getBuckets().stream().map(item -> {
        CatalogVo catalogVo = new CatalogVo();
        //获取分类ID
        String catelogId = item.getKeyAsString();
        catalogVo.setCatalogId(Long.parseLong(catelogId));
        //获取分类名
        ParsedStringTerms catalog_name_agg = item.getAggregations().get("catalog_name_agg");
        String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString();
        catalogVo.setCatalogName(catalogName);
        return catalogVo;
    }).collect(Collectors.toList());
    result.setCatelogs(catalogVos);

    //=========以上从聚合信息中获取===========
    //5.分页信息-页码
    result.setPageNum(param.getPageNum());
    //5.分页信息-总记录数
    long total = hits.getTotalHits().value;
    result.setTotal(total);
    //5.分页信息-总页码
    boolean flag=total%EsConstant.PRODUCT_PAGESIZE == 0;
    int totalPage=flag?((int)total/EsConstant.PRODUCT_PAGESIZE):((int)total/EsConstant.PRODUCT_PAGESIZE+1);
    result.setTotalPages(totalPage);
    List<Integer> page = new ArrayList<>();
    for (int i=1;i<=totalPage;i++){
        page.add(i);
    }
    result.setPageNavs(page);

    return result;
}

六、result放入Model后,前端thymeleaf渲染list.html

1、初步效果:

35.jpg

2、动态拼接筛选条件

2.1、动态拼接参数,我们抽象成一个公共方法:

//统一拼接检索商品参数的方法
function searchProducts(name, value){
    //原来的url,如果包含"?"则拼接"&",如果不包含"?"则拼接"?"
    var href = location.href;
    if (href.indexOf("?") != -1){
        location.href = location.href + "&" + name + "=" + value;
    }else {
        location.href = location.href + "?" + name + "=" + value;
    }
}

2.2、而在使用过程中:

拼接品牌brandId:
<a href="#" th:href="${'javascript:searchProducts(&quot;brandId&quot;,' + brand.brandId+ ')'}"> </a>
拼接分类catalog3Id:
<a href="#" th:href="${'javascript:searchProducts(&quot;catalog3Id&quot;,' + catalog.catalogId+ ')'}" th:text="${catalog.catalogName}"> </a>
拼接属性attrs
<a href="#" th:text="${val}" th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;' + attr.attrId + '_' + val + '&quot;)'}"> </a>

最终我们得到的url如下:

http://search.zidanmall.com/list.html?brandId=14&catalog3Id=225&attrs=12_MT6765

七、页面搜索框搜索实现

1、html部分:

<div class="header_form">
    <input type="text" id="keywordInput" placeholder="手机" th:value="${param.keyword}"/>
    <a href="javascript:searchByKeyword()">搜索</a>
</div>

2、searchByKeyword()方法:

//页面搜索框,按照keyword进行搜索
function searchByKeyword() {
    var keywordInput = $("#keywordInput").val();
    searchProducts("keyword", keywordInput);
}

八、实现分页跳转功能

1、html部分:

<div class="filter_page">
    <div class="page_wrap">
        <span class="page_span1">
            <a class="page_a" th:attr="pn=${result.pageNum - 1}"
               th:if="${result.pageNum > 1}">
                < 上一页
            </a>
            <a class="page_a" th:each="nav:${result.pageNavs}"
               th:attr="pn=${nav}, style=${nav == result.pageNum?'border: 0;color:#ee2222;background: #fff':''}">
                [[${nav}]]
            </a>
            <a class="page_a" th:attr="pn=${result.pageNum + 1}"
               th:if="${result.pageNum < result.totalPages}">
                下一页 >
            </a>
        </span>
        <span class="page_span2">
            <em>共<b>[[${result.totalPages}]]</b>页&nbsp;&nbsp;到第</em>
            <input type="number" value="1">
            <em>页</em>
            <a class="page_submit">确定</a>
        </span>
    </div>
</div>

2、javascript事件触发方法:

//翻页事件
$(".page_a").click(function () {
    var pn = $(this).attr("pn");
    var href = location.href;
    console.log("pn",pn);
    if (href.indexOf("pageNum") != -1){
        //替换pageNum的值
        location.href = replaceParamVal(location.href, "pageNum", pn);
    }else {
        location.href = location.href + "&pageNum=" + pn;
    }
    return false;
})

function replaceParamVal(url, paramName, replaceVal) {
    var oUrl = url.toString();
    console.log(oUrl);
    var re = eval('/(' + paramName + '=)([^&]*)/gi');
    console.log("re",re);
    var nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
    return nUrl;
}

九、完成综合排序和回显功能

1、html部分:

<div class="filter_top_left" th:with="p = ${param.sort}"> <!--使用p临时变量获取参数sort的值,供下文使用-->
    <a th:class="${(!#strings.isEmpty(p)&&(#strings.equals(p, 'hotScore_desc')))?'sort_a desc':'sort_a'}"
       th:attr="style=${(#strings.isEmpty(p) || #strings.startsWith(p,'hotScore'))?'color: #FFF; border-color: #e4393c; background: #e4393c':'color: #333; border-color: #CCC; background: #FFF'}"
       sort="hotScore" href="#">
        综合排序[[${(!#strings.isEmpty(p) &&(#strings.equals(p, 'hotScore_desc')))?'↓':'↑'}]]</a>
    <a th:class="${(!#strings.isEmpty(p)&&(#strings.equals(p,'saleCount_desc')))?'sort_a desc':'sort_a'}"
       th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount'))?'color: #FFF; border-color: #e4393c; background: #e4393c':'color: #333; border-color: #CCC; background: #FFF'}"
       sort="saleCount" href="#">
        销量[[${(!#strings.isEmpty(p) &&(#strings.equals(p, 'saleCount_desc')))?'↓':'↑'}]]</a>
    <a th:class="${(!#strings.isEmpty(p)&&(#strings.equals(p,'skuPrice_desc')))?'sort_a desc':'sort_a'}"
       th:attr="style=${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice'))?'color: #FFF; border-color: #e4393c; background: #e4393c':'color: #333; border-color: #CCC; background: #FFF'}"
       sort="skuPrice" href="#">
        价格[[${(!#strings.isEmpty(p) &&(#strings.equals(p, 'skuPrice_desc')))?'↓':'↑'}]]</a>
    <a class="sort_a" href="#">评论分</a>
    <a class="sort_a" href="#">上架时间</a>
</div>

2、javascript事件方法:

//点击排序改变样式
$(".sort_a").click(function () {
    //1、改变元素样式
    changeStyle(this);
    //2、拼接参数sort=saleCount_asc/saleCount_desc
    var sort = $(this).attr("sort");
    sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc"
    location.href = replaceAndAddParamVal(location.href, "sort", sort);
    return false;
});

//改变排序选项样式
function changeStyle(ele) {
    //1、清除其它兄弟样式
    $(".sort_a").css({"color": "#333", "border-color": "#CCC", "background": "#FFF"})
    $(".sort_a").each(function () {
        var text = $(this).text().replace("↓", "").replace("↑", "");
        $(this).text(text);
    })
    //2、给自己增加样式
    $(ele).css({"color": "#FFF", "border-color": "#e4393c", "background": "#e4393c"});
    //3、切换升降序图标
    $(ele).toggleClass("desc");
    if ($(ele).hasClass("desc")) {
        var text = $(ele).text().replace("↓", "").replace("↑", "");
        $(ele).text(text + "↓");
    } else {
        var text = $(ele).text().replace("↓", "").replace("↑", "");
        $(ele).text(text + "↑");
    }
}

//替换or新增参数
function replaceAndAddParamVal(url, paramName, replaceVal) {
    var oUrl = url.toString();
    if (oUrl.indexOf(paramName) !== -1) {
        var re = eval('/(' + paramName + '=)([^&]*)/gi');
        return oUrl.replace(re, paramName + '=' + replaceVal);
    } else {
        if (oUrl.indexOf("?") !== -1) {
            return oUrl + "&" + paramName + "=" + replaceVal;
        } else {
            return oUrl + "?" + paramName + "=" + replaceVal;
        }
    }
}

3、页面效果如下(keyword条件已做了高亮显示):

36.jpg


十、实现价格区间搜索

1、html部分(其中priceRange是定义的临时变量,和上文p一样):

<div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}">
    <input id="skuPriceFrom" type="number" style="width: 60px;margin-left: 30px;"
       th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
    >
    -
    <input id="skuPriceTo" type="number" style="width: 60px;margin-right: 10px;"
       th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
    >
    <button id="skuPriceSearchBtn">确定</button>
</div>

2、javascript事件方法:

//按价格区间查询
$("#skuPriceSearchBtn").click(function () {
    var from = $("#skuPriceFrom").val();
    var to = $("#skuPriceTo").val();
    var query = from + "_" + to;
    location.href = replaceAndAddParamVal(location.href, "skuPrice", query);
});

十一、实现“仅显示有货”的checkbox选项

1、html部分:

<li>
    <a href="#" th:with="check=${param.hasStock}">
        <input id="showHasStock" type="checkbox"
               th:checked="${#strings.equals(check,'1')}"
        >
        仅显示有货
    </a>
</li>

2、javascript事件方法:

//仅显示有货
$("#showHasStock").change(function () {
    if ($(this).prop('checked')) {
        location.href = replaceAndAddParamVal(location.href, "hasStock", 1);
    } else {
        //没选中
        var re = eval('/(&hasStock=)([^&]*)/gi');
        location.href = (location.href + "").replace(re, '');
    }
    return false;
});

十二、实现面包屑导航功能

1、在返回结果模型中增加面包屑导航元素List<NavVo>:

@Data
public class SearchResult {

    //查询到的所有商品信息
    private List<SkuEsModel> products;

    //分页信息
    private Integer pageNum;  //当前页码
    private Long total;   //总记录数
    private Integer totalPages;  //总页数


    //查询到的所有品牌信息
    private List<BrandVo> brands;   //当前查询到的结果,所有涉及到的品牌——用来缩小查询范围

    //查询所涉及到的所有属性
    private List<AttrVo> attrs;     //当前查询到的结果,所有涉及到的属性

    //查询所涉及到的所有分类信息
    private List<CatalogVo> catalogs;   //当前查询到的结果,所有涉及到的分类

    //===============以上是返回给页面的所有信息================

    //可选页码值
    private List<Integer> pageNavs;

    //面包屑导航
    private List<NavVo> navs = new ArrayList<>();
    private List<Long> attrIds = new ArrayList<>();  //存放那些attrId已经被筛选了,方便做筛选条件联动

    @Data
    public static class NavVo{
        private String navName;  //导航项的名字
        private String navValue; //导航项的值
        private String link;     //当取消后,应该跳转到的地址
    }
}

2、MallSearchServiceImpl.java的search()方法的buildSearchResult()方法:

//6、构建面包屑导航功能
if (!CollectionUtils.isEmpty(param.getAttrs())){
    List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
        SearchResult.NavVo navVo = new SearchResult.NavVo();
        //分析每个attr传递过来的参数值:格式 attrs=2_5寸:
        String[] s = attr.split("_");
        navVo.setNavValue(s[1]);
        R r = productFeignService.attrInfo(Long.parseLong(s[0]));
        if (r.getCode() == 0){
            AttrResponseVo attrResponse = r.getData("attr", new TypeReference<AttrResponseVo>() {});
            navVo.setNavName(attrResponse.getAttrName());
        }else {
            navVo.setNavName(s[0]);
        }
        //将条件中已经有的id传递到result中,便于筛选条件联动
        result.getAttrIds().add(Long.parseLong(s[0]));

        //取消这个面包屑后,我们将要跳转的链接————将当前请求地址中的本条件清除掉即可
        //获取所有的查询条件————然后去掉自己
        String encode = "";
        try {
            encode = URLEncoder.encode(attr, "UTF-8");
            encode.replace("+", "20%"); //是因为浏览器对空格的编码和java不一致导致的
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String replace = param.get_queryString().replace("&attrs=" + encode, "").replace("attrs=" + encode, "");
        navVo.setLink("http://search.zidanmall.com/list.html?" + replace);
        return navVo;
    }).collect(Collectors.toList());
    result.setNavs(collect);
}

//品牌的面包屑导航功能
if(!CollectionUtils.isEmpty(param.getBrandId())){
    List<SearchResult.NavVo> navs = result.getNavs();
    SearchResult.NavVo navVo = new SearchResult.NavVo();
    navVo.setNavName("品牌");
    //TODO 远程查询所有品牌
    R r = productFeignService.getBrands(param.getBrandId());
    if(r.getCode()==0){
        List<BrandShortVo> brands = r.getData("brands", new TypeReference<List<BrandShortVo>>() {
        });
        StringBuffer buffer=new StringBuffer();
        String replace="";
        for (BrandShortVo brandVo:brands){
            buffer.append(brandVo.getName()+";");
            String encode = "";
            try {
                encode = URLEncoder.encode(brandVo.getBrandId() + "", "UTF-8");
                encode.replace("+", "20%"); //是因为浏览器对空格的编码和java不一致导致的
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            replace = param.get_queryString().replace("&brandId=" + encode, "").replace("brandId=" + encode, "");
        }
        navVo.setNavValue(buffer.toString());
        navVo.setLink("http://search.zidanmall.com/list.html?" + replace);
    }

    navs.add(navVo);
    result.setNavs(navs);
}

3、html页面遍历使用:

<div class="JD_ipone_one c">
    <a th:href="${nav.link}"
       th:each="nav:${result.navs}">
        <span th:text="${nav.navName}"></span>:<span th:text="${nav.navValue}"></span>
        ×</a>
</div>

十三、实现条件筛选联动

(效果——当某个查询条件已经被选择,那么将不再出现它的遍历结果)

1、品牌的条件联动效果:

<div th:if="${#strings.isEmpty(brandId)}" class="JD_nav_wrap">
    <div class="sl_key">
        <span>品牌:</span>
    </div>
    ....
</div>

2、其它属性的条件联动效果:上面的后端代码中,已经考虑到属性的联动,而放置了attrIds

<!--遍历所有需要展示的属性--> 
<div class="JD_pre" th:each="attr:${result.attrs}" th:if="${!#lists.contains(result.attrIds, attr.attrId)}">
    <div class="sl_key">
        <span th:text="${attr.attrName}">屏幕尺寸:</span>
    </div>
    <div class="sl_value">
        <ul>
            <li th:each="val:${attr.attrValue}">
                <a href="#" th:text="${val}"
                   th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;' + attr.attrId + '_' + val + '&quot;)'}">
                    以上
                </a>
            </li>
        </ul>
    </div>
</div>

3、最终效果

38.jpg

到这里,整个商品,基于ElasticSearch的检索功能差不多就做完了,前端花了大量的时间;

以后的章节中,就不会过多得描述前端实现了;重点是架构和后端的技术点;

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐