<!-- 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
将资料中的前端页面放到 search 服务模块下的 resource/templates 下;
配置 Windows hosts 文件:
192.168.56.10 search.gulimall.com
找到 Nginx 的配置文件,编辑 gulimall.conf,将所有 *.gulimall.com 的请求都经由 Nginx 转发给网关;
server {
listen 80;
server_name gulimall.com *.gulimall.com;
...
}
然后重启 Nginx
docker restart nginx
- id: gulimall-search
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
例如:
第一处: gulimall-search index.html
第二处:gulimall-search index.html
配置 /list.html 请求转发到 list 模板
/**
* 自动将页面提交过来的所有请求参数封装成我们指定的对象
*
* @param param
* @return
*/
@Controller
public class SearchController {
@GetMapping("/list.html")
public String listPage(){
return "list";
}
}
完整查询参数
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_高清屏
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
}
}
}
}
}
}
}
}
@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;
}
@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;
}
}
注意:
这里由于之前设置的映射 设置一些字段的 doc_value 为 false,导致后面聚合查询时报错!修改完映射 mapping 要同步修改检索服务中的常量类中的 es 索引常量,二者要求对应。
# 查看原来的映射规则
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"
}
}
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
}
}
}
}
}
}
}
}
将商品的基本属性渲染出来
<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>
将结果的品牌、分类、商品属性进行遍历显示,并且点击某个属性值时可以通过拼接url进行跳转
<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("brandId",'+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("catalogId",'+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("attrs","'+attr.attrId+'_'+val+'")'}">0-安卓(Android)</a></li>
</ul>
</div>
</div>
</div>
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;
};
将页码绑定至属性pn,当点击某页码时,通过获取pn值进行url拼接跳转页面
<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>页 到第</em>
<input type="number" value="1" class="page_input">
<em>页</em>
<a href="#">确定</a>
</span>
</div>
</div>
$(".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))
})
点击某个排序时首先按升序显示,再次点击再变为降序,并且还会显示上升或下降箭头
页面排序跳转的思路是通过点击某个按钮时会向其class
属性添加/去除desc
,并根据属性值进行url拼接
<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>
$(".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);
}
}
价格区间搜索函数
$("#skuPriceSearchBtn").click(function () {
var skuPriceFrom = $("#skuPriceFrom").val();
var skuPriceTo = $("#skuPriceTo").val();
location.href = replaceParamVal(location.href, "skuPrice", skuPriceFrom + "_" + skuPriceTo, false);
})
在封装结果时,将查询的属性值进行封装
@FeignClient("gulimall-product")
public interface ProductFeignService {
@GetMapping("/product/attr/info/{attrId}")
public R info(@PathVariable("attrId") Long attrId);
}
// 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);
}
页面渲染
<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>
<div class="header_form">
<input id="keyword_input" type="text" placeholder="手机" th:value="${param.keyword}"/>
<a href="javascript:searchByKeyword()">搜索</a>
</div>
function searchByKeyword() {
searchProducts("keyword", $("#keyword_input").val());
}
自己修改
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());
}
SearchResult.java
// 面包屑导航数据
private List<NavVo> navs = new ArrayList<>();
private List<Long> attrIds = new ArrayList<>();
result.getAttrIds().add(Long.parseLong(s[0]));
// 品牌,分类
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)
@GetMapping("/product/brand/infos")
public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds);
BrandServiceImpl.java
@Override
public List<BrandEntity> getBrandsByIds(List<Long> brandIds) {
return baseMapper.selectList(new QueryWrapper<BrandEntity>().in("brand_id",brandIds));
}
BrandController.java
@GetMapping("/infos")
public R brandsInfo(@RequestParam("brandIds") List<Long> brandIds){
List<BrandEntity> brand = brandService.getBrandsByIds(brandIds);
return R.ok().put("brand",brand);
}
list.html
<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>