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:
110
251010使用TRAEai.md
Normal file
110
251010使用TRAEai.md
Normal 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
595
README_API.md
Normal 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, // 父留言ID,null表示主留言
|
||||||
|
"articleid": null // 关联的文章ID,null表示无关联
|
||||||
|
},
|
||||||
|
// 更多留言...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
6767
logs/web_project.log
Normal file
File diff suppressed because it is too large
Load Diff
11
pom.xml
11
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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查询文章信息的方法
|
||||||
|
* 使用JPQL(Java 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
// 可根据需要添加自定义查询方法
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user