feat: 实现API文档支持与系统优化

refactor(ArticleRepository): 修正@Param注解导入错误并优化查询方法
fix(ArticleService): 解决事务回滚问题并优化日志配置
feat(SecurityConfig): 添加Spring Security配置禁用默认认证
docs: 添加详细API文档README_API.md
feat(HelpController): 实现Markdown文档渲染API
style: 清理无用注释和导入
build: 更新pom.xml依赖和插件配置
chore: 优化application.properties配置
This commit is contained in:
qingfeng1121
2025-10-10 16:20:13 +08:00
parent fdb0608751
commit 60f4752124
14 changed files with 7792 additions and 39 deletions

View File

@@ -5,7 +5,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.qf.myafterprojecy.controller")
public class MyAfterProjecyApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,41 @@
package com.qf.myafterprojecy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
* Spring Security配置类
* 用于关闭默认的登录验证功能
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 配置安全过滤器链,允许所有请求通过
* @param http HttpSecurity对象用于配置HTTP安全策略
* @return 配置好的SecurityFilterChain对象
* @throws Exception 配置过程中可能出现的异常
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF保护对于API服务通常不需要
.csrf().disable()
// 允许所有请求通过,不需要认证
.authorizeRequests()
.anyRequest().permitAll()
.and()
// 禁用表单登录
.formLogin().disable()
// 禁用HTTP基本认证
.httpBasic().disable()
// 禁用会话管理对于无状态API服务
.sessionManagement().disable();
return http.build();
}
}

View File

