feat: 实现题目服务完整校验责任链和流量控制
- 责任链校验系统 * 题目创建参数校验(标题、内容、难度、判题配置、标签) * 题目编辑参数校验(可选字段校验) * 题目更新参数校验(管理员、存在性校验) * 题目提交参数校验(存在性、状态、语言、代码安全) - Sentinel 流量控制 * 添加 Sentinel 依赖和配置 * 题目提交接口添加限流注解和降级处理 - 数据模型优化 * QuestionResponseDTO 返回对象类型(JudgeConfig、JudgeCase) * 实现 Entity 与 DTO 的 JSON 转换 - 接口文档 * 生成博客服务完整 API 文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
708
aioj-backend-blog-service/接口文档.md
Normal file
708
aioj-backend-blog-service/接口文档.md
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
# AIOJ 博客服务接口文档
|
||||||
|
|
||||||
|
## 服务信息
|
||||||
|
|
||||||
|
- **服务名称**: aioj-blog-service
|
||||||
|
- **服务端口**: 18086
|
||||||
|
- **基础路径**: `/api`
|
||||||
|
- **API 文档**: `/swagger-ui.html`
|
||||||
|
|
||||||
|
## 通用说明
|
||||||
|
|
||||||
|
### 统一响应格式
|
||||||
|
|
||||||
|
所有接口均使用统一的响应格式 `Result<T>`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0", // 响应码,"0"表示成功
|
||||||
|
"message": "success", // 响应信息
|
||||||
|
"data": {} // 响应数据
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分页响应格式
|
||||||
|
|
||||||
|
分页查询返回 `IPage<T>` 格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"records": [], // 数据列表
|
||||||
|
"total": 100, // 总记录数
|
||||||
|
"size": 10, // 每页数量
|
||||||
|
"current": 1, // 当前页码
|
||||||
|
"pages": 10 // 总页数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 文章管理接口
|
||||||
|
|
||||||
|
### 1.1 创建文章
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/article/create`
|
||||||
|
- **描述**: 创建新文章,支持 Markdown 格式
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 | 示例 |
|
||||||
|
|------|------|------|------|------|
|
||||||
|
| title | String | 是 | 文章标题 | Spring Boot 3.5.7 新特性详解 |
|
||||||
|
| slug | String | 否 | 文章别名(URL友好标识) | spring-boot-3-5-7-features |
|
||||||
|
| summary | String | 否 | 文章摘要 | 本文详细介绍Spring Boot 3.5.7版本的新特性... |
|
||||||
|
| content | String | 是 | 文章内容(Markdown格式) | # 欢迎使用... |
|
||||||
|
| coverImage | String | 否 | 封面图片URL | https://example.com/images/cover.jpg |
|
||||||
|
| categoryId | Long | 是 | 分类ID | 1 |
|
||||||
|
| tagIds | List\<Long\> | 否 | 标签ID列表 | [1, 2, 3] |
|
||||||
|
| isTop | Integer | 否 | 是否置顶(0=否,1=是) | 0 |
|
||||||
|
| isEssence | Integer | 否 | 是否精华(0=否,1=是) | 0 |
|
||||||
|
| isPublished | Integer | 否 | 是否发布(0=草稿,1=已发布) | 1 |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"message": "success",
|
||||||
|
"data": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 更新文章
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/article/update`
|
||||||
|
- **描述**: 更新已存在的文章信息
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | Long | 是 | 文章ID |
|
||||||
|
| 其他字段 | - | - | 同创建文章 |
|
||||||
|
|
||||||
|
### 1.3 删除文章
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/article/delete/{articleId}`
|
||||||
|
- **描述**: 逻辑删除指定文章
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| articleId | Long | 是 | 文章ID |
|
||||||
|
|
||||||
|
### 1.4 查询文章详情
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/article/{articleId}`
|
||||||
|
- **描述**: 根据文章ID查询文章详细信息
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "Spring Boot 3.5.7 新特性详解",
|
||||||
|
"slug": "spring-boot-3-5-7-features",
|
||||||
|
"summary": "本文详细介绍Spring Boot 3.5.7版本的新特性...",
|
||||||
|
"content": "# 欢迎使用...",
|
||||||
|
"contentHtml": "<h1>欢迎使用...</h1>",
|
||||||
|
"coverImage": "https://example.com/images/cover.jpg",
|
||||||
|
"authorId": 1,
|
||||||
|
"authorName": "admin",
|
||||||
|
"categoryId": 1,
|
||||||
|
"categoryName": "算法题解",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "数据结构",
|
||||||
|
"slug": "data-structure",
|
||||||
|
"color": "#3498db"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"viewCount": 100,
|
||||||
|
"likeCount": 10,
|
||||||
|
"commentCount": 5,
|
||||||
|
"collectCount": 3,
|
||||||
|
"isTop": 0,
|
||||||
|
"isEssence": 0,
|
||||||
|
"isPublished": 1,
|
||||||
|
"status": 1,
|
||||||
|
"publishTime": "2026-01-26T10:00:00",
|
||||||
|
"createTime": "2026-01-26T09:00:00",
|
||||||
|
"updateTime": "2026-01-26T10:00:00",
|
||||||
|
"isLiked": false,
|
||||||
|
"isCollected": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 根据slug查询文章
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/article/slug/{slug}`
|
||||||
|
|
||||||
|
### 1.6 分页查询文章列表
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/article/list`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| current | Integer | 否 | 页码,默认1 |
|
||||||
|
| size | Integer | 否 | 每页数量,默认10 |
|
||||||
|
| id | Long | 否 | 文章ID |
|
||||||
|
| title | String | 否 | 文章标题(模糊查询) |
|
||||||
|
| categoryId | Long | 否 | 分类ID |
|
||||||
|
| tagId | Long | 否 | 标签ID |
|
||||||
|
| authorId | Long | 否 | 作者ID |
|
||||||
|
| isTop | Integer | 否 | 是否置顶 |
|
||||||
|
| isEssence | Integer | 否 | 是否精华 |
|
||||||
|
| isPublished | Integer | 否 | 是否发布(0=草稿,1=已发布) |
|
||||||
|
| status | Integer | 否 | 文章状态(1=正常,2=审核中,3=已关闭,4=已删除) |
|
||||||
|
| sortField | String | 否 | 排序字段(view_count/like_count/comment_count/publish_time) |
|
||||||
|
| sortOrder | String | 否 | 排序方式(asc/desc) |
|
||||||
|
|
||||||
|
### 1.7 发布文章
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/article/publish/{articleId}`
|
||||||
|
|
||||||
|
### 1.8 取消发布文章
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/article/unpublish/{articleId}`
|
||||||
|
|
||||||
|
### 1.9 增加文章浏览量
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/article/view/{articleId}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 分类管理接口
|
||||||
|
|
||||||
|
### 2.1 创建分类
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/category/create`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| name | String | 是 | 分类名称 |
|
||||||
|
| slug | String | 否 | 分类别名 |
|
||||||
|
| description | String | 否 | 分类描述 |
|
||||||
|
| icon | String | 否 | 分类图标 |
|
||||||
|
| sortOrder | Integer | 否 | 排序序号 |
|
||||||
|
| parentId | Long | 否 | 父分类ID |
|
||||||
|
| isEnabled | Integer | 否 | 是否启用 |
|
||||||
|
|
||||||
|
### 2.2 更新分类
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/category/update`
|
||||||
|
|
||||||
|
### 2.3 删除分类
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/category/delete/{categoryId}`
|
||||||
|
|
||||||
|
### 2.4 查询分类详情
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/category/{categoryId}`
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "算法题解",
|
||||||
|
"slug": "algorithm-solutions",
|
||||||
|
"description": "算法题目解题思路和代码分享",
|
||||||
|
"icon": "💡",
|
||||||
|
"sortOrder": 1,
|
||||||
|
"parentId": 0,
|
||||||
|
"articleCount": 10,
|
||||||
|
"isEnabled": 1,
|
||||||
|
"createTime": "2026-01-26T09:00:00",
|
||||||
|
"updateTime": "2026-01-26T09:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 根据slug查询分类
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/category/slug/{slug}`
|
||||||
|
|
||||||
|
### 2.6 查询所有分类
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/category/list/all`
|
||||||
|
|
||||||
|
### 2.7 查询启用的分类
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/category/list/enabled`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 标签管理接口
|
||||||
|
|
||||||
|
### 3.1 创建标签
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/tag/create`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| name | String | 是 | 标签名称 |
|
||||||
|
| slug | String | 否 | 标签别名 |
|
||||||
|
| description | String | 否 | 标签描述 |
|
||||||
|
| color | String | 否 | 标签颜色 |
|
||||||
|
| isEnabled | Integer | 否 | 是否启用 |
|
||||||
|
|
||||||
|
### 3.2 更新标签
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/tag/update`
|
||||||
|
|
||||||
|
### 3.3 删除标签
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/tag/delete/{tagId}`
|
||||||
|
|
||||||
|
### 3.4 查询标签详情
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/tag/{tagId}`
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "数据结构",
|
||||||
|
"slug": "data-structure",
|
||||||
|
"description": "数据结构与算法相关内容",
|
||||||
|
"color": "#3498db",
|
||||||
|
"articleCount": 5,
|
||||||
|
"isEnabled": 1,
|
||||||
|
"createTime": "2026-01-26T09:00:00",
|
||||||
|
"updateTime": "2026-01-26T09:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 根据slug查询标签
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/tag/slug/{slug}`
|
||||||
|
|
||||||
|
### 3.6 查询所有标签
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/tag/list/all`
|
||||||
|
|
||||||
|
### 3.7 查询启用的标签
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/tag/list/enabled`
|
||||||
|
|
||||||
|
### 3.8 查询文章的标签
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/tag/article/{articleId}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 评论管理接口
|
||||||
|
|
||||||
|
### 4.1 发表评论
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/comment/create`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| articleId | Long | 是 | 文章ID |
|
||||||
|
| content | String | 是 | 评论内容(支持Markdown) |
|
||||||
|
| parentId | Long | 否 | 父评论ID(0表示一级评论) |
|
||||||
|
| replyToId | Long | 否 | 回复的评论ID(用于@提醒) |
|
||||||
|
|
||||||
|
### 4.2 回复评论
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/comment/reply`
|
||||||
|
|
||||||
|
### 4.3 删除评论
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/comment/delete/{commentId}`
|
||||||
|
|
||||||
|
### 4.4 查询评论详情
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/comment/{commentId}`
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"articleId": 1,
|
||||||
|
"userId": 1,
|
||||||
|
"userName": "admin",
|
||||||
|
"userAvatar": "https://example.com/avatar.jpg",
|
||||||
|
"parentId": 0,
|
||||||
|
"replyToId": 0,
|
||||||
|
"replyToName": null,
|
||||||
|
"content": "这篇文章写得很好!",
|
||||||
|
"contentHtml": "<p>这篇文章写得很好!</p>",
|
||||||
|
"likeCount": 5,
|
||||||
|
"replyCount": 2,
|
||||||
|
"isAuthor": 1,
|
||||||
|
"status": 1,
|
||||||
|
"createTime": "2026-01-26T10:00:00",
|
||||||
|
"updateTime": "2026-01-26T10:00:00",
|
||||||
|
"isLiked": false,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 查询文章评论
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/comment/article/{articleId}`
|
||||||
|
- **描述**: 根据文章ID查询该文章的所有评论(树形结构)
|
||||||
|
|
||||||
|
### 4.6 分页查询评论列表
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/comment/list`
|
||||||
|
|
||||||
|
### 4.7 查询子评论
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/comment/children/{parentId}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 点赞管理接口
|
||||||
|
|
||||||
|
### 5.1 点赞
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/like/like`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| targetId | Long | 是 | 目标ID |
|
||||||
|
| targetType | Integer | 是 | 目标类型(1=文章,2=评论) |
|
||||||
|
|
||||||
|
### 5.2 取消点赞
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/like/unlike`
|
||||||
|
|
||||||
|
### 5.3 查询点赞状态
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/like/check`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| targetId | Long | 是 | 目标ID |
|
||||||
|
| targetType | Integer | 是 | 目标类型(1=文章,2=评论) |
|
||||||
|
|
||||||
|
### 5.4 切换点赞状态
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/like/toggle`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 收藏管理接口
|
||||||
|
|
||||||
|
### 6.1 收藏文章
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/collection/collect`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| articleId | Long | 是 | 文章ID |
|
||||||
|
| folderId | Long | 否 | 收藏夹ID |
|
||||||
|
|
||||||
|
### 6.2 取消收藏文章
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/collection/uncollect/{articleId}`
|
||||||
|
|
||||||
|
### 6.3 查询收藏状态
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/collection/check/{articleId}`
|
||||||
|
|
||||||
|
### 6.4 创建收藏夹
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/collection/folder/create`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| name | String | 是 | 收藏夹名称 |
|
||||||
|
| description | String | 否 | 收藏夹描述 |
|
||||||
|
|
||||||
|
### 6.5 删除收藏夹
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/collection/folder/delete/{folderId}`
|
||||||
|
|
||||||
|
### 6.6 查询收藏夹列表
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/collection/folder/list`
|
||||||
|
|
||||||
|
### 6.7 分页查询收藏列表
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/collection/list`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| current | Integer | 是 | 页码 |
|
||||||
|
| size | Integer | 是 | 每页数量 |
|
||||||
|
| folderId | Long | 否 | 收藏夹ID |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 关注管理接口
|
||||||
|
|
||||||
|
### 7.1 关注用户
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/follow/follow`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| followingId | Long | 是 | 被关注者ID |
|
||||||
|
|
||||||
|
### 7.2 取消关注用户
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/follow/unfollow/{followingId}`
|
||||||
|
|
||||||
|
### 7.3 查询关注状态
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/follow/check/{followingId}`
|
||||||
|
|
||||||
|
### 7.4 查询关注列表
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/follow/following/list`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| userId | Long | 是 | 用户ID |
|
||||||
|
| current | Integer | 是 | 页码 |
|
||||||
|
| size | Integer | 是 | 每页数量 |
|
||||||
|
|
||||||
|
### 7.5 查询粉丝列表
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/follow/followers/list`
|
||||||
|
|
||||||
|
### 7.6 查询关注数
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/follow/following/count`
|
||||||
|
|
||||||
|
### 7.7 查询粉丝数
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/follow/followers/count`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 通知管理接口
|
||||||
|
|
||||||
|
### 8.1 分页查询通知列表
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/notification/list`
|
||||||
|
|
||||||
|
### 8.2 标记通知为已读
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/notification/read/{notificationId}`
|
||||||
|
|
||||||
|
### 8.3 批量标记通知为已读
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/notification/read/batch`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| - | List\<Long\> | 是 | 通知ID列表 |
|
||||||
|
|
||||||
|
### 8.4 标记所有通知为已读
|
||||||
|
|
||||||
|
- **接口**: `PUT /api/v1/notification/read/all`
|
||||||
|
|
||||||
|
### 8.5 清空所有通知
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/notification/clear/all`
|
||||||
|
|
||||||
|
### 8.6 查询未读通知数量
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/notification/unread/count`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 草稿箱管理接口
|
||||||
|
|
||||||
|
### 9.1 保存草稿
|
||||||
|
|
||||||
|
- **接口**: `POST /api/v1/draft/save`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| title | String | 否 | 草稿标题 |
|
||||||
|
| content | String | 否 | 草稿内容 |
|
||||||
|
| coverImage | String | 否 | 封面图片 |
|
||||||
|
| categoryId | Long | 否 | 分类ID |
|
||||||
|
| tagIds | List\<Long\> | 否 | 标签ID列表 |
|
||||||
|
|
||||||
|
### 9.2 删除草稿
|
||||||
|
|
||||||
|
- **接口**: `DELETE /api/v1/draft/delete/{draftId}`
|
||||||
|
|
||||||
|
### 9.3 查询草稿详情
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/draft/{draftId}`
|
||||||
|
|
||||||
|
### 9.4 分页查询草稿列表
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/draft/list`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| current | Integer | 是 | 页码 |
|
||||||
|
| size | Integer | 是 | 每页数量 |
|
||||||
|
|
||||||
|
### 9.5 查询最新草稿
|
||||||
|
|
||||||
|
- **接口**: `GET /api/v1/draft/latest`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:数据模型
|
||||||
|
|
||||||
|
### ArticleResponseDTO(文章响应)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | Long | 文章ID |
|
||||||
|
| title | String | 文章标题 |
|
||||||
|
| slug | String | 文章别名 |
|
||||||
|
| summary | String | 文章摘要 |
|
||||||
|
| content | String | 文章内容(Markdown格式) |
|
||||||
|
| contentHtml | String | 渲染后的HTML内容 |
|
||||||
|
| coverImage | String | 封面图片URL |
|
||||||
|
| authorId | Long | 作者用户ID |
|
||||||
|
| authorName | String | 作者用户名 |
|
||||||
|
| categoryId | Long | 分类ID |
|
||||||
|
| categoryName | String | 分类名称 |
|
||||||
|
| tags | List\<TagResponseDTO\> | 标签列表 |
|
||||||
|
| viewCount | Integer | 浏览次数 |
|
||||||
|
| likeCount | Integer | 点赞数 |
|
||||||
|
| commentCount | Integer | 评论数 |
|
||||||
|
| collectCount | Integer | 收藏数 |
|
||||||
|
| isTop | Integer | 是否置顶 |
|
||||||
|
| isEssence | Integer | 是否精华 |
|
||||||
|
| isPublished | Integer | 是否发布 |
|
||||||
|
| status | Integer | 文章状态 |
|
||||||
|
| publishTime | Date | 发布时间 |
|
||||||
|
| createTime | Date | 创建时间 |
|
||||||
|
| updateTime | Date | 更新时间 |
|
||||||
|
| isLiked | Boolean | 当前用户是否点赞 |
|
||||||
|
| isCollected | Boolean | 当前用户是否收藏 |
|
||||||
|
|
||||||
|
### ArticleListResponseDTO(文章列表响应)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | Long | 文章ID |
|
||||||
|
| title | String | 文章标题 |
|
||||||
|
| slug | String | 文章别名 |
|
||||||
|
| summary | String | 文章摘要 |
|
||||||
|
| coverImage | String | 封面图片URL |
|
||||||
|
| authorId | Long | 作者用户ID |
|
||||||
|
| authorName | String | 作者用户名 |
|
||||||
|
| authorAvatar | String | 作者头像 |
|
||||||
|
| categoryId | Long | 分类ID |
|
||||||
|
| categoryName | String | 分类名称 |
|
||||||
|
| tags | List\<TagResponseDTO\> | 标签列表 |
|
||||||
|
| viewCount | Integer | 浏览次数 |
|
||||||
|
| likeCount | Integer | 点赞数 |
|
||||||
|
| commentCount | Integer | 评论数 |
|
||||||
|
| collectCount | Integer | 收藏数 |
|
||||||
|
| isTop | Integer | 是否置顶 |
|
||||||
|
| isEssence | Integer | 是否精华 |
|
||||||
|
| publishTime | Date | 发布时间 |
|
||||||
|
| createTime | Date | 创建时间 |
|
||||||
|
| isLiked | Boolean | 当前用户是否点赞 |
|
||||||
|
| isCollected | Boolean | 当前用户是否收藏 |
|
||||||
|
|
||||||
|
### CommentResponseDTO(评论响应)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | Long | 评论ID |
|
||||||
|
| articleId | Long | 文章ID |
|
||||||
|
| userId | Long | 评论用户ID |
|
||||||
|
| userName | String | 评论用户名 |
|
||||||
|
| userAvatar | String | 评论用户头像 |
|
||||||
|
| parentId | Long | 父评论ID |
|
||||||
|
| replyToId | Long | 回复的评论ID |
|
||||||
|
| replyToName | String | 回复的用户名 |
|
||||||
|
| content | String | 评论内容 |
|
||||||
|
| contentHtml | String | 渲染后的HTML内容 |
|
||||||
|
| likeCount | Integer | 点赞数 |
|
||||||
|
| replyCount | Integer | 回复数 |
|
||||||
|
| isAuthor | Integer | 是否为作者评论 |
|
||||||
|
| status | Integer | 状态 |
|
||||||
|
| createTime | Date | 创建时间 |
|
||||||
|
| updateTime | Date | 更新时间 |
|
||||||
|
| isLiked | Boolean | 当前用户是否点赞 |
|
||||||
|
| children | List\<CommentResponseDTO\> | 子评论列表 |
|
||||||
|
|
||||||
|
### CategoryResponseDTO(分类响应)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | Long | 分类ID |
|
||||||
|
| name | String | 分类名称 |
|
||||||
|
| slug | String | 分类别名 |
|
||||||
|
| description | String | 分类描述 |
|
||||||
|
| icon | String | 分类图标 |
|
||||||
|
| sortOrder | Integer | 排序序号 |
|
||||||
|
| parentId | Long | 父分类ID |
|
||||||
|
| articleCount | Integer | 该分类下的文章数量 |
|
||||||
|
| isEnabled | Integer | 是否启用 |
|
||||||
|
| createTime | Date | 创建时间 |
|
||||||
|
| updateTime | Date | 更新时间 |
|
||||||
|
|
||||||
|
### TagResponseDTO(标签响应)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | Long | 标签ID |
|
||||||
|
| name | String | 标签名称 |
|
||||||
|
| slug | String | 标签别名 |
|
||||||
|
| description | String | 标签描述 |
|
||||||
|
| color | String | 标签颜色 |
|
||||||
|
| articleCount | Integer | 使用该标签的文章数量 |
|
||||||
|
| isEnabled | Integer | 是否启用 |
|
||||||
|
| createTime | Date | 创建时间 |
|
||||||
|
| updateTime | Date | 更新时间 |
|
||||||
@@ -77,6 +77,17 @@
|
|||||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Sentinel 流量控制-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!--Sentinel数据源 - 持久化规则到Nacos-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.csp</groupId>
|
||||||
|
<artifactId>sentinel-datasource-nacos</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- ==================== 测试 ==================== -->
|
<!-- ==================== 测试 ==================== -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -18,11 +18,21 @@ public enum ChainMarkEnums {
|
|||||||
*/
|
*/
|
||||||
QUESTION_UPDATE_PARAM_VERIFY_CHAIN("question_update_param_verify_chain", "题目更新参数校验责任链"),
|
QUESTION_UPDATE_PARAM_VERIFY_CHAIN("question_update_param_verify_chain", "题目更新参数校验责任链"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑参数校验(用户编辑)
|
||||||
|
*/
|
||||||
|
QUESTION_EDIT_PARAM_VERIFY_CHAIN("question_edit_param_verify_chain", "题目编辑参数校验责任链"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试用例创建参数校验
|
* 测试用例创建参数校验
|
||||||
*/
|
*/
|
||||||
TEST_CASE_CREATE_PARAM_VERIFY_CHAIN("test_case_create_param_verify_chain", "测试用例创建参数校验责任链"),
|
TEST_CASE_CREATE_PARAM_VERIFY_CHAIN("test_case_create_param_verify_chain", "测试用例创建参数校验责任链"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目提交参数校验
|
||||||
|
*/
|
||||||
|
QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN("question_submit_req_param_verify_chain", "题目提交参数校验责任链"),
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,10 +52,7 @@ public class QuestionController {
|
|||||||
@PathVariable("id") Long id,
|
@PathVariable("id") Long id,
|
||||||
@Parameter(description = "题目信息", required = true)
|
@Parameter(description = "题目信息", required = true)
|
||||||
@RequestBody @Valid QuestionEditRequestDTO request) {
|
@RequestBody @Valid QuestionEditRequestDTO request) {
|
||||||
Question question = new Question();
|
questionService.updateQuestionWithChain(id, request);
|
||||||
BeanUtils.copyProperties(request, question);
|
|
||||||
question.setId(id);
|
|
||||||
questionService.updateQuestion(question);
|
|
||||||
return Results.success();
|
return Results.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import cn.meowrain.aioj.backend.framework.core.web.Results;
|
|||||||
import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit;
|
import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit;
|
||||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
import cn.meowrain.aioj.backend.question.service.QuestionSubmitService;
|
import cn.meowrain.aioj.backend.question.service.QuestionSubmitService;
|
||||||
|
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||||
|
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -30,17 +32,21 @@ public class QuestionSubmitController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "提交代码", description = "用户提交代码答案")
|
@Operation(summary = "提交代码", description = "用户提交代码答案")
|
||||||
|
@SentinelResource(value = "submit-question",blockHandler = "handleException")
|
||||||
public Result<Long> submitQuestion(
|
public Result<Long> submitQuestion(
|
||||||
@Parameter(description = "提交信息", required = true)
|
@Parameter(description = "提交信息", required = true)
|
||||||
@RequestBody @Valid QuestionSubmitRequestDTO request) {
|
@RequestBody @Valid QuestionSubmitRequestDTO request) {
|
||||||
QuestionSubmit questionSubmit = new QuestionSubmit();
|
QuestionSubmit questionSubmit = new QuestionSubmit();
|
||||||
BeanUtils.copyProperties(request, questionSubmit);
|
BeanUtils.copyProperties(request, questionSubmit);
|
||||||
// 设置初始状态为待判题
|
|
||||||
questionSubmit.setStatus(0);
|
|
||||||
Long submitId = questionSubmitService.createSubmit(questionSubmit);
|
Long submitId = questionSubmitService.createSubmit(questionSubmit);
|
||||||
return Results.success(submitId);
|
return Results.success(submitId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String handleException(BlockException ex) {
|
||||||
|
System.out.println("被限流了: " + ex.getClass().getCanonicalName());
|
||||||
|
return "系统繁忙,请稍后再试!(这是自定义的限流提示)";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取提交详情
|
* 获取提交详情
|
||||||
* GET /v1/question-submits/{id}
|
* GET /v1/question-submits/{id}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户代码校验责任链处理器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class CodeVerifyChain implements AbstractChianHandler<QuestionSubmitRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小代码长度(字符数)
|
||||||
|
*/
|
||||||
|
private static final int MIN_CODE_LENGTH = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大代码长度(字符数)- 10KB
|
||||||
|
*/
|
||||||
|
private static final int MAX_CODE_LENGTH = 10240;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionSubmitRequestDTO requestParam) {
|
||||||
|
String code = requestParam.getCode();
|
||||||
|
|
||||||
|
// 校验代码不为空
|
||||||
|
if (StringUtils.isBlank(code)) {
|
||||||
|
throw new ClientException("代码不能为空", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除首尾空白后重新校验
|
||||||
|
String trimmedCode = code.trim();
|
||||||
|
if (trimmedCode.isEmpty()) {
|
||||||
|
throw new ClientException("代码不能为空", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验代码长度
|
||||||
|
if (trimmedCode.length() < MIN_CODE_LENGTH) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("代码长度不能少于 %d 个字符", MIN_CODE_LENGTH),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedCode.length() > MAX_CODE_LENGTH) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("代码长度不能超过 %d 个字符", MAX_CODE_LENGTH),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全检查:检测危险代码模式(根据需要扩展)
|
||||||
|
String[] dangerousPatterns = {
|
||||||
|
"Runtime.getRuntime().exec",
|
||||||
|
"ProcessBuilder",
|
||||||
|
"System.exec",
|
||||||
|
"<script",
|
||||||
|
"eval("
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String pattern : dangerousPatterns) {
|
||||||
|
if (trimmedCode.contains(pattern)) {
|
||||||
|
log.warn("检测到危险代码模式: {}", pattern);
|
||||||
|
// 根据业务需求,可以选择:
|
||||||
|
// 1. 直接拒绝:throw new ClientException("代码包含危险操作", ErrorCode.FORBIDDEN_ERROR);
|
||||||
|
// 2. 记录警告但允许通过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("代码校验通过,代码长度: {} 字符", trimmedCode.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编程语言校验责任链处理器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class LanguageVerifyChain implements AbstractChianHandler<QuestionSubmitRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的编程语言列表
|
||||||
|
*/
|
||||||
|
private static final List<String> SUPPORTED_LANGUAGES = Arrays.asList(
|
||||||
|
"java",
|
||||||
|
"cpp",
|
||||||
|
"python",
|
||||||
|
"go",
|
||||||
|
"javascript",
|
||||||
|
"c",
|
||||||
|
"csharp",
|
||||||
|
"rust",
|
||||||
|
"php",
|
||||||
|
"swift",
|
||||||
|
"kotlin",
|
||||||
|
"typescript",
|
||||||
|
"ruby",
|
||||||
|
"shell"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionSubmitRequestDTO requestParam) {
|
||||||
|
String language = requestParam.getLanguage();
|
||||||
|
|
||||||
|
// 校验语言不为空
|
||||||
|
if (StringUtils.isBlank(language)) {
|
||||||
|
throw new ClientException("编程语言不能为空", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验语言是否支持(不区分大小写)
|
||||||
|
String normalizedLanguage = language.toLowerCase().trim();
|
||||||
|
if (!SUPPORTED_LANGUAGES.contains(normalizedLanguage)) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("不支持的编程语言: %s,支持的语言: %s",
|
||||||
|
language,
|
||||||
|
String.join(", ", SUPPORTED_LANGUAGES)),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("编程语言校验通过: {}", normalizedLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑时内容校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionEditContentVerifyChain implements AbstractChianHandler<QuestionEditRequestDTO> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionEditRequestDTO requestParam) {
|
||||||
|
String content = requestParam.getContent();
|
||||||
|
|
||||||
|
// 内容是可选的,如果为空则跳过校验
|
||||||
|
if (StringUtils.isBlank(content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了内容,则进行校验
|
||||||
|
if (content.length() < 20) {
|
||||||
|
throw new ClientException("题目内容过短,至少需要20个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length() > 10000) {
|
||||||
|
throw new ClientException("题目内容过长,最多支持10000个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目编辑内容校验通过");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_EDIT_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑时难度校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionEditDifficultyVerifyChain implements AbstractChianHandler<QuestionEditRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许的难度等级
|
||||||
|
*/
|
||||||
|
private static final List<String> ALLOWED_DIFFICULTIES = Arrays.asList("easy", "medium", "hard");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionEditRequestDTO requestParam) {
|
||||||
|
String difficulty = requestParam.getDifficulty();
|
||||||
|
|
||||||
|
// 难度是可选的,如果为空则跳过校验
|
||||||
|
if (StringUtils.isBlank(difficulty)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了难度,则进行校验
|
||||||
|
String normalizedDifficulty = difficulty.toLowerCase().trim();
|
||||||
|
if (!ALLOWED_DIFFICULTIES.contains(normalizedDifficulty)) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("题目难度必须是以下之一: %s", String.join(", ", ALLOWED_DIFFICULTIES)),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目编辑难度校验通过: {}", normalizedDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_EDIT_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.JudgeConfig;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑时判题配置校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionEditJudgeConfigVerifyChain implements AbstractChianHandler<QuestionEditRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认时间限制(毫秒)
|
||||||
|
*/
|
||||||
|
private static final Long DEFAULT_TIME_LIMIT = 3000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大时间限制(毫秒)- 10秒
|
||||||
|
*/
|
||||||
|
private static final Long MAX_TIME_LIMIT = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小时间限制(毫秒)
|
||||||
|
*/
|
||||||
|
private static final Long MIN_TIME_LIMIT = 100L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认内存限制(MB)
|
||||||
|
*/
|
||||||
|
private static final Long DEFAULT_MEMORY_LIMIT = 256L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大内存限制(MB)- 1GB
|
||||||
|
*/
|
||||||
|
private static final Long MAX_MEMORY_LIMIT = 1024L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小内存限制(MB)
|
||||||
|
*/
|
||||||
|
private static final Long MIN_MEMORY_LIMIT = 16L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionEditRequestDTO requestParam) {
|
||||||
|
JudgeConfig judgeConfig = requestParam.getJudgeConfig();
|
||||||
|
|
||||||
|
// 判题配置是可选的,如果为空则跳过校验
|
||||||
|
if (judgeConfig == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了判题配置,则进行校验
|
||||||
|
Long timeLimit = judgeConfig.getTimeLimit();
|
||||||
|
if (timeLimit != null) {
|
||||||
|
if (timeLimit < MIN_TIME_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("时间限制不能小于 %d 毫秒", MIN_TIME_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (timeLimit > MAX_TIME_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("时间限制不能大于 %d 毫秒", MAX_TIME_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Long memoryLimit = judgeConfig.getMemoryLimit();
|
||||||
|
if (memoryLimit != null) {
|
||||||
|
if (memoryLimit < MIN_MEMORY_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("内存限制不能小于 %d MB", MIN_MEMORY_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (memoryLimit > MAX_MEMORY_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("内存限制不能大于 %d MB", MAX_MEMORY_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目编辑判题配置校验通过: timeLimit={}ms, memoryLimit={}MB",
|
||||||
|
timeLimit != null ? timeLimit : DEFAULT_TIME_LIMIT,
|
||||||
|
memoryLimit != null ? memoryLimit : DEFAULT_MEMORY_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_EDIT_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑时标签校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionEditTagsVerifyChain implements AbstractChianHandler<QuestionEditRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大标签数量
|
||||||
|
*/
|
||||||
|
private static final int MAX_TAGS_COUNT = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个标签最大长度
|
||||||
|
*/
|
||||||
|
private static final int MAX_TAG_LENGTH = 20;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionEditRequestDTO requestParam) {
|
||||||
|
List<String> tags = requestParam.getTags();
|
||||||
|
|
||||||
|
// 标签是可选的,如果为空则跳过校验
|
||||||
|
if (tags == null || tags.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了标签,则进行校验
|
||||||
|
if (tags.size() > MAX_TAGS_COUNT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("标签数量不能超过 %d 个", MAX_TAGS_COUNT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String tag : tags) {
|
||||||
|
if (StringUtils.isBlank(tag)) {
|
||||||
|
throw new ClientException("标签不能为空", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.trim().length() > MAX_TAG_LENGTH) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("标签长度不能超过 %d 个字符", MAX_TAG_LENGTH),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.contains(",") || tag.contains(";") || tag.contains("|")) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("标签 '%s' 包含非法字符", tag),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目编辑标签校验通过,共 {} 个标签", tags.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_EDIT_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑时标题校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionEditTitleVerifyChain implements AbstractChianHandler<QuestionEditRequestDTO> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionEditRequestDTO requestParam) {
|
||||||
|
String title = requestParam.getTitle();
|
||||||
|
|
||||||
|
// 标题是可选的,如果为空则跳过校验
|
||||||
|
if (StringUtils.isBlank(title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了标题,则进行校验
|
||||||
|
if (title.length() < 2) {
|
||||||
|
throw new ClientException("题目标题长度不能少于2个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.length() > 100) {
|
||||||
|
throw new ClientException("题目标题长度不能超过100个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目编辑标题校验通过: {}", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_EDIT_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
|
import cn.meowrain.aioj.backend.question.service.QuestionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目存在性校验责任链处理器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuestionExistVerifyChain implements AbstractChianHandler<QuestionSubmitRequestDTO> {
|
||||||
|
|
||||||
|
private final QuestionService questionService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionSubmitRequestDTO requestParam) {
|
||||||
|
Long questionId = requestParam.getQuestionId();
|
||||||
|
|
||||||
|
// 校验题目是否存在
|
||||||
|
boolean exists = questionService.lambdaQuery()
|
||||||
|
.eq(cn.meowrain.aioj.backend.question.dao.entity.Question::getId, questionId)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
throw new ClientException("题目不存在,题目ID: " + questionId, ErrorCode.NOT_FOUND_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目存在性校验通过,题目ID: {}", questionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dao.entity.Question;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
|
import cn.meowrain.aioj.backend.question.service.QuestionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目状态校验责任链处理器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuestionStatusVerifyChain implements AbstractChianHandler<QuestionSubmitRequestDTO> {
|
||||||
|
|
||||||
|
private final QuestionService questionService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionSubmitRequestDTO requestParam) {
|
||||||
|
Long questionId = requestParam.getQuestionId();
|
||||||
|
|
||||||
|
// 查询题目详情
|
||||||
|
Question question = questionService.getById(questionId);
|
||||||
|
|
||||||
|
if (question == null) {
|
||||||
|
throw new ClientException("题目不存在", ErrorCode.NOT_FOUND_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验题目是否可用(未删除、状态正常)
|
||||||
|
if (question.getIsDelete() != null && question.getIsDelete() == 1) {
|
||||||
|
throw new ClientException("题目已被删除,无法提交", ErrorCode.FORBIDDEN_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以添加更多状态校验,比如题目是否草稿状态、是否暂停提交等
|
||||||
|
// if (question.getStatus() != null && question.getStatus() != 1) {
|
||||||
|
// throw new ClientException("题目当前不可用", ErrorCode.FORBIDDEN_ERROR);
|
||||||
|
// }
|
||||||
|
|
||||||
|
log.debug("题目状态校验通过,题目ID: {}", questionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新时内容校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionUpdateContentVerifyChain implements AbstractChianHandler<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionUpdateRequestDTO requestParam) {
|
||||||
|
String content = requestParam.getContent();
|
||||||
|
|
||||||
|
// 内容是可选的,如果为空则跳过校验
|
||||||
|
if (StringUtils.isBlank(content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了内容,则进行校验
|
||||||
|
if (content.length() < 20) {
|
||||||
|
throw new ClientException("题目内容过短,至少需要20个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length() > 10000) {
|
||||||
|
throw new ClientException("题目内容过长,最多支持10000个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目更新内容校验通过");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_UPDATE_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新时难度校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionUpdateDifficultyVerifyChain implements AbstractChianHandler<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许的难度等级
|
||||||
|
*/
|
||||||
|
private static final List<String> ALLOWED_DIFFICULTIES = Arrays.asList("easy", "medium", "hard");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionUpdateRequestDTO requestParam) {
|
||||||
|
String difficulty = requestParam.getDifficulty();
|
||||||
|
|
||||||
|
// 难度是可选的,如果为空则跳过校验
|
||||||
|
if (StringUtils.isBlank(difficulty)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了难度,则进行校验
|
||||||
|
String normalizedDifficulty = difficulty.toLowerCase().trim();
|
||||||
|
if (!ALLOWED_DIFFICULTIES.contains(normalizedDifficulty)) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("题目难度必须是以下之一: %s", String.join(", ", ALLOWED_DIFFICULTIES)),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目更新难度校验通过: {}", normalizedDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_UPDATE_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import cn.meowrain.aioj.backend.question.service.QuestionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新时题目存在性校验
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuestionUpdateExistVerifyChain implements AbstractChianHandler<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
private final QuestionService questionService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionUpdateRequestDTO requestParam) {
|
||||||
|
Long questionId = requestParam.getId();
|
||||||
|
|
||||||
|
// 校验题目ID不为空
|
||||||
|
if (questionId == null) {
|
||||||
|
throw new ClientException("题目ID不能为空", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验题目是否存在
|
||||||
|
boolean exists = questionService.lambdaQuery()
|
||||||
|
.eq(cn.meowrain.aioj.backend.question.dao.entity.Question::getId, questionId)
|
||||||
|
.exists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
throw new ClientException("题目不存在,无法更新,题目ID: " + questionId, ErrorCode.NOT_FOUND_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目更新存在性校验通过,题目ID: {}", questionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_UPDATE_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.JudgeConfig;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新时判题配置校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionUpdateJudgeConfigVerifyChain implements AbstractChianHandler<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认时间限制(毫秒)
|
||||||
|
*/
|
||||||
|
private static final Long DEFAULT_TIME_LIMIT = 3000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大时间限制(毫秒)- 10秒
|
||||||
|
*/
|
||||||
|
private static final Long MAX_TIME_LIMIT = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小时间限制(毫秒)
|
||||||
|
*/
|
||||||
|
private static final Long MIN_TIME_LIMIT = 100L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认内存限制(MB)
|
||||||
|
*/
|
||||||
|
private static final Long DEFAULT_MEMORY_LIMIT = 256L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大内存限制(MB)- 1GB
|
||||||
|
*/
|
||||||
|
private static final Long MAX_MEMORY_LIMIT = 1024L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小内存限制(MB)
|
||||||
|
*/
|
||||||
|
private static final Long MIN_MEMORY_LIMIT = 16L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionUpdateRequestDTO requestParam) {
|
||||||
|
JudgeConfig judgeConfig = requestParam.getJudgeConfig();
|
||||||
|
|
||||||
|
// 判题配置是可选的,如果为空则跳过校验
|
||||||
|
if (judgeConfig == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了判题配置,则进行校验
|
||||||
|
Long timeLimit = judgeConfig.getTimeLimit();
|
||||||
|
if (timeLimit != null) {
|
||||||
|
if (timeLimit < MIN_TIME_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("时间限制不能小于 %d 毫秒", MIN_TIME_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (timeLimit > MAX_TIME_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("时间限制不能大于 %d 毫秒", MAX_TIME_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Long memoryLimit = judgeConfig.getMemoryLimit();
|
||||||
|
if (memoryLimit != null) {
|
||||||
|
if (memoryLimit < MIN_MEMORY_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("内存限制不能小于 %d MB", MIN_MEMORY_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (memoryLimit > MAX_MEMORY_LIMIT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("内存限制不能大于 %d MB", MAX_MEMORY_LIMIT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目更新判题配置校验通过: timeLimit={}ms, memoryLimit={}MB",
|
||||||
|
timeLimit != null ? timeLimit : DEFAULT_TIME_LIMIT,
|
||||||
|
memoryLimit != null ? memoryLimit : DEFAULT_MEMORY_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_UPDATE_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新时标签校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionUpdateTagsVerifyChain implements AbstractChianHandler<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大标签数量
|
||||||
|
*/
|
||||||
|
private static final int MAX_TAGS_COUNT = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个标签最大长度
|
||||||
|
*/
|
||||||
|
private static final int MAX_TAG_LENGTH = 20;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionUpdateRequestDTO requestParam) {
|
||||||
|
List<String> tags = requestParam.getTags();
|
||||||
|
|
||||||
|
// 标签是可选的,如果为空则跳过校验
|
||||||
|
if (tags == null || tags.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了标签,则进行校验
|
||||||
|
if (tags.size() > MAX_TAGS_COUNT) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("标签数量不能超过 %d 个", MAX_TAGS_COUNT),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String tag : tags) {
|
||||||
|
if (StringUtils.isBlank(tag)) {
|
||||||
|
throw new ClientException("标签不能为空", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.trim().length() > MAX_TAG_LENGTH) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("标签长度不能超过 %d 个字符", MAX_TAG_LENGTH),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.contains(",") || tag.contains(";") || tag.contains("|")) {
|
||||||
|
throw new ClientException(
|
||||||
|
String.format("标签 '%s' 包含非法字符", tag),
|
||||||
|
ErrorCode.PARAMS_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目更新标签校验通过,共 {} 个标签", tags.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_UPDATE_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新时标题校验(可选字段)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QuestionUpdateTitleVerifyChain implements AbstractChianHandler<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(QuestionUpdateRequestDTO requestParam) {
|
||||||
|
String title = requestParam.getTitle();
|
||||||
|
|
||||||
|
// 标题是可选的,如果为空则跳过校验
|
||||||
|
if (StringUtils.isBlank(title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了标题,则进行校验
|
||||||
|
if (title.length() < 2) {
|
||||||
|
throw new ClientException("题目标题长度不能少于2个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.length() > 100) {
|
||||||
|
throw new ClientException("题目标题长度不能超过100个字符", ErrorCode.PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("题目更新标题校验通过: {}", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String mark() {
|
||||||
|
return ChainMarkEnums.QUESTION_UPDATE_PARAM_VERIFY_CHAIN.getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains.context;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.CommonChainContext;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目编辑参数校验责任链上下文(用户编辑)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class QuestionEditRequestParamVerifyContext extends CommonChainContext<QuestionEditRequestDTO> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains.context;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.CommonChainContext;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目提交参数校验责任链上下文
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class QuestionSubmitRequestParamVerifyContext extends CommonChainContext<QuestionSubmitRequestDTO> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package cn.meowrain.aioj.backend.question.dto.chains.context;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.CommonChainContext;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionUpdateRequestDTO;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目更新参数校验责任链上下文
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class QuestionUpdateRequestParamVerifyContext extends CommonChainContext<QuestionUpdateRequestDTO> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,14 @@ public interface QuestionService extends IService<Question> {
|
|||||||
*/
|
*/
|
||||||
Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO);
|
Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新题目(使用责任链校验)
|
||||||
|
* @param questionId 题目ID
|
||||||
|
* @param requestDTO 题目编辑请求DTO
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
Boolean updateQuestionWithChain(Long questionId, cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO requestDTO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建题目
|
* 创建题目
|
||||||
* @param question 题目信息
|
* @param question 题目信息
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
|||||||
import cn.meowrain.aioj.backend.question.dao.entity.Question;
|
import cn.meowrain.aioj.backend.question.dao.entity.Question;
|
||||||
import cn.meowrain.aioj.backend.question.dao.mapper.QuestionMapper;
|
import cn.meowrain.aioj.backend.question.dao.mapper.QuestionMapper;
|
||||||
import cn.meowrain.aioj.backend.question.dto.chains.context.QuestionCreateRequestParamVerifyContext;
|
import cn.meowrain.aioj.backend.question.dto.chains.context.QuestionCreateRequestParamVerifyContext;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.chains.context.QuestionEditRequestParamVerifyContext;
|
||||||
import cn.meowrain.aioj.backend.question.dto.req.*;
|
import cn.meowrain.aioj.backend.question.dto.req.*;
|
||||||
import cn.meowrain.aioj.backend.question.dto.resp.QuestionResponseDTO;
|
import cn.meowrain.aioj.backend.question.dto.resp.QuestionResponseDTO;
|
||||||
import cn.meowrain.aioj.backend.question.service.QuestionService;
|
import cn.meowrain.aioj.backend.question.service.QuestionService;
|
||||||
@@ -17,6 +18,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,8 +31,10 @@ import java.util.List;
|
|||||||
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
|
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
|
||||||
|
|
||||||
private final QuestionCreateRequestParamVerifyContext questionCreateChainContext;
|
private final QuestionCreateRequestParamVerifyContext questionCreateChainContext;
|
||||||
|
private final QuestionEditRequestParamVerifyContext questionEditChainContext;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO) {
|
public Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO) {
|
||||||
// 执行责任链校验
|
// 执行责任链校验
|
||||||
log.info("开始执行题目创建责任链校验");
|
log.info("开始执行题目创建责任链校验");
|
||||||
@@ -79,17 +84,86 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Long createQuestion(Question question) {
|
public Long createQuestion(Question question) {
|
||||||
this.save(question);
|
this.save(question);
|
||||||
return question.getId();
|
return question.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Boolean updateQuestion(Question question) {
|
public Boolean updateQuestion(Question question) {
|
||||||
return this.updateById(question);
|
return this.updateById(question);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean updateQuestionWithChain(Long questionId, QuestionEditRequestDTO requestDTO) {
|
||||||
|
// 先检查题目是否存在
|
||||||
|
Question existingQuestion = this.getById(questionId);
|
||||||
|
if (existingQuestion == null) {
|
||||||
|
throw new cn.meowrain.aioj.backend.framework.core.exception.ClientException(
|
||||||
|
"题目不存在,题目ID: " + questionId,
|
||||||
|
cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode.NOT_FOUND_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行责任链校验
|
||||||
|
log.info("开始执行题目编辑责任链校验,题目ID: {}", questionId);
|
||||||
|
questionEditChainContext.handler(
|
||||||
|
ChainMarkEnums.QUESTION_EDIT_PARAM_VERIFY_CHAIN.getMark(),
|
||||||
|
requestDTO
|
||||||
|
);
|
||||||
|
log.info("题目编辑责任链校验通过");
|
||||||
|
|
||||||
|
// 校验通过,更新题目(只更新非空字段)
|
||||||
|
Question questionToUpdate = new Question();
|
||||||
|
questionToUpdate.setId(questionId);
|
||||||
|
|
||||||
|
// 使用 BeanUtils.copyProperties 的忽略空值特性
|
||||||
|
// 这里需要手动处理每个字段,因为 copyProperties 会覆盖 null 值
|
||||||
|
if (StringUtils.isNotBlank(requestDTO.getTitle())) {
|
||||||
|
questionToUpdate.setTitle(requestDTO.getTitle());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(requestDTO.getContent())) {
|
||||||
|
questionToUpdate.setContent(requestDTO.getContent());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(requestDTO.getDifficulty())) {
|
||||||
|
questionToUpdate.setDifficulty(requestDTO.getDifficulty());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(requestDTO.getAnswer())) {
|
||||||
|
questionToUpdate.setAnswer(requestDTO.getAnswer());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理复杂字段
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
if (requestDTO.getTags() != null && !requestDTO.getTags().isEmpty()) {
|
||||||
|
try {
|
||||||
|
questionToUpdate.setTags(mapper.writeValueAsString(requestDTO.getTags()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("序列化 tags 失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requestDTO.getJudgeConfig() != null) {
|
||||||
|
try {
|
||||||
|
questionToUpdate.setJudgeConfig(mapper.writeValueAsString(requestDTO.getJudgeConfig()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("序列化 judgeConfig 失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requestDTO.getJudgeCase() != null && !requestDTO.getJudgeCase().isEmpty()) {
|
||||||
|
try {
|
||||||
|
questionToUpdate.setJudgeCase(mapper.writeValueAsString(requestDTO.getJudgeCase()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("序列化 judgeCase 失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.updateById(questionToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Boolean deleteQuestion(Long questionId) {
|
public Boolean deleteQuestion(Long questionId) {
|
||||||
return this.removeById(questionId);
|
return this.removeById(questionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,69 @@
|
|||||||
package cn.meowrain.aioj.backend.question.service.impl;
|
package cn.meowrain.aioj.backend.question.service.impl;
|
||||||
|
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||||
|
import cn.meowrain.aioj.backend.framework.core.exception.ClientException;
|
||||||
|
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||||
import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit;
|
import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit;
|
||||||
import cn.meowrain.aioj.backend.question.dao.mapper.QuestionSubmitMapper;
|
import cn.meowrain.aioj.backend.question.dao.mapper.QuestionSubmitMapper;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.chains.context.QuestionSubmitRequestParamVerifyContext;
|
||||||
|
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||||
import cn.meowrain.aioj.backend.question.service.QuestionSubmitService;
|
import cn.meowrain.aioj.backend.question.service.QuestionSubmitService;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 题目提交服务实现
|
* 题目提交服务实现
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class QuestionSubmitServiceImpl extends ServiceImpl<QuestionSubmitMapper, QuestionSubmit> implements QuestionSubmitService {
|
public class QuestionSubmitServiceImpl extends ServiceImpl<QuestionSubmitMapper, QuestionSubmit> implements QuestionSubmitService {
|
||||||
|
|
||||||
@Override
|
private final QuestionSubmitRequestParamVerifyContext submitChainContext;
|
||||||
public Long createSubmit(QuestionSubmit questionSubmit) {
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
@Override
|
||||||
|
public Long createSubmit(QuestionSubmit questionSubmit) {
|
||||||
|
return createSubmitWithChain(questionSubmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用责任链模式创建题目提交
|
||||||
|
*/
|
||||||
|
private Long createSubmitWithChain(QuestionSubmit questionSubmit) {
|
||||||
|
// 将 QuestionSubmit 转换为 QuestionSubmitRequestDTO 用于责任链校验
|
||||||
|
QuestionSubmitRequestDTO requestDTO = new QuestionSubmitRequestDTO();
|
||||||
|
requestDTO.setQuestionId(questionSubmit.getQuestionId());
|
||||||
|
requestDTO.setLanguage(questionSubmit.getLanguage());
|
||||||
|
requestDTO.setCode(questionSubmit.getCode());
|
||||||
|
|
||||||
|
// 执行责任链校验
|
||||||
|
log.info("开始执行题目提交责任链校验,题目ID: {}", questionSubmit.getQuestionId());
|
||||||
|
submitChainContext.handler(
|
||||||
|
ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark(),
|
||||||
|
requestDTO
|
||||||
|
);
|
||||||
|
log.info("题目提交责任链校验通过");
|
||||||
|
|
||||||
|
// 校验通过,保存提交记录
|
||||||
|
// 设置初始状态:0 - 待判题
|
||||||
|
questionSubmit.setStatus(0);
|
||||||
|
|
||||||
this.save(questionSubmit);
|
this.save(questionSubmit);
|
||||||
return questionSubmit.getId();
|
return questionSubmit.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean updateSubmitStatus(QuestionSubmit questionSubmit) {
|
public Boolean updateSubmitStatus(QuestionSubmit questionSubmit) {
|
||||||
return this.updateById(questionSubmit);
|
return this.updateById(questionSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public QuestionSubmit getSubmitById(Long submitId) {
|
public QuestionSubmit getSubmitById(Long submitId) {
|
||||||
return this.getById(submitId);
|
return this.getById(submitId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,14 @@ spring:
|
|||||||
server-addr: 10.0.0.10:8848
|
server-addr: 10.0.0.10:8848
|
||||||
username: nacos
|
username: nacos
|
||||||
password: nacos
|
password: nacos
|
||||||
|
sentinel:
|
||||||
|
transport:
|
||||||
|
dashboard: 10.0.0.10:8081
|
||||||
|
port: 8719
|
||||||
|
client-ip: 10.0.0.1
|
||||||
|
datasource:
|
||||||
|
flow:
|
||||||
|
nacos:
|
||||||
|
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||||
|
data-id: ${spring.application.name}-flow-rules
|
||||||
|
rule-type: flow
|
||||||
|
|||||||
Reference in New Issue
Block a user