一、添加模板页面

1
2
3
4
5
<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

将资料中的前端页面放到 search 服务模块下的 resource/templates 下;

image-20210824225137968

二、配置请求跳转

1、配置 Nginx 转发

配置 Windows hosts 文件:

1
192.168.56.10	search.gulimall.com

image-20210824230058346

找到 Nginx 的配置文件,编辑 gulimall.conf,将所有 *.gulimall.com 的请求都经由 Nginx 转发给网关;

1
2
3
4
5
server {
listen 80;
server_name gulimall.com *.gulimall.com;
...
}

然后重启 Nginx

1
docker restart nginx

2、配置网关服务转发到 search 服务

1
2
3
4
- id: gulimall-search
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com

image-20210824231648262

3、静态页面处理

  • 把资料中的前端页面的 css、img、js 放到 nginx 的 static 目录下

image-20210824232059503

  • 修改 gulimall-search 项目中 index.html 中的资源路径

例如:

image-20210824233316515

image-20210824233340069

4、配置页面跳转

第一处: gulimall-search index.html

image-20210825000123002

第二处:gulimall-search index.html

image-20210825000209003

配置 /list.html 请求转发到 list 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 自动将页面提交过来的所有请求参数封装成我们指定的对象
*
* @param param
* @return
*/
@Controller
public class SearchController {

@GetMapping("/list.html")
public String listPage(){
return "list";
}
}

image-20210825002935226

image-20210825002958192

image-20210825003015834

三、检索条件分析

  • 全文检索:skuTitle -> keyword

  • 排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)

  • 过滤:hasStock、skuPrice 区间、brandId、catalog3Id、attrs

  • 聚合:attrs

完整查询参数
keyword=小米&sort=saleCount_desc/asc&hasStock=0/1&skuPrice=400_1900&brandId=1&catalog3Id=1&at trs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏

四、DSL 分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"2"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "6"
}
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {"skuTitle": {}},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brandAgg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brandNameAgg": {
"terms": {
"field": "brandName",
"size": 10
}
},

"brandImgAgg": {
"terms": {
"field": "brandImg",
"size": 10
}
}

}
},
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attrs":{
"nested": {
"path": "attrs"
},
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
}
}

五、检索代码编写

1、请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
public class SearchParam {
// 页面传递过来的全文匹配关键字
private String keyword;

// 品牌id,可以多选
private List<Long> brandId;

// 三级分类
private Long catalog3Id;

// 排序条件: sort=price/salecount/hotscore_desc/asc
private String sort;

// 是否显示有货
private Integer hasStock;

// 价格区间查询
private String skuPrice;

// 按照属性进行筛选
private List<String> attrs;

// 页码
private Integer pageNum = 1;

// 原生的所有查询条件
private String _queryString;
}

2、返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Data
public class SearchResult {
// 查询到的所有商品信息
private List<SkuEsModel> product;

// 当前页面
private Integer pageNum;

// 总记录数
private Long total;

// 总页码
private Integer totalPages;

// 页码遍历结果集(分页)
private List<Integer> pageNavs;

// 当前查询到的结果,所有涉及到的品牌
private List<BrandVo> brands;

private List<AttrVo> attrs;

private List<CatalogVo> catalogs;

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

// 面包屑导航数据
private List<NavVo> navs;

@Data
public static class NavVo{
private String navName;
private String navValue;
private String link;
}

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

@Data
@AllArgsConstructor
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}

@Data
@AllArgsConstructor
public static class CatalogVo{
private Long catalogId;
private String catalogName;
}
}

六、检索功能

1、ES mall-product 映射 mapping 修改

注意:这里由于之前设置的映射 设置一些字段的 doc_value 为 false,导致后面聚合查询时报错!修改完映射 mapping 要同步修改检索服务中的常量类中的 es 索引常量,二者要求对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 查看原来的映射规则
GET gulimall_product/_mapping
# 修改为新的映射 并创建新的索引,下面进行数据迁移
PUT /mall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "long"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hosStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
# 数据迁移
POST _reindex
{
"source": {
"index": "gulimall_product"
},
"dest": {
"index": "mall_product"
}
}

image-20210829235308930