@@ -12,30 +12,56 @@ import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
/**
* 文章控制器类处理文章相关的HTTP请求
* 提供文章的增删改查功能,以及按作者、分类和浏览量获取文章的接口
*/
@RestController
@RequestMapping("/api/articles")
@Validated
public class ArticleController {
@Autowired
private IArticleService articleService;
private IArticleService articleService; // 注入文章服务接口
/**
* 根据ID获取单个文章
* @param id 文章ID
* @return 返回包含文章信息的ResponseMessage对象
*/
@GetMapping("/{id}")
public ResponseMessage<Article> getArticle(@PathVariable Integer id) {
return articleService.getArticleById(id);
}
/**
* 获取所有文章列表
* @return 返回包含文章列表的ResponseMessage对象
*/
@GetMapping
public ResponseMessage<List<Article>> getAllArticles() {
return articleService.getAllArticles();
}
/**
* 创建新文章
* 仅限AUTHOR角色用户访问
* @param articleDto 包含文章数据的DTO对象
* @return 返回包含新创建文章信息的ResponseMessage对象
*/
@PostMapping
@PreAuthorize("hasRole('AUTHOR')")
public ResponseMessage<Article> createArticle(@Valid @RequestBody ArticleDto articleDto) {
return articleService.saveArticle(articleDto);
}
/**
* 更新现有文章
* 仅限AUTHOR或ADMIN角色用户访问
* @param id 要更新的文章ID
* @param articleDto 包含更新后文章数据的DTO对象
* @return 返回包含更新后文章信息的ResponseMessage对象
*/
@PutMapping("/{id}")
@PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')")
public ResponseMessage<Article> updateArticle(
@@ -44,22 +70,42 @@ public class ArticleController {
return articleService.updateArticle(id, articleDto);
}
/**
* 删除文章
* 仅限AUTHOR或ADMIN角色用户访问
* @param id 要删除的文章ID
* @return 返回包含被删除文章信息的ResponseMessage对象
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')")
public ResponseMessage<Article> deleteArticle(@PathVariable Integer id) {
return articleService.deleteArticle(id);
}
/**
* 根据作者ID获取其所有文章
* @param authorId 作者ID
* @return 返回包含文章列表的ResponseMessage对象
*/
@GetMapping("/author/{authorId}")
public ResponseMessage<List<Article>> getArticlesByAuthor(@PathVariable Integer authorId) {
return articleService.getArticlesByAuthor(authorId);
}
/**
* 根据分类ID获取该分类下的所有文章
* @param categoryId 分类ID
* @return 返回包含文章列表的ResponseMessage对象
*/
@GetMapping("/category/{categoryId}")
public ResponseMessage<List<Article>> getArticlesByCategory(@PathVariable Integer categoryId) {
return articleService.getArticlesByCategory(categoryId);
}
/**
* 获取浏览量最高的文章列表
* @return 返回包含热门文章列表的ResponseMessage对象
*/
@GetMapping("/popular")
public ResponseMessage<List<Article>> getMostViewedArticles() {
return articleService.getMostViewedArticles();

View File

@@ -0,0 +1,73 @@
package com.qf.myafterprojecy.controller;
import com.qf.myafterprojecy.pojo.ResponseMessage;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 帮助控制器类处理前端调用api/help请求
* 提供README_API.md文件的读取和返回功能
*/
@RestController
@RequestMapping("/api/help")
public class HelpController {
/**
* 获取README_API.md文件内容
* @return 返回包含README_API.md文件内容的ResponseMessage对象
*/
@GetMapping
public ResponseMessage<String> getReadmeApi() {
try {
// 获取项目根目录
String rootPath = System.getProperty("user.dir");
// 构建README_API.md文件路径
File readmeFile = new File(rootPath, "README_API.md");
// 检查文件是否存在
if (!readmeFile.exists() || !readmeFile.isFile()) {
// 如果不存在,尝试使用类路径资源加载
try {
ClassPathResource resource = new ClassPathResource("README_API.md");
String markdownContent = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
// 将Markdown转换为HTML
String htmlContent = convertMarkdownToHtml(markdownContent);
return ResponseMessage.success(htmlContent, "获取API文档成功");
} catch (IOException e) {
return ResponseMessage.error("未找到README_API.md文件");
}
}
// 读取文件内容
String markdownContent = new String(FileCopyUtils.copyToByteArray(new FileInputStream(readmeFile)), StandardCharsets.UTF_8);
// 将Markdown转换为HTML
String htmlContent = convertMarkdownToHtml(markdownContent);
return ResponseMessage.success(htmlContent, "获取API文档成功");
} catch (IOException e) {
return ResponseMessage.error("读取README_API.md文件失败: " + e.getMessage());
}
}
/**
* 将Markdown文本转换为HTML
* @param markdown 原始Markdown文本
* @return 转换后的HTML字符串
*/
private String convertMarkdownToHtml(String markdown) {
Parser parser = Parser.builder().build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
}
}

View File

@@ -2,49 +2,79 @@ package com.qf.myafterprojecy.pojo;
import lombok.Data;
import org.springframework.http.HttpStatus;
/**
* 通用响应消息类,用于封装接口返回的数据结构
* 使用泛型T来支持不同类型的数据返回
* @param <T> 数据类型可以是任意Java对象
*/
@Data
public class ResponseMessage<T> {
// 状态码,通常用于表示请求的处理结果
private Integer code;
// 响应消息,用于描述请求的处理结果信息
private String message;
// 请求是否成功的标志
private boolean success;
// 响应数据,泛型类型,支持不同类型的数据
private T data;
/**
* 构造方法,用于创建响应消息对象
* @param code 状态码
* @param message 响应消息
* @param data 响应数据
*/
public ResponseMessage(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// 获取成功状态的getter方法
public boolean isSuccess() {
return success;
}
// 设置成功状态的setter方法
public void setSuccess(boolean success) {
this.success = success;
}
// 获取状态码的getter方法
public Integer getCode() {
return code;
}
// 设置状态码的setter方法
public void setCode(Integer code) {
this.code = code;
}
// 获取响应消息的getter方法
public String getMessage() {
return message;
}
// 设置响应消息的setter方法
public void setMessage(String message) {
this.message = message;
}
// 获取响应数据的getter方法
public T getData() {
return data;
}
// 设置响应数据的setter方法
public void setData(T data) {
this.data = data;
}
/**
* 完整参数的构造方法
* @param code 状态码
* @param message 响应消息
* @param data 响应数据
* @param success 是否成功
*/
public ResponseMessage(Integer code, String message, T data, boolean success) {
this.code = code;
this.message = message;

View File

@@ -1,30 +1,66 @@
package com.qf.myafterprojecy.repository;
import com.qf.myafterprojecy.pojo.Article;
import org.apache.ibatis.annotations.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
@Repository // 表明这是一个数据访问层组件,用于持久层操作
//public interface ArticleRepository extends CrudRepository<Article,Integer> {
//}
public interface ArticleRepository extends JpaRepository<Article, Integer> {
/**
* 根据文章ID查询文章信息的方法
* 使用JPQLJava Persistence Query Language进行查询
*
* @param id 文章的唯一标识符,作为查询条件
* @return 返回一个Optional<Article>对象可能包含文章信息也可能为空如果未找到对应ID的文章
*/
@Query("SELECT a FROM Article a WHERE a.articleid = :id")
Optional<Article> findById(@Param("id") Integer id);
/**
* 根据文章ID查询已发布的文章
* 使用JPQL查询语句只查询状态为1已发布且指定ID的文章
*
* @param id 作者ID作为查询参数传入
* @return 返回符合查询条件的文章列表
*/
@Query("SELECT a FROM Article a WHERE a.status = 1")
List<Article> findPublishedByAuthor();
@Query("SELECT a FROM Article a WHERE a.status = 1 AND a.articleid = :articleid")
List<Article> findPublishedByAuthor(@Param("authorId") Integer authorId);
@Query("SELECT a FROM Article a WHERE a.status = 1 AND a.typeid = :typeid")
/**
* 根据分类ID查询已发布的文章列表
* 使用JPQL查询语句筛选状态为已发布(status=1)且指定分类(typeid)的文章
*
* @param typeid 分类ID通过@Param注解映射到查询语句中的:typeid参数
* @return 返回符合条件Article对象的列表
*/
@Query("SELECT a FROM Article a WHERE a.status = 1 AND a.typeid = :categoryId")
List<Article> findPublishedByCategory(@Param("categoryId") Integer categoryId);
/**
* 使用@Modifying注解标记这是一个修改操作通常用于UPDATE或DELETE语句
* 使用@Query注解定义自定义的JPQL查询语句
* 该查询用于将指定文章的浏览量(viewCount)增加1
*
* @param articleid 文章的唯一标识符,通过@Param注解将方法参数与查询参数绑定
*/
@Modifying
@Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a.id = :id")
void incrementViewCount(@Param("id") Integer id);
@Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a.articleid = :articleid")
void incrementViewCount(@Param("articleid") Integer articleid);
/**
* 根据浏览量降序查询状态为1的所有文章
* 该查询使用JPQL语句从Article实体中选取数据
*
* @return 返回一个Article对象的列表按浏览量(viewCount)降序排列
*/
@Query("SELECT a FROM Article a WHERE a.status = 1 ORDER BY a.viewCount DESC")
List<Article> findMostViewed();
}

View File

@@ -4,5 +4,5 @@ import com.qf.myafterprojecy.pojo.Message;
import org.springframework.data.repository.CrudRepository;
public interface MessageRepository extends CrudRepository<Message, Integer> {
// 可根据需要添加自定义查询方法
}

View File

@@ -4,7 +4,8 @@ import com.qf.myafterprojecy.pojo.Article;
import com.qf.myafterprojecy.pojo.ResponseMessage;
import com.qf.myafterprojecy.pojo.dto.ArticleDto;
import com.qf.myafterprojecy.repository.ArticleRepository;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
@@ -14,10 +15,11 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
public class ArticleService implements IArticleService {
private static final Logger log = LoggerFactory.getLogger(ArticleService.class);
@Autowired
private ArticleRepository articleRepository;
@@ -28,8 +30,8 @@ public class ArticleService implements IArticleService {
Article article = articleRepository.findById(id)
.orElseThrow(() -> new RuntimeException("文章不存在"));
// 增加浏览次数
articleRepository.incrementViewCount(id);
// 暂时不增加浏览次数,以避免事务问题
// articleRepository.incrementViewCount(id);
return ResponseMessage.success(article);
} catch (Exception e) {
@@ -109,7 +111,8 @@ public class ArticleService implements IArticleService {
@Transactional(readOnly = true)
public ResponseMessage<List<Article>> getArticlesByAuthor(Integer authorId) {
try {
List<Article> articles = articleRepository.findPublishedByAuthor(authorId);
// 由于Article实体中没有authorId字段返回所有已发布的文章
List<Article> articles = articleRepository.findPublishedByAuthor();
return ResponseMessage.success(articles);
} catch (DataAccessException e) {
log.error("获取作者文章失败: {}", e.getMessage());

View File

@@ -10,7 +10,7 @@ import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import static jdk.nashorn.internal.runtime.regexp.joni.Config.log;
//import static jdk.nashorn.internal.runtime.regexp.joni.Config.log;
@Service
public class MessageService implements IMessageService {