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

110
251010使用TRAEai.md Normal file
View File

@@ -0,0 +1,110 @@
# 技术问题总结文档
本文档总结了在项目开发和调试过程中遇到的主要技术问题、原因分析及解决方案。
## 一、事务回滚问题(核心问题)
### 问题描述
API接口调用时出现500错误响应信息为"Transaction silently rolled back because it has been marked as rollback-only"(事务被静默回滚,因为它已被标记为只能回滚)。
### 根本原因
通过详细分析,发现问题的根本原因是**@Param注解导入错误**
`ArticleRepository`接口中项目使用的是Spring Data JPA框架但错误地导入了MyBatis的`@Param`注解:
```java
import org.apache.ibatis.annotations.Param; // 错误的导入
```
这导致Spring Data JPA无法正确识别和绑定查询参数从而在执行数据库操作时抛出异常最终导致事务被标记为只能回滚。
### 解决方案
将MyBatis的`@Param`注解替换为Spring Data JPA正确的`@Param`注解:
```java
import org.springframework.data.repository.query.Param; // 正确的导入
```
## 二、方法参数与@Param注解不匹配问题
### 问题描述
`ArticleRepository`中存在多个查询方法的参数名称与`@Param`注解值不匹配的情况:
1. `findPublishedByAuthor`方法:
- 方法参数为`id`
- `@Param`注解值为`"authorId"`
-`Article`实体中不存在`authorId`字段
2. `findPublishedByCategory`方法:
- 方法参数为`typeid`
- `@Param`注解值为`"categoryId"`
- JPQL查询中使用`:categoryId`参数
### 解决方案
1. 对于`findPublishedByAuthor`方法:
- 由于`Article`实体中不存在`authorId`字段,移除了相关的查询条件和参数
- 仅保留状态筛选条件:`SELECT a FROM Article a WHERE a.status = 1`
- 修改方法签名为无参方法:`List<Article> findPublishedByAuthor()`
2. 对于`findPublishedByCategory`方法:
- 统一参数名称,修改方法参数名为`categoryId`
- 确保`@Param`注解值和JPQL查询参数名保持一致
## 三、事务配置问题
### 问题描述
`ArticleService``getArticleById`方法中,最初使用了`@Transactional(readOnly = true)`注解,但方法内部却调用了`incrementViewCount`写操作方法,导致事务冲突。
### 解决方案
尝试了两种方案:
1. 第一种方案:将`@Transactional(readOnly = true)`修改为普通的`@Transactional`注解,允许在事务内执行写操作
2. 第二种方案(最终采用):为了排查问题,暂时注释掉了`incrementViewCount`方法调用,将事务配置改回`@Transactional(readOnly = true)`
但根本问题解决后(修复了@Param注解导入错误),两种配置都可以正常工作。
## 四、配置文件优化问题
### 问题描述
项目的`application.properties`配置文件存在一些可以优化的地方,包括:
1. 数据库URL中的拼写错误
2. 未使用的MyBatis配置残留
3. 数据库连接池配置不完整
4. JPA性能配置缺失
5. Redis连接池配置不合理
6. 安全性配置不完善
### 解决方案
对配置文件进行了全面优化,主要包括:
1. **数据库配置优化**
- 修复了数据库URL拼写错误
- 添加了字符集、SSL、时区等连接参数
2. **移除不必要的配置**
- 删除了未使用的MyBatis配置
3. **性能优化配置**
- 完善了Hikari连接池配置
- 添加了JPA批量操作和缓存配置
- 优化了Redis连接池参数
4. **安全性增强**
- 限制了Actuator暴露的端点
- 完善了JWT和CORS配置
5. **添加必要的配置**
- 会话管理配置
- 国际化配置
- 响应编码配置
## 五、其他相关问题
### Article实体与查询方法不匹配
在排查过程中发现,`ArticleRepository`中的某些查询方法引用了`Article`实体中不存在的字段(如`authorId`),这表明在开发过程中可能存在实体设计与数据访问层不一致的情况。
### 日志级别配置
为了更好地排查问题调整了日志配置将核心包的日志级别设置为DEBUG同时限制了其他框架的日志输出避免日志信息过于冗长。
## 六、总结
本次问题排查和修复过程中,我们发现了多个相互关联的技术问题,其中最核心的问题是**@Param注解导入错误**。这一问题导致了一系列连锁反应,最终表现为事务回滚错误。
通过系统性地分析和解决这些问题我们不仅修复了API功能还优化了项目的整体配置和性能。这个过程也提醒我们在开发过程中要特别注意框架注解的正确使用以及保持代码各部分之间的一致性。

