做網(wǎng)站一般什么配置超級(jí)外鏈自動(dòng)發(fā)布工具
文章目錄
- 今日內(nèi)容
- 1 搭建es環(huán)境
- 1.1 拉取es鏡像
- 1.2 創(chuàng)建容器
- 1.3 配置中文分詞器ik
- 1.4 測(cè)試
- 2 app文章搜索
- 2.1 需求說明
- 2.2 思路分析
- 2.3 創(chuàng)建索引和映射
- 2.3.1 PUT請(qǐng)求添加映射
- 2.3.2 其他操作
- 2.4 初始化索引庫(kù)數(shù)據(jù)
- 2.4.1 導(dǎo)入es-init
- 2.4.2 es-init配置
- 2.4.3 導(dǎo)入數(shù)據(jù)
- 2.4.4 查詢已導(dǎo)入的文檔
- 2.5 接口定義
- 2.5.1 UserSearchDto
- 2.6 app端文章搜索項(xiàng)目準(zhǔn)備
- 2.6.1 導(dǎo)入heima-leadnews-search
- 2.6.2 導(dǎo)入依賴
- 2.6.3 導(dǎo)入配置
- 2.7 實(shí)現(xiàn)app端文章搜索
- 2.7.1 Controller接口定義
- 2.7.2 業(yè)務(wù)層
- 2.7.3 為app端文章搜索添加網(wǎng)關(guān)
- 2.7.3 測(cè)試
- 3 新增文章創(chuàng)建索引
- 3.1 思路分析
- 3.2 SearchArticleVo
- 3.3 創(chuàng)建kafka的topic
- 3.3 修改文章微服務(wù)配置
- 3.4 靜態(tài)文件路徑生成后文章生產(chǎn)通知
- 3.5 搜索微服務(wù)監(jiān)聽消息
- 3.5.1 修改搜索微服務(wù)配置
- 3.5.2 定義監(jiān)聽消息
- 3.6 綜合測(cè)試
- 4 app端搜索-保存搜索記錄
- 4.1 需求說明
- 4.2 安裝mongoDB
- 4.2.1 拉取鏡像
- 4.2.2 創(chuàng)建容器
- 4.2.3 本地連接mongodb
- 4.3 SpringBoot集成mongoDB
- 4.3.1 導(dǎo)入資料中的mongo-demo
- 4.3.2 導(dǎo)入依賴
- 4.3.3. 配置mongoDB
- 4.3.4 添加表映射
- 4.3.5 核心方法
- 4.3.5.1 保存
- 4.3.5.2 查詢
- 4.3.5.3 條件查詢
- 4.3.5.4 刪除
- 4.4 保存搜索記錄
- 4.4.1 實(shí)現(xiàn)思路
- 4.4.2 為搜索微服務(wù)添加mongoDB
- 4.4.3 在nacos中配置mongoDB
- 4.4.4 運(yùn)行sql腳本
- 4.4.5 導(dǎo)入對(duì)應(yīng)的表的實(shí)體類
- 4.4.6 Service
- 4.4.7 過濾器解析token獲取id放入頭部,攔截器將id存入線程
- 4.4.8 Search微服務(wù)也采用這樣的方法
- 4.4.9 異步調(diào)用保存搜索記錄
- 4.5 測(cè)試
- 5 app端搜索-加載搜索歷史
- 5.1 接口
- 5.2 Controller
- 5.3 Service
- 5.4 測(cè)試
- 6 app端搜索-刪除搜索歷史
- 6.1 接口
- 6.2 Dto
- 6.3 Controller
- 6.4 Service
- 6.5 測(cè)試
- 7 app端搜索-關(guān)鍵字聯(lián)想功能
- 7.1 需求分析
- 7.2 接口定義
- 7.3 自動(dòng)補(bǔ)全py插件
- 7.4 自定義分詞器
今日內(nèi)容
1 搭建es環(huán)境
1.1 拉取es鏡像
docker pull elasticsearch:7.4.0
1.2 創(chuàng)建容器
docker run -id \
--name es -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
1.3 配置中文分詞器ik
把資料中的elasticsearch-analysis-ik-7.4.0.zip上傳到服務(wù)器上,放到對(duì)應(yīng)目錄(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
并且重啟當(dāng)前es容器
docker restart es
1.4 測(cè)試
發(fā)送post請(qǐng)求 192.168.204.129:9200/_analyze
{"analyzer":"ik_max_word","text":"歡迎來到黑馬學(xué)習(xí)java"
}
返回
{"tokens": [{"token": "歡迎","start_offset": 0,"end_offset": 2,"type": "CN_WORD","position": 0},{"token": "迎來","start_offset": 1,"end_offset": 3,"type": "CN_WORD","position": 1},{"token": "來到","start_offset": 2,"end_offset": 4,"type": "CN_WORD","position": 2},{"token": "黑馬","start_offset": 4,"end_offset": 6,"type": "CN_WORD","position": 3},{"token": "學(xué)習(xí)","start_offset": 6,"end_offset": 8,"type": "CN_WORD","position": 4},{"token": "java","start_offset": 8,"end_offset": 12,"type": "ENGLISH","position": 5}]
}
2 app文章搜索
2.1 需求說明
2.2 思路分析
2.3 創(chuàng)建索引和映射
2.3.1 PUT請(qǐng)求添加映射
PUT請(qǐng)求:192.168.204.129:9200/app_info_article
請(qǐng)求體設(shè)置映射
{"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"}}}
}
返回:
{"acknowledged": true,"shards_acknowledged": true,"index": "app_info_article"
}
2.3.2 其他操作
GET請(qǐng)求查詢映射:192.168.204.129:9200/app_info_article
{"app_info_article": {"aliases": {},"mappings": {"properties": {"authorId": {"type": "long"},"authorName": {"type": "text"},"content": {"type": "text","analyzer": "ik_smart"},"id": {"type": "long"},"images": {"type": "keyword","index": false},"layout": {"type": "integer"},"publishTime": {"type": "date"},"staticUrl": {"type": "keyword","index": false},"title": {"type": "text","analyzer": "ik_smart"}}},"settings": {"index": {"creation_date": "1712574364409","number_of_shards": "1","number_of_replicas": "1","uuid": "IPlVoSqUSOm5dRBfJmaV_A","version": {"created": "7040099"},"provided_name": "app_info_article"}}}
}
DELETE請(qǐng)求,刪除索引及映射:192.168.204.129:9200/app_info_article
GET請(qǐng)求,查詢所有文檔:192.168.204.129:9200/app_info_article/_search
2.4 初始化索引庫(kù)數(shù)據(jù)
2.4.1 導(dǎo)入es-init
將es-init導(dǎo)入heima-leadnews-testk模塊中
將其引入heima-leadnews-testk模塊的pom文件中
<modules><module>freemarker-demo</module><module>minio-demo</module><module>tess4j-demo</module><module>kafka-demo</module><module>es-init</module>
</modules>
2.4.2 es-init配置
通過配置類進(jìn)行RestHighLevelClient的初始化
2.4.3 導(dǎo)入數(shù)據(jù)
在com.heima.es.ApArticleTest中編寫測(cè)試方法導(dǎo)入數(shù)據(jù)
先創(chuàng)建BulkRequest,在把一條條數(shù)據(jù)組成IndexRequest再放到BulkRequest中,在用RestHighLevelClient的bulk方法批量添加
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApArticleTest {@Autowiredprivate ApArticleMapper apArticleMapper;@Autowiredprivate RestHighLevelClient restHighLevelClient;/*** 注意:數(shù)據(jù)量的導(dǎo)入,如果數(shù)據(jù)量過大,需要分頁(yè)導(dǎo)入* @throws Exception*/@Testpublic void init() throws Exception {//1. 查詢文章列表List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();//2. 將數(shù)據(jù)導(dǎo)入到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);//添加到批量請(qǐng)求中bulkRequest.add(indexRequest);}restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);}
}
2.4.4 查詢已導(dǎo)入的文檔
GET請(qǐng)求,查詢所有文檔:192.168.204.129:9200/app_info_article/_search,有23條數(shù)據(jù)
2.5 接口定義
2.5.1 UserSearchDto
@Data
public class UserSearchDto {/*** 搜索關(guān)鍵字*/String searchWords;/*** 當(dāng)前頁(yè)*/int pageNum;/*** 分頁(yè)條數(shù)*/int pageSize;/*** 最小時(shí)間*/Date minBehotTime;public int getFromIndex(){if(this.pageNum<1)return 0;if(this.pageSize<1) this.pageSize = 10;return this.pageSize * (pageNum-1);}
}
2.6 app端文章搜索項(xiàng)目準(zhǔn)備
2.6.1 導(dǎo)入heima-leadnews-search
導(dǎo)入heima-leadnews-search到heima-leadnews-service中
引入heima-leadnews-search到pom文件中
2.6.2 導(dǎo)入依賴
在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>
2.6.3 導(dǎo)入配置
server:port: 51804
spring:application:name: leadnews-searchcloud:nacos:discovery:server-addr: 192.168.204.129:8848config:server-addr: 192.168.204.129:8848file-extension: yml
這里并沒有數(shù)據(jù)庫(kù)相關(guān)配置,需要在nacos配置,在nacos中創(chuàng)建leadnews-search
因?yàn)闀簳r(shí)不需要數(shù)據(jù)庫(kù),所以取消掉DataSourceAutoConfiguration自動(dòng)配置類
spring:autoconfigure:exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:host: 192.168.204.129port: 9200
minio:accessKey: miniosecretKey: minio123bucket: leadnewsendpoint: http://192.168.204.129:9000readPath: http://192.168.204.129:9000
2.7 實(shí)現(xiàn)app端文章搜索
2.7.1 Controller接口定義
創(chuàng)建com.heima.search.controller.v1.ArticleSearchController
@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {@Autowiredprivate ArticleSearchService articleSearchService;@PostMapping("/search")public ResponseResult search(@RequestBody UserSearchDto userSearchDto) {return articleSearchService.search(userSearchDto);}
}
2.7.2 業(yè)務(wù)層
創(chuàng)建com.heima.search.service.ArticleSearchService接口
public interface ArticleSearchService {ResponseResult search(UserSearchDto userSearchDto);
}
實(shí)現(xiàn)
@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {@Autowiredprivate RestHighLevelClient restHighLevelClient;@Overridepublic ResponseResult search(UserSearchDto userSearchDto) {//1. 檢查參數(shù)if(userSearchDto == null|| StringUtils.isBlank(userSearchDto.getSearchWords())){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"參數(shù)不合法");}//2. 設(shè)置查詢條件SearchRequest searchRequest = new SearchRequest("app_info_article");SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();//復(fù)合查詢需要使用boolQueryBoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//2.1 關(guān)鍵詞的分詞之查詢QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(userSearchDto.getSearchWords()).field("title").field("content").defaultOperator(Operator.OR);boolQueryBuilder.must(queryStringQueryBuilder);//2.2 查詢小于mindate的數(shù)據(jù)RangeQueryBuilder publishTime = QueryBuilders.rangeQuery("publishTime").lt(userSearchDto.getMinBehotTime().getTime());boolQueryBuilder.filter(publishTime);//2.3 分頁(yè)查詢searchSourceBuilder.from(0).size(userSearchDto.getPageSize());//2.4 按照時(shí)間倒序排序searchSourceBuilder.sort("publishTime", SortOrder.DESC);//2.5 設(shè)置高亮顯示titleHighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field("title");highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");highlightBuilder.postTags("</font>");searchSourceBuilder.highlighter(highlightBuilder);//2.6 查詢searchSourceBuilder.query(boolQueryBuilder);searchRequest.source(searchSourceBuilder);try {SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);//3. 結(jié)果封裝List<Map> list= new ArrayList<>();SearchHits searchHits= response.getHits();SearchHit[] hits = searchHits.getHits();for (SearchHit hit : hits) {//3.1 獲取sourceString json= hit.getSourceAsString();Map map = JSON.parseObject(json, Map.class);//3.2 獲取高亮數(shù)據(jù)Map<String, HighlightField> highlightFields = hit.getHighlightFields();if(!CollectionUtils.isEmpty(highlightFields)){HighlightField highlightField = highlightFields.get("title");if(highlightField!=null){//3.3 拿到所有高亮數(shù)據(jù)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);}//4. 返回結(jié)果return ResponseResult.okResult(list);} catch (IOException e) {throw new RuntimeException(e);}}
}
2.7.3 為app端文章搜索添加網(wǎng)關(guān)
需要在app的網(wǎng)關(guān)中添加搜索微服務(wù)的路由配置
#搜索微服務(wù)
- id: leadnews-searchuri: lb://leadnews-searchpredicates:- Path=/search/**filters:- StripPrefix= 1
spring:cloud:gateway:globalcors:add-to-simple-url-handler-mapping: truecorsConfigurations:'[/**]':allowedHeaders: "*"allowedOrigins: "*"allowedMethods:- GET- POST- DELETE- PUT- OPTIONroutes:# 用戶管理- id: useruri: lb://leadnews-userpredicates:- Path=/user/**filters:- StripPrefix= 1# 文章管理- id: articleuri: lb://leadnews-articlepredicates:- Path=/article/**filters:- StripPrefix= 1#搜索微服務(wù)- id: leadnews-searchuri: lb://leadnews-searchpredicates:- Path=/search/**filters:- StripPrefix= 1
2.7.3 測(cè)試
啟動(dòng)對(duì)應(yīng)微服務(wù)
打開 localhost:8801
數(shù)據(jù)顯示沒有問題,測(cè)試成功
3 新增文章創(chuàng)建索引
3.1 思路分析
3.2 SearchArticleVo
文章問微服務(wù)需要組裝SearchArticleVo給搜索微服務(wù)
所以需要先定義SearchArticleVo,創(chuàng)建com.heima.model.search.SearchArticleVo類
@Data
public class SearchArticleVo {// 文章idprivate Long id;// 文章標(biāo)題private String title;// 文章發(fā)布時(shí)間private Date publishTime;// 文章布局private Integer layout;// 封面private String images;// 作者idprivate Long authorId;// 作者名詞private String authorName;//靜態(tài)urlprivate String staticUrl;//文章內(nèi)容private String content;
}
3.3 創(chuàng)建kafka的topic
在com.heima.common.constants.ArticleConstants類中創(chuàng)建新的屬性
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_INDEX_TOPIC = "article.es.syn.topic";}
3.3 修改文章微服務(wù)配置
因?yàn)槲恼挛⒎?wù)相當(dāng)于生產(chǎn)者,所以要更新文章微服務(wù)的nacos配置
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=falseusername: rootpassword: 123sjbsjbkafka:bootstrap-servers: 192.168.204.129:9092producer:retries: 10key-serializer: org.apache.kafka.common.serialization.StringSerializervalue-serializer: org.apache.kafka.common.serialization.StringSerializerconsumer:group-id: ${spring.application.name}key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.apache.kafka.common.serialization.StringDeserializer# 設(shè)置Mapper接口所對(duì)應(yīng)的XML文件位置,如果你在Mapper接口中有自定義方法,需要進(jìn)行該配置
mybatis-plus:mapper-locations: classpath*:mapper/*.xml# 設(shè)置別名包掃描路徑,通過該屬性可以給包中的類注冊(cè)別名type-aliases-package: com.heima.model.article.pojosglobal-config:datacenter-id: 1workerId: 1
minio:accessKey: miniosecretKey: minio123bucket: leadnewsendpoint: http://192.168.204.129:9000readPath: http://192.168.204.129:9000
3.4 靜態(tài)文件路徑生成后文章生產(chǎn)通知
在com.heima.article.service.impl.ArticleFreemarkerServiceImpl類中
最后生成
//4.把靜態(tài)頁(yè)面的路徑保存到數(shù)據(jù)庫(kù)
apArticleService.update(Wrappers.<ApArticle>lambdaUpdate().eq(ApArticle::getId,apArticle.getId()).set(ApArticle::getStaticUrl,path));//5. 發(fā)送消息到kafka,創(chuàng)建es索引
createArticleESIndex(apArticle,content,path);
創(chuàng)建es索引
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;/*** 創(chuàng)建文章索引* @param apArticle* @param content* @param path*/
private void createArticleESIndex(ApArticle apArticle, String content, String path) {SearchArticleVo searchArticleVo = new SearchArticleVo();BeanUtils.copyProperties(apArticle,searchArticleVo);searchArticleVo.setContent(content);searchArticleVo.setStaticUrl(path);kafkaTemplate.send(ArticleConstants.ARTICLE_ES_INDEX_TOPIC, JSON.toJSONString(searchArticleVo));
}
3.5 搜索微服務(wù)監(jiān)聽消息
3.5.1 修改搜索微服務(wù)配置
搜索微服務(wù)作為kafka的消費(fèi)者,進(jìn)行nacos配置
spring:autoconfigure:exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationkafka:bootstrap-servers: 192.168.204.129:9092consumer:group-id: ${spring.application.name}key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.apache.kafka.common.serialization.StringDeserializer
elasticsearch:host: 192.168.204.129port: 9200
minio:accessKey: miniosecretKey: minio123bucket: leadnewsendpoint: http://192.168.204.129:9000readPath: http://192.168.204.129:9000
3.5.2 定義監(jiān)聽消息
在heima-leadnews-search模塊中創(chuàng)建com.heima.search.listen.SyncArticleListener類
@Component
@Slf4j
public class SyncArticleListener {@Autowiredprivate RestHighLevelClient restHighLevelClient;@KafkaListener(topics = ArticleConstants.ARTICLE_ES_INDEX_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);}}}
}
3.6 綜合測(cè)試
啟動(dòng)es、kafka、redis、rabbitmq、minio、nacos、zookeeper
再啟動(dòng)相應(yīng)啟動(dòng)類
現(xiàn)在app端顯示
在自媒體端添加測(cè)試:黑馬測(cè)試搜索123
點(diǎn)擊發(fā)布,自動(dòng)審核成功
重新加載app端,此時(shí)審核已上架的文章已經(jīng)顯示
搜索欄中嘗試搜搜
成功搜索到
查看SearchApplication的日志顯示
2024-04-09 13:41:44.683 INFO 16292 --- [ntainer#0-0-C-1] c.h.search.listen.SyncArticleListener : SyncArticleListener,message={"authorId":1102,"authorName":"admin","content":"[{\"type\":\"image\",\"value\":\"http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg\"},{\"type\":\"text\",\"value\":\"請(qǐng)?jiān)谶@里輸入正文\"}]","id":1777572583391236097,"images":"http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg","publishTime":1712641152000,"staticUrl":"http://192.168.204.129:9000/leadnews/2024/04/09/1777572583391236097.html","title":"黑馬測(cè)試搜索123"}
說明我們的測(cè)試非常成功。
4 app端搜索-保存搜索記錄
4.1 需求說明
4.2 安裝mongoDB
4.2.1 拉取鏡像
docker pull mongo
4.2.2 創(chuàng)建容器
docker run -di --name mongo \
--restart=always \
-p 27017:27017 \
-v ~/data/mongodata:/data mongo
4.2.3 本地連接mongodb
使用navicat,,同時(shí)創(chuàng)建leadnews-history數(shù)據(jù)庫(kù)
4.3 SpringBoot集成mongoDB
4.3.1 導(dǎo)入資料中的mongo-demo
導(dǎo)入資料中的mongo-demo并將其導(dǎo)入test模塊中的pom文件里
4.3.2 導(dǎo)入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
4.3.3. 配置mongoDB
創(chuàng)建application.yaml
server:port: 9998
spring:data:mongodb:host: 192.168.204.129port: 27017database: leadnews-history
4.3.4 添加表映射
@Data
@Document("ap_associate_words")
public class ApAssociateWords implements Serializable {private static final long serialVersionUID = 1L;private String id;/*** 聯(lián)想詞*/private String associateWords;/*** 創(chuàng)建時(shí)間*/private Date createdTime;}
4.3.5 核心方法
com.itheima.mongo.test.MongoTest測(cè)試方法
@SpringBootTest(classes = MongoApplication.class)
@RunWith(SpringRunner.class)
public class MongoTest {@Autowiredprivate MongoTemplate mongoTemplate;//保存,會(huì)自動(dòng)創(chuàng)建相應(yīng)的表結(jié)構(gòu)@Testpublic void saveTest(){ApAssociateWords apAssociateWords = new ApAssociateWords();apAssociateWords.setAssociateWords("黑馬頭條");apAssociateWords.setCreatedTime(new Date());mongoTemplate.save(apAssociateWords);}//查詢一個(gè)@Testpublic void saveFindOne(){ApAssociateWords apAssociateWords = mongoTemplate.findById("6614e7011f52f0112ac7df19", ApAssociateWords.class);System.out.println(apAssociateWords);}//條件查詢@Testpublic 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);}//刪除@Testpublic void testDel(){mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("黑馬頭條")),ApAssociateWords.class);}
}
4.3.5.1 保存
//保存
@Test
public void saveTest(){ApAssociateWords apAssociateWords = new ApAssociateWords();apAssociateWords.setAssociateWords("黑馬頭條");apAssociateWords.setCreatedTime(new Date());mongoTemplate.save(apAssociateWords);
}
4.3.5.2 查詢
//查詢一個(gè)
@Test
public void saveFindOne(){ApAssociateWords apAssociateWords = mongoTemplate.findById("6614e7011f52f0112ac7df19", ApAssociateWords.class);System.out.println(apAssociateWords);
}
2024-04-09 15:04:51.338 INFO 13360 --- [ main] o.m.d.connection : Opened connection [connectionId{localValue:2, serverValue:16}] to 192.168.204.129:27017
[ApAssociateWords(id=6614e7011f52f0112ac7df19, associateWords=黑馬頭條, createdTime=Tue Apr 09 14:58:08 CST 2024)]
4.3.5.3 條件查詢
//條件查詢
@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);
}
2024-04-09 15:06:09.968 INFO 13672 --- [ main] o.m.d.connection : Opened connection [connectionId{localValue:2, serverValue:18}] to 192.168.204.129:27017
[ApAssociateWords(id=6614e7011f52f0112ac7df19, associateWords=黑馬頭條, createdTime=Tue Apr 09 14:58:08 CST 2024)]
4.3.5.4 刪除
//刪除
@Test
public void testDel(){mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("黑馬頭條")),ApAssociateWords.class);
}
4.4 保存搜索記錄
4.4.1 實(shí)現(xiàn)思路
4.4.2 為搜索微服務(wù)添加mongoDB
為heima-leadnews-search搜索微服務(wù)添加mongoDB
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
4.4.3 在nacos中配置mongoDB
spring:data:mongodb:host: 192.168.204.129port: 27017database: leadnews-historykafka:bootstrap-servers: 192.168.204.129:9092producer:retries: 10key-serializer: org.apache.kafka.common.serialization.StringSerializervalue-serializer: org.apache.kafka.common.serialization.StringSerializerconsumer:group-id: ${spring.application.name}key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.apache.kafka.common.serialization.StringDeserializerautoconfigure:exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:host: 192.168.204.129port: 9200
minio:accessKey: miniosecretKey: minio123bucket: leadnewsendpoint: http://192.168.204.129:9000readPath: http://192.168.204.129:9000
4.4.4 運(yùn)行sql腳本
4.4.5 導(dǎo)入對(duì)應(yīng)的表的實(shí)體類
@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;/*** 創(chuàng)建時(shí)間*/private Date createdTime;
}
@Data
@Document("ap_associate_words")
public class ApAssociateWords implements Serializable {private static final long serialVersionUID = 1L;private String id;/*** 聯(lián)想詞*/private String associateWords;/*** 創(chuàng)建時(shí)間*/private Date createdTime;
}
4.4.6 Service
接口
public interface ApUserSearchService {/*** 保存用戶搜索歷史記錄* @param keyword* @param userId*/void insert(String keyword,Integer userId);
}
實(shí)現(xiàn)
@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {@Autowiredprivate MongoTemplate mongoTemplate;@Overridepublic 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. 存在則更新時(shí)間if(apUserSearch != null) {apUserSearch.setCreatedTime(new Date());mongoTemplate.save(apUserSearch);return;}//3. 不存在則插入,判斷是否超過10條,超過則刪除最早的一條long count = mongoTemplate.count(Query.query(Criteria.where("userId").is(userId)), ApUserSearch.class);if(count >= 10) {Query query1 = Query.query(Criteria.where("userId").is(userId)).with(Sort.by(Sort.Order.asc("createdTime"))).limit(1);ApUserSearch apUserSearch1 = mongoTemplate.findOne(query1, ApUserSearch.class);mongoTemplate.remove(apUserSearch1);}else{ApUserSearch apUserSearch1 = new ApUserSearch();apUserSearch1.setUserId(userId);apUserSearch1.setKeyword(keyword);apUserSearch1.setCreatedTime(new Date());mongoTemplate.save(apUserSearch1);}}
}
4.4.7 過濾器解析token獲取id放入頭部,攔截器將id存入線程
針對(duì)每個(gè)用戶的保存歷史記錄,需要放在對(duì)應(yīng)的userid下
在gateway網(wǎng)關(guān)中通過過濾器fliter解析token獲取的jwt令牌獲取用戶id
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//1.獲取Request對(duì)象和Response對(duì)象ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();//2.判斷當(dāng)前請(qǐng)求是否為登錄請(qǐng)求,如果是,直接放行if (request.getURI().getPath().contains("/login")) {//放行return chain.filter(exchange);}//3.獲取當(dāng)前請(qǐng)求的token信息String token = request.getHeaders().getFirst("token");//4.判斷token是否存在if(StringUtils.isBlank(token)) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//5.判斷token是否有效//5.1 解析tokentry{Claims body = AppJwtUtil.getClaimsBody(token);//5.2 判斷token是否有效int result = AppJwtUtil.verifyToken(body);if(result == 1||result == 2) {//5.3 token過期response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//獲取用戶信息Integer userId = (Integer) body.get("id");//將用戶信息放入到header中ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {httpHeaders.add("userId", userId + "");}).build();//重置請(qǐng)求exchange.mutate().request(serverHttpRequest);}catch (Exception e) {e.printStackTrace();//5.4 token無效response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//6.放行return chain.filter(exchange);}
解析完token后,將其發(fā)在httpheaders的頭部,重置請(qǐng)求request
然后再通過攔截器解析request獲取userid,再存入線程中
public class WmTokenInterceptor implements HandlerInterceptor {/*** 攔截器的前置方法,得到header中的用戶信息,存入到當(dāng)前線程中* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String userId = request.getHeader("userId");if(userId != null){//存入當(dāng)前線程WmUser wmUser = new WmUser();wmUser.setId(Integer.valueOf(userId));WmThreadLocalUtil.setUser(wmUser);}return true;}/*** 后置方法,清除當(dāng)前線程中的用戶信息* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {WmThreadLocalUtil.clear();}
}
4.4.8 Search微服務(wù)也采用這樣的方法
在WmTokenInterceptor中存放userid的是WmThreadLocalUtil,search微服務(wù)也需要一個(gè)線程空間來存放userid,自然創(chuàng)建AppThreadLocalUtil,在heima-leadnews-utils模塊下創(chuàng)建com.heima.utils.thread.AppThreadLocalUtil類
public class AppThreadLocalUtil {private static final ThreadLocal<ApUser> APP_USER_THREAD_LOCAL = new ThreadLocal<>();public static void setUser(ApUser user) {APP_USER_THREAD_LOCAL.set(user);}public static ApUser getUser() {return APP_USER_THREAD_LOCAL.get();}public static void clear() {APP_USER_THREAD_LOCAL.remove();}
}
在heima-leadnews-search模塊中添加com.heima.search.interceptor.AppTokenInterceptor類
public class AppTokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String userId = request.getHeader("userId");if(userId != null){//存入當(dāng)前線程ApUser apUser = new ApUser();apUser.setId(Integer.valueOf(userId));AppThreadLocalUtil.setUser(apUser);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {AppThreadLocalUtil.clear();}
}
要想使攔截器AppTokenInterceptor生效,需要將攔截器載入攔截器注冊(cè)器里,在heima-leadnews-search模塊中添加com.heima.search.config.WebMvcConfig類
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new AppTokenInterceptor()).addPathPatterns("/**");}
}
將攔截器AppTokenInterceptor加入到攔截器注冊(cè)器里,攔截所有path
4.4.9 異步調(diào)用保存搜索記錄
在搜索之前就可以進(jìn)行保存了,所以在com.heima.search.service.impl.ArticleSearchServiceImpl檢查完參數(shù)之后就可以寫進(jìn)mongoDB里了,并且為apUserSearchService.insert(userSearchDto.getSearchWords(), AppThreadLocalUtil.getUser().getId());
添加異步調(diào)用方法@Async
userSearchDto.getFromIndex()==0是因?yàn)橹挥性诘谝豁?yè)的時(shí)候才保存,你都翻到第二頁(yè)了,不用再保存一次關(guān)鍵字了。
@Override
public ResponseResult search(UserSearchDto userSearchDto) {//1. 檢查參數(shù)if(userSearchDto == null|| StringUtils.isBlank(userSearchDto.getSearchWords())){return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"參數(shù)不合法");}ApUser user=AppThreadLocalUtil.getUser();//1.1 異步調(diào)用保存搜索記錄if(user!=null&& userSearchDto.getFromIndex()==0){apUserSearchService.insert(userSearchDto.getSearchWords(), AppThreadLocalUtil.getUser().getId());}//2. 設(shè)置查詢條件SearchRequest searchRequest = new SearchRequest("app_info_article");SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
為apUserSearchService.insert(userSearchDto.getSearchWords(), AppThreadLocalUtil.getUser().getId());
添加異步調(diào)用方法@Async
@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. 存在則更新時(shí)間
并且在com.heima.search.SearchApplication啟動(dòng)類中添加啟動(dòng)異步方法@EnableAsync
@SpringBootApplication
@EnableDiscoveryClient
@EnableAsync
public class SearchApplication {public static void main(String[] args) {SpringApplication.run(SearchApplication.class,args);}
}
4.5 測(cè)試
啟動(dòng)
搜索黑馬
查看mongoDB,說明插入成功
測(cè)試10個(gè)會(huì)不會(huì)刪除
需求滿足
5 app端搜索-加載搜索歷史
5.1 接口
5.2 Controller
創(chuàng)建com.heima.search.controller.v1.ApUserSearchController
@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController {@Autowiredprivate ApUserSearchService apUserSearchService;@PostMapping("/load")public ResponseResult load() {return apUserSearchService.load();}
}
5.3 Service
接口:
public interface ApUserSearchService {public void insert(String keyword, Integer userId);public ResponseResult load();}
實(shí)現(xiàn)
@Override
public ResponseResult load() {//1. 獲取當(dāng)前用戶ApUser user = AppThreadLocalUtil.getUser();if(user == null) {return ResponseResult.errorResult(400, "請(qǐng)先登錄");}//2. 查詢用戶搜索歷史記錄Query query = Query.query(Criteria.where("userId").is(user.getId())).with(Sort.by(Sort.Order.desc("createdTime")));List<ApUserSearch> apUserSearches = mongoTemplate.find(query, ApUserSearch.class);return ResponseResult.okResult(apUserSearches);}
5.4 測(cè)試
探花都在
6 app端搜索-刪除搜索歷史
6.1 接口
6.2 Dto
創(chuàng)建com.heima.model.search.dtos.HistorySearchDto類接收歷史記錄id
public class HistorySearchDto {/*** 接收歷史記錄id*/String id;
}
6.3 Controller
@PostMapping("/del")
public ResponseResult del(@RequestBody HistorySearchDto historySearchDto) {return apUserSearchService.deleteHistorySearch(historySearchDto);
}
6.4 Service
接口:
ResponseResult deleteHistorySearch(HistorySearchDto historySearchDto);
實(shí)現(xiàn):
@Override
public ResponseResult deleteHistorySearch(HistorySearchDto historySearchDto) {//1. 獲取當(dāng)前用戶ApUser user = AppThreadLocalUtil.getUser();if(user == null) {return ResponseResult.errorResult(400, "請(qǐng)先登錄");}//2. 刪除用戶搜索歷史記錄Query query = Query.query(Criteria.where("userId").is(user.getId()).and("id").is(historySearchDto.getId()));mongoTemplate.remove(query, ApUserSearch.class);return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
6.5 測(cè)試
探花7已經(jīng)沒有了
查看數(shù)據(jù)庫(kù)
永失我探花7,測(cè)試成功
7 app端搜索-關(guān)鍵字聯(lián)想功能
7.1 需求分析
es的補(bǔ)全使這個(gè)數(shù)據(jù)本身在es的數(shù)據(jù)庫(kù)里就有,才能補(bǔ)全,而這個(gè)是不管有沒有都能補(bǔ)全。
這是與es的補(bǔ)全不同的地方。
我覺得老師這個(gè)不好,因?yàn)镋S補(bǔ)全的是可以搜索到文章的,我要用ES實(shí)現(xiàn)。
7.2 接口定義
7.3 自動(dòng)補(bǔ)全py插件
要實(shí)現(xiàn)根據(jù)字母自動(dòng)補(bǔ)全,就必須對(duì)文章按照拼音分詞。在GitHub上恰好有es的拼音分詞插件,版本是7.4.0。
將py插件放到es的插件庫(kù)里
重啟es
docker restart es
測(cè)試
{"analyzer":"pinyin","text":"歡迎來到黑馬學(xué)習(xí)java"
}
響應(yīng)
{"tokens": [{"token": "huan","start_offset": 0,"end_offset": 0,"type": "word","position": 0},{"token": "hyldhmxxjava","start_offset": 0,"end_offset": 0,"type": "word","position": 0},{"token": "ying","start_offset": 0,"end_offset": 0,"type": "word","position": 1},{"token": "lai","start_offset": 0,"end_offset": 0,"type": "word","position": 2},{"token": "dao","start_offset": 0,"end_offset": 0,"type": "word","position": 3},{"token": "hei","start_offset": 0,"end_offset": 0,"type": "word","position": 4},{"token": "ma","start_offset": 0,"end_offset": 0,"type": "word","position": 5},{"token": "xue","start_offset": 0,"end_offset": 0,"type": "word","position": 6},{"token": "xi","start_offset": 0,"end_offset": 0,"type": "word","position": 7},{"token": "ja","start_offset": 0,"end_offset": 0,"type": "word","position": 8},{"token": "v","start_offset": 0,"end_offset": 0,"type": "word","position": 9},{"token": "a","start_offset": 0,"end_offset": 0,"type": "word","position": 10}]
}
7.4 自定義分詞器
刪除以前索引和文檔
創(chuàng)建自定義分詞器以及新的索引
{"settings": {"analysis": {"analyzer": { "my_analyzer": { "tokenizer": "ik_max_word","filter": "py"},"completion_analyzer": {"tokenizer": "keyword","filter": "py"}},"filter": {"py": { "type": "pinyin","keep_full_pinyin": false,"keep_joined_full_pinyin": true,"keep_original": true,"limit_first_letter_length": 16,"remove_duplicated_term": true,"none_chinese_pinyin_tokenize": false}}}},"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":"my_analyzer","search_analyzer":"ik_smart","copy_to": "all"},"content":{"type":"text","analyzer":"my_analyzer"},"suggestion":{"type":"completion","analyzer":"completion_analyzer" }}}
}
因?yàn)槎嗔藄uggestion字段,因此創(chuàng)建一個(gè)實(shí)體類接收com.heima.model.search.vos.SearchArticlewithSuggestion
@Data
public class SearchArticlewithSuggestion {// 文章idprivate Long id;// 文章標(biāo)題private String title;// 文章發(fā)布時(shí)間private Date publishTime;// 文章布局private Integer layout;// 封面private String images;// 作者idprivate Long authorId;// 作者名詞private String authorName;//靜態(tài)urlprivate String staticUrl;//文章內(nèi)容private String content;//建議private List<String> suggestion;SearchArticlewithSuggestion(SearchArticleVo vo){BeanUtils.copyProperties(vo,this);this.suggestion= Arrays.asList(this.title);}}
響應(yīng)
{"acknowledged": true,"shards_acknowledged": true,"index": "app_info_article"
}
導(dǎo)入數(shù)據(jù),創(chuàng)建新的單元測(cè)試
@Test
void testBatchInsertIndexDocument2() throws IOException {///1. 查詢文章列表List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();//2. 將數(shù)據(jù)導(dǎo)入到es中BulkRequest bulkRequest = new BulkRequest("app_info_article");for(SearchArticleVo searchArticleVo : searchArticleVos){SearchArticlewithSuggestion searchArticleVoWithSuggestion = new SearchArticlewithSuggestion(searchArticleVo);IndexRequest indexRequest = new IndexRequest().id(searchArticleVoWithSuggestion.getId().toString()).source(JSON.toJSONString(searchArticleVoWithSuggestion), XContentType.JSON);//添加到批量請(qǐng)求中bulkRequest.add(indexRequest);}restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
導(dǎo)入后查看數(shù)據(jù),成功有suggestion
測(cè)試自動(dòng)補(bǔ)全
{"suggest": {"title_suggest": {"text": "h", "completion": {"field": "suggestion", "skip_duplicates": false, "size": 10 }}}
}
{"took": 30,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 0,"relation": "eq"},"max_score": null,"hits": []},"suggest": {"title_suggest": [{"text": "h","offset": 0,"length": 1,"options": [{"text": "黃齡工作室發(fā)視頻回應(yīng)","_index": "app_info_article","_type": "_doc","_id": "1302977754114826241","_score": 1.0,"_source": {"authorId": 4,"authorName": "admin","content": "[{\"type\":\"text\",\"value\":\"3黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)\"},{\"type\":\"image\",\"value\":\"http://192.168.200.130/group1/M00/00/00/wKjIgl892vuAXr_MAASCMYD0yzc919.jpg\"},{\"type\":\"text\",\"value\":\"請(qǐng)?jiān)谶@里輸入正文黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)黃齡工作室發(fā)視頻回應(yīng)\"}]","id": 1302977754114826241,"images": "http://192.168.204.129:9000/leadnews/2024/03/25/71413acd3df847759c70b121ed526ff9.jpg","layout": 1,"publishTime": 1599489079000,"suggestion": ["黃齡工作室發(fā)視頻回應(yīng)"],"title": "黃齡工作室發(fā)視頻回應(yīng)"}},{"text": "黑馬測(cè)試搜索123","_index": "app_info_article","_type": "_doc","_id": "1777572583391236097","_score": 1.0,"_source": {"authorId": 1102,"authorName": "admin","content": "[{\"type\":\"image\",\"value\":\"http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg\"},{\"type\":\"text\",\"value\":\"請(qǐng)?jiān)谶@里輸入正文\"}]","id": 1777572583391236097,"images": "http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg","layout": 1,"publishTime": 1712641152000,"staticUrl": "http://192.168.204.129:9000/leadnews/2024/04/09/1777572583391236097.html","suggestion": ["黑馬測(cè)試搜索123"],"title": "黑馬測(cè)試搜索123"}}]}]}
}
只能匹配到第一個(gè)字。
分析:如果想要句中也能匹配,就要把completion_analyzer的tokenizer變成ik_max_word,保證都能覆蓋到,而不是keyword作為一個(gè)整體。
如果想要做成黑馬旅游那樣,就要改前端了,這不是本節(jié)課的重點(diǎn),我也就不進(jìn)行了。