1. 安装elasticsearch+ik分词器插件
sh
sudo wget https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.13.4.zip
sudo mkdir -p ./es_plugins/analysis-ik
sudo mkdir ./es_data
sudo unzip elasticsearch-analysis-ik-8.13.4.zip -d ./es_plugins/analysis-ik
sudo chown liber:liber es_data/ es_plugins/
1.1 docker-compose
sh
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=true
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
mem_limit: 1g
ports:
- "9200:9200"
- "9300:9300"
networks:
- esnet
volumes:
- ./es_data:/usr/share/elasticsearch/data
- ./es_plugins:/usr/share/elasticsearch/plugins
networks:
esnet:
启动容器命令:
docker-compose up -d
1.2 修改密码
进入容器:
docker exec -it elasticsearch bash
验证分词器的安装:
sh
bin/elasticsearch-plugin list
手动更改密码 : 使用 Elasticsearch 提供的 elasticsearch-reset-password
工具重置 elastic
用户的密码:
bin/elasticsearch-reset-password -u elastic
1.3 可能会用到的指令
删除索引:
curl -u elastic:dxOHCIBIWu+2djY6qtF1 -X DELETE "http://127.0.0.1:9200/articles"
测试ik分词器:
curl -X GET "http://127.0.0.1:9200/_analyze" -u elastic:dxOHCIBIWu+2djY6qtF1 -H 'Content-Type: application/json' -d'
{
"analyzer": "ik_max_word",
"text": "Spring Boot 使用入门"
}'
说明:dxOHCIBIWu+2djY6qtF1是密码。
2. 项目结构
2.1 完整的项目
说明:全局控制版本的依赖可以参考上一篇文章,本文只介绍artice-service模块。
2.2 artice-service模块
3. 数据库操作
sql
create database if not exists blog;
use blog;
CREATE TABLE if not exists users
(
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 用户的唯一标识符,自动递增的主键
email VARCHAR(100) NOT NULL UNIQUE, -- 电子邮件,不能为空且唯一,长度限制为100个字符
username VARCHAR(12) NOT NULL UNIQUE, -- 用户名,不能为空且唯一,长度限制为12个字符
password VARCHAR(255) NOT NULL, -- 用户密码,不能为空,存储为加密后的字符串
name VARCHAR(50), -- 用户显示名称,非必填,长度限制为50个字符
avatar_url VARCHAR(255), -- 用户头像的URL或文件路径,非必填,长度限制为255个字符
role VARCHAR(20) NOT NULL DEFAULT 'USER', -- 用户角色,不能为空,默认值为 'USER'
enabled BOOLEAN NOT NULL DEFAULT TRUE, -- 账户启用状态,不能为空,默认值为TRUE(启用)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间,默认值为当前时间戳
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 记录最后更新时间,自动更新为当前时间戳
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
CREATE TABLE articles
(
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 主键,自增ID
title VARCHAR(255) NOT NULL, -- 文章标题,非空
content TEXT NOT NULL, -- 文章内容 (Markdown格式),非空
author VARCHAR(100) NOT NULL, -- 作者名称,非空
user_id BIGINT NOT NULL, -- 关联用户ID,外键
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间,默认当前时间
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间,默认当前时间,更新时自动更新
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE -- 外键约束,用户删除时级联删除文章
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci; -- 使用utf8mb4字符集和Unicode排序规则
INSERT INTO articles (title, content, author, user_id)
VALUES ('Spring Boot 入门教程', '本文介绍如何使用 Spring Boot 快速创建一个应用程序。', '张三', 1),
('MyBatis 使用指南', '这篇文章将讲解 MyBatis 的基本使用方法。', '李四', 1),
('Elasticsearch 搜索引擎', '深入了解 Elasticsearch 的功能和用法。', '王五', 1),
('Java 并发编程', '本文详细讨论了 Java 中的并发编程技术。', '赵六', 1),
('Docker 容器化技术', '这篇文章介绍了如何使用 Docker 进行应用容器化。', '孙七', 1),
('微服务架构设计', '讨论了微服务架构的设计原则与实践。', '周八', 1),
('Redis 数据库应用', '本文详细介绍了 Redis 的常用操作与应用场景。', '吴九', 1),
('Spring Cloud 微服务', '使用 Spring Cloud 构建分布式系统。', '郑十', 1),
('RabbitMQ 消息队列', '探讨了 RabbitMQ 在消息队列中的应用。', '张三', 1),
('Kubernetes 集群管理', '介绍 Kubernetes 的基本概念与使用。', '李四', 1),
('RESTful API 设计', '探讨如何设计和实现 RESTful API。', '王五', 1),
('CI/CD 持续集成与部署', '本文介绍了 CI/CD 的概念和工具。', '赵六', 1),
('分布式系统概念', '讨论了分布式系统的核心概念。', '孙七', 1),
('大数据处理技术', '探讨了 Hadoop 和 Spark 在大数据处理中的应用。', '周八', 1),
('NoSQL 数据库应用', '介绍了 NoSQL 数据库的特点与应用场景。', '吴九', 1),
('前端开发框架 React', '详细介绍了 React 的基本概念与用法。', '郑十', 1),
('Vue.js 框架使用', '讨论了如何使用 Vue.js 构建用户界面。', '张三', 1),
('Angular 开发指南', '本文讲解了如何使用 Angular 进行开发。', '李四', 1),
('Git 版本控制系统', '介绍了 Git 的基本操作与分支管理。', '王五', 1)
('Agile 敏捷开发实践', '探讨了敏捷开发的原则与实践。', '赵六', 1);
4. pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>info.liberx</groupId>
<artifactId>blog</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>article-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
</project>
5. application.yml
yaml
spring:
application:
name: article-service
datasource:
url: jdbc:mysql://localhost:3306/blog?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
port: 6379
host: 192.168.186.77
password: 123456
timeout: 10000
jackson:
serialization:
write-dates-as-timestamps: false
elasticsearch:
url: 192.168.186.77 # Elasticsearch 地址
port: 9200
username: elastic # 如果有设置认证,提供用户名
password: dxOHCIBIWu+2djY6qtF1 # 如果有设置认证,提供密码
mybatis:
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: true
fetch-registry: true
server:
port: 8002
说明:Eurka中心,Redis在本案例暂时没有演示,需要Eurka中心是为了注册到服务中心通过Gateway网关进行转发,读者可以自行去掉一些不必要的依赖项还有配置项。
6. ElasticsearchConfig.java
java
package info.liberx.articleservice.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticsearchConfig {
@Value("${elasticsearch.url}")
private String url;
@Value("${elasticsearch.username}")
private String username;
@Value("${elasticsearch.password}")
private String password;
@Value("${elasticsearch.port}")
private int port;
@Bean
public ElasticsearchClient elasticsearchClient() {
// 配置 Jackson 的 ObjectMapper 并注册 JavaTimeModule
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
// 创建自定义的 JacksonJsonpMapper
JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper(objectMapper);
// 配置身份验证
final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));
// 配置 RestClient
RestClientBuilder builder = RestClient.builder(new HttpHost(url, port))
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
RestClient restClient = builder.build();
// 使用 RestClientTransport 创建 ElasticsearchClient
RestClientTransport transport = new RestClientTransport(
restClient, jsonpMapper);
return new ElasticsearchClient(transport);
}
}
说明:注册了一个 JavaTimeModule,它使得 Jackson 能够正确处理 Java 8 的日期和时间 API。
7. ArticleController.java
java
package info.liberx.articleservice.controller;
import com.github.pagehelper.PageInfo;
import info.liberx.articleservice.model.Article;
import info.liberx.articleservice.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@RestController
@RequestMapping("/articles")
public class ArticleController {
private final ArticleService articleService;
@Autowired
public ArticleController(ArticleService articleService) {
this.articleService = articleService;
}
// 搜索文章
@GetMapping("/search")
public ResponseEntity<Page<Article>> searchArticles(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
try {
Page<Article> articles = articleService.searchArticles(keyword, pageable);
return new ResponseEntity<>(articles, HttpStatus.OK);
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 创建新文章
@PostMapping
public ResponseEntity<Article> createArticle(@RequestBody Article article) {
int result = articleService.createArticle(article);
if (result > 0) {
return new ResponseEntity<>(article, HttpStatus.CREATED);
} else {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
// 根据文章ID获取文章
@GetMapping("/{id}")
public ResponseEntity<Article> getArticleById(@PathVariable Long id) {
Article article = articleService.getArticleById(id);
if (article != null) {
return new ResponseEntity<>(article, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// 更新文章
@PutMapping("/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable Long id, @RequestBody Article article) {
article.setId(id); // 确保更新的是指定ID的文章
int result = articleService.updateArticle(article);
if (result > 0) {
return new ResponseEntity<>(article, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
// 删除文章
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteArticle(@PathVariable Long id) {
int result = articleService.deleteArticle(id);
if (result > 0) {
return new ResponseEntity<>("文章删除成功", HttpStatus.OK);
} else {
return new ResponseEntity<>("文章删除失败", HttpStatus.NOT_FOUND);
}
}
// 获取所有文章
// 获取所有文章,支持分页
@GetMapping("/get/{userid}")
public ResponseEntity<PageInfo<Article>> getAllArticles(
@PathVariable Long userid,
@RequestParam(defaultValue = "1") int page, // 注意:PageHelper 的页码从1开始
@RequestParam(defaultValue = "10") int size) {
// 获取分页结果
PageInfo<Article> articles = articleService.getArticlesByUserId(userid, page, size);
return new ResponseEntity<>(articles, HttpStatus.OK);
}
}
8. ArticleMapper.java
java
package info.liberx.articleservice.mapper;
import info.liberx.articleservice.model.Article;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface ArticleMapper {
@Insert("INSERT INTO articles(title, content, author, user_id) VALUES(#{title}, #{content}, #{author}, #{userId})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertArticle(Article article);
@Select("SELECT * FROM articles WHERE id = #{id}")
Article findArticleById(Long id);
@Select("SELECT * FROM articles WHERE user_id = #{userId}")
List<Article> findArticlesByUserId(Long userId);
@Update("UPDATE articles SET title = #{title}, content = #{content}, author = #{author} WHERE id = #{id}")
int updateArticle(Article article);
@Delete("DELETE FROM articles WHERE id = #{id}")
int deleteArticle(Long id);
@Select("SELECT * FROM articles")
List<Article> findAllArticles();
}
9. Article.java
java
package info.liberx.articleservice.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.time.OffsetDateTime;
@Data
@Document(indexName = "articles") // 定义为 Elasticsearch 索引
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略未识别的字
public class Article {
@Id // 标记为 Elasticsearch 的文档ID
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String content; // Markdown 格式内容
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String author; // 使用 IK 分词器进行分词和搜索
@Field(type = FieldType.Long)
private Long userId; // 关联的用户ID
@Field(type = FieldType.Date, format = {}, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private OffsetDateTime createdAt;
@Field(type = FieldType.Date, format = {}, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
private OffsetDateTime updatedAt;
}
10. ArticleRepository.java
java
package info.liberx.articleservice.repository;
import info.liberx.articleservice.model.Article;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ArticleRepository extends ElasticsearchRepository<Article, Long> {
}
11. ArticleService.java
java
package info.liberx.articleservice.service;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import info.liberx.articleservice.mapper.ArticleMapper;
import info.liberx.articleservice.model.Article;
import info.liberx.articleservice.repository.ArticleRepository;
import jakarta.annotation.PostConstruct;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class ArticleService {
private final ArticleMapper articleMapper;
private final ArticleRepository articleRepository;
private final ElasticsearchClient elasticsearchClient;
public ArticleService(ElasticsearchClient elasticsearchClient, ArticleMapper articleMapper, ArticleRepository articleRepository) {
this.elasticsearchClient = elasticsearchClient;
this.articleMapper = articleMapper;
this.articleRepository = articleRepository;
}
@PostConstruct
// 同步数据库中的所有文章到 Elasticsearch
public void indexAllArticles() {
List<Article> articles = articleMapper.findAllArticles();
articleRepository.saveAll(articles);
}
public int createArticle(Article article) {
int result = articleMapper.insertArticle(article);
if (result > 0) {
articleRepository.save(article);
}
return result;
}
public Article getArticleById(Long id) {
return articleMapper.findArticleById(id);
}
// 分页查询文章
public PageInfo<Article> getArticlesByUserId(Long userId, int page, int size) {
// 使用 PageHelper 设置分页参数
PageHelper.startPage(page, size);
// 查询结果
List<Article> articles = articleMapper.findArticlesByUserId(userId);
// 包装查询结果为 PageInfo 对象
return new PageInfo<>(articles);
}
public int updateArticle(Article article) {
int result = articleMapper.updateArticle(article);
if (result > 0) {
articleRepository.save(article);
}
return result;
}
public int deleteArticle(Long id) {
int result = articleMapper.deleteArticle(id);
if (result > 0) {
articleRepository.deleteById(id);
}
return result;
}
public Page<Article> searchArticles(String keyword, Pageable pageable) throws IOException {
// 构建布尔查询,首先匹配完全符合的记录,然后匹配部分符合的记录
Query query = QueryBuilders.bool()
.should(QueryBuilders.multiMatch()
.fields("title", "content", "author")
.query(keyword)
.type(co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType.Phrase) // 完全匹配
.boost(2.0f) // 提高完全匹配的权重
.build()._toQuery())
.should(QueryBuilders.multiMatch()
.fields("title", "content", "author")
.query(keyword)
.build()._toQuery()) // 部分匹配
.build()._toQuery();
// 构建搜索请求,设置索引、查询条件、高亮设置、分页参数
SearchRequest searchRequest = new SearchRequest.Builder()
.index("articles")
.query(query)
.highlight(h -> h
.fields("title", f -> f.preTags("<strong>").postTags("</strong>"))
.fields("content", f -> f.preTags("<strong>").postTags("</strong>"))
.fields("author", f -> f.preTags("<strong>").postTags("</strong>"))
)
.from((int) pageable.getOffset())
.size(pageable.getPageSize())
.build();
// 执行搜索请求
SearchResponse<Article> searchResponse = elasticsearchClient.search(searchRequest, Article.class);
HitsMetadata<Article> hitsMetadata = searchResponse.hits();
// 处理搜索结果,提取高亮信息并设置到对应的Article对象中
List<Article> articles = hitsMetadata.hits().stream()
.map(hit -> {
Article article = hit.source();
if (article != null && hit.highlight() != null) {
hit.highlight().forEach((field, highlights) -> {
switch (field) {
case "title":
article.setTitle(String.join(" ", highlights));
break;
case "content":
article.setContent(String.join(" ", highlights));
break;
case "author":
article.setAuthor(String.join(" ", highlights));
break;
}
});
}
return article;
}).collect(Collectors.toList());
// 返回包含分页和高亮结果的Page对象
return new PageImpl<>(articles, pageable, Objects.requireNonNull(hitsMetadata.total()).value());
}
}
12. ArticleServiceApplication.java
java
package info.liberx.articleservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
@SpringBootApplication(exclude = {LoadBalancerAutoConfiguration.class, LoadBalancerBeanPostProcessorAutoConfiguration.class})
@EnableDiscoveryClient
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
public class ArticleServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ArticleServiceApplication.class, args);
}
}
13. 测试接口(postman)
13.1 测试创建文章 (POST /articles)
POST http://localhost:8002/articles
Body:
选择 raw
-> JSON
格式,示例如下:
json
{
"title": "Spring Boot 保姆级教程",
"content": "本文介绍如何使用 Spring Boot 快速创建一个应用程序。",
"author": "张三",
"userId": 1
}
13.2 测试根据文章 ID 获取文章 (GET /articles/{id})
GET http://localhost:8002/articles/1
13.3 测试更新文章 (PUT /articles/{id})
PUT http://localhost:8002/articles/1
Body:
选择 raw
-> JSON
格式:
json
{
"title": "Spring Boot 入门教程 - 更新",
"content": "本文详细介绍了如何使用 Spring Boot 创建应用程序,并提供更新。",
"author": "张三",
"userId": 1
}
13.4 测试删除文章 (DELETE /articles/{id})
DELETE http://localhost:8002/articles/1
13.5 测试搜索文章 (GET /articles/search)
GET http://localhost:8002/articles/search?keyword=Spring&page=0&size=10
13.6 测试获取某用户的所有文章并分页 (GET /articles/get/{userid})
GET http://localhost:8002/articles/get/1?page=1&size=10