2、ES 检索 DSL 语句分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
GET mall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": { # 检索出华为
"skuTitle": "华为"
}
}
],
"filter": [ #过滤
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"2"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": { #价格1k-7k
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs", #聚合名字
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "7"
}
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": { #高亮字段
"fields": {"skuTitle": {}}, #前缀
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": { #查完后聚合
"brandAgg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": { #子聚合
"brandNameAgg": { # 每个商品id的品牌
"terms": {
"field": "brandName",
"size": 10
}
},
"brandImgAgg":{
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attrs":{
"nested": {
"path": "attrs"
},
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg":{
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
}
}

七、页面效果

详细代码具体请参考:https://gitee.com/oycodesite/gulimall.git

1、 基本数据渲染

将商品的基本属性渲染出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<div class="rig_tab">
<!-- 遍历各个商品-->
<div th:each="product : ${result.getProduct()}">
<div class="ico">
<i class="iconfont icon-weiguanzhu"></i>
<a href="/static/search/#">关注</a>
</div>
<p class="da">
<a th:href="|http://item.gulimall.com/${product.skuId}.html|">
<!--图片 -->
<img class="dim" th:src="${product.skuImg}" />
</a>
</p>
<ul class="tab_im">
<li>
<a href="/static/search/#" title="黑色">
<img th:src="${product.skuImg}"
/></a>
</li>
</ul>
<p class="tab_R">
<!-- 价格 -->
<span th:text="'¥' + ${product.skuPrice}">¥5199.00</span>
</p>
<p class="tab_JE">
<!-- 标题 -->
<!-- 使用utext标签,使检索时高亮不会被转义-->
<a href="/static/search/#" th:utext="${product.skuTitle}">
Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
</a>
</p>
<p class="tab_PI">
已有<span>11万+</span>热门评价
<a href="/static/search/#">二手有售</a>
</p>
<p class="tab_CP">
<a href="/static/search/#" title="谷粒商城Apple产品专营店"
>谷粒商城Apple产品...</a
>
<a href="#" title="联系供应商进行咨询">
<img src="/static/search/img/xcxc.png" />
</a>
</p>
<div class="tab_FO">
<div class="FO_one">
<p>
自营
<span>谷粒商城自营,品质保证</span>
</p>
<p>
满赠
<span>该商品参加满赠活动</span>
</p>
</div>
</div>
</div>
</div>

2、筛选条件渲染

将结果的品牌、分类、商品属性进行遍历显示,并且点击某个属性值时可以通过拼接 url 进行跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<div class="JD_nav_logo">
<!--品牌-->
<div class="JD_nav_wrap">
<div class="sl_key">
<span>品牌:</span>
</div>
<div class="sl_value">
<div class="sl_value_logo">
<ul>
<li th:each="brand: ${result.getBrands()}">
<!--替换url-->
<a
href="#"
th:href="${'javascript:searchProducts(&quot;brandId&quot;,'+brand.brandId+')'}"
>
<img
src="/static/search/img/598033b4nd6055897.jpg"
alt=""
th:src="${brand.brandImg}"
/>
<div th:text="${brand.brandName}">华为(HUAWEI)</div>
</a>
</li>
</ul>
</div>
</div>
<div class="sl_ext">
<a href="#">
更多
<i style='background: url("image/search.ele.png")no-repeat 3px 7px'></i>
<b
style='background: url("image/search.ele.png")no-repeat 3px -44px'
></b>
</a>
<a href="#">
多选
<i>+</i>
<span>+</span>
</a>
</div>
</div>
<!--分类-->
<div class="JD_pre" th:each="catalog: ${result.getCatalogs()}">
<div class="sl_key">
<span>分类:</span>
</div>
<div class="sl_value">
<ul>
<li>
<a
href="#"
th:text="${catalog.getCatalogName()}"
th:href="${'javascript:searchProducts(&quot;catalogId&quot;,'+catalog.catalogId+')'}"
>0-安卓(Android)</a
>
</li>
</ul>
</div>
</div>
<!--价格-->
<div class="JD_pre">
<div class="sl_key">
<span>价格:</span>
</div>
<div class="sl_value">
<ul>
<li><a href="#">0-499</a></li>
<li><a href="#">500-999</a></li>
<li><a href="#">1000-1699</a></li>
<li><a href="#">1700-2799</a></li>
<li><a href="#">2800-4499</a></li>
<li><a href="#">4500-11999</a></li>
<li><a href="#">12000以上</a></li>
<li class="sl_value_li">
<input type="text" />
<p>-</p>
<input type="text" />
<a href="#">确定</a>
</li>
</ul>
</div>
</div>
<!--商品属性-->
<div class="JD_pre" th:each="attr: ${result.getAttrs()}">
<div class="sl_key">
<span th:text="${attr.getAttrName()}">系统:</span>
</div>
<div class="sl_value">
<ul>
<li th:each="val: ${attr.getAttrValue()}">
<a
href="#"
th:text="${val}"
th:href="${'javascript:searchProducts(&quot;attrs&quot;,&quot;'+attr.attrId+'_'+val+'&quot;)'}"
>0-安卓(Android)</a
>
</li>
</ul>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function searchProducts(name, value) {
//原來的页面
location.href = replaceParamVal(location.href, name, value, true);
}

