1 搜索记录
1.1 需求分析
- 展示用户的搜索记录10条,按照搜索关键词的时间倒序
- 可以删除搜索记录
- 保存历史记录
- 异步的保存
1.2 思路分析
表结构:
ap_user_search APP用户搜索信息表 — 存储到 MongoDB/Redis 数据库中
对应实体:
package com.heima.model.search.pojos;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.Date;
/**
* APP用户搜索信息表
* @author itheima
*/
@Data
@Document("ap_user_search")
public class ApUserSearch implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private String id;
/**
* 行为实体id
*/
private String entryId;
/**
* 搜索词
*/
private String keyword;
/**
* 创建时间
*/
private Date createdTime;
}
1.3 保存用户搜索记录
1.3.1 实现思路
- 用户输入关键字进行搜索的异步记录关键字
- 利用线程池重用,减少创建和销毁线程的性能开销
- 保存关键字需要先查询行为实体(登录用户或设备用户)
- 如果是已经存在的关键字,如果则修改时间
1.3.2 定义远程接口调用查询行为实体
行为微服务提供行为实体调用web接口
package com.heima.behavior.controller.v1;
import com.heima.behavior.service.ApBehaviorEntryService;
import com.heima.model.behavior.pojos.ApBehaviorEntry;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/behavior_entry")
public class ApBehaviorEntryController {
@Autowired
ApBehaviorEntryService apBehaviorEntryService;
@GetMapping("/one")
public ResponseResult<ApBehaviorEntry> findByUserIdOrEquipmentId
(@RequestParam(value = "userId",required = false) Integer userId,
@RequestParam(value = "equipmentId",required = false) Integer equipmentId){
ApBehaviorEntry apBehaviorEntry = apBehaviorEntryService.findByUserIdOrEquipmentId(userId, equipmentId);
return ResponseResult.okResult(apBehaviorEntry);
}
}
定义行为微服务feign接口
@FeignClient(value = "leadnews-behavior",
fallbackFactory = BehaviorFeignFallback.class,
configuration = HeimaFeignAutoConfiguration.class)
public interface BehaviorFeign {
@GetMapping("/api/v1/behavior_entry/one")
public ResponseResult<ApBehaviorEntry> findByUserIdOrEquipmentId(@RequestParam("userId") Integer userId,
@RequestParam("equipmentId") Integer equipmentId);
}
服务降级方法
@Component
@Slf4j
public class BehaviorFeignFallback implements FallbackFactory<BehaviorFeign> {
@Override
public BehaviorFeign create(Throwable throwable) {
throwable.printStackTrace();
return new BehaviorFeign() {
@Override
public ResponseResult<ApBehaviorEntry> findByUserIdOrEquipmentId(Integer userId, Integer equipmentId) {
log.error("Feign服务降级触发 远程调用:BehaviorFeign findByUserIdOrEquipmentId 失败,参数:userId={} equipmentId={}",userId,equipmentId);
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"远程服务调用出现异常");
}
};
}
}
1.3.3 集成AppTokenFilter过滤器
@Order(1)
@WebFilter(filterName = "appTokenFilter", urlPatterns = "/*")
@Component
@Slf4j
public class AppTokenFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 测试和开发环境不过滤
String userId = request.getHeader("userId");
//如果userId为0,说明当前设备没有登录
if(userId!=null && Integer.valueOf(userId).intValue()!=0){
ApUser apUser = new ApUser();
apUser.setId(Integer.valueOf(userId));
AppThreadLocalUtils.setUser(apUser);
}
chain.doFilter(req,res);
// 在过滤器的后置方法中清除当前登录用户
AppThreadLocalUtils.clear();
}
}
1.3.4 修改文章搜索方法
修改类ArticleSearchServiceImpl类的search方法,完整代码如下:
public ResponseResult search(UserSearchDto userSearchDto) {
//1 参数检查
String searchWords = userSearchDto.getSearchWords();
if (userSearchDto == null || StringUtils.isBlank(searchWords)) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_REQUIRE);
}
//=================== 记录搜索历史记录
ApUser user = AppThreadLocalUtils.getUser();
if(user !=null) userSearchDto.setEntryId(user.getId());
// 异步调用保存用户输入关键词记录
apUserSearchService.insert( userSearchDto);
// ==========================
//2 构建查询执行查询
1.3.5 搜索历史异步任务
新增ApUserSearchService接口,由于是在搜索的时候异步调用保存关键字,暂时不需要定义api接口
package com.heima.search.service;
import com.heima.model.search.dtos.UserSearchDTO;
public interface ApUserSearchService {
/**
* 保存用户搜索历史记录
* @param userSearchDto
*/
public void insert( UserSearchDTO userSearchDto);
}
实现类:
在插入方法中使用注解@Async("taskExecutor")
进行异步调用,使得该方法加入线程池运行,这个值为在是线程池中的方法名称,需要与@EnableAsync
一块使用
配置线程池
package com.heima.search.config;
import lombok.Data;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Data
@Configuration
@EnableAsync //开启异步请求
public class ThreadPoolConfig {
private static final int corePoolSize = 10; // 核心线程数(默认线程数)
private static final int maxPoolSize = 100; // 最大线程数
private static final int keepAliveTime = 10; // 允许线程空闲时间(单位:默认为秒)
private static final int queueCapacity = 500; // 缓冲队列数
/**
* 默认异步线程池
* @return
*/
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor(){
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setThreadNamePrefix("--------------全局线程池-----------------");
pool.setCorePoolSize(corePoolSize);
pool.setMaxPoolSize(maxPoolSize);
pool.setKeepAliveSeconds(keepAliveTime);
pool.setQueueCapacity(queueCapacity);
// 直接在execute方法的调用线程中运行
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
pool.initialize();
return pool;
}
}
实现类
package com.heima.search.service.impl;
import com.heima.common.exception.CustException;
import com.heima.feigns.BehaviorFeign;
import com.heima.model.behavior.pojos.ApBehaviorEntry;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.search.dtos.UserSearchDTO;
import com.heima.model.search.pojos.ApUserSearch;
import com.heima.search.service.ApUserSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {
@Autowired
MongoTemplate mongoTemplate;
@Autowired
BehaviorFeign behaviorFeign;
/**
* 保存用户搜索历史记录
* @param userSearchDto
*/
@Override
@Async("taskExecutor")
public void insert( UserSearchDTO userSearchDto) {
//1 参数检查
String searchWords = userSearchDto.getSearchWords();
Integer entryId = userSearchDto.getEntryId();
//2 查询行为实体
ResponseResult<ApBehaviorEntry> behaviorResponse = behaviorFeign.findByUserIdOrEquipmentId(entryId, userSearchDto.getEquipmentId());
if (!behaviorResponse.checkCode()) {
CustException.cust(AppHttpCodeEnum.REMOTE_SERVER_ERROR);
}
ApBehaviorEntry apBehaviorEntry = behaviorResponse.getData();
if (apBehaviorEntry==null) {
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST);
}
log.info("ApUserSearchServiceImpl insert entryId:{}, searchWords:{}", entryId, searchWords);
//3 查询当前用户搜索的关键词是否存在
ApUserSearch apUserSearch = mongoTemplate.findOne(Query.query(Criteria.where("entryId").is(entryId).and("keyword").is(searchWords)), ApUserSearch.class);
//3.1 存在则更新最新时间,更新状态
if (apUserSearch != null) {
apUserSearch.setCreatedTime(new Date());
mongoTemplate.save(apUserSearch);
return;
}
//3.2 不存在则新增
apUserSearch = new ApUserSearch();
apUserSearch.setCreatedTime(new Date());
apUserSearch.setEntryId(apBehaviorEntry.getId());
apUserSearch.setKeyword(searchWords);
mongoTemplate.save(apUserSearch);
}
}
1.3.6 测试
在app端搜索一个词,是否能存入到MongoDB数据库中,搜索相同的词,只保存一次,按照时间倒序。
1.4 加载搜索记录列表
1.4.1 思路分析
根据行为实体查询用户搜索记录表,默认加载10条数据,按照时间倒序
1.4.2 接口定义
package com.heima.search.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDTO;
import com.heima.search.service.ApUserSearchService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* APP用户搜索信息表 前端控制器
* @author itheima
*/
@Slf4j
@Api(value="搜索历史记录API",tags="搜索历史记录API")
@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController{
@Autowired
private ApUserSearchService apUserSearchService;
@ApiOperation("加载搜索历史记录")
@PostMapping("/load")
public ResponseResult findUserSearch(@RequestBody UserSearchDTO userSearchDto) {
return apUserSearchService.findUserSearch(userSearchDto);
}
}
1.4.3 业务层
在ApUserSearchService中新增接口方法
/**
查询搜索历史
@param userSearchDto
@return
*/
ResponseResult findUserSearch(UserSearchDTO userSearchDto);
实现方法
/**
* 查询用户搜索历史记录 显示10条
* @param userSearchDto
* @return
*/
@Override
public ResponseResult findUserSearch(UserSearchDTO userSearchDto) {
if(userSearchDto.getMinBehotTime() == null){
userSearchDto.setMinBehotTime(new Date());
}
List<ApUserSearch> userSearchList = new ArrayList<>();
int size = 10;
ApUser user = AppThreadLocalUtils.getUser();
//1 查询实体 60ed86609d88f41a1da5e770
ResponseResult<ApBehaviorEntry> behaviorResult = behaviorFeign.findByUserIdOrEquipmentId(user==null?null:user.getId(), userSearchDto.getEquipmentId());
if (behaviorResult.getCode().intValue()!=0) {
CustException.cust(AppHttpCodeEnum.SERVER_ERROR,behaviorResult.getErrorMessage());
}
ApBehaviorEntry apBehaviorEntry = behaviorResult.getData();
if (apBehaviorEntry == null) {
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST);
}
if (apBehaviorEntry != null) {
// 查询当前用户的搜索列表 按照时间倒序 .limit(userSearchDto.getPageSize())
userSearchList = mongoTemplate.find(Query.query(Criteria.where("entryId").is(apBehaviorEntry.getId()).and("createdTime").lt(userSearchDto.getMinBehotTime()))
.with(Sort.by(Sort.Direction.DESC,"createdTime")).limit(size),ApUserSearch.class);
return ResponseResult.okResult(userSearchList);
}
return ResponseResult.okResult(userSearchList);
}
1.4.5 控制器
package com.heima.search.controller.v1;
import com.heima.apis.search.ApUserSearchControllerApi;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.search.service.ApUserSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* APP用户搜索信息表 前端控制器
* @author itheima
*/
@Slf4j
@Api(value="搜索历史记录API",tags="搜索历史记录API")
@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController{
@Autowired
private ApUserSearchService apUserSearchService;
@ApiOperation("加载搜索历史记录")
@PostMapping("/load")
public ResponseResult findUserSearch(@RequestBody UserSearchDto userSearchDto) {
return apUserSearchService.findUserSearch(userSearchDto);
}
}
1.4.6 测试
打开app的搜索页面,可以查看搜索记录列表
1.5 删除搜索记录
1.5.1 思路分析
根据行为实体和当前id删除搜索记录
1.5.2 接口定义
在ApUserSearchController接口新增方法
@ApiOperation("删除搜索历史记录")
@PostMapping("/del")
public ResponseResult delUserSearch(@RequestBody HistorySearchDTO dto) {
return apUserSearchService.delUserSearch(dto);
}
添加 HistorySearchDto
package com.heima.model.search.dtos;
import lombok.Data;
@Data
public class HistorySearchDTO {
// 设备ID
Integer equipmentId;
/**
* 接收搜索历史记录id
*/
String id;
}
1.5.3 业务层
在ApUserSearchService中新增方法
/**
删除搜索历史
@param userSearchDto
@return
*/
ResponseResult delUserSearch(HistorySearchDTO historySearchDto);
实现方法
@Override
public ResponseResult delUserSearch(HistorySearchDTO historySearchDto) {
if(historySearchDto.getId() == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
ApUser user = AppThreadLocalUtils.getUser();
ResponseResult<ApBehaviorEntry> behaviorEntryResult = behaviorFeign.findByUserIdOrEquipmentId(user == null ? null : user.getId(), historySearchDto.getEquipmentId());
if(!behaviorEntryResult.checkCode()){
return ResponseResult.errorResult(AppHttpCodeEnum.REMOTE_SERVER_ERROR,"远程调用行为服务失败");
}
ApBehaviorEntry behaviorEntry = behaviorEntryResult.getData();
if(behaviorEntry == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"行为实体数据不存在");
}
mongoTemplate.remove(Query.query(Criteria.where("_id").is(historySearchDto.getId()).and("entryId").is(behaviorEntry.getId())),ApUserSearch.class);
return ResponseResult.okResult();
}
1.5.4 控制器
修改ApUserSearchController,新增方法
@ApiOperation("删除搜索历史记录")
@PostMapping("/del")
public ResponseResult delUserSearch(@RequestBody HistorySearchDTO historySearchDto) {
return apUserSearchService.delUserSearch(historySearchDto);
}
1.5.5 测试
打开app可以删除搜索记录
2 关键字联想词
2.1 需求分析
当关键词在一定的时间被频繁搜索,会被判定为受到用户更需求的行为,搜索功能根据用户搜索的时候给到下拉的联想推荐。
操作方法如下:
- 根据用户输入的关键字展示联想词
2.2 思路分析
表结构:
ap_associate_words 联想词表
{
_id: "5fd46fe74d6459434536705",
associateWords: "",
createdTime: ISODate("2099-02-18T08:43:53.716Z")
}
对应实体:
对应实体:
package com.heima.search.pojo;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.Date;
/**
* 联想词表
* @author itheima
*/
@Data
@Document("ap_associate_words")
public class ApAssociateWords implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
/**
* 联想词
*/
private String associateWords;
/**
* 创建时间
*/
private Date createdTime;
}
根据当前时间进行检索数据
2.3 功能实现
2.3.1 接口定义
新建接口
package com.heima.search.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDTO;
import com.heima.search.service.ApAssociateWordsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 联想词表 前端控制器
* @author itheima
*/
@Api(value="搜索联想词API",tags="搜索联想词API")
@Slf4j
@RestController
@RequestMapping("/api/v1/associate")
public class ApAssociateWordsController {
@Autowired
private ApAssociateWordsService apAssociateWordsService;
@ApiOperation("搜索联想词")
@PostMapping("/search")
public ResponseResult findAssociate(@RequestBody UserSearchDTO userSearchDto) {
return apAssociateWordsService.findAssociate(userSearchDto);
}
}
2.3.2 业务层
新建联想词业务层接口
package com.heima.search.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDTO;
import com.heima.model.search.pojos.ApAssociateWords;
/**
* <p>
* 联想词表 服务类
* </p>
*
* @author itheima
*/
public interface ApAssociateWordsService {
/**
联想词
@param userSearchDto
@return
*/
ResponseResult findAssociate(UserSearchDTO userSearchDto);
}
实现类
package com.heima.search.service.impl;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.search.dtos.UserSearchDTO;
import com.heima.model.search.pojos.ApAssociateWords;
import com.heima.search.service.ApAssociateWordsService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ApAssociateWordsServiceImpl implements ApAssociateWordsService {
@Autowired
MongoTemplate mongoTemplate;
@Override
public ResponseResult findAssociate(UserSearchDTO userSearchDto) {
//1 参数检查
if(StringUtils.isBlank(userSearchDto.getSearchWords())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2 执行查询 模糊查询
List<ApAssociateWords> wordsList = mongoTemplate.find(Query.query(Criteria.where("associateWords")
.regex(".*" +userSearchDto.getSearchWords()+ ".*")), ApAssociateWords.class);
return ResponseResult.okResult(wordsList);
}
}
2.3.3 控制器
新建联想词控制器
package com.heima.search.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDTO;
import com.heima.search.service.ApAssociateWordsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(value="搜索联想词API",tags="搜索联想词API")
@Slf4j
@RestController
@RequestMapping("/api/v1/associate")
public class ApAssociateWordsController {
@Autowired
private ApAssociateWordsService apAssociateWordsService;
@ApiOperation("搜索联想词")
@PostMapping("/search")
public ResponseResult findAssociate(@RequestBody UserSearchDTO userSearchDto) {
return apAssociateWordsService.findAssociate(userSearchDto);
}
}
2.3.4 测试
打开前端联调测试效果