07 app端文章搜索
1) 今日内容介绍
1.1)App端搜索-效果图
1.2)今日内容
2) 搭建ElasticSearch环境
2.1) 拉取镜像
docker pull elasticsearch:7.4.0
2.2) 创建容器
docker run -id --name elasticsearch -d --restart=always -p 9200:9200 -p 9300:9300 -v /usr/share/elasticsearch/plugins:/usr/share/elasticsearch/plugins -e "discovery.type=single-node" elasticsearch:7.4.0
2.3) 配置中文分词器 ik
因为在创建elasticsearch容器的时候,映射了目录,所以可以在宿主机上进行配置ik中文分词器
在去选择ik分词器的时候,需要与elasticsearch的版本好对应上
把资料中的elasticsearch-analysis-ik-7.4.0.zip上传到服务器上,放到对应目录(plugins)解压
#切换目录
cd /usr/share/elasticsearch/plugins
#新建目录
mkdir analysis-ik
cd analysis-ik
#root根目录中拷贝文件
mv elasticsearch-analysis-ik-7.4.0.zip /usr/share/elasticsearch/plugins/analysis-ik
#解压文件
cd /usr/share/elasticsearch/plugins/analysis-ik
unzip elasticsearch-analysis-ik-7.4.0.zip
2.4) 使用postman测试
192.168.200.130:9200/_analyze
{
"analyzer":"ik_max_word",
"text":"欢迎来到黑马程序员学习"
}
3) app端文章搜索
3.1) 需求分析
用户输入关键可搜索文章列表
关键词高亮显示
文章列表展示与home展示一样,当用户点击某一篇文章,可查看文章详情
3.2) 思路分析
为了加快检索的效率,在查询的时候不会直接从数据库中查询文章,需要在elasticsearch中进行高速检索。
3.3) 创建索引和映射
使用postman添加映射
put请求 : http://192.168.200.130:9200/app_info_article
{
"mappings":{
"properties":{
"id":{
"type":"long"
},
"publishTime":{
"type":"date"
},
"layout":{
"type":"integer"
},
"images":{
"type":"keyword",
"index": false
},
"staticUrl":{
"type":"keyword",
"index": false
},
"authorId": {
"type": "long"
},
"authorName": {
"type": "text"
},
"title":{
"type":"text",
"analyzer":"ik_smart"
},
"content":{
"type":"text",
"analyzer":"ik_smart"
}
}
}
}
GET请求查询映射:http://192.168.200.130:9200/app_info_article
DELETE请求,删除索引及映射:http://192.168.200.130:9200/app_info_article
GET请求,查询所有文档:http://192.168.200.130:9200/app_info_article/_search
3.4) 数据初始化到索引库
3.4.1)导入es-init到heima-leadnews-test工程下
3.4.1)查询所有的文章信息,批量导入到es索引库中
package com.heima.es;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApArticleTest {
@Autowired
private ApArticleMapper apArticleMapper;
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
* 注意:数据量的导入,如果数据量过大,需要分页导入
* @throws Exception
*/
@Test
public void init() throws Exception {
//1.查询所有符合条件的文章数据
List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();
//2.批量导入到es索引库
BulkRequest bulkRequest = new BulkRequest("app_info_article");
for (SearchArticleVo searchArticleVo : searchArticleVos) {
IndexRequest indexRequest = new IndexRequest().id(searchArticleVo.getId().toString())
.source(JSON.toJSONString(searchArticleVo), XContentType.JSON);
//批量添加数据
bulkRequest.add(indexRequest);
}
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
}
3.4.3)测试
postman查询所有的es中数据 GET请求: http://192.168.200.130:9200/app_info_article/_search
3.5) 文章搜索功能实现
3.5.1)搭建搜索微服务
(1)导入 heima-leadnews-search
(2)在heima-leadnews-service的pom中添加依赖
<!--elasticsearch-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.4.0</version>
</dependency>
(3)nacos配置中心leadnews-search
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:
host: 192.168.200.130
port: 9200
3.5.2) 搜索接口定义
package com.heima.search.controller.v1;
import java.io.IOException;
@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {
@PostMapping("/search")
public ResponseResult search(@RequestBody UserSearchDto dto) throws IOException {
return null;
}
}
UserSearchDto
package com.heima.model.search.dtos;
import lombok.Data;
import java.util.Date;
@Data
public class UserSearchDto {
/**
* 搜索关键字
*/
String searchWords;
/**
* 当前页
*/
int pageNum;
/**
* 分页条数
*/
int pageSize;
/**
* 最小时间
*/
Date minBehotTime;
public int getFromIndex(){
if(this.pageNum<1)return 0;
if(this.pageSize<1) this.pageSize = 10;
return this.pageSize * (pageNum-1);
}
}
3.5.3) 业务层实现
创建业务层接口:ApArticleSearchService
package com.heima.search.service;
import java.io.IOException;
public interface ArticleSearchService {
/**
ES文章分页搜索
@return
*/
ResponseResult search(UserSearchDto userSearchDto) throws IOException;
}
实现类:
package com.heima.search.service.impl;
import java.util.Map;
@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
* es文章分页检索
*
* @param dto
* @return
*/
@Override
public ResponseResult search(UserSearchDto dto) throws IOException {
//1.检查参数
if(dto == null || StringUtils.isBlank(dto.getSearchWords())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.设置查询条件
SearchRequest searchRequest = new SearchRequest("app_info_article");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//关键字的分词之后查询
QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(dto.getSearchWords()).field("title").field("content").defaultOperator(Operator.OR);
boolQueryBuilder.must(queryStringQueryBuilder);
//查询小于mindate的数据
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime").lt(dto.getMinBehotTime().getTime());
boolQueryBuilder.filter(rangeQueryBuilder);
//分页查询
searchSourceBuilder.from(0);
searchSourceBuilder.size(dto.getPageSize());
//按照发布时间倒序查询
searchSourceBuilder.sort("publishTime", SortOrder.DESC);
//设置高亮 title
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//3.结果封装返回
List<Map> list = new ArrayList<>();
SearchHit[] hits = searchResponse.getHits().getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
Map map = JSON.parseObject(json, Map.class);
//处理高亮
if(hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0){
Text[] titles = hit.getHighlightFields().get("title").getFragments();
String title = StringUtils.join(titles);
//高亮标题
map.put("h_title",title);
}else {
//原始标题
map.put("h_title",map.get("title"));
}
list.add(map);
}
return ResponseResult.okResult(list);
}
}
3.5.4) 控制层实现
新建控制器ArticleSearchController
package com.heima.search.controller.v1;
import java.io.IOException;
@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {
@Autowired
private ArticleSearchService articleSearchService;
@PostMapping("/search")
public ResponseResult search(@RequestBody UserSearchDto dto) throws IOException {
return articleSearchService.search(dto);
}
}
3.5.5) 测试
需要在app的网关中添加搜索微服务的路由配置
#搜索微服务
- id: leadnews-search
uri: lb://leadnews-search
predicates:
- Path=/search/**
filters:
- StripPrefix= 1
启动项目进行测试,至少要启动文章微服务,用户微服务,搜索微服务,app网关微服务,app前端工程
3.6) 文章自动审核构建索引
3.6.1)思路分析
3.6.2)文章微服务发送消息
1.把SearchArticleVo放到model工程下
package com.heima.model.search.vos;
import lombok.Data;
import java.util.Date;
@Data
public class SearchArticleVo {
// 文章id
private Long id;
// 文章标题
private String title;
// 文章发布时间
private Date publishTime;
// 文章布局
private Integer layout;
// 封面
private String images;
// 作者id
private Long authorId;
// 作者名词
private String authorName;
//静态url
private String staticUrl;
//文章内容
private String content;
}
2.文章微服务的ArticleFreemarkerService中的buildArticleToMinIO方法中收集数据并发送消息
完整代码如下:
package com.heima.article.service.impl;
import java.util.Map;
@Service
@Slf4j
@Transactional
public class ArticleFreemarkerServiceImpl implements ArticleFreemarkerService {
***前面没变
//4.4 修改ap_article表,保存static_url字段
apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId,apArticle.getId())
.set(ApArticle::getStaticUrl,path));
//发送消息,创建索引
createArticleESIndex(apArticle,content,path);
}
}
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
/**
* 送消息,创建索引
* @param apArticle
* @param content
* @param path
*/
private void createArticleESIndex(ApArticle apArticle, String content, String path) {
SearchArticleVo vo = new SearchArticleVo();
BeanUtils.copyProperties(apArticle,vo);
vo.setContent(content);
vo.setStaticUrl(path);
kafkaTemplate.send(ArticleConstants.ARTICLE_ES_SYNC_TOPIC, JSON.toJSONString(vo));
}
}
在ArticleConstants类中添加新的常量,完整代码如下
package com.heima.common.constants;
public class ArticleConstants {
public static final Short LOADTYPE_LOAD_MORE = 1;
public static final Short LOADTYPE_LOAD_NEW = 2;
public static final String DEFAULT_TAG = "__all__";
public static final String ARTICLE_ES_SYNC_TOPIC = "article.es.sync.topic";
}
3.文章微服务集成kafka发送消息
在文章微服务的nacos的配置中心添加如下配置
kafka:
bootstrap-servers: 192.168.200.130:9092
producer:
retries: 10
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
3.6.3)搜索微服务接收消息并创建索引
1.搜索微服务中添加kafka的配置,nacos配置如下
spring:
kafka:
bootstrap-servers: 192.168.200.130:9092
consumer:
group-id: ${spring.application.name}
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
2.定义监听接收消息,保存索引数据
package com.heima.search.listener;
import java.io.IOException;
@Component
@Slf4j
public class SyncArticleListener {
@Autowired
private RestHighLevelClient restHighLevelClient;
@KafkaListener(topics = ArticleConstants.ARTICLE_ES_SYNC_TOPIC)
public void onMessage(String message){
if(StringUtils.isNotBlank(message)){
log.info("SyncArticleListener,message={}",message);
SearchArticleVo searchArticleVo = JSON.parseObject(message, SearchArticleVo.class);
IndexRequest indexRequest = new IndexRequest("app_info_article");
indexRequest.id(searchArticleVo.getId().toString());
indexRequest.source(message, XContentType.JSON);
try {
restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
log.error("sync es error={}",e);
}
}
}
}
4) app端搜索-搜索记录
4.1) 需求分析
展示用户的搜索记录10条,按照搜索关键词的时间倒序
可以删除搜索记录
保存历史记录,保存10条,多余的则删除最久的历史记录
4.2)数据存储说明
用户的搜索记录,需要给每一个用户都保存一份,数据量较大,要求加载速度快,通常这样的数据存储到mongodb更合适,不建议直接存储到关系型数据库中
4.3)MongoDB安装及集成
4.3.1)安装MongoDB
拉取镜像
docker pull mongo
创建容器
docker run -di --name mongo-service --restart=always -p 27017:27017 -v ~/data/mongodata:/data mongo
4.3.2)导入资料中的mongo-demo项目到heima-leadnews-test中
其中有三项配置比较关键:
第一:mongo依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
第二:mongo配置
server:
port: 9998
spring:
data:
mongodb:
host: 192.168.200.130
port: 27017
database: leadnews-history
第三:映射
package com.itheima.mongo.pojo;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 联想词表
* </p>
*
* @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;
}
4.3.3)核心方法
package com.itheima.mongo.test;
import java.util.Date;
import java.util.List;
@SpringBootTest(classes = MongoApplication.class)
@RunWith(SpringRunner.class)
public class MongoTest {
@Autowired
private MongoTemplate mongoTemplate;
//保存
@Test
public void saveTest(){
/*for (int i = 0; i < 10; i++) {
ApAssociateWords apAssociateWords = new ApAssociateWords();
apAssociateWords.setAssociateWords("黑马头条");
apAssociateWords.setCreatedTime(new Date());
mongoTemplate.save(apAssociateWords);
}*/
ApAssociateWords apAssociateWords = new ApAssociateWords();
apAssociateWords.setAssociateWords("黑马直播");
apAssociateWords.setCreatedTime(new Date());
mongoTemplate.save(apAssociateWords);
}
//查询一个
@Test
public void saveFindOne(){
ApAssociateWords apAssociateWords = mongoTemplate.findById("60bd973eb0c1d430a71a7928", ApAssociateWords.class);
System.out.println(apAssociateWords);
}
//条件查询
@Test
public void testQuery(){
Query query = Query.query(Criteria.where("associateWords").is("黑马头条"))
.with(Sort.by(Sort.Direction.DESC,"createdTime"));
List<ApAssociateWords> apAssociateWordsList = mongoTemplate.find(query, ApAssociateWords.class);
System.out.println(apAssociateWordsList);
}
@Test
public void testDel(){
mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("黑马头条")),ApAssociateWords.class);
}
}
4.4)保存搜索记录
4.4.1)实现思路
用户输入关键字进行搜索的异步记录关键字
用户搜索记录对应的集合,对应实体类:
package com.heima.search.pojos;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* APP用户搜索信息表
* </p>
* @author itheima
*/
@Data
@Document("ap_user_search")
public class ApUserSearch implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 搜索词
*/
private String keyword;
/**
* 创建时间
*/
private Date createdTime;
}
4.4.2)实现步骤
1.搜索微服务集成mongodb
①:pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
②:nacos配置
spring:
data:
mongodb:
host: 192.168.200.130
port: 27017
database: leadnews-history
③:在当天资料中找到对应的实体类拷贝到搜索微服务下
2.创建ApUserSearchService新增insert方法
public interface ApUserSearchService {
/**
* 保存用户搜索历史记录
* @param keyword
* @param userId
*/
public void insert(String keyword,Integer userId);
}
实现类:
@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 保存用户搜索历史记录
* @param keyword
* @param userId
*/
@Override
@Async
public void insert(String keyword, Integer userId) {
//1.查询当前用户的搜索关键词
Query query = Query.query(Criteria.where("userId").is(userId).and("keyword").is(keyword));
ApUserSearch apUserSearch = mongoTemplate.findOne(query, ApUserSearch.class);
//2.存在 更新创建时间
if(apUserSearch != null){
apUserSearch.setCreatedTime(new Date());
mongoTemplate.save(apUserSearch);
return;
}
//3.不存在,判断当前历史记录总数量是否超过10
apUserSearch = new ApUserSearch();
apUserSearch.setUserId(userId);
apUserSearch.setKeyword(keyword);
apUserSearch.setCreatedTime(new Date());
Query query1 = Query.query(Criteria.where("userId").is(userId));
query1.with(Sort.by(Sort.Direction.DESC,"createdTime"));
List<ApUserSearch> apUserSearchList = mongoTemplate.find(query1, ApUserSearch.class);
if(apUserSearchList == null || apUserSearchList.size() < 10){
mongoTemplate.save(apUserSearch);
}else {
ApUserSearch lastUserSearch = apUserSearchList.get(apUserSearchList.size() - 1);
mongoTemplate.findAndReplace(Query.query(Criteria.where("id").is(lastUserSearch.getId())),apUserSearch);
}
}
}
3.参考自媒体相关微服务,在搜索微服务中获取当前登录的用户
4.在ArticleSearchService的search方法中调用保存历史记录
完整代码如下:
package com.heima.search.service.impl;
import java.util.Map;
@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {
@Autowired
private ApUserSearchService apUserSearchService;
/**
* es文章分页检索
*
* @param dto
* @return
*/
@Override
public ResponseResult search(UserSearchDto dto) throws IOException {
//1.检查参数
if(dto == null || StringUtils.isBlank(dto.getSearchWords())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
ApUser user = AppThreadLocalUtil.getUser();
//异步调用 保存搜索记录
if(user != null && dto.getFromIndex() == 0){
apUserSearchService.insert(dto.getSearchWords(), user.getId());
}
//2.设置查询条件
SearchRequest searchRequest = new SearchRequest("app_info_article");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
****都没变
return ResponseResult.okResult(list);
}
}
5.保存历史记录中开启异步调用,添加注解@Async
6.在搜索微服务引导类上开启异步调用
7.测试,搜索后查看结果
4.5) 加载搜索记录列表
4.5.1) 思路分析
按照当前用户,按照时间倒序查询
说明 | |
接口路径 | /api/v1/history/load |
请求方式 | POST |
参数 | 无 |
响应结果 | ResponseResult |
4.5.2) 接口定义
/**
* <p>
* APP用户搜索信息表 前端控制器
* </p>
*
* @author itheima
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController{
@PostMapping("/load")
@Override
public ResponseResult findUserSearch() {
return null;
}
}
4.5.3) mapper
已定义
4.5.4) 业务层
在ApUserSearchService中新增方法
/**
查询搜索历史
@return
*/
ResponseResult findUserSearch();
实现方法
/**
* 查询搜索历史
*
* @return
*/
@Override
public ResponseResult findUserSearch() {
//获取当前用户
ApUser user = AppThreadLocalUtil.getUser();
if(user == null){
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
//根据用户查询数据,按照时间倒序
List<ApUserSearch> apUserSearches = mongoTemplate.find(Query.query(Criteria.where("userId").is(user.getId())).with(Sort.by(Sort.Direction.DESC, "createdTime")), ApUserSearch.class);
return ResponseResult.okResult(apUserSearches);
}
4.5.5) 控制器
/**
* <p>
* APP用户搜索信息表 前端控制器
* </p>
* @author itheima
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController{
@Autowired
private ApUserSearchService apUserSearchService;
@PostMapping("/load")
public ResponseResult findUserSearch() {
return apUserSearchService.findUserSearch();
}
}
4.5.6) 测试
打开app的搜索页面,可以查看搜索记录列表
4.6) 删除搜索记录
4.6.1) 思路分析
按照搜索历史id删除
说明 | |
接口路径 | /api/v1/history/del |
请求方式 | POST |
参数 | HistorySearchDto |
响应结果 | ResponseResult |
4.6.2) 接口定义
在ApUserSearchController接口新增方法
@PostMapping("/del")
public ResponseResult delUserSearch(@RequestBody HistorySearchDto historySearchDto) {
return null;
}
HistorySearchDto
@Data
public class HistorySearchDto {
/**
* 接收搜索历史记录id
*/
String id;
}
4.6.3) 业务层
在ApUserSearchService中新增方法
/**
删除搜索历史
@param historySearchDto
@return
*/
ResponseResult delUserSearch(HistorySearchDto historySearchDto);
实现方法
/**
* 删除历史记录
*
* @param dto
* @return
*/
@Override
public ResponseResult delUserSearch(HistorySearchDto dto) {
//1.检查参数
if(dto.getId() == null){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.判断是否登录
ApUser user = AppThreadLocalUtil.getUser();
if(user == null){
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
//3.删除
mongoTemplate.remove(Query.query(Criteria.where("userId").is(user.getId()).and("id").is(dto.getId())),ApUserSearch.class);
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
4.6.4) 控制器
修改ApUserSearchController,补全方法
@PostMapping("/del")
public ResponseResult delUserSearch(@RequestBody HistorySearchDto historySearchDto) {
return apUserSearchService.delUserSearch(historySearchDto);
}
4.6.5) 测试
打开app可以删除搜索记录
5) app端搜索-关键字联想词
5.1 需求分析
根据用户输入的关键字展示联想词
对应实体类
package com.heima.search.pojos;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 联想词表
* </p>
*
* @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;
}
5.2)搜索词-数据来源
通常是网上搜索频率比较高的一些词,通常在企业中有两部分来源:
第一:自己维护搜索词
通过分析用户搜索频率较高的词,按照排名作为搜索词
第二:第三方获取
关键词规划师(百度)、5118、爱站网
导入资料中的ap_associate_words.js脚本到mongo中
5.3 功能实现
5.3.1 接口定义
说明 | |
接口路径 | /api/v1/associate/search |
请求方式 | POST |
参数 | UserSearchDto |
响应结果 | ResponseResult |
新建接口
package com.heima.search.controller.v1;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/associate")
public class ApAssociateWordsController {
@PostMapping("/search")
public ResponseResult search(@RequestBody UserSearchDto userSearchDto) {
return null;
}
}
5.3.3 业务层
新建联想词业务层接口
package com.heima.search.service;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
/**
* <p>
* 联想词表 服务类
* </p>
*
* @author itheima
*/
public interface ApAssociateWordsService {
/**
联想词
@param userSearchDto
@return
*/
ResponseResult findAssociate(UserSearchDto userSearchDto);
}
实现类
package com.heima.search.service.impl;
import java.util.List;
/**
* @Description:
* @Version: V1.0
*/
@Service
public class ApAssociateWordsServiceImpl implements ApAssociateWordsService {
@Autowired
MongoTemplate mongoTemplate;
/**
* 联想词
* @param userSearchDto
* @return
*/
@Override
public ResponseResult findAssociate(UserSearchDto userSearchDto) {
//1 参数检查
if(userSearchDto == null || StringUtils.isBlank(userSearchDto.getSearchWords())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//分页检查
if (userSearchDto.getPageSize() > 20) {
userSearchDto.setPageSize(20);
}
//3 执行查询 模糊查询
Query query = Query.query(Criteria.where("associateWords").regex(".*?\\" + userSearchDto.getSearchWords() + ".*"));
query.limit(userSearchDto.getPageSize());
List<ApAssociateWords> wordsList = mongoTemplate.find(query, ApAssociateWords.class);
return ResponseResult.okResult(wordsList);
}
}
5.3.4 控制器
新建联想词控制器
package com.heima.search.controller.v1;
/**
* <p>
* 联想词表 前端控制器
* </p>
* @author itheima
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/associate")
public class ApAssociateWordsController{
@Autowired
private ApAssociateWordsService apAssociateWordsService;
@PostMapping("/search")
public ResponseResult findAssociate(@RequestBody UserSearchDto userSearchDto) {
return apAssociateWordsService.findAssociate(userSearchDto);
}
}
5.3.5 测试
同样,打开前端联调测试效果
平台管理-需求说明
要求:
每个组长要分配任务,共同完成这些需求
需要创建一个开源项目(每个组创建一个),其他组员共同提交到这个仓库里,最后合并为一个完整的项目
组长创建一个git项目
基础代码是day07完成后的代码
每个组员需要克隆到本地
每个组员创建一个分支去开发
合并为一个完整的项目
28号晚上答辩
展示完成的项目功能
给同学在讲台上,人选:最好是不爱说话或不敢说话的人
0)平台管理前端
查看资料中的admin-web文件夹,使用nginx部署
nginx中的配置为:
upstream heima-admin-gateway{
server localhost:6001;
}
server {
listen 8803;
location / {
root D:/workspace/admin-web/;
index index.html;
}
location ~/service_6001/(.*) {
proxy_pass http://heima-admin-gateway/$1;
proxy_set_header HOST $host; # 不改变源请求头的值
proxy_pass_request_body on; #开启获取请求体
proxy_pass_request_headers on; #开启获取请求头
proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息
}
}
0 准备工作
0.1数据库和类
数据库导入记得加表名;
给了两个类ApUserRealname、ApUser 按照包名导入就可以啦
0.2 项目结构参考前面的项目
model层、Service层、bootstrap连接nacos
1)登录及网关
1.1)登录
平台管理的表,请查看资料中导入到数据库中 leadnews_admin.sql
用户根据用户名和密码登录
密码需要手动加盐验证
需要返回用户的token和用户信息
1登录接口文档
接口地址:/login/in
请求方式:POST
请求数据类型:application/json
响应数据类型:*/*
接口描述:
请求示例:
{
"name": "",
"password": ""
}
请求参数:
参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
dto | dto | body | true | AdUserDto | AdUserDto |
name | true | string | |||
password | true | string |
响应状态:
状态码 | 说明 | schema |
200 | OK | ResponseResult |
201 | Created | |
401 | Unauthorized | |
403 | Forbidden | |
404 | Not Found |
响应参数:
参数名称 | 参数说明 | 类型 | schema |
code | integer(int32) | integer(int32) | |
data | object | ||
errorMessage | string | ||
host | string |
响应示例:
{
"host":null,
"code":1002,
"errorMessage":"数据不存在",
"data":null
}
{
"host":null,
"code":2,
"errorMessage":"密码错误",
"data":null
}
{
"host":null,
"code":200,
"errorMessage":"操作成功",
"data":{
"user":{
"id":"3",
"name":"guest",
"password":"",
"salt":"",
"nickname":"gu",
"image":null,
"phone":"13412345676",
"status":1,
"email":"guest@qq.com",
"loginTime":1596092403000,
"createdTime":1596092406000
},
"token":"eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQrEIAwA_5JzBTUaa3-jNGUtFIRYaFn2700Pe5thmC_so8ECawobbnE2jiubQJlMRe8Mxcghc6aIHiZoZcDiyCePwVqaQM6qt9wy-Hi7iOqH21HUyrmqld6V-er_M9F7Nm34ewAJrbr_gAAAAA.TxoXtzsWAdaqCLWeMUdSnMngZSXndTsoYq6Dz5_r_SDZWcMp8ZS2BJhxoRVHG4KxvOn2ZN3MATemX2EZ4KnrLw"
}
}
1.1参考User的登录写法
AdUSerDto
package com.heima.model.admin.dtos;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class AdUserDto {
/**
* 用户名
*/
@ApiModelProperty(value = "用户名",required = true)
private String name;
/**
* 密码
*/
@ApiModelProperty(value = "密码",required = true)
private String password;
}
AdUserMapper
package com.heima.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.admin.pojos.AdUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AdUserMapper extends BaseMapper<AdUser> {}
LoginController
package com.heima.admin.controller.v1;
import com.heima.admin.service.UserLoginService;
import com.heima.model.admin.pojos.AdUser;
import com.heima.model.common.dtos.ResponseResult;
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;
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private UserLoginService userLoginService;
@PostMapping("/in")
public ResponseResult login(@RequestBody AdUser user) {
return userLoginService.login(user);
}
}
AdminLoginService
package com.heima.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.admin.pojos.AdUser;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.user.pojos.ApUser;
public interface AdminLoginService extends IService<AdUser> {
/**
* 登录
*
* @param dto
* @return
*/
ResponseResult login(AdUser dto);
}
AdminLoginServiceImpl
package com.heima.admin.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.admin.mapper.AdUserMapper;
import com.heima.admin.service.AdminLoginService;
import com.heima.model.admin.pojos.AdUser;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.utils.common.AppJwtUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.HashMap;
import java.util.Map;
@Service
@SuppressWarnings("all")
public class AdminLoginServiceImpl extends ServiceImpl<AdUserMapper, AdUser> implements AdminLoginService {
@Autowired
private AdUserMapper adUserMapper;
/**
* 登录
* @param dto
* @return
*/
@Override
public ResponseResult login(AdUser dto) {
//1.正常登录 用户名和密码
if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())){
//1.1 根据手机号查询用户信息
AdUser dbUser = getOne(Wrappers.<AdUser>lambdaQuery().eq(AdUser::getName, dto.getName()));
if(dbUser == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
}
//1.2 比对密码
String salt = dbUser.getSalt();
String password = dto.getPassword();
String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
if(!pswd.equals(dbUser.getPassword())){
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
//1.3 返回数据 jwt user
String token = AppJwtUtil.getToken(dbUser.getId().longValue());
Map<String,Object> map = new HashMap<>();
map.put("token",token);
dbUser.setSalt("");
dbUser.setPassword("");
map.put("user",dbUser);
return ResponseResult.okResult(map);
}
else
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
}
}
AdminApplication
package com.heima.admin;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 实现后台管理系统
*/
@SpringBootApplication
@EnableDiscoveryClient//集成注册中心
@MapperScan("com.heima.admin.mapper")
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
呵呵 真的逆天,网关不给我端口,让我咋猜;
一直报错502 试都试不了;就在我马上要发疯的时候,发现虽然day12的没有但是day11的有;ok我不急了;
。。。真的逆天,nacos没给,不知道网关咋配的一直登录不上
但是postMan测方法成功;
1.2)网关
平台管理端,是作为平台管理员使用的后台服务,所有后台的请求,都应该经过网关访问,需要创建平台管理的网关,并使用nacos配置
需要校验jwt
路由其他微服务
2)频道管理
2.1)新增
前台输入内容进行频道的保存
频道名词不能重复
2.2)查询列表
查询需要按照创建时间倒序查询
按照频道名称模糊查询
可以按照状态进行精确查找(1:启用 true 0:禁用 false)
分页查询
2.3)修改
点击编辑后可以修改频道
如果频道被引用则不能禁用
2.4)删除
只有禁用的频道才能删除
WmchannelController
package com.heima.wemedia.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.dtos.ChannelDto;
import com.heima.model.wemedia.pojos.WmChannel;
import com.heima.wemedia.service.WmChannelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/channel")
public class WmchannelController {
@Autowired
private WmChannelService wmChannelService;
@GetMapping("/channels")
public ResponseResult findAll(){
return wmChannelService.findAll();
}
@PostMapping("/list")
public ResponseResult findByNameAndPage(@RequestBody ChannelDto dto){
return wmChannelService.findByNameAndPage(dto);
}
@PostMapping("/save")
public ResponseResult insert(@RequestBody WmChannel adChannel){
return wmChannelService.insert(adChannel);
}
@PostMapping("/update")
public ResponseResult update(@RequestBody WmChannel adChannel){
return wmChannelService.update(adChannel);
}
@GetMapping("/del/{id}")
public ResponseResult delete(@PathVariable("id") Integer id){
return wmChannelService.delete(id);
}
}
3)敏感词管理
3.1)新增
弹出的输入框,输入敏感词可直接保存
已存在的敏感词则不能保存
3.2)查询列表
查询需要按照创建时间倒序查询
按照敏感词名称模糊查询
分页查询
3.3)修改
3.4)删除
直接删除即可
WmSensitiveController
package com.heima.wemedia.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.dtos.SensitiveDto;
import com.heima.model.wemedia.pojos.WmSensitive;
import com.heima.wemedia.service.WmSensitiveService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/sensitive")
public class WmSensitiveController {
@Autowired
private WmSensitiveService wmSensitiveService;
@PostMapping("/list")
public ResponseResult list(@RequestBody SensitiveDto dto){
return wmSensitiveService.list(dto);
}
@PostMapping("/save")
public ResponseResult insert(@RequestBody WmSensitive wmSensitive){
return wmSensitiveService.insert(wmSensitive);
}
@PostMapping("/update")
public ResponseResult update(@RequestBody WmSensitive wmSensitive){
return wmSensitiveService.update(wmSensitive);
}
@DeleteMapping("/del/{id}")
public ResponseResult delete(@PathVariable("id") Integer id){
return wmSensitiveService.delete(id);
}
}
4)用户认证审核
在app端的个人中心用户可以实名认证,需要材料为:姓名、身份证号、身份证正面照、身份证反面照、手持照片、活体照片(通过微笑、眨眼、张嘴、摇头、点头等组合动作,确保操作的为真实活体人脸。),当用户提交审核后就到了后端让运营管理人员进行审核
平台运营端查看用户认证信息,进行审核,其中审核包括了用户身份审核,需要对接公安系统校验身份证信息
用户通过审核后需要开通自媒体账号(该账号的用户名和密码与app一致)
4.1)分页查询认证列表
可根据审核状态条件查询
需要分页查询
4.2)审核
人工审核
拒绝
审核成功
ApUserRealnameController
package com.heima.user.controller.v1;
import com.heima.common.constants.UserConstants;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.user.dtos.AuthDto;
import com.heima.user.service.ApUserRealnameService;
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;
@RestController
@RequestMapping("/api/v1/auth")
public class ApUserRealnameController {
@Autowired
private ApUserRealnameService apUserRealnameService;
@PostMapping("/list")
public ResponseResult loadListByStatus(@RequestBody AuthDto dto){
return apUserRealnameService.loadListByStatus(dto);
}
@PostMapping("/authPass")
public ResponseResult authPass(@RequestBody AuthDto dto ){
return apUserRealnameService.updateStatus(dto, UserConstants.PASS_AUTH);
}
@PostMapping("/authFail")
public ResponseResult authFail(@RequestBody AuthDto dto ){
return apUserRealnameService.updateStatus(dto, UserConstants.FAIL_AUTH);
}
}
5)文章人工审核
自媒体文章如果没有自动审核成功,而是到了人工审核(自媒体文章状态为3),需要在admin端人工处理文章的审核
平台管理员可以查看待人工审核的文章信息,如果存在违规内容则驳回(状态改为2,文章审核失败)
平台管理员可以查看待人工审核的文章信息,如果不存在违规,则需要创建app端的文章信息,并更新自媒体文章的状态
也可以通过点击查看按钮,查看文章详细信息,查看详情后可以根据内容判断是否需要通过审核
5.1)文章列表查询
分页查询自媒体文章
可以按照标题模糊查询
可以按照审核状态进行精确 检索
文章查询按照创建时间倒序查询
注意:需要展示作者名称
5.2)查询文章详情
可以查看文章详细内容
注意:需要展示作者名称
5.3)人工审核
5.3.1) 拒绝
拒绝以后需要给出原因,并修改文章的状态为2
5.3.2)审核成功
需要创建app端的文章信息,并更新自媒体文章的状态
WmNewsController
package com.heima.wemedia.controller.v1;
import com.heima.common.constants.WemediaConstants;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.dtos.NewsAuthDto;
import com.heima.model.wemedia.dtos.WmNewsDto;
import com.heima.model.wemedia.dtos.WmNewsPageReqDto;
import com.heima.wemedia.service.WmNewsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/news")
public class WmNewsController {
@Autowired
private WmNewsService wmNewsService;
@PostMapping("/list")
public ResponseResult findAll(@RequestBody WmNewsPageReqDto dto){
return wmNewsService.findAll(dto);
}
@PostMapping("/submit")
public ResponseResult submitNews(@RequestBody WmNewsDto dto){
return wmNewsService.submitNews(dto);
}
@GetMapping("/one/{id}")
public ResponseResult findOne(@PathVariable("id") Integer id){
return wmNewsService.findOne(id);
}
@PostMapping("/down_or_up")
public ResponseResult downOrUp(@RequestBody WmNewsDto dto){
return wmNewsService.downOrUp(dto);
}
//------------------上面这四个是之前的
@PostMapping("/list_vo")
public ResponseResult findList(@RequestBody NewsAuthDto dto){
return wmNewsService.findList(dto);
}
@GetMapping("/one_vo/{id}")
public ResponseResult findWmNewsVo(@PathVariable("id") Integer id){
return wmNewsService.findWmNewsVo(id);
}
@PostMapping("/auth_pass")
public ResponseResult authPass(@RequestBody NewsAuthDto dto){
return wmNewsService.updateStatus(WemediaConstants.WM_NEWS_AUTH_PASS,dto);
}
@PostMapping("/auth_fail")
public ResponseResult authFail(@RequestBody NewsAuthDto dto){
return wmNewsService.updateStatus(WemediaConstants.WM_NEWS_AUTH_FAIL,dto);
}
}