/**
* @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;
}

3、 分页数据渲染

将页码绑定至属性 pn,当点击某页码时,通过获取 pn 值进行 url 拼接跳转页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<div class="filter_page">
<div class="page_wrap">
<span class="page_span1">
<!-- 不是第一页时显示上一页 -->
<a
class="page_a"
href="#"
th:if="${result.pageNum>1}"
th:attr="pn=${result.getPageNum()-1}"
>
< 上一页
</a>
<!-- 将各个页码遍历显示,并将当前页码绑定至属性pn -->
<a
href="#"
class="page_a"
th:each="page: ${result.pageNavs}"
th:text="${page}"
th:style="${page==result.pageNum?'border: 0;color:#ee2222;background: #fff':''}"
th:attr="pn=${page}"
>1</a
>
<!-- 不是最后一页时显示下一页 -->
<a
href="#"
class="page_a"
th:if="${result.pageNum<result.totalPages}"
th:attr="pn=${result.getPageNum()+1}"
>
下一页 >
</a>
</span>
<span class="page_span2">
<em><b th:text="${result.totalPages}">169</b>&nbsp;&nbsp;到第</em>
<input type="number" value="1" class="page_input" />
<em></em>
<a href="#">确定</a>
</span>
</div>
</div>
1
2
3
4
5
$(".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));
});

4、 页面排序和价格区间

页面排序功能需要保证,点击某个按钮时,样式会变红,并且其他的样式保持最初的样子;

点击某个排序时首先按升序显示,再次点击再变为降序,并且还会显示上升或下降箭头

页面排序跳转的思路是通过点击某个按钮时会向其class属性添加/去除desc,并根据属性值进行 url 拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<div class="filter_top">
<div
class="filter_top_left"
th:with="p = ${param.sort}, priceRange = ${param.skuPrice}"
>
<!-- 通过判断当前class是否有desc来进行样式的渲染和箭头的显示-->
<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
>
<a sort="hotScore" class="sort_a">评论分</a>
<a sort="hotScore" class="sort_a">上架时间</a>
<!--价格区间搜索-->
<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>
</div>
<div class="filter_top_right">
<span class="fp-text"> <b>1</b><em>/</em><i>169</i> </span>
<a href="#" class="prev"><</a>
<a href="#" class="next"> > </a>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$(".sort_a").click(function () {
changeStyle(this);
//添加、剔除desc
//$(this).toggleClass("desc");
//获取sort属性值并进行url跳转
let sort = $(this).attr("sort");
sort = $(this).hasClass("desc") ? sort + "_desc" : sort + "_asc";
location.href = replaceParamVal(location.href, "sort", sort, false);
return false;
});

function changeStyle(ele) {
// location.href = replaceParamVal(href, "pageNum", pn,flase);
// color: #333; border-color: #ccc; background: #fff
// color: #fff; border-color: #e4393c; background: #e4393c
$(".sort_a").css({
color: "#333",
"border-color": "#ccc",
background: "#fff",
});
$(".sort_a").each(function () {
let text = $(this).text().replace("↓", "").replace("↑", "");
$(this).text(text);
});

$(ele).css({
color: "#FFF",
"border-color": "#e4393c",
background: "#e4393c",
});
$(ele).toggleClass("desc");

if ($(ele).hasClass("desc")) {
let text = $(ele).text().replace("↓", "").replace("↑", "");
text = text + "↓";
$(ele).text(text);
} else {
let text = $(ele).text().replace("↓", "").replace("↑", "");
text = text + "↑";
$(ele).text(text);
}
}

价格区间搜索函数

1
2
3
4
5
6
7
8
9
10
$("#skuPriceSearchBtn").click(function () {
var skuPriceFrom = $("#skuPriceFrom").val();
var skuPriceTo = $("#skuPriceTo").val();
location.href = replaceParamVal(
location.href,
"skuPrice",
skuPriceFrom + "_" + skuPriceTo,
false
);
});

5、面包屑导航

在封装结果时,将查询的属性值进行封装

1
2
3
4
5
6
@FeignClient("gulimall-product")
public interface ProductFeignService {

@GetMapping("/product/attr/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 6. 构建面包屑导航
List<String> attrs = searchParam.getAttrs();
if (attrs != null && attrs.size() > 0) {
List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
String[] split = attr.split("_");
SearchResult.NavVo navVo = new SearchResult.NavVo();
//6.1 设置属性值
navVo.setNavValue(split[1]);
//6.2 查询并设置属性名
try {
R r = productFeignService.info(Long.parseLong(split[0]));
if (r.getCode() == 0) {
AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
}
} catch (Exception e) {
log.error("远程调用商品服务查询属性失败", e);
}
//6.3 设置面包屑跳转链接(当点击该链接时剔除点击属性)
String queryString = searchParam.get_queryString();
String replace = queryString.replace("&attrs=" + attr, "").replace("attrs=" + attr+"&", "").replace("attrs=" + attr, "");
navVo.setLink("http://search.gulimall.com/search.html" + (replace.isEmpty()?"":"?"+replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}

页面渲染

1
2
3
4
5
6
7
8
9
<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>

6、搜索框

1
2
3
4
5
6
7
8
9
<div class="header_form">
<input
id="keyword_input"
type="text"
placeholder="手机"
th:value="${param.keyword}"
/>
<a href="javascript:searchByKeyword()">搜索</a>
</div>
1
2
3
function searchByKeyword() {
searchProducts("keyword", $("#keyword_input").val());
}

自己修改

1
2
3
4
5
6
7
8
9
10
11
12
13
function searchProducts(name, value) {
//原來的页面
if(name === "keyword"){
location.href = replaceParamVal(location.href,"keyword",value)
return;
}
location.href = replaceParamVal(location.href,name,value,true)
}


function searchByKeyword() {
searchProducts("keyword", $("#keyword_input").val());
}

7、 条件筛选联动

SearchResult.java

1
2
3
4
//    面包屑导航数据
private List<NavVo> navs = new ArrayList<>();

private List<Long> attrIds = new ArrayList<>();

image-20211004234759432

1
result.getAttrIds().add(Long.parseLong(s[0]));

image-20211004234846794

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 品牌,分类
if(param.getBrandId() != null && param.getBrandId().size() > 0){
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();

navVo.setNavName("品牌");
// TODO 远程查询所有品牌
R r = productFeignService.brandsInfo(param.getBrandId());
if(r.getCode() == 0){
List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (BrandVo brandVo : brand) {
buffer.append(brandVo.getBrandName()+";");
replace = replaceQueryString(param,brandVo.getBrandId()+"","brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink("http://search.gulimall.com/list.html?"+replace);
}
navs.add(navVo);
}

private String replaceQueryString(SearchParam param, String value, String key) {
//拿到所有的查询条件,去掉当前
String encode = null;
try {
encode = URLEncoder.encode(value,"UTF-8");
encode.replace("+","%20"); //浏览器对空格的编码和Java不一样,差异化处理
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return param.get_queryString().replace("&"+key+"=" + encode, "");
}

Feign (ProductFeignService)

image-20211004234155223

1
2
@GetMapping("/product/brand/infos")
public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);

BrandServiceImpl.java

1
2
3
4
@Override
public List<BrandEntity> getBrandsByIds(List<Long> brandIds) {
return baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id",brandIds));
}

BrandController.java

1
2
3
4
5
6
@GetMapping("/infos")
public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds){
List<BrandEntity> brand = brandService.getBrandsByIds(brandIds);

return R.ok().put("brand",brand);
}

list.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="JD_nav_logo" th:with="brandid= ${param.brandId}">
<!--品牌-->
<div th:if="${#strings.isEmpty(brandid)}" class="JD_nav_wrap">
<div class="sl_key">
<span><b>品牌:</b></span>
</div>
.....

<!--其它的所有需要展示的属性-->
<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>
</div>
</div>