目标
- 实现品优购搜索结果高亮显示功能
- 说出品优购搜索的业务规则和实现思路
- 完成查询分类列表的功能
- 完成缓存品牌和规格数据的功能
- 完成显示品牌和规格数据的功能
- 完成过滤条件构建的功能
- 完成过滤查询的功能
1. 品优购-高亮显示
1. 需求分析
将用户输入的关键字在标题中以红色的字体显示,就是搜索中常用的高亮显示
2. 后端代码
修改服务层代码ItemSearchServiceImpl.java
@Override
public Map search(Map searchMap) {
Map map = new HashMap();
/*
Query query = new SimpleQuery("*:*");
// 添加查询条件
Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
ScoredPage<TbItem> page = solrTemplate.queryForPage(query, TbItem.class);
map.put("rows",page.getContent());*/
// 高亮显示
HighlightQuery query = new SimpleHighlightQuery();
HighlightOptions highlightOptions = new HighlightOptions().addField("item_title");//设置高亮的域
highlightOptions.setSimplePrefix("<em style='color:red'>");//高亮前缀
highlightOptions.setSimplePostfix("</em>");// 高亮后缀
query.setHighlightOptions(highlightOptions);
// 按照关键字查询
Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
HighlightPage<TbItem> page = solrTemplate.queryForHighlightPage(query,TbItem.class);
for (HighlightEntry<TbItem> h : page.getHighlighted()) {//循环高亮的入口集合
TbItem item = h.getEntity();//获取原实体类
if(h.getHighlights().size()>0 && h.getHighlights().get(0).getSnipplets().size()>0){
item.setTitle(h.getHighlights().get(0).getSnipplets().get(0));// 设置高亮的效果
}
}
map.put("rows",page.getContent());
return map;
}
考虑到后续代码的可扩展性,代码的可读性,创建私有方法,用于返回查询列表的结果(高亮)
@Override
public Map search(Map searchMap) {
Map map = new HashMap();
// 查询列表
map.putAll(searchList(searchMap));
return map;
}
private Map searchList(Map searchMap){
Map map = new HashMap();
HighlightQuery query = new SimpleHighlightQuery();
HighlightOptions highlightOptions = new HighlightOptions().addField("item_title");//设置高亮的域
highlightOptions.setSimplePrefix("<em style='color:red'>");//高亮前缀
highlightOptions.setSimplePostfix("</em>");// 高亮后缀
query.setHighlightOptions(highlightOptions);
// 按照关键字查询
Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
HighlightPage<TbItem> page = solrTemplate.queryForHighlightPage(query,TbItem.class);
for (HighlightEntry<TbItem> h : page.getHighlighted()) {//循环高亮的入口集合
TbItem item = h.getEntity();//获取原实体类
if(h.getHighlights().size()>0 && h.getHighlights().get(0).getSnipplets().size()>0){
item.setTitle(h.getHighlights().get(0).getSnipplets().get(0));// 设置高亮的效果
}
}
map.put("rows",page.getContent());
return map;
}
}
3. 前端代码
测试后发现高亮显示的HTML代码原样输出,<em>{{item.title}}</em>
,这是angularJS为了防止HTML攻击的安全机制。我们如何在页面显示HTML结果呢?用$sce
服务的trustAsHtml方法来实现转换。
因为该功能具有一定通用性,采用angularJS的过滤器来简化开发,方便调用
- 修改base.js
// 过滤器
app.filter('trustHtml',['$sce',function ($sce) {
return function (data) {// 传入的参数是被过滤的内容
return $sce.trustAsHtml(data);
}
}]);
- 使用过滤器
ng-bind-html指令用于显示HTML内容
竖线|
用于调用过滤器
<div class="attr" ng-bind-html="item.title | trustHtml">
<!--<em>{{item.title}}</em>-->
</div>
2. 搜索业务规则分析
1. 需求分析
目标是在关键字搜索的基础上添加面板搜索功能。
面板上有商品分类、品牌、各种规格和价格区间等条件
业务规则:
- 当用户输入关键字搜索后,除了显示列表结果外,还应该显示通过这个关键字搜索到的记录都有哪些商品分类
- 根据第一个商品分类查询对应的模板,根据模板查询出品牌列表
- 根据第一个商品分类查询对应的模板,根据模板查询出规格列表
- 当用户点击搜索面板的商品分类时,显示按照这个关键字查询结果的基础上,筛选此分类的结果
- 当用户点击搜索面板的品牌时,显示在以上结果的基础上,筛选此品牌的结果
- 当用户点击搜索面板的规格时,显示在以上结果的基础上,筛选此规格的结果
- 当用户点击价格区间,显示在以上结果的基础上,按价格进行筛选的结果
- 当用户点击搜索面板的响应条件时,隐藏已点击的条件。
2. 实现思路
- 搜索面板的商品分类要spring data solr的分组查询来实现
- 为了提高查询速度,需要把查询面板的品牌、规格数据提前放到redis
- 查询条件的构建、面板的隐藏需要使用angularJS来实现
- 后端的分类、品牌、规格、价格区间查询需要使用过滤查询实现
3. 查询分类列表
1. 需求分析
根据搜索关键字查询商品分类名称列表,效果见图1
2. 后端代码
修改SearchItemServiceImpl.java创建方法
// 查询分类列表
private List searchCategoryList(Map searchMap){
List<String> list = new ArrayList<>();
Query query = new SimpleQuery("*:*");
// 按照关键字查询
Criteria criteria = new Criteria("item_category").is(searchMap.get("keywords"));
query.addCriteria(criteria);// 相当于SQL的where部分
// 设置分组选项 相当于SQL的group by
GroupOptions groupOptions = new GroupOptions().addGroupByField("item_category");
query.setGroupOptions(groupOptions);
// 得到分组页
GroupPage<TbItem> page = solrTemplate.queryForGroupPage(query, TbItem.class);
// 根据列得到分组结果集
GroupResult<TbItem> groupResult = page.getGroupResult("item_category");
// 得到分组结果入口页
Page<GroupEntry<TbItem>> groupEntries = groupResult.getGroupEntries();
// 得到分组入口集合
List<GroupEntry<TbItem>> content = groupEntries.getContent();
for (GroupEntry<TbItem> entry : content) {
list.add(entry.getGroupValue());
}
return list;
}
search方法调用
public Map search(Map searchMap) {
Map map = new HashMap();
// 查询列表
map.putAll(searchList(searchMap));
// 根据关键字查询商品分类
List categoryList = searchCategoryList(searchMap);
map.put("categoryList",categoryList);
return map;
}
3. 前端代码
修改search.html
<div class="type-wrap" ng-if="resultMap.categoryList!=null">
<div class="fl key">商品分类</div>
<div class="fl value">
<span ng-repeat="category in resultMap.categoryList">
<a href="#">{{category}}</a>
</span>
</div>
<div class="fl ext"></div>
</div>
4. 缓存品牌和规格数据
1. 需求分析
将商品分类数据、品牌数据、规格数据都放入redis存储
- 当用户进入运营商后台的商品分类页面时,将商品分类数据放入缓存hash,以分类名称作为key,以模板id作为值
- 当用户进入运营商后台的模板管理页面时,分别将品牌数据和规格数据放入缓存hash,以模板id作为key,以品牌列表和规格列表作为值。
2. 缓存商品分类数据
将商品分类表存入缓存 sellergoods-service工程引入pinyougou-common依赖
修改sellergoods-service的ItemCatServiceImpl.java,添加redisTemplate
@Autowired
private RedisTemplate redisTemplate;
// 将商品分类数据放入缓存,分类名称为key,模板id为value
@Override
public List<TbItemCat> findByParentId(Long parentId) {
TbItemCatExample example = new TbItemCatExample();
Criteria criteria = example.createCriteria();
criteria.andParentIdEqualTo(parentId);
// 每次增删改后都需要执行该方法,在此进行缓存的存储
List<TbItemCat> list = findAll();
for (TbItemCat itemCat : list) {
redisTemplate.boundHashOps("itemCat").put(itemCat.getName(),itemCat.getTypeId());
}
System.out.println("redis of the category........");
return itemCatMapper.selectByExample(example);
}
3. 缓存品牌和规格列表数据
- 修改sellergoods-service的TypeTemplateServiceImpl.java
/**
* 数据放入缓存,品牌和规格列表
*/
private void saveToRedis(){
// 获取模板数据
List<TbTypeTemplate> typeTemplateList = findAll();
for (TbTypeTemplate typeTemplate : typeTemplateList) {
// 存储品牌列表
List<Map> brandList = JSON.parseArray(typeTemplate.getBrandIds(), Map.class);
redisTemplate.boundHashOps("brandList").put(typeTemplate.getId(),brandList);
// 存储规格列表
List<Map> specList = findSpecList(typeTemplate.getId());// 根据id查询规格列表
redisTemplate.boundHashOps("specList").put(typeTemplate.getId(),specList);
}
}
- 在findPage方法调用该方法
// ...
saveToRedis();// 存入数据到缓存
return new PageResult(page.getTotal(), page.getResult());
}
这样在增删改后会自动调用该方法
4. 加载缓存数据
启动redis,content-service和sellergoods-service以及manager-web
5. 显示品牌和规格数据
1. 需求分析
在搜索面板区域显示第一个分类的品牌和规格列表,效果见图1
2. 后端代码
修改ItemSearchServiceImpl.java,增加方法
@Autowired
private RedisTemplate redisTemplate;
private Map searchBrandAndSpecList(String category){
Map map = new HashMap();
// 获取模板id
Long typeId = (Long) redisTemplate.boundHashOps("itemCat").get(category);
if (typeId!=null) {
// 根据模板id查询品牌列表
List brandList = (List) redisTemplate.boundHashOps("brandList").get(typeId);
map.put("brandList",brandList);// 返回值添加到品牌列表
// 根据模板id查询规格列表
List specList = (List) redisTemplate.boundHashOps("specList").get(typeId);
map.put("specList",specList);
}
return map;
}
search方法调用该方法
@Override
public Map search(Map searchMap) {
Map map = new HashMap();
// 查询列表
map.putAll(searchList(searchMap));
// 根据关键字查询商品分类
List categoryList = searchCategoryList(searchMap);
map.put("categoryList",categoryList);
// 查询品牌和规格列表
map.putAll(searchBrandAndSpecList((String) categoryList.get(0)));
return map;
}
3. 前端代码
1. 获取品牌列表
修改search.html,实现品牌列表
<div class="type-wrap logo" ng-if="resultMap.brandList!=null">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<li ng-repeat="brand in resultMap.brandList">
{{brand.text}}
</li>
</ul>
</div>
<div class="ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
<a href="javascript:void(0);">更多</a>
</div>
</div>
2. 获取规格列表
<div class="type-wrap" ng-repeat="spec in resultMap.specList">
<div class="fl key">{{spec.text}}</div>
<div class="fl value">
<ul class="type-list">
<li ng-repeat="pojo in spec.options">
<a>{{pojo.optionName}}</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
6. 过滤条件构建
1. 需求分析
点击搜索面板上的分类、品牌和规格,实现查询条件的构建,查询条件以面包屑的形式显示
当面包屑显示分类、品牌和规格时,要同时隐藏搜索面板对应的区域
用户可以点击面包屑的x,撤销查询条件,撤销后显示搜索面包对应的区域
2. 添加搜索项
1. 添加搜索项方法
修改search-web的searchController.js
$scope.searchMap = {"keywords":"","category":"","brand":"","spec":{}};// 搜索对象
// 添加搜索项,改变searchMap的值
$scope.addSearchItem = function (key, value) {
if(key==="category" || key==="brand"){
$scope.searchMap[key] = value;
}else{
$scope.searchMap.spec[key] = value;
}
};
2. 点击搜索项
修改search-web的search.html,为搜索面板添加点击事件
点击商品分类标签
<a href="#" ng-click="addSearchItem('category',category)">{{category}}</a>
点击品牌标签
<a href="#" ng-click="addSearchItem('brand',brand.text)">{{brand.text}}</a>
点击规格标签
<a href="#" ng-click="addSearchItem(spec.text,pojo.optionName)">{{pojo.optionName}}</a>
3. 显示面包屑
修改search-web的search.html,面包屑形式显示搜索条件
<ul class="fl sui-breadcrumb">搜索条件</ul>
<ul class="tags-choose">
<li class="tag" ng-if="searchMap.category!=''" ng-click="removeSearchItem('category')">商品分类:{{searchMap.category}}<i class="sui-icon icon-tb-close"></i></li>
<li class="tag" ng-if="searchMap.brand!=''" ng-click="removeSearchItem('brand')">品牌:{{searchMap.brand}}<i class="sui-icon icon-tb-close"></i></li>
<li class="tag" ng-repeat="(key,value) in searchMap.spec" ng-click="removeSearchItem(key)">{{key}}:{{value}}<i class="sui-icon icon-tb-close"></i></li>
</ul>
3. 撤销搜索项
1. 撤销搜索项的方法
修改searchController.js
// 撤销搜索项
$scope.removeSearchItem = function (key) {
if(key==="category" || key==="brand"){
$scope.searchMap[key] = "";
}else{
delete $scope.searchMap.spec[key];
}
$scope.search();//执行搜索
}
2. 页面调用方法
search.html
见之前代码,搜索部分的ng-click,注意规格部分比较特殊
4. 隐藏查询面板
1. 隐藏分类面板
search.html
<div class="type-wrap" ng-if="resultMap.categoryList!=null && searchMap.category==''">
<div class="fl key">商品分类</div>
<div class="fl value">
<span ng-repeat="category in resultMap.categoryList">
<a href="#" ng-click="addSearchItem('category',category)">{{category}}</a>
</span>
</div>
<div class="fl ext"></div>
</div>
2. 隐藏品牌面板
<div class="type-wrap logo" ng-if="resultMap.brandList!=null && searchMap.brand==''">
<div class="fl key brand">品牌</div>
3. 隐藏规格面板
<div class="type-wrap" ng-repeat="spec in resultMap.specList" ng-if="searchMap.spec[spec.text]==null">
<div class="fl key">{{spec.text}}</div>
<div class="fl value">
<ul class="type-list">
<li ng-repeat="pojo in spec.options">
<a href="#" ng-click="addSearchItem(spec.text,pojo.optionName)">{{pojo.optionName}}</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
5. 提交查询
修改searchController.js,在添加和删除筛选条件时自动调用搜索方法
代码部分见之前的
7. 过滤查询
1. 需求分析
根据上一步构建的查询条件,实现分类、品牌和规格的过滤条件
2. 代码实现
1. 分类过滤
修改search-service的SearchItemServiceImpl.java
private Map searchList(Map searchMap){
Map map = new HashMap();
// 高亮选项初始化
HighlightQuery query = new SimpleHighlightQuery();
HighlightOptions highlightOptions = new HighlightOptions().addField("item_title");//设置高亮的域
highlightOptions.setSimplePrefix("<em style='color:red'>");//高亮前缀
highlightOptions.setSimplePostfix("</em>");// 高亮后缀
query.setHighlightOptions(highlightOptions);
// 1.1 按照关键字查询
Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
// 1.2 按照商品分类过滤
if (!"".equals(searchMap.get("category"))) {
FilterQuery filterQuery = new SimpleFilterQuery();
Criteria filterCriteria = new Criteria("item_category").is(searchMap.get("category"));
filterQuery.addCriteria(filterCriteria);
query.addFilterQuery(filterQuery);
}
// 1.3 按照品牌过滤
if (!"".equals(searchMap.get("brand"))) {
FilterQuery filterQuery = new SimpleFilterQuery();
Criteria filterCriteria = new Criteria("item_brand").is(searchMap.get("brand"));
filterQuery.addCriteria(filterCriteria);
query.addFilterQuery(filterQuery);
}
// 1.4 规格过滤
if (searchMap.get("spec")!=null) {
Map<String,String> specMap = (Map) searchMap.get("spec");
for (String key : specMap.keySet()) {
FilterQuery filterQuery = new SimpleFilterQuery();
Criteria filterCriteria = new Criteria("item_spec_"+key).is(searchMap.get(key));
filterQuery.addCriteria(filterCriteria);
query.addFilterQuery(filterQuery);
}
}
// ************** 获取高亮结果集 ***************
// 高亮页对象
HighlightPage<TbItem> page = solrTemplate.queryForHighlightPage(query,TbItem.class);
for (HighlightEntry<TbItem> h : page.getHighlighted()) {//循环高亮的入口集合
TbItem item = h.getEntity();//获取原实体类
if(h.getHighlights().size()>0 && h.getHighlights().get(0).getSnipplets().size()>0){
item.setTitle(h.getHighlights().get(0).getSnipplets().get(0));// 设置高亮的效果
}
}
map.put("rows",page.getContent());
return map;
}
2. 品牌过滤
代码见上
3. 规格过滤
规格有多项,需要循环过滤,循环规格查询条件,根据key得到域名称,根据value设置过滤条件
代码见上
4. 根据分类查询品牌规格列表
public Map search(Map searchMap) {
Map map = new HashMap();
// 查询列表
map.putAll(searchList(searchMap));
// 根据关键字查询商品分类
List categoryList = searchCategoryList(searchMap);
map.put("categoryList",categoryList);
// 查询品牌和规格列表
String categoryName = (String) searchMap.get("category");
// 如果有分类名称
if (!"".equals(categoryName)) {
map.putAll(searchBrandAndSpecList(categoryName));
}else{ // 没有分类名称,按照第一个查询
map.putAll(searchBrandAndSpecList((String) categoryList.get(0)));
}
return map;
}