1.1 需求分析
- 面板上有商品分类、品牌、各种规格、价格区间等过滤条件。
- 业务规则:
- 当用户点击搜索面板的商品分类时, 显示按照这个关键字查询结果的基础上,筛选此分类的结果。
- 当用户点击搜索面板的品牌时,显示在以上结果的基础上,筛选此品牌的结果。
- 当用户点击搜索面板的规格时,显示在以上结果的基础上,筛选此规格结果。
- 当用户点击价格区间时,显示在以上结果的基础上,按价格进行筛选的结果。
- 当用户点击搜索面板的相应条件时,隐藏已点击的条件。
1.2 实现思路
- 查询条件的构建、面板的隐藏需要使用AngularJS来实现。
- 后端的商品分类、品牌、规格选项、价格区间查询需要使用过滤查询来实现。
1.3 搜索业务规则分析
- 业务规则:
- 根据关键字搜索
- 高亮显示
- 根据商品分类过滤
- 根据品牌过滤
- 根据规则选项过滤
- 根据价格区间过滤
- 价格排序
- 新品排序
- 销量排序
- 评论数排序
- 综合排序
2.添加过滤条件【前端代码】
- 封装过滤条件参数
- 实现步骤:
- 第一步:为页面上的过滤条件绑定点击事件。
- 第二步:提供一个事件方法,封装过滤条件(需要把过滤条件传到后台)
- 第三步:在页面导航区域显示用户选择的过滤条件。
前端参数封装如下:
{"keywords":"","category":"手机","brand":"苹果","price":"1500-2000","spec":{"网络":"电信4G","机身内存":"128G"}}
2.1 添加搜索选项
2.1.1 绑定点击事件(为页面上的过滤条件绑定点击事件)
修改pinyougou-search-web/src/main/webapp/search.html页面,为搜索面板添加点击事件
点击商品分类标签(168多行):
<div class="fl value">
<a href="javascript:;" style="margin-right: 10px"
ng-click="addSearchItem('category','手机')">手机</a>
</div>
点击品牌标签(177多行):
<ul class="logo-list">
<li style="cursor: pointer"
ng-click="addSearchItem('brand','苹果')">
<img src="img/_/phone01.jpg"/></li>
<li style="cursor: pointer"
ng-click="addSearchItem('brand','三星')">
<img src="img/_/phone02.jpg"/></li>
<li style="cursor: pointer"
ng-click="addSearchItem('brand','华为')">
<img src="img/_/phone03.jpg"/></li>
<li style="cursor: pointer"
ng-click="addSearchItem('brand','小米')">
<img src="img/_/phone04.jpg"/></li>
<li style="cursor: pointer"
ng-click="addSearchItem('brand','OPPO')">
<img src="img/_/phone05.jpg"/></li>
<li style="cursor: pointer"
ng-click="addSearchItem('brand','VIVO')">
<img src="img/_/phone06.jpg"/></li>
</ul>
点击网格标签(205多行):(网络标签的数据格式比较特殊)
<li>
<a ng-click="addSearchItem('网络','移动3G')">移动3G</a>
</li>
<li>
<a ng-click="addSearchItem('网络','联通3G')">联通3G</a>
</li>
<li>
<a ng-click="addSearchItem('网络','电信3G')">电信3G</a>
</li>
<li>
<a ng-click="addSearchItem('网络','移动4G')">移动4G</a>
</li>
<li>
<a ng-click="addSearchItem('网络','联通4G')">联通4G</a>
</li>
<li>
<a ng-click="addSearchItem('网络','电信4G')">电信4G</a>
</li>
点击机身内存标签(231多行):
<li>
<a ng-click="addSearchItem('机身内存','16G')">16G</a>
</li>
<li>
<a ng-click="addSearchItem('机身内存','32G')">32G</a>
</li>
<li>
<a ng-click="addSearchItem('机身内存','64G')">64G</a>
</li>
<li>
<a ng-click="addSearchItem('机身内存','128G')">128G</a>
</li>
点击价格区间标签(251行):
<li>
<a ng-click="addSearchItem('price','0-500')">0-500元</a>
</li>
<li>
<a ng-click="addSearchItem('price','500-1000')">500-1000元</a>
</li>
<li>
<a ng-click="addSearchItem('price','1000-1500')">1000-1500元</a>
</li>
<li>
<a ng-click="addSearchItem('price','1500-2000')">1500-2000元</a>
</li>
<li>
<a ng-click="addSearchItem('price','2000-3000')">2000-3000元 </a>
</li>
<li>
<a ng-click="addSearchItem('price','3000-*')">3000元以上</a>
</li>
2.1.2 添加搜索选项方法 提供一个事件方法,封装过滤条件(需要把过滤条件传到后台)
pinyougou-search-web/src/main/webapp/js/controller/searchController.js:
/** 定义搜索参数对象 */
$scope.searchParam = {keywords : '', category : '',
brand : '', price : '', spec : {}};
/** 添加搜索选项方法 */
$scope.addSearchItem = function(key, value){
/** 判断是商品分类、品牌、价格 */
if(key == 'category' || key == 'brand' || key == 'price'){
$scope.searchParam[key] = value;
}else{
/** 规格选项 */
$scope.searchParam.spec[key] = value;
}
};
2.1.3 显示面包屑 (在页面导航区域显示用户选择的过滤条件)
修改pinyougou-search-web/src/main/webapp/search.html页面,用面包屑形式显示搜索条件(147行)
- 在js的事件方法中定义了搜索参数对象,searchParam 放在了scope中,所以在页面上相对应的显示出来即可。由于机身内存和网络数据格式的特殊性,需要特殊处理 。格式: {"机身内存":"16G","网络":"联通4G"}
<!--bread--> <div class="bread"> <ul class="fl sui-breadcrumb"> <li>搜索条件</li> </ul> <ul class="tags-choose"> <li class="tag" ng-if="searchParam.category != ''"> 商品分类:{{searchParam.category}} <i class="sui-icon icon-tb-close"></i> </li> <li class="tag" ng-if="searchParam.brand != ''"> 品牌:{{searchParam.brand}} <i class="sui-icon icon-tb-close"></i> </li> <li class="tag" ng-repeat="(key,value) in searchParam.spec"> {{key}} : {{value}} <i class="sui-icon icon-tb-close"></i> </li>
<li class="tag" ng-if="searchParam.price != ''">
价格:{{searchParam.price}}
<i class="sui-icon icon-tb-close"></i>
</li>
</ul>
</div>
<!--selector-->
3.添加过滤条件【前端代码】
- 减少过滤条件参数。
- 实现步骤:
- 为页面上的<i>标签绑定点击事件。
- 在js的控制器中定义一个事件方法
3.1 绑定点击事件 (为页面上的<i>标签绑定点击事件)
修改pinyougou-search-web/src/main/webapp/search.html页面
<ul class="tags-choose">
<li class="tag" ng-if="searchParam.category != ''">
商品分类:{{searchParam.category}}
<i class="sui-icon icon-tb-close"
ng-click="removeSearchItem('category')"></i>
</li>
<li class="tag" ng-if="searchParam.brand != ''">
品牌:{{searchParam.brand}}
<i class="sui-icon icon-tb-close"
ng-click="removeSearchItem('brand')"></i>
</li>
<li class="tag" ng-repeat="(key,value) in searchParam.spec">
{{key}} : {{value}}
<i class="sui-icon icon-tb-close"
ng-click="removeSearchItem(key)"></i>
</li>
<li class="tag" ng-if="searchParam.price != ''">
价格:{{searchParam.price}}
<i class="sui-icon icon-tb-close"
ng-click="removeSearchItem('price')"></i>
</li>
</ul>
3.2 删除搜索选项方法 (在js的控制器中定义一个事件方法 )
pinyougou-search-web/src/main/webapp/js/controller/searchController.js,增加删除搜索选项方法:
/** 删除搜索选项方法 */
$scope.removeSearchItem = function(key){
/** 判断是商品分类、品牌、价格 */
if(key == "category" || key == "brand" || key == 'price'){
$scope.searchParam[key] = "";
}else{
/** 删除规格选项 */
delete $scope.searchParam.spec[key];
}
};
4.隐藏查询面板【前端代码】
页面动画效果
- 实现步骤:
- 控制搜索面板显示还是隐藏
ng-if="searchParam.spec['机身内存'] == null" // 规格选项
ng-if="searchParam.brand == ''" // 品牌
4.1 隐藏商品分类面板
修改pinyougou-search-web/src/main/webapp/search.html(175行)
<div class="type-wrap" ng-if="searchParam.category == ''">
<div class="fl key">商品分类</div>......
<div class="fl ext"></div>
</div>
4.2 隐藏品牌面板
修改pinyougou-search-web/src/main/webapp/search.html(183行)
<div class="type-wrap logo" ng-if="searchParam.brand == ''"> <div class="fl key brand">品牌</div> <div class="value logos">
......
</div>
</div>
4.3 隐藏网络面板
修改pinyougou-search-web/src/main/webapp/search.html(211行)
<div class="type-wrap" ng-if="searchParam.spec['网络'] == null">
<div class="fl key">网络</div>
......
<div class="fl ext"></div>
</div>
4.4 隐藏机身内存面板
修改pinyougou-search-web/src/main/webapp/search.html(237行)
<div class="type-wrap" ng-if="searchParam.spec['机身内存'] == null">
<div class="fl key">机身内存</div>
......
<div class="fl ext"></div>
</div>
4.5 隐藏价格区间面板
修改pinyougou-search-web/src/main/webapp/search.html(257行)
<div class="type-wrap" ng-if="searchParam.price == ''">
<div class="fl key">价格</div>
......
<div class="fl ext"></div>
</div>
4.6 提交查询
pinyougou-search-web/src/main/webapp/js/controller/searchController.js ,在添加和删除筛选条件时调用搜索方法
/** 添加搜索选项方法 */
$scope.addSearchItem = function(key, value){
/** 判断是商品分类、品牌、价格 */
if(key == 'category' || key == 'brand' || key == 'price'){
$scope.searchParam[key] = value;
}else{
/** 规格选项 */
$scope.searchParam.spec[key] = value;
}
/** 执行搜索 */
$scope.search();
};
/** 删除搜索选项方法 */
$scope.removeSearchItem = function(key){
/** 判断是商品分类、品牌、价格 */
if(key == "category" || key == "brand" || key == 'price'){
$scope.searchParam[key] = "";
}else{
/** 删除规格选项 */
delete $scope.searchParam.spec[key];
}
/** 执行搜索 */
$scope.search();
};
5.过滤查询【后端代码】
5.1 分类过滤【后端代码】
请求参数:
{keywords: "", category: "手机", brand: "三星", price: "1500-2000", spec: {网络: "电信3G", 机身内存: "128G"}}
- 实现步骤:
- 第一步:判断商品分类category是否有参数值
- 第二步:按商品分类过滤查询(添加过滤查询条件)
// 商品分类过滤
String category = (String)params.get("category");
if (StringUtils.isNoneBlank(category)){
// 过滤条件
Criteria criteria1 = new Criteria("category").is(category);
// 添加过滤查询
query.addFilterQuery(new SimpleFilterQuery(criteria1));
}
5.2 品牌过滤【后端代码】
- 实现步骤:
- 第一步:判断商品品牌brand是否有参数值
- 第二步:按商品品牌过滤查询(添加过滤查询条件)
// 按商品品牌过滤
String brand = (String)params.get("brand");
if (StringUtils.isNoneBlank(brand)){
// 过滤条件
Criteria criteria1 = new Criteria("brand").is(brand);
// 添加过滤查询
query.addFilterQuery(new SimpleFilterQuery(criteria1));
}
5.3 规格过滤【后端代码】
- 实现思路:规格有多选项,需要循环过滤。循环规格查询条件,根据key得到域名称,根据value设置过滤条件。
- 实现步骤:
- 判断规格spec是否有参数值
- 按规格过滤查询(添加过滤查询条件)
规格Field是动态域: spec_*
"spec_网络": "联通4G",
spec机身内存": "64G",
// 按规格过滤 spec: {网络: "电信3G", 机身内存: "128G"}
Map<String, String> specMap = (Map<String, String>)params.get("spec");
if (specMap != null && specMap.size() > 0){
// 迭代规格
for (String key : specMap.keySet()){
// 过滤条件 spec_*
Criteria criteria1 = new Criteria("spec_" + key).is(specMap.get(key));
// 添加过滤查询
query.addFilterQuery(new SimpleFilterQuery(criteria1));
}
}
5.4 价格过滤【后端代码】
- 实现步骤:
- 第一步:判断价格price是否有参数值
- 第二步:按价格区间过滤查询(添加过滤查询条件)
// 按价格区间过滤 0-500 1000-1500 3000-*
String price = (String)params.get("price");
if (StringUtils.isNoneBlank(price)){
// 得到价格区间数组
String[] priceArr = price.split("-");// 判断起始价格是不是零,不是零添加过滤条件
if (!"0".equals(priceArr[0])){
// 过滤条件 price >= ?
Criteria criteria1 = new Criteria("price").greaterThanEqual(priceArr[0]);
// 添加过滤查询
query.addFilterQuery(new SimpleFilterQuery(criteria1));
}// 判断结束价格是不是星号,不是星号添加过滤条件
if (!"*".equals(priceArr[1])){
// 过滤条件 price <= ?
Criteria criteria1 = new Criteria("price").lessThanEqual(priceArr[1]);
// 添加过滤查询
query.addFilterQuery(new SimpleFilterQuery(criteria1));
}
}
5.5 完整代码
@Service(interfaceName = "com.pinyougou.service.ItemSearchService")
public class ItemSearchServiceImpl implements ItemSearchService {
@Autowired
private SolrTemplate solrTemplate;
/**
* @Author: hyh
* @Description:搜索结果高亮显示
* @Date: 17:50 2018/12/1
*/
@Override
public Map<String, Object> search(Map<String, Object> params) {
//1.创建Map集合封装返回的数据
Map<String, Object> data = new HashMap<>();
//2.获取查询关键字
String keywords = (String) params.get("keywords");
//获取当前页码
Integer page = (Integer) params.get("page");
if (page == null){
//默认第一页
page = 1;
}
//获取每页显示的记录数
Integer rows = (Integer) params.get("rows");
if (rows == null){
/**默认20条记录*/
rows = 20;
}
//3.判断检索关键字是否为空
if (StringUtils.isNoneBlank(keywords)) {//高亮查询
//创建高亮查询对象
HighlightQuery highlightQuery = new SimpleHighlightQuery();
//创建高亮选项对象
HighlightOptions highlightOptions = new HighlightOptions();
//设置高亮域
highlightOptions.addField("title");
//设置高亮前缀
highlightOptions.setSimplePrefix("<font color='red'>");
//设置高亮后缀
highlightOptions.setSimplePostfix("</font>");
//设置高亮选项
highlightQuery.setHighlightOptions(highlightOptions);
//创建查询条件对象
Criteria criteria = new Criteria("keywords").is(keywords);
//添加查询条件(关键字)
highlightQuery.addCriteria(criteria);
/**按商品分类过滤*/
if (!"".equals(params.get("category"))){
Criteria criteria1 = new Criteria("category").is(params.get("category"));
/**添加过滤条件*/
highlightQuery.addFilterQuery(new SimpleFilterQuery(criteria1));
}
/**按品牌过滤*/
if (!"".equals(params.get("brand"))){
Criteria criteria1 = new Criteria("brand").is(params.get("brand"));
/**添加过滤条件*/
highlightQuery.addFilterQuery(new SimpleFilterQuery(criteria1));
}
/**按规格过滤*/
if (params.get("spec")!=null){
Map<String,String> specMap = (Map) params.get("spec");
for (String key : specMap.keySet()) {
Criteria criteria1 = new Criteria("spec_"+key).is(specMap.get(key));
/**添加过滤条件*/
highlightQuery.addFilterQuery(new SimpleFilterQuery(criteria1));
}
}
/**按价格过滤*/
if (!"".equals(params.get("price"))){
/**得到价格的范围数组*/
String[] price = params.get("price").toString().split("-");
/**如果价格区间起点不等于0*/
if (!price[0].equals("0")){
Criteria criteria1 = new Criteria("price").greaterThanEqual(price[0]);
/**添加过滤条件*/
highlightQuery.addFilterQuery(new SimpleFilterQuery(criteria1));
}
/**如果价格区间起点不等于星号*/
if (!price[1].equals("*")){
Criteria criteria1 = new Criteria("price").lessThanEqual(price[1]);
/**添加过滤条件*/
highlightQuery.addFilterQuery(new SimpleFilterQuery(criteria1));
}
}
/**设置起始记录查询数*/
highlightQuery.setOffset((page-1)*rows);
/**设置每页显示记录数*/
highlightQuery.setRows(rows);
//分页查询,得到高亮分页查询对象
HighlightPage<SolrItem> highlightPage = solrTemplate
.queryForHighlightPage(highlightQuery,SolrItem.class);
/**循环高亮项集合*/
for (HighlightEntry<SolrItem> he :highlightPage.getHighlighted() ) {
/**获取检索到的原实体*/
SolrItem solrItem = he.getEntity();
/**判断高亮集合及集合中第一个Field的高亮内容*/
if (he.getHighlights().size()>0 &&
he.getHighlights().get(0).getSnipplets().size()>0){
/**设置高亮的结果*/
solrItem.setTitle(he.getHighlights().get(0).getSnipplets().get(0));
}
}
data.put("rows",highlightPage.getContent());
/**设置总页数*/
data.put("totalPages",highlightPage.getTotalPages());
/**设置总记录数*/
data.put("total",highlightPage.getTotalElements());
} else {
//简单查询:创建简单查询对象
SimpleQuery simpleQuery = new SimpleQuery("*:*");
/**设置起始记录查询数*/
simpleQuery.setOffset((page-1)*rows);
/**设置吗,每页显示记录数*/
simpleQuery.setRows(rows);
ScoredPage scoredPage = solrTemplate.queryForPage(simpleQuery, SolrItem.class);
data.put("rows",scoredPage.getContent());
data.put("totalPages",scoredPage.getTotalPages());
data.put("total",scoredPage.getTotalElements());
}
return data;
}
}