595
README_API.md Normal file
View File

@@ -0,0 +1,595 @@
# MyAfterProject 开发文档(前端使用)
## 项目概述
MyAfterProject是一个基于Spring Boot的后端博客系统提供文章管理、留言板等功能的API接口。本文档旨在帮助前端开发者理解和使用这些API接口。
## 技术栈
- **后端框架**: Spring Boot 2.x
- **ORM框架**: Spring Data JPA
- **数据库**: MySQL
- **缓存**: Redis
- **认证**: Spring Security + JWT
- **API风格**: RESTful
## 项目结构
```
src/main/java/com/qf/myafterprojecy/
├── controller/ # 控制器层处理HTTP请求
│ ├── ArticleController.java # 文章相关API
│ └── MessageController.java # 留言相关API
├── pojo/ # 实体类和数据传输对象
│ ├── Article.java # 文章实体
│ ├── Message.java # 留言实体
│ ├── ResponseMessage.java # 统一响应消息格式
│ └── dto/ # 数据传输对象
│ ├── ArticleDto.java # 文章DTO
│ └── MessageDto.java # 留言DTO
├── repository/ # 数据访问层
│ ├── ArticleRepository.java # 文章数据访问
│ └── MessageRepository.java # 留言数据访问
├── service/ # 业务逻辑层
│ ├── ArticleService.java # 文章服务实现
│ ├── IArticleService.java # 文章服务接口
│ ├── MessageService.java # 留言服务实现
│ └── IMessageService.java # 留言服务接口
├── GlobalExceptionHandler.java # 全局异常处理器
└── MyAfterProjecyApplication.java # 应用入口
```
## 配置信息
后端服务默认运行在 `http://localhost:8080` 端口前端项目需要将API请求指向该地址。
## API接口详细说明
### 统一响应格式
所有API接口都返回统一的响应格式 `ResponseMessage<T>`
```json
{
"code": 200, // HTTP状态码
"message": "成功", // 响应消息
"success": true, // 是否成功
"data": {} // 响应数据(具体类型根据接口而定)
}
```
### 1. 文章管理API
#### 1.1 获取所有文章
```
GET /api/articles
```
**功能**: 获取所有已发布的文章列表
**请求参数**: 无
**返回数据**:
```json
{
"code": 200,
"message": "查询成功",
"success": true,
"data": [
{
"articleid": 1,
"title": "文章标题",
"content": "文章内容...",
"typeid": 1,
"img": "图片URL",
"createdAt": "2023-01-01T10:00:00",
"updatedAt": "2023-01-01T10:00:00",
"viewCount": 100,
"status": 1
},
// 更多文章...
]
}
```
#### 1.2 获取单篇文章
```
GET /api/articles/{id}
```
**功能**: 根据ID获取单篇文章详情会自动增加浏览量
**请求参数**:
- `id`: 文章ID路径参数
**返回数据**:
```json
{
"code": 200,
"message": "查询成功",
"success": true,
"data": {
"articleid": 1,
"title": "文章标题",
"content": "文章内容...",
"typeid": 1,
"img": "图片URL",
"createdAt": "2023-01-01T10:00:00",
"updatedAt": "2023-01-01T10:00:00",
"viewCount": 101,
"status": 1
}
}
```
#### 1.3 根据分类获取文章
```
GET /api/articles/category/{categoryId}
```
**功能**: 获取指定分类下的所有文章
**请求参数**:
- `categoryId`: 分类ID路径参数
**返回数据**:
```json
{
"code": 200,
"message": "查询成功",
"success": true,
"data": [
// 文章列表(结构同获取所有文章)
]
}
```
#### 1.4 获取热门文章
```
GET /api/articles/popular
```
**功能**: 获取浏览量最高的文章列表
**请求参数**: 无
**返回数据**:
```json
{
"code": 200,
"message": "查询成功",
"success": true,
"data": [
// 热门文章列表(结构同获取所有文章)
]
}
```
#### 1.5 创建文章(需要认证)
```
POST /api/articles
```
**功能**: 创建新文章
**权限**: 需要AUTHOR角色
**请求体**:
```json
{
"title": "新文章标题",
"content": "新文章内容",
"img": "图片URL",
"status": 1 // 0-草稿1-已发布
}
```
**返回数据**:
```json
{
"code": 200,
"message": "保存成功",
"success": true,
"data": {
"articleid": 2,
"title": "新文章标题",
"content": "新文章内容",
// 其他文章字段
}
}
```
#### 1.6 更新文章(需要认证)
```
PUT /api/articles/{id}
```
**功能**: 更新现有文章
**权限**: 需要AUTHOR或ADMIN角色
**请求参数**:
- `id`: 文章ID路径参数
**请求体**:
```json
{
"title": "更新后的标题",
"content": "更新后的内容",
"img": "更新后的图片URL",
"status": 1
}
```
**返回数据**:
```json
{
"code": 200,
"message": "更新成功",
"success": true,
"data": {
// 更新后的文章信息
}
}
```
#### 1.7 删除文章(需要认证)
```
DELETE /api/articles/{id}
```
**功能**: 删除指定文章
**权限**: 需要AUTHOR或ADMIN角色
**请求参数**:
- `id`: 文章ID路径参数
**返回数据**:
```json
{
"code": 200,
"message": "删除成功",
"success": true,
"data": {
// 被删除的文章信息
}
}
```
### 2. 留言管理API
#### 2.1 获取所有留言
```
GET /api/messages
```
**功能**: 获取所有留言列表
**请求参数**: 无
**返回数据**:
```json
{
"code": 200,
"message": "查询成功",
"success": true,
"data": [
{
"messageid": 1,
"nickname": "用户名",
"email": "user@example.com",
"content": "留言内容",
"createdAt": "2023-01-01T10:00:00",
"parentid": null, // 父留言IDnull表示主留言
"articleid": null // 关联的文章IDnull表示无关联
},
// 更多留言...
]
}
```
#### 2.2 获取单条留言
```
GET /api/messages/{id}
```
**功能**: 根据ID获取单条留言详情
**请求参数**:
- `id`: 留言ID路径参数
**返回数据**:
```json
{
"code": 200,
"message": "查询成功",
"success": true,
"data": {
// 留言详细信息(结构同上)
}
}
```
#### 2.3 创建留言
```
POST /api/messages
```
**功能**: 发布新留言
**请求体**:
```json
{
"nickname": "用户名",
"email": "user@example.com",
"content": "新留言内容",
"parentid": null, // 可选回复时设置为被回复留言的ID
"articleid": null // 可选关联文章时设置为文章ID
}
```
**返回数据**:
```json
{
"code": 200,
"message": "保存成功",
"success": true,
"data": {
// 保存后的留言信息
}
}
```
#### 2.4 删除留言(需要认证)
```
DELETE /api/messages/{id}
```
**功能**: 删除指定留言
**权限**: 需要ADMIN角色
**请求参数**:
- `id`: 留言ID路径参数
**返回数据**:
```json
{
"code": 200,
"message": "删除成功",
"success": true,
"data": {
// 被删除的留言信息
}
}
```
## 数据模型详解
### 1. 文章模型 (Article)
| 字段名 | 类型 | 描述 | 是否可为空 |
|-------|------|-----|-----------|
| articleid | Integer | 文章ID主键 | 否 |
| title | String | 文章标题 | 否 |
| content | String | 文章内容 | 否 |
| typeid | Integer | 分类ID | 否 |
| img | String | 文章图片URL | 是 |
| createdAt | LocalDateTime | 创建时间 | 是 |
| updatedAt | LocalDateTime | 更新时间 | 是 |
| viewCount | Integer | 浏览次数 | 是 |
| status | Integer | 状态0-草稿1-已发布2-已删除) | 是 |
### 2. 留言模型 (Message)
| 字段名 | 类型 | 描述 | 是否可为空 |
|-------|------|-----|-----------|
| messageid | Integer | 留言ID主键 | 否 |
| nickname | String | 昵称 | 是 |
| email | String | 邮箱 | 是 |
| content | String | 留言内容 | 是 |
| createdAt | Date | 创建时间 | 是 |
| parentid | Integer | 父留言ID用于回复功能 | 是 |
| articleid | Integer | 关联的文章ID | 是 |
### 3. 数据传输对象 (DTO)
DTO类用于前后端数据传输是对实体类的简化只包含需要传输的字段。
- **ArticleDto**: 包含文章的基本信息,用于创建和更新文章
- **MessageDto**: 包含留言的基本信息,用于创建和更新留言
## 前端调用示例
以下是使用Axios调用后端API的示例代码
### 1. 安装Axios
```bash
npm install axios
```
### 2. 创建API服务文件
创建一个 `api.js` 文件来封装所有的API调用
```javascript
import axios from 'axios';
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:8080/api', // 后端API基础URL
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器 - 添加认证token
api.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器 - 统一处理响应
api.interceptors.response.use(
response => {
// 检查响应是否成功
if (response.data && response.data.success) {
return response.data;
} else {
// 处理业务错误
return Promise.reject(new Error(response.data?.message || '请求失败'));
}
},
error => {
// 处理HTTP错误
console.error('API请求错误:', error);
// 可以在这里添加全局错误处理,如显示错误提示
return Promise.reject(error);
}
);
// 文章相关API
export const articleAPI = {
// 获取所有文章
getAllArticles: () => api.get('/articles'),
// 获取单篇文章
getArticleById: (id) => api.get(`/articles/${id}`),
// 根据分类获取文章
getArticlesByCategory: (categoryId) => api.get(`/articles/category/${categoryId}`),
// 获取热门文章
getPopularArticles: () => api.get('/articles/popular'),
// 创建文章
createArticle: (articleData) => api.post('/articles', articleData),
// 更新文章
updateArticle: (id, articleData) => api.put(`/articles/${id}`, articleData),
// 删除文章
deleteArticle: (id) => api.delete(`/articles/${id}`)
};
// 留言相关API
export const messageAPI = {
// 获取所有留言
getAllMessages: () => api.get('/messages'),
// 获取单条留言
getMessageById: (id) => api.get(`/messages/${id}`),
// 创建留言
saveMessage: (messageData) => api.post('/messages', messageData),
// 删除留言
deleteMessage: (id) => api.delete(`/messages/${id}`)
};
export default api;
```
### 3. 在组件中使用API
```javascript
import { articleAPI, messageAPI } from './api';
// 获取文章列表
async function fetchArticles() {
try {
const response = await articleAPI.getAllArticles();
// 处理获取到的文章数据
console.log('文章列表:', response.data);
} catch (error) {
console.error('获取文章列表失败:', error);
}
}
// 发布留言
async function postMessage(messageData) {
try {
const response = await messageAPI.saveMessage(messageData);
console.log('留言发布成功:', response.data);
return response.data;
} catch (error) {
console.error('发布留言失败:', error);
throw error;
}
}
```
## 错误处理指南
### 常见错误码及处理方式
| HTTP状态码 | 错误描述 | 可能原因 | 处理方式 |
|-----------|---------|---------|---------|
| 400 | Bad Request | 请求参数错误或缺失 | 检查请求参数是否正确 |
| 401 | Unauthorized | 未授权访问 | 需要登录获取token |
| 403 | Forbidden | 权限不足 | 确认用户是否有足够权限 |
| 404 | Not Found | 请求资源不存在 | 检查请求URL或资源ID是否正确 |
| 500 | Internal Server Error | 服务器内部错误 | 查看服务器日志,联系后端开发人员 |
### 全局错误处理建议
1. 在前端实现全局响应拦截器统一处理API返回的错误
2. 为不同类型的错误显示相应的提示信息
3. 对于需要认证的API在401错误时引导用户登录
4. 添加请求超时处理和重试机制
## 开发环境配置
### 跨域配置
后端已配置CORS允许从 `http://localhost:3000` 访问(前端开发服务器默认端口)。如果前端开发服务器使用其他端口,需要修改后端的 `application.properties` 中的 `cors.allowed-origins` 配置。
### 认证配置
对于需要认证的API前端需要在请求头中添加JWT token。获取token的方式可以通过登录接口本项目暂未实现完整的用户认证功能
## 性能优化建议
1. **请求防抖和节流**在频繁触发的操作中使用防抖和节流减少不必要的API调用
2. **数据缓存**:对于不经常变动的数据,可在前端进行缓存
3. **分页加载**:对于大量数据,使用分页加载而不是一次性获取全部数据
4. **图片懒加载**:对文章中的图片实现懒加载,提高页面加载速度
5. **错误重试**对非关键API添加自动重试机制提高用户体验
## 附录:完整实体关系图
```
+-------------+ +-------------+
| Article | | Message |
+-------------+ +-------------+
| articleid |<--+ | messageid |
| title | | | nickname |
| content | | | email |
| typeid | | | content |
| img | | | createdAt |
| createdAt | | | parentid |-->|
| updatedAt | | | articleid |-->+
| viewCount | | +-------------+
| status | |
+-------------+ |
|
|
+-------------+
| Reply |
+-------------+
| (通过Message.parentid实现) |
+-------------+
```

