需求分析
我们今天要完成的目标是在关键字搜索的基础上添加面板搜索功能。
面板上有1、商品分类、2、品牌、3、各种规格和价格区间(暂时不写)等条件
业务规则:
(1)当用户输入关键字搜索后,除了显示列表结果外,还应该显示通过这个关键字搜索到的记录都有哪些商品分类。
(2)根据第一个商品分类查询对应的模板,根据模板查询出品牌列表
(3)根据第一个商品分类查询对应的模板,根据模板查询出规格列表
(4)当用户点击搜索面板的商品分类时,显示按照这个关键字查询结果的基础上,筛选此分类的结果。
(5)当用户点击搜索面板的品牌时,显示在以上结果的基础上,筛选此品牌的结果
(6)当用户点击搜索面板的规格时,显示在以上结果的基础上,筛选此规格的结果
(7)当用户点击价格区间时,显示在以上结果的基础上,按价格进行筛选的结果
(8)当用户点击搜索面板的相应条件时,隐藏已点击的条件。
实现思路
(1)搜索面板的商品分类需要使用Spring Data Solr的分组查询来实现
(2)为了能够提高查询速度,我们需要把查询面板的品牌、规格数据提前放入redis
(3)查询条件的构建、面板的隐藏需要使用angularJS来实现
(4)后端的分类、品牌、规格、价格区间查询需要使用过滤查询来实现
目标:(1)当用户输入关键字搜索后,除了显示列表结果外,还应该显示通过这个关键字搜索到的记录都有哪些商品分类。(也就是实现上面图片红框中1的那部分,动态显示)
实现:(1)搜索面板的商品分类需要使用Spring Data Solr的分组查询来实现
/**
* 查询分类列表
* @param searchMap
* @return
*/
private List searchCategoryList(Map searchMap){
List<String> list=new ArrayList();
Query query=new SimpleQuery();
//按照关键字查询
Criteria criteria=new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
//设置分组选项
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方法调用
@Override
public Map<String, Object> search(Map searchMap) {
Map<String,Object> map=new HashMap<>();
//1.按关键字查询(高亮显示)(一步一步完善)(括号中间的方法是高亮显示查询列表)
map.putAll(searchList(searchMap));
//2.根据关键字查询商品分类
List categoryList = searchCategoryList(searchMap);
map.put("categoryList",categoryList); //把查询到的分组结果集存储到map集合中,到时候前端就可以直接取出来用了
return map;
}
<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>
目标:为了能够提高查询速度,把分类、品牌和规格放入redis缓存中,这样,每次查询之后
实现:
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TbItemCatMapper itemCatMapper;
/**
* 查询全部
*/
@Override
public List<TbItemCat> findAll() {
return itemCatMapper.selectByExample(null);
}
/**
* 根据上级ID查询列表(这个是三级联动下拉列表的方法,第一次运行的时候会传入0)(根据本工程来说,这个方法是每次增删改之后都要运行的方法,所以写在这里很合适)
*/
@Override
public List<TbItemCat> findByParentId(Long parentId) {
TbItemCatExample example1=new TbItemCatExample();
Criteria criteria1 = example1.createCriteria();
criteria1.andParentIdEqualTo(parentId);
//每次执行查询的时候,一次性读取缓存进行存储 (因为每次增删改都要执行此方法)
List<TbItemCat> list = findAll();//后面这个是查询搜索的方法
for(TbItemCat itemCat:list){
redisTemplate.boundHashOps("itemCat").put(itemCat.getName(), itemCat.getTypeId());
}
System.out.println("更新缓存:商品分类表");
return itemCatMapper.selectByExample(example1);
}
@Autowired
private RedisTemplate redisTemplate;
/**
* 将数据存入缓存
*/
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);
}
}
public PageResult findPage(TbTypeTemplate typeTemplate, int pageNum, int pageSize) {
......
saveToRedis();//存入数据到缓存
return new PageResult(page.getTotal(), page.getResult());
}
目标:(2)根据第一个商品分类查询对应的模板,根据模板查询出品牌列表
(3)根据第一个商品分类查询对应的模板,根据模板查询出规格列表
实现:
@Autowired
private RedisTemplate redisTemplate;
/**
* 查询品牌和规格列表
* @param category 分类名称
* @return
*/
private Map searchBrandAndSpecList(String category){
Map map=new HashMap();
Long typeId = (Long) redisTemplate.boundHashOps("itemCat").get(category);//获取模板ID
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(Map searchMap)
@Override
public Map<String, Object> search(Map searchMap) {
Map<String,Object> map=new HashMap<>(); //查询之后的所有结果都储存到这个集合中去了
//1.按关键字查询(高亮显示)
map.putAll(searchList(searchMap));
//2.分组查询 商品分类列表(根据关键字查询商品分类)
List<String> categoryList = searchCategoryList(searchMap);
map.put("categoryList", categoryList);
//3.查询品牌和规格列表
if(categoryList.size()>0){
map.putAll(searchBrandAndSpecList(categoryList.get(0)));//这里直接传入(0是表示查询集合中的一个值,相当于这里是随便传入一个分类来进行查询,后续会继续完善,根据搜索内容进行查询)
}
return map;
}
<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"><!-- resultMaps是我们前端js定的的,具体看上一篇 -->
{{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>
<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>
目标:当面包屑显示分类、品牌和规格时,要同时隐藏搜索面板对应的区域。 //相当于上面的(8)
点击搜索面板上的分类、品牌和规格以及价格,实现查询条件的构建。查询条件以面包屑的形式显示。
用户可以点击面包屑上的X 撤销查询条件。撤销后显示搜索面包相应的区域。
实现:(3)查询条件的构建、面板的隐藏需要使用angularJS来实现
$scope.searchMap={'keywords':'','category':'','brand':'','spec':{},'price':''};//搜索对象(关键字,分类,品牌,规格,价格)
//添加搜索项(点击分类标签会运行这个方法)
$scope.addSearchItem=function(key,value){
if(key=='category' || key=='brand' || key=='price'){//如果点击的是分类或者是品牌或是价格
$scope.searchMap[key]=value;
}else{
$scope.searchMap.spec[key]=value;
}
}
<!-- 点击商品分类标签(这是完整的标签,下面两个省略了) -->
<!-- 隐藏分类面板: && searchMap.category=='' ,这段就是隐藏分类面板的方法,判断他是否有值,如果有值,则直接隐藏这个选项-->
<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>
...
<!-- 点击品牌标签 -->
<a href="#" ng-click="addSearchItem('brand',brand.text)">{{brand.text}}</a>
...
<!-- 点击规格标签-->
<a href="#" ng-click="addSearchItem(spec.text,pojo.optionName)">
{{pojo.optionName}}</a>
<!-- 价格区间 -->
<ul class="type-list" ng-if="searchMap.price==''">
<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>
</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>
<li class="tag" ng-if="searchMap.price!=''" ng-click="removeSearchItem('price')">价格:{{searchMap.price}}<i class="sui-icon icon-tb-close"></i></li>
</ul>
//移除复合搜索条件(点击面包屑上面的 x 会运行这个方法)
$scope.removeSearchItem=function(key){
if(key=="category" || key=="brand" || key==''price''){//如果是分类或品牌或者价格
$scope.searchMap[key]="";
}else{//否则是规格
delete $scope.searchMap.spec[key];//移除此属性
}
}
$scope.searchMap={'keywords':'','category':'','brand':'','spec':{},'price':''};//搜索对象(关键字,分类,品牌,规格,价格)
//添加复合搜索条件
$scope.addSearchItem=function(key,value){
if(key=="category" || key=="brand" || key==''price''){//如果是分类或品牌
$scope.searchMap[key]=value;
}else{//否则是规格
$scope.searchMap.spec[key]=value;
}
$scope.search();//执行搜索 (其他都是重复代码,只有这句是新加上去的)
}
//移除复合搜索条件
$scope.removeSearchItem=function(key){
if(key=="category" || key=="brand" || key==''price''){//如果是分类或品牌
$scope.searchMap[key]="";
}else{//否则是规格
delete $scope.searchMap.spec[key];//移除此属性
}
$scope.search();//执行搜索 (其他都是重复代码,只有这句是新加上去的)
}
前端(html)
<!--searchAutoComplete-->
<div class="input-append">
<input type="text" id="autocomplete" ng-model="searchMap.keywords" type="text" class="input-error input-xxlarge" />
<button class="sui-btn btn-xlarge btn-danger" ng-click="search()" type="button">搜索</button>
</div>
前端(controller层)
//都在同一个
$scope.searchMap={'keywords':'','category':'','brand':'','spec':{},'price':''};//搜索对象(关键字,分类,品牌,规格,价格)(注意细节,这里的规格是: a:{} 这种格式,里面的是数据时键值对: a{a:b,c:d})
//搜索
$scope.search=function(){
searchService.search($scope.searchMap).success(//注意:这里searchMap参数就是上面我们的添加进去的搜索条件
function(response){
$scope.resultMap=response;
}
);
}
前端(service层)
this.search=function(searchMap){
return $http.post('itemsearch/search.do',searchMap);
}
后端(service层)
@Override
public Map search(Map searchMap) {
Map map=new HashMap();
//1.查询列表
map.putAll(searchList(searchMap));
//2.分组查询 商品分类列表
List<String> categoryList = searchCategoryList(searchMap);
map.put("categoryList", categoryList);
//3.查询品牌和规格列表
String category= (String) searchMap.get("category");
if(!category.equals("")){
map.putAll(searchBrandAndSpecList(category));
}else{
if(categoryList.size()>0){
map.putAll(searchBrandAndSpecList(categoryList.get(0)));
}
}
return map;
}
后端(Controller层)
@Reference
private ItemSearchService itemSearchService;
@RequestMapping("/search")
public Map search(@RequestBody Map searchMap){
return itemSearchService.search(searchMap);
}
目标:(4)当用户点击搜索面板的商品分类时,显示按照这个关键字查询结果的基础上,筛选此分类的结果。
(5)当用户点击搜索面板的品牌时,显示在以上结果的基础上,筛选此品牌的结果
(6)当用户点击搜索面板的规格时,显示在以上结果的基础上,筛选此规格的结果
(7)当用户点击价格区间时,显示在以上结果的基础上,按价格进行筛选的结果
(8)当用户点击价格区间时,显示在以上结果的基础上,按价格进行筛选的结果
实现:
/**
* 根据关键字搜索列表 + 根据分类查询
* @param keywords
* @return
*/
private Map searchList(Map searchMap){
.......
//1.1关键字查询......
//1.2按分类筛选(判断传过来的值中 category 是否为空,不为空则把这个查询条件添加进去)
if(!"".equals(searchMap.get("category"))){
Criteria filterCriteria=new Criteria("item_category").is(searchMap.get("category"));
FilterQuery filterQuery=new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
//高亮显示处理.....
}
/**
* 根据关键字搜索列表 + 根据分类查询 + 根据品牌查询
* @param keywords
* @return
*/
private Map searchList(Map searchMap){
.......
//1.1关键字查询
.......
//1.2按分类筛选
.......
//1.3按品牌筛选
if(!"".equals(searchMap.get("brand"))){
Criteria filterCriteria=new Criteria("item_brand").is(searchMap.get("brand"));
FilterQuery filterQuery=new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
//高亮显示处理...............
}
/**
* 根据关键字搜索列表 + 根据分类查询 + 根据品牌查询 + 根据规格查询
* @param keywords
* @return
*/
private Map searchList(Map searchMap){
......
//1.1关键字查询
....
//1.2按分类筛选
.....
//1.3按品牌筛选
......
//1.4过滤规格
if(searchMap.get("spec")!=null){
Map<String,String> specMap= (Map) searchMap.get("spec");
for(String key:specMap.keySet() ){
Criteria filterCriteria=new Criteria("item_spec_"+key).is( specMap.get(key) );
FilterQuery filterQuery=new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
}
//高亮显示处理.....
}
@Override
public Map<String, Object> search(Map searchMap) {
Map<String,Object> map=new HashMap<>();
//1.按关键字查询(高亮显示)
......
//2.根据关键字查询商品分类
List<String> categoryList = searchCategoryList(searchMap);
map.put("categoryList", categoryList);
......
//3.查询品牌和规格列表
String categoryName=(String)searchMap.get("category");//获得分类
if(!"".equals(categoryName)){//如果有分类名称(这是是前端传过来的数据来判断是否有值,如果选择了分类这里就有值)
map.putAll(searchBrandAndSpecList(categoryName)); //根据商品分类名称查询品牌和规格列表(下同),并把它全部put到map集合中
}else{//如果没有分类名称,按照第一个查询
if(categoryList.size()>0){//判断我们是否在数据库中有获取到category(这里是从数据库中查询出来的值,,理论上说不会为空,这个判断只是为了以防万一,怕没获取到)
map.putAll(searchBrandAndSpecList(categoryList.get(0)));
}
}
return map;
}
/**
* 根据关键字搜索列表
* @param keywords
* @return
*/
private Map searchList(Map searchMap){
......
//1.1关键字查询.....
//1.2按分类筛选.....
//1.3按品牌筛选.....
//1.4过滤规格 ......
//1.5按价格筛选.....
if(!"".equals(searchMap.get("price"))){
String[] price = ((String) searchMap.get("price")).split("-");
if(!price[0].equals("0")){//如果区间起点不等于0
Criteria filterCriteria=new Criteria("item_price").greaterThanEqual(price[0]);//这个是大于或者等于的方法
FilterQuery filterQuery=new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
if(!price[1].equals("*")){//如果区间终点不等于*
Criteria filterCriteria=new Criteria("item_price").lessThanEqual(price[1]);//这个是小于或者等于的方法
FilterQuery filterQuery=new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
}
//高亮显示处理 .....
}