谷粒商城笔记合集
分布式基础篇 | 分布式高级篇 | 高可用集群篇 |
---|---|---|
===简介&环境搭建=== | ===Elasticsearch=== | |
项目简介与分布式概念(第一、二章) | Elasticsearch:全文检索(第一章) | |
基础环境搭建(第三章) | ===商品服务开发=== | |
===整合SpringCloud=== | 商品服务 & 商品上架(第二章) | |
整合SpringCloud、SpringCloud alibaba(第四、五章) | ===商城首页开发=== | |
===前端知识=== | 商城业务:首页整合、Nginx 域名访问、性能优化与压力测试 (第三、四、五章) | |
前端开发基础知识(第六章) | 缓存与分布式锁(第六章) | |
===商品服务开发=== | ===商城检索开发=== | |
商品服务开发:基础概念、三级分类(第七、八章) | 商城业务:商品检索(第七章) | |
商品服务开发:品牌管理(第九章) | ||
商品服务开发:属性分组、平台属性(第十、十一章) | ||
商品服务:商品维护(第十二、十三章) | ||
===仓储服务开发=== | ||
仓储服务:仓库维护(第十四章) | ||
基础篇总结(第十五章) |
七、商城业务 & 商品检索⚠️
7.1 整合页面:Thymeleaf⚠️
-
添加本机的域名映射规则并 清除DNS缓存 :C:\Windows\System32\drivers\etc\hosts
192.168.10.10 bilimall.com 192.168.10.10 search.bilimall.com #清除DNS缓存内容 PS C:\Users\ZW L> ipconfig /flushdns Windows IP 配置 已成功刷新 DNS 解析缓存。 #显示DNS缓存内容 PS C:\Users\ZW L> ipconfig /displaydns
-
引入 Thymeleaf 依赖
<!-- thymeleaf模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
-
在 检索服务 中关闭 Thymeleaf 缓存application.yaml
spring: thymeleaf: cache: false
-
拷贝检索首页到 检索服务 中:bilimall-search/src/main/resources/templates/index.html
-
在 检索服务 中修改 检索首页 :静态资源路径、thymeleaf命名空间、html文件格式等
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <!-- 修改为: --> <link rel="stylesheet" href="/static/search/css/index.css"> <script src="/static/search/js/jquery-1.12.4.js"></script> <img src="/static/search/image/down-@1x.png" /> <img src="/static/search/img/01.png" /> ...
-
拷贝静态资源到 nginx 中,修改 nginx 配置文件并重新启动 nginx 服务:F:\software\Nginx\conf\nginx.conf
... http { ... server { listen 80; server_name *.bilimall.com; ... } ... }
-
在 网关服务 中添加 检索系统 的路由规则:application.yaml
spring: cloud: nacos: discovery: server-addr: 114.132.162.129:8848 gateway: routes: - id: bilimall_route uri: lb://bilimall-product predicates: - Host=bilimall.com - id: bilimall_search_route uri: lb://bilimall-search predicates: - Host=search.bilimall.com
7.2 整合 dev-tools⚠️
-
在检索服务的 pom.xml 中引入dev-tools依赖
<!-- devtools --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
-
让页面修改实时生效:CTRL+F9、CTRL+SHIFT+F9
7.3 商城首页(跳转)检索首页
-
修改 商城系统 中点击三级分类的跳转地址:F:\software\Nginx\html\static\index\js\catalogLoader.js
var cata3link = $("<a href=\"http://search.bilimall.com/list.html?catalog3Id="+ctg3.id+"\"
-
在 检索服务 中修改首页文件名称,并创建首页控制器:list.html、cn.lzwei.bilimall.search.controller.SearchController
@Controller public class SearchController { @GetMapping(value = {"/","/list.html"}) public String index(){ return "list"; } }
-
修改 商城系统 中检索商品的跳转地址:bilimall-product/src/main/resources/templates/index.html
function search() { var keyword=$("#searchText").val() window.location.href="http://search.gulimall.com/list.html?keyword="+keyword; }
-
在 检索服务 中修改检索页点击商城首页的跳转地址
<!--头部--> <div class="header_head"> <div class="header_head_box"> <b class="header_head_p"> <div style="overflow: hidden"> <a href="http://bilimall.com" class="header_head_p_a1" style="width:73px;"> 谷粒商城首页 </a> <!--搜索导航--> <div class="header_sous"> <div class="logo"> <a href="http://bilimall.com">...</a> </div>
7.4 检索业务分析💡
7.4.1 检索条件VO分析💡
打个比例吧 你肯定上过京东、淘宝买过东西吧? 那么你想要购买什么东西,你需要在搜索框中搜索你想要购买的物品,那么系统就会给你响应
我在京东搜索 手机
他会显示出相对应的产品
我们分析可能存在的检索条件,并创建对应的VO类:cn.lzwei.bilimall.search.vo.SearchParamVo
/**
* 封装页面所有可能传递过来的查询条件
*/
@Data
public class SearchParamVo {
//页面传递过来的全文匹配关键字
private String keyword;
//三级分类id
private Long catalog3Id;
/**
* sort=saleCout_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
//排序条件
private String sort;
/**
* hasStock=0/1
* skuPrice=1_500/_500/500_
* brandId=1
* attrs=1_红色:黑色&attrs=2_5寸:8寸&
*/
private Integer hasStock;//是否有库存:1-有库存、0-无库存
private String skuPrice;//价格区间查询
private List<Long> brandId;//按照品牌进行查询,可以多选:&1_5寸:8寸&
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1;//页码
}
7.4.2 检索结果VO分析💡
那么返回的数据我们是不是也要创建一个 VO 用来返回页面的数据?借鉴京东的实例来做参考
抽取出结果VO类:cn.lzwei.bilimall.search.vo.SearchResultVo
/**
* 检索结果返回
*/
@Data
public class SearchResultVo {
/**
* 查询到所有商品的商品信息
*/
private List<SpuUpTo> products;
/**
* 以下是分页信息
*/
private Integer pageNum;//当前页码
private Long total;//总共记录数
private Integer totalPages;//总页码
/**
* 当前查询到的结果,所有设计的品牌
*/
private List<BrandVo> brands;
/**
* 当前查询结果,所有涉及到的分类
*/
private List<CatalogVo> catalogs;
/**
* 当前查询到的结果,所有涉及到的所有属性
*/
private List<AttrVo> attrs;
/**
* 页码
*/
private List<Integer> pageNavs;
//==================以上是要返回给页面的所有信息
/**
* 品牌信息
*/
@Data
public static class BrandVo {
private Long brandId; //品牌id
private String brandName; //品牌名字
private String brandImg; //品牌图片
}
/**
* 分类信息
*/
@Data
public static class CatalogVo {
private Long catalogId; //分类id
private String CatalogName; //品牌名字
}
/**
* 属性信息
*/
@Data
public static class AttrVo {
private Long attrId; //属性id
private String attrName; //属性名字
private List<String> attrValue; //属性值
}
}
7.4.3 检索语句分析:DSL
那么这个 DSL 编写我们就在 Kibana 中测试
#GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"7",
"8",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "3"
}
}
},
{
"terms": {
"attrs.attrValue": [
"2019"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 7000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"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": 1
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg",
"size": 1
}
}
}
},
"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
}
}
}
}
}
}
}
}
7.4.4 /product/_mapping
#PUT product
{
"mappings":{
"properties":{
"skuId":{
"type":"long"
},
"spuId":{
"type":"keyword"
},
"skuTitle":{
"type":"text",
"analyzer": "ik_smart"
},
"skuPrice":{
"type":"keyword"
},
"skuImg":{
"type":"text",
"analyzer": "ik_smart"
},
"saleCount":{
"type":"long"
},
"hasStock":{
"type":"boolean"
},
"hotScore":{
"type":"long"
},
"brandId":{
"type":"long"
},
"catelogId":{
"type":"long"
},
"brandName":{
"type":"keyword"
},
"brandImg":{
"type":"keyword"
},
"catalogName":{
"type":"keyword"
},
"attrs":{
"type":"nested",
"properties": {
"attrId":{
"type":"long"
},
"attrName":{
"type":"keyword"
},
"attrValue": {
"type":"keyword"
}
}
}
}
}
}
7.5 API:构建检索请求条件💡
7.5.1 代码编写
代码对照着 7.4.3小节 生成的DSL检索条件进行编写
term 和 terms 不要调用错误
-
cn.lzwei.bilimall.search.constant.EsConstant
public class EsConstant { //商品索引 public static final String PRODUCT_INDEX="product"; //页大小 public static final int SEARCH_PAGESIZE=12; }
-
cn.lzwei.bilimall.search.controller.SearchController
/** * 检索系统首页控制器 */ @Controller public class SearchController { @Resource SearchService searchService; @GetMapping(value = {"/","/list.html"}) public String index(SearchParamVo searchParamVo, Model model){ //在返回页面中设置检索结果 SearchResultVo result=searchService.search(searchParamVo); model.addAttribute("result",result); return "list"; } }
-
cn.lzwei.bilimall.search.service.SearchService
/** * 检索业务 */ public interface SearchService { /** * 返回检索的结果 */ SearchResultVo search(SearchParamVo searchParamVo); }
-
cn.lzwei.bilimall.search.service.impl.SearchServiceImpl
/** * 检索业务 */ @Service(value = "searchService") public class SearchServiceImpl implements SearchService { @Resource RestHighLevelClient restHighLevelClient; /** * 返回检索的结果 */ @Override public SearchResultVo search(SearchParamVo searchParamVo) { //1.构建请求 SearchRequest searchRequest = buildSearchRequest(searchParamVo); System.out.println("构建的请求"+searchRequest.source().toString()); //2.执行请求 SearchResponse response=null; try { response = restHighLevelClient.search(searchRequest, BilimallElasticsearchConfig.COMMON_OPTIONS); } catch (IOException e) { e.printStackTrace(); } //3.封装返回结果 SearchResultVo searchResultVo=buildSearchResultVo(response,searchParamVo); return searchResultVo; } //构建请求 private SearchRequest buildSearchRequest(SearchParamVo searchParamVo) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); /** * 查询 */ BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //1.检索关键字 if(!StringUtils.isEmpty(searchParamVo.getKeyword())){ boolQuery.must(QueryBuilders.matchQuery("skuTitle",searchParamVo.getKeyword())); } //2.过滤三级分类 if(searchParamVo.getCatalog3Id()!=null){ boolQuery.filter(QueryBuilders.termQuery("catalogId",searchParamVo.getCatalog3Id())); } //3.过滤是否有库存 if(searchParamVo.getHasStock()!=null){ boolQuery.filter(QueryBuilders.termQuery("hasStock",searchParamVo.getHasStock()==1)); } //4.过滤价格区间:_500/1_500/500_ if(!StringUtils.isEmpty(searchParamVo.getSkuPrice())){ String skuPrice = searchParamVo.getSkuPrice(); String[] s = searchParamVo.getSkuPrice().split("_"); if(s.length==1){ if(skuPrice.startsWith("_")){ boolQuery.filter(QueryBuilders.rangeQuery("skuPrice").lte(s[0])); } if(skuPrice.endsWith("_")){ boolQuery.filter(QueryBuilders.rangeQuery("skuPrice").gte(s[0])); } }else { boolQuery.filter(QueryBuilders.rangeQuery("skuPrice").gte(s[0]).lte(s[1])); } } //5.过滤品牌 if(searchParamVo.getBrandId()!=null){ boolQuery.filter(QueryBuilders.termsQuery("brandId",searchParamVo.getBrandId())); } //6.过滤\属性 if(searchParamVo.getAttrs()!=null && searchParamVo.getAttrs().size()>0){ for (String attr : searchParamVo.getAttrs()) { String[] attrInfo = attr.split("_"); BoolQueryBuilder attrBoolQuery = QueryBuilders.boolQuery(); //6.1、属性id attrBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrInfo[0])); //6.2、属性值 attrBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrInfo[1].split(":"))); boolQuery.filter(QueryBuilders.nestedQuery("attrs",attrBoolQuery, ScoreMode.None)); } } searchSourceBuilder.query(boolQuery); /** * 排序 * sort=saleCount_asc/desc * sort=skuPrice_asc/desc * sort=hotScore_asc/desc * 分页 */ //7.排序 if(!StringUtils.isEmpty(searchParamVo.getSort())){ String[] sortInfo = searchParamVo.getSort().split("_"); if(sortInfo[0].equalsIgnoreCase("skuPrice")){ if(sortInfo[1].equalsIgnoreCase(SortOrder.ASC.toString())){ searchSourceBuilder.sort("skuPrice", SortOrder.ASC); } if(sortInfo[1].equalsIgnoreCase(SortOrder.DESC.toString())){ searchSourceBuilder.sort("skuPrice", SortOrder.DESC); } } } //8.页码 if(searchParamVo.getPageNum()!=null){ searchSourceBuilder.from((searchParamVo.getPageNum()-1)*EsConstant.SEARCH_PAGESIZE); searchSourceBuilder.size(EsConstant.SEARCH_PAGESIZE); } /** * 高亮、聚合 */ //9.高亮 if (!StringUtils.isEmpty(searchParamVo.getKeyword())){ HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("skuTitle").preTags("<b style=color:red>").postTags("</b>"); searchSourceBuilder.highlighter(highlightBuilder); } //10.聚合 //10.1、品牌 TermsAggregationBuilder brandIdAgg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);//品牌id brandIdAgg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));//品牌名 brandIdAgg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));//品牌图片 searchSourceBuilder.aggregation(brandIdAgg); //10.2、分类 TermsAggregationBuilder catalogIdAgg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(50);//分类id catalogIdAgg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));//分类名 searchSourceBuilder.aggregation(catalogIdAgg); //10.3、属性 NestedAggregationBuilder attrsNestedAgg = AggregationBuilders.nested("attr_agg", "attrs"); TermsAggregationBuilder attrAgg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId").size(50); attrAgg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1)); attrAgg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50)); attrsNestedAgg.subAggregation(attrAgg); searchSourceBuilder.aggregation(attrsNestedAgg); SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder); return searchRequest; } //封装返回结果 private SearchResultVo buildSearchResultVo(SearchResponse response) { SearchResultVo searchResultVo = new SearchResultVo(); return searchResultVo; } }
7.5.2 测试
使用PostMan发送请求获取检索条件
验证检索条件
7.6 API:封装检索结果💡
7.6.1 代码编写
在Debug模式下对照着 resultVO 对返回的结果进行封装
-
cn.lzwei.bilimall.search.constant.EsConstant
public class EsConstant { //商品索引 public static final String PRODUCT_INDEX="product"; //页大小 public static final int SEARCH_PAGESIZE=12; }
-
cn.lzwei.bilimall.search.controller.SearchController:返回检索页面
/** * 检索系统首页控制器 */ @Controller public class SearchController { @Resource SearchService searchService; @GetMapping(value = {"/","/list.html"}) public String index(SearchParamVo searchParamVo, Model model){ //在返回页面中设置检索结果 SearchResultVo result=searchService.search(searchParamVo); model.addAttribute("result",result); return "list"; } }
-
cn.lzwei.bilimall.search.service.SearchService:检索并封装结果
/** * 检索业务 */ public interface SearchService { /** * 返回检索的结果 */ SearchResultVo search(SearchParamVo searchParamVo); }
-
cn.lzwei.bilimall.search.service.impl.SearchServiceImpl:检索并封装结果
/** * 检索业务 */ @Service(value = "searchService") public class SearchServiceImpl implements SearchService { @Resource RestHighLevelClient restHighLevelClient; /** * 返回检索的结果 */ @Override public SearchResultVo search(SearchParamVo searchParamVo) { //1.构建请求 SearchRequest searchRequest = buildSearchRequest(searchParamVo); System.out.println("构建的请求"+searchRequest.source().toString()); //2.执行请求 SearchResponse response=null; try { response = restHighLevelClient.search(searchRequest, BilimallElasticsearchConfig.COMMON_OPTIONS); } catch (IOException e) { e.printStackTrace(); } //3.封装返回结果 SearchResultVo searchResultVo=buildSearchResultVo(response,searchParamVo); return searchResultVo; } //构建请求 private SearchRequest buildSearchRequest(SearchParamVo searchParamVo) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); ... return searchRequest; } //封装返回结果 private SearchResultVo buildSearchResultVo(SearchResponse response,SearchParamVo searchParamVo) { SearchResultVo searchResultVo = new SearchResultVo(); /** * 所有商品信息 */ //1.封装商品信息 List<SpuUpTo> products=new ArrayList<>(); SearchHit[] hits = response.getHits().getHits(); for (SearchHit hit : hits) { String sourceAsString = hit.getSourceAsString(); SpuUpTo sourceParseObject = JSON.parseObject(sourceAsString, new TypeReference<SpuUpTo>() { }); HighlightField skuTitle = hit.getHighlightFields().get("skuTitle"); if(skuTitle!=null && !StringUtils.isEmpty(searchParamVo.getKeyword())){ sourceParseObject.setSkuTitle(skuTitle.getFragments()[0].string()); } products.add(sourceParseObject); } searchResultVo.setProducts(products); /** * 涉及到的所有品牌 */ List<SearchResultVo.BrandVo> brands=new ArrayList<>(); ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg"); for (Terms.Bucket bucket : brand_agg.getBuckets()) { SearchResultVo.BrandVo brandVo = new SearchResultVo.BrandVo(); brandVo.setBrandId(bucket.getKeyAsNumber().longValue());//设置品牌id ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg"); brandVo.setBrandImg(brand_img_agg.getBuckets().get(0).getKeyAsString());//设置品牌图片 ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg"); brandVo.setBrandName(brand_name_agg.getBuckets().get(0).getKeyAsString());//设置品牌名 brands.add(brandVo); } searchResultVo.setBrands(brands); /** * 涉及到的所有分类 */ List<SearchResultVo.CatalogVo> catalogs=new ArrayList<>(); ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg"); for (Terms.Bucket bucket : catalog_agg.getBuckets()) { SearchResultVo.CatalogVo catalogVo = new SearchResultVo.CatalogVo(); catalogVo.setCatalogId(bucket.getKeyAsNumber().longValue());//设置分类id ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg"); catalogVo.setCatalogName(catalog_name_agg.getBuckets().get(0).getKeyAsString());//设置分类名 catalogs.add(catalogVo); } searchResultVo.setCatalogs(catalogs); /** * 涉及到的所有属性 */ List<SearchResultVo.AttrVo> attrs=new ArrayList<>(); ParsedNested attr_agg = response.getAggregations().get("attr_agg"); ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg"); for (Terms.Bucket bucket : attr_id_agg.getBuckets()) { SearchResultVo.AttrVo attrVo = new SearchResultVo.AttrVo(); attrVo.setAttrId((Long) bucket.getKey());//设置属性id ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg"); attrVo.setAttrName(attr_name_agg.getBuckets().get(0).getKeyAsString());//设置属性名 ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg"); List<String> attrValues = attr_value_agg.getBuckets().stream().map(item -> item.getKeyAsString() ).collect(Collectors.toList()); attrVo.setAttrValue(attrValues);//设置属性值 attrs.add(attrVo); } searchResultVo.setAttrs(attrs); /** * 分页信息 */ long total = response.getHits().getTotalHits().value;//获取总记录数 //设置总记录数 searchResultVo.setTotal(total); //设置总页码 Integer totalPage=(int) (total%EsConstant.SEARCH_PAGESIZE==0?(total/EsConstant.SEARCH_PAGESIZE):(total/EsConstant.SEARCH_PAGESIZE+1)); searchResultVo.setTotalPages(totalPage); //设置当前页码 searchResultVo.setPageNum(searchParamVo.getPageNum()); //设置页码列表 List<Integer> pageNavs=new ArrayList<>(); for (int i = 1; i <= totalPage; i++) { pageNavs.add(i); } searchResultVo.setPageNavs(pageNavs); return searchResultVo; } }
7.6.2 测试
7.7 前端:检索页面渲染
7.7.1 渲染:商品&筛选条件
7.7.2 检索:增加筛选条件
<!--品牌-->
<li th:each="brand:${result.brands}">
<a href="/static/search/#" th:href="${'javascript:searchProducts("brandId",'+brand.brandId+')'}">
<img th:src="${brand.brandImg}" alt="">
<div th:text="${brand.brandName}">
华为(HUAWEI)
</div>
</a>
</li>
<!--分类-->
<li th:each="catalog:${result.catalogs}">
<a href="/static/search/#" th:href="${'javascript:searchProducts("catalog3Id",'+catalog.catalogId+')'}">
<div th:text="${catalog.catalogName}">
手机
</div>
</a>
</li>
<!--其他属性-->
<li th:each="value:${attr.attrValue}">
<a href="/static/search/#"
th:href="${'javascript:searchProducts("attrs","'+attr.attrId+'_'+value+'")'}"
th:text="${value}">
5.56英寸及以上
</a>
</li>
<script>
function searchProducts(name, value) {
//原來的页面
location.href = replaceParamVal(location.href,name,value,true)
};
</script>
检索首页
筛选 手机、iphone
筛选 上市日期、入网型号
7.7.3 检索:分页&关键字
分页
关键字
<!--关键字-->
<div class="header_form">
<input type="text" id="keyword_input" placeholder="手机" th:value="${param.keyword}"/>
<a href="javascript:searchByKeyword();">搜索</a>
</div>
<!--分页-->
<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>页 到第</em>
<input type="number" value="1">
<em>页</em>
<a href="#">确定</a>
</span>
</div>
<script>
<!--分页函数-->
$(".page_a").click(function () {
var pn=$(this).attr("pn");
location.href=replaceParamVal(location.href,"pageNum",pn,false);
console.log(replaceParamVal(location.href,"pageNum",pn,false))
});
<!--关键字-->
function searchByKeyword() {
location.href = replaceParamVal(location.href,"keyword", $("#keyword_input").val(),false);
};
/**
* @param url 目前的url
* @param paramName 需要替换的参数属性名
* @param replaceVal 需要替换的参数的新属性值
* @param forceAdd 该参数是否可以重复查询(attrs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏)
* @returns {string} 替换或添加后的url
*/
function replaceParamVal(url, paramName, replaceVal,forceAdd) {
var oUrl = url.toString();
var nUrl;
if (oUrl.indexOf(paramName) != -1) {
if( forceAdd && oUrl.indexOf(paramName+"="+replaceVal)==-1) {
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
} else {
var re = eval('/(' + paramName + '=)([^&]*)/gi');
nUrl = oUrl.replace(re, paramName + '=' + replaceVal);
}
} else {
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
}
return nUrl;
};
</script>
7.7.4 检索:排序
<!--综合排序、销量排序、价格排序-->
<div class="filter_top_left" th:with="p = ${param.sort}, priceRange = ${param.skuPrice}">
<a sort="hotScore"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') && #strings.endsWith(p,'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;' }">
综合排序[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'hotScore') &&
#strings.endsWith(p,'desc')) ?'↓':'↑' }]]
</a>
<a sort="saleCount"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') && #strings.endsWith(p,'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;' }">
销量[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'saleCount') &&
#strings.endsWith(p,'desc'))?'↓':'↑' }]]
</a>
<a sort="skuPrice"
th:class="${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') && #strings.endsWith(p,'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;' }">
价格[[${(!#strings.isEmpty(p) && #strings.startsWith(p,'skuPrice') &&
#strings.endsWith(p,'desc'))?'↓':'↑' }]]
</a>
</div>
<script>
/* 筛选排序条件 */
$(".sort_a").click(function () {
//1.点击排序条件:改变当前元素以及兄弟元素的样式
// changeStyle(this);
//2.跳转到指定位置
$(this).toggleClass("desc");
let sort = $(this).attr("sort");
sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
location.href = replaceParamVal(location.href, "sort", sort,false);
//禁用默认行为
return false;
});
</script>
7.7.5 检索:价格区间&库存
<!--价格区间-->
<input id="skuPriceFrom" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringBefore(priceRange,'_')}"
style="width: 100px; margin-left: 30px">
-
<input id="skuPriceTo" type="number"
th:value="${#strings.isEmpty(priceRange)?'':#strings.substringAfter(priceRange,'_')}"
style="width: 100px">
<button id="skuPriceSearchBtn">确定</button>
<!--是否有货-->
<a th:with="check = ${param.hasStock}">
<input id="showHasStock" type="checkbox" th:checked="${#strings.equals(check,'1')?true:false}">
仅显示有货
</a>
<script>
<!--仅显示有货-->
$("#showHasStock").click(function () {
let checked = $(this).prop("checked");
console.log(checked);
location.href=replaceParamVal(location.href,"hasStock",checked?1:0,false)
});
<!--价格区间-->
$("#skuPriceSearchBtn").click(function () {
var skuPriceFrom = $("#skuPriceFrom").val();
var skuPriceTo = $("#skuPriceTo").val();
location.href = replaceParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
});
</script>
7.8 前后端开发:面包屑导航💡
后端开发
-
cn.lzwei.bilimall.search.vo.SearchResultVo:在页面返回数据Vo中添加面包屑集合
/** * 检索结果返回 */ @Data public class SearchResultVo { /** * 面包屑 */ private List<NavVo> Navs; //==================以上是要返回给页面的所有信息 /** * 面包屑Vo */ @Data public static class NavVo{ private String navName; private String navValue; private String link; } }
-
cn.lzwei.bilimall.search.vo.SearchParamVo:在请求参数VO中添加原始请求参数
@Data public class SearchParamVo { private String _queryString;//请求参数 }
-
cn.lzwei.bilimall.search.controller.SearchController:添加请求参数
@Controller public class SearchController { @GetMapping(value = {"/","/list.html"}) public String index(SearchParamVo searchParamVo, Model model, HttpServletRequest httpServletRequest){ searchParamVo.set_queryString(httpServletRequest.getQueryString());//设置原始请求参数 ... } }
-
cn.lzwei.bilimall.search.service.impl.SearchServiceImpl:在页面返回数据Vo的封装中添加面包屑的封装
/** * 检索业务 */ @Service(value = "searchService") public class SearchServiceImpl implements SearchService { //封装返回结果 private SearchResultVo buildSearchResultVo(SearchResponse response,SearchParamVo searchParamVo) { SearchResultVo searchResultVo = new SearchResultVo(); /** * 涉及到的所有属性 */ List<SearchResultVo.AttrVo> attrs=new ArrayList<>(); ParsedNested attr_agg = response.getAggregations().get("attr_agg"); ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg"); ... /** * 面包屑 */ List<SearchResultVo.NavVo> navs=null; if(searchParamVo.getAttrs()!=null && searchParamVo.getAttrs().size()>0){ navs=searchParamVo.getAttrs().stream().map(attr->{ //attrs=1_2018:2019 SearchResultVo.NavVo navVo = new SearchResultVo.NavVo(); String[] str = attr.split("_"); //1.属性名(在es返回中获取) for (Terms.Bucket bucket : attr_id_agg.getBuckets()) { if(bucket.getKey().equals(Long.parseLong(str[0]))){ ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg"); navVo.setNavName(attrNameAgg.getBuckets().get(0).getKeyAsString()); } } //2.属性值 navVo.setNavValue(str[1]); //3.导航链接 //3.1、获取请求参数 String encode=null; try { encode = URLEncoder.encode(attr, "UTF-8"); encode.replace("+","%20");//浏览器与java对空格的编码不一致 } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String queryString = searchParamVo.get_queryString(); //3.2、将当前属性替换为空串 String replace = queryString.replace("&attrs=" + encode, ""); navVo.setLink("http://search.bilimall.com/list.html?"+replace); return navVo; }).collect(Collectors.toList()); } searchResultVo.setNavs(navs); return searchResultVo; } }
前端开发
bilimall-search/src/main/resources/templates/list.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>
x
</a>
</div>