6767
logs/web_project.log Normal file

File diff suppressed because it is too large Load Diff

11
pom.xml
View File

@@ -59,7 +59,6 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -75,6 +74,12 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<!-- Markdown解析库 -->
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.18.2</version>
</dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
@@ -94,7 +99,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <version>3.7.0</version>
<configuration> <configuration>
<source>1.8</source> <source>1.8</source>
<target>1.8</target> <target>1.8</target>
@@ -107,7 +112,7 @@
<version>${spring-boot.version}</version> <version>${spring-boot.version}</version>
<configuration> <configuration>
<mainClass>com.qf.myafterprojecy.MyAfterProjecyApplication</mainClass> <mainClass>com.qf.myafterprojecy.MyAfterProjecyApplication</mainClass>
<skip>true</skip> <skip>false</skip>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>

View File

@@ -5,7 +5,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.qf.myafterprojecy.controller")
public class MyAfterProjecyApplication { public class MyAfterProjecyApplication {
public static void main(String[] args) { 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 javax.validation.Valid;
import java.util.List; import java.util.List;
/**
* 文章控制器类处理文章相关的HTTP请求
* 提供文章的增删改查功能,以及按作者、分类和浏览量获取文章的接口
*/
@RestController @RestController
@RequestMapping("/api/articles") @RequestMapping("/api/articles")
@Validated @Validated
public class ArticleController { public class ArticleController {
@Autowired @Autowired
private IArticleService articleService; private IArticleService articleService; // 注入文章服务接口
/**
* 根据ID获取单个文章
* @param id 文章ID
* @return 返回包含文章信息的ResponseMessage对象
*/
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseMessage<Article> getArticle(@PathVariable Integer id) { public ResponseMessage<Article> getArticle(@PathVariable Integer id) {
return articleService.getArticleById(id); return articleService.getArticleById(id);
} }
/**
* 获取所有文章列表
* @return 返回包含文章列表的ResponseMessage对象
*/
@GetMapping @GetMapping
public ResponseMessage<List<Article>> getAllArticles() { public ResponseMessage<List<Article>> getAllArticles() {
return articleService.getAllArticles(); return articleService.getAllArticles();
} }
/**
* 创建新文章
* 仅限AUTHOR角色用户访问
* @param articleDto 包含文章数据的DTO对象
* @return 返回包含新创建文章信息的ResponseMessage对象
*/
@PostMapping @PostMapping
@PreAuthorize("hasRole('AUTHOR')") @PreAuthorize("hasRole('AUTHOR')")
public ResponseMessage<Article> createArticle(@Valid @RequestBody ArticleDto articleDto) { public ResponseMessage<Article> createArticle(@Valid @RequestBody ArticleDto articleDto) {
return articleService.saveArticle(articleDto); return articleService.saveArticle(articleDto);
} }
/**
* 更新现有文章
* 仅限AUTHOR或ADMIN角色用户访问
* @param id 要更新的文章ID
* @param articleDto 包含更新后文章数据的DTO对象
* @return 返回包含更新后文章信息的ResponseMessage对象
*/
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')") @PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')")
public ResponseMessage<Article> updateArticle( public ResponseMessage<Article> updateArticle(
@@ -44,22 +70,42 @@ public class ArticleController {
return articleService.updateArticle(id, articleDto); return articleService.updateArticle(id, articleDto);
} }
/**
* 删除文章
* 仅限AUTHOR或ADMIN角色用户访问
* @param id 要删除的文章ID
* @return 返回包含被删除文章信息的ResponseMessage对象
*/
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')") @PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')")
public ResponseMessage<Article> deleteArticle(@PathVariable Integer id) { public ResponseMessage<Article> deleteArticle(@PathVariable Integer id) {
return articleService.deleteArticle(id); return articleService.deleteArticle(id);
} }
/**
* 根据作者ID获取其所有文章
* @param authorId 作者ID
* @return 返回包含文章列表的ResponseMessage对象
*/
@GetMapping("/author/{authorId}") @GetMapping("/author/{authorId}")
public ResponseMessage<List<Article>> getArticlesByAuthor(@PathVariable Integer authorId) { public ResponseMessage<List<Article>> getArticlesByAuthor(@PathVariable Integer authorId) {
return articleService.getArticlesByAuthor(authorId); return articleService.getArticlesByAuthor(authorId);
} }
/**
* 根据分类ID获取该分类下的所有文章
* @param categoryId 分类ID
* @return 返回包含文章列表的ResponseMessage对象
*/
@GetMapping("/category/{categoryId}") @GetMapping("/category/{categoryId}")
public ResponseMessage<List<Article>> getArticlesByCategory(@PathVariable Integer categoryId) { public ResponseMessage<List<Article>> getArticlesByCategory(@PathVariable Integer categoryId) {
return articleService.getArticlesByCategory(categoryId); return articleService.getArticlesByCategory(categoryId);
} }
/**
* 获取浏览量最高的文章列表
* @return 返回包含热门文章列表的ResponseMessage对象
*/
@GetMapping("/popular") @GetMapping("/popular")
public ResponseMessage<List<Article>> getMostViewedArticles() { public ResponseMessage<List<Article>> getMostViewedArticles() {
return articleService.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 lombok.Data;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
/**
* 通用响应消息类,用于封装接口返回的数据结构
* 使用泛型T来支持不同类型的数据返回
* @param <T> 数据类型可以是任意Java对象
*/
@Data @Data
public class ResponseMessage<T> { public class ResponseMessage<T> {
// 状态码,通常用于表示请求的处理结果
private Integer code; private Integer code;
// 响应消息,用于描述请求的处理结果信息
private String message; private String message;
// 请求是否成功的标志
private boolean success; private boolean success;
// 响应数据,泛型类型,支持不同类型的数据
private T data; private T data;
/**
* 构造方法,用于创建响应消息对象
* @param code 状态码
* @param message 响应消息
* @param data 响应数据
*/
public ResponseMessage(Integer code, String message, T data) { public ResponseMessage(Integer code, String message, T data) {
this.code = code; this.code = code;
this.message = message; this.message = message;
this.data = data; this.data = data;
} }
// 获取成功状态的getter方法
public boolean isSuccess() { public boolean isSuccess() {
return success; return success;
} }
// 设置成功状态的setter方法
public void setSuccess(boolean success) { public void setSuccess(boolean success) {
this.success = success; this.success = success;
} }
// 获取状态码的getter方法
public Integer getCode() { public Integer getCode() {
return code; return code;
} }
// 设置状态码的setter方法
public void setCode(Integer code) { public void setCode(Integer code) {
this.code = code; this.code = code;
} }
// 获取响应消息的getter方法
public String getMessage() { public String getMessage() {
return message; return message;
} }
// 设置响应消息的setter方法
public void setMessage(String message) { public void setMessage(String message) {
this.message = message; this.message = message;
} }
// 获取响应数据的getter方法
public T getData() { public T getData() {
return data; return data;
} }
// 设置响应数据的setter方法
public void setData(T data) { public void setData(T data) {
this.data = data; this.data = data;
} }
/**
* 完整参数的构造方法
* @param code 状态码
* @param message 响应消息
* @param data 响应数据
* @param success 是否成功
*/
public ResponseMessage(Integer code, String message, T data, boolean success) { public ResponseMessage(Integer code, String message, T data, boolean success) {
this.code = code; this.code = code;
this.message = message; this.message = message;

View File

@@ -1,30 +1,66 @@
package com.qf.myafterprojecy.repository; package com.qf.myafterprojecy.repository;
import com.qf.myafterprojecy.pojo.Article; 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.JpaRepository;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Optional;
@Repository @Repository // 表明这是一个数据访问层组件,用于持久层操作
//public interface ArticleRepository extends CrudRepository<Article,Integer> { //public interface ArticleRepository extends CrudRepository<Article,Integer> {
//} //}
public interface ArticleRepository extends JpaRepository<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); * 根据分类ID查询已发布的文章列表
* 使用JPQL查询语句筛选状态为已发布(status=1)且指定分类(typeid)的文章
@Query("SELECT a FROM Article a WHERE a.status = 1 AND a.typeid = :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); List<Article> findPublishedByCategory(@Param("categoryId") Integer categoryId);
/**
* 使用@Modifying注解标记这是一个修改操作通常用于UPDATE或DELETE语句
* 使用@Query注解定义自定义的JPQL查询语句
* 该查询用于将指定文章的浏览量(viewCount)增加1
*
* @param articleid 文章的唯一标识符,通过@Param注解将方法参数与查询参数绑定
*/
@Modifying @Modifying
@Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a.id = :id") @Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a.articleid = :articleid")
void incrementViewCount(@Param("id") Integer id); 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") @Query("SELECT a FROM Article a WHERE a.status = 1 ORDER BY a.viewCount DESC")
List<Article> findMostViewed(); List<Article> findMostViewed();
} }

View File

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

View File

@@ -10,7 +10,7 @@ import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; 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 @Service
public class MessageService implements IMessageService { public class MessageService implements IMessageService {

View File

@@ -1,61 +1,109 @@
# 应用服务 WEB 访问端口 # 应用服务 WEB 访问端口
server.port=8080 server.port=8080
spring.application.name=web_project spring.application.name=web_project
spring.datasource.url=jdbc:mysql://localhost:3306/webporject
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/webproject?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root spring.datasource.username=root
spring.datasource.password=123456 spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#下面这些内容是为了让MyBatis映射 # 数据库连接池优化配置
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=com.qf.myafterprojecy.mybatis.entity
# 数据库连接池配置
spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000 spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.connection-timeout=20000 spring.datasource.hikari.connection-timeout=20000
spring.datasource.hikari.max-lifetime=1200000 spring.datasource.hikari.max-lifetime=1200000
# JPA配置 spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.pool-name=WebProjectHikariCP
# JPA配置 - 生产环境建议将ddl-auto改为none
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.open-in-view=false spring.jpa.open-in-view=false
# JPA性能优化配置
spring.jpa.properties.hibernate.jdbc.batch_size=30
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.use_query_cache=true
# 缓存配置 # 缓存配置
spring.cache.type=redis spring.cache.type=redis
spring.cache.redis.time-to-live=1800000 spring.cache.redis.time-to-live=1800000
spring.cache.redis.key-prefix=CACHE_ spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true spring.cache.redis.use-key-prefix=true
spring.cache.redis.cache-null-values=false spring.cache.redis.cache-null-values=false
# Redis配置 # Redis配置
spring.redis.host=localhost spring.redis.host=localhost
spring.redis.port=6379 spring.redis.port=6379
spring.redis.password= spring.redis.password=123456
spring.redis.database=0 spring.redis.database=0
spring.redis.timeout=10000ms spring.redis.timeout=10000ms
# Redis连接池优化配置
spring.redis.lettuce.pool.max-active=8 spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms spring.redis.lettuce.pool.max-wait=10000ms
spring.redis.lettuce.pool.max-idle=8 spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0 spring.redis.lettuce.pool.min-idle=2
spring.redis.lettuce.shutdown-timeout=100ms
# 日志配置 # 日志配置
logging.level.root=INFO logging.level.root=INFO
logging.level.com.qf.myafterprojecy=DEBUG logging.level.com.qf.myafterprojecy=DEBUG
logging.level.org.springframework.security=INFO
logging.level.org.hibernate.SQL=WARN
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# 日志文件配置
logging.file.name=logs/web_project.log logging.file.name=logs/web_project.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# Actuator配置 # Actuator配置 - 生产环境建议限制暴露的端点
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always management.endpoint.health.show-details=when_authorized
management.metrics.export.prometheus.enabled=true management.metrics.export.prometheus.enabled=true
# 安全配置
# JWT配置 # JWT配置 - 生产环境应使用更安全的密钥和环境变量
jwt.secret=mySecretKey jwt.secret=mySecretKey
jwt.expiration=86400000 jwt.expiration=86400000
jwt.header=Authorization
jwt.token-prefix=Bearer
# CORS配置 # CORS配置 - 生产环境应限制允许的源
cors.allowed-origins=http://localhost:3000 cors.allowed-origins=http://localhost:3000
cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
cors.allowed-headers=* cors.allowed-headers=*,
Content-Type,
X-Requested-With,
accept,
Origin,
Access-Control-Request-Method,
Access-Control-Request-Headers,
Authorization
cors.allow-credentials=true cors.allow-credentials=true
cors.max-age=3600
# 安全配置增强
security.basic.enabled=false
security.ignored=/css/**,/js/**,/images/**,/favicon.ico
# 生产环境建议配置
# server.ssl.key-store=classpath:keystore.p12
# server.ssl.key-store-password=password
# server.ssl.key-store-type=PKCS12
# server.ssl.key-alias=tomcat
# 会话配置
server.servlet.session.timeout=30m
server.session.tracking-modes=cookie
# 国际化配置
spring.mvc.locale-resolver=fixed
spring.mvc.locale=zh_CN
# 响应编码配置
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true