diff --git a/aioj-backend-blog-service/接口文档.md b/aioj-backend-blog-service/接口文档.md new file mode 100644 index 0000000..721d279 --- /dev/null +++ b/aioj-backend-blog-service/接口文档.md @@ -0,0 +1,708 @@ +# AIOJ 博客服务接口文档 + +## 服务信息 + +- **服务名称**: aioj-blog-service +- **服务端口**: 18086 +- **基础路径**: `/api` +- **API 文档**: `/swagger-ui.html` + +## 通用说明 + +### 统一响应格式 + +所有接口均使用统一的响应格式 `Result`: + +```json +{ + "code": "0", // 响应码,"0"表示成功 + "message": "success", // 响应信息 + "data": {} // 响应数据 +} +``` + +### 分页响应格式 + +分页查询返回 `IPage` 格式: + +```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\ | 否 | 标签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": "

欢迎使用...

", + "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": "

这篇文章写得很好!

", + "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\ | 是 | 通知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\ | 否 | 标签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\ | 标签列表 | +| 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\ | 标签列表 | +| 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\ | 子评论列表 | + +### 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 | 更新时间 | diff --git a/aioj-backend-question-service/pom.xml b/aioj-backend-question-service/pom.xml index e4590b1..4a53feb 100644 --- a/aioj-backend-question-service/pom.xml +++ b/aioj-backend-question-service/pom.xml @@ -77,6 +77,17 @@ spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + com.alibaba.csp + sentinel-datasource-nacos + + org.springframework.boot diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/ChainMarkEnums.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/ChainMarkEnums.java index 53466f5..9eb5664 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/ChainMarkEnums.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/common/enums/ChainMarkEnums.java @@ -18,11 +18,21 @@ public enum ChainMarkEnums { */ 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", "测试用例创建参数校验责任链"), + /** + * 题目提交参数校验 + */ + QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN("question_submit_req_param_verify_chain", "题目提交参数校验责任链"), + ; /** diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionController.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionController.java index 88936b3..dcc3d36 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionController.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionController.java @@ -52,10 +52,7 @@ public class QuestionController { @PathVariable("id") Long id, @Parameter(description = "题目信息", required = true) @RequestBody @Valid QuestionEditRequestDTO request) { - Question question = new Question(); - BeanUtils.copyProperties(request, question); - question.setId(id); - questionService.updateQuestion(question); + questionService.updateQuestionWithChain(id, request); return Results.success(); } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java index fe2d609..abaf9d6 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/controller/QuestionSubmitController.java @@ -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.dto.req.QuestionSubmitRequestDTO; 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.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -30,17 +32,21 @@ public class QuestionSubmitController { */ @PostMapping @Operation(summary = "提交代码", description = "用户提交代码答案") + @SentinelResource(value = "submit-question",blockHandler = "handleException") public Result submitQuestion( @Parameter(description = "提交信息", required = true) @RequestBody @Valid QuestionSubmitRequestDTO request) { QuestionSubmit questionSubmit = new QuestionSubmit(); BeanUtils.copyProperties(request, questionSubmit); - // 设置初始状态为待判题 - questionSubmit.setStatus(0); Long submitId = questionSubmitService.createSubmit(questionSubmit); return Results.success(submitId); } + public String handleException(BlockException ex) { + System.out.println("被限流了: " + ex.getClass().getCanonicalName()); + return "系统繁忙,请稍后再试!(这是自定义的限流提示)"; + } + /** * 获取提交详情 * GET /v1/question-submits/{id} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/CodeVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/CodeVerifyChain.java new file mode 100644 index 0000000..3e0963d --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/CodeVerifyChain.java @@ -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 { + + /** + * 最小代码长度(字符数) + */ + 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", + " { + + /** + * 支持的编程语言列表 + */ + private static final List 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditContentVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditContentVerifyChain.java new file mode 100644 index 0000000..80c2c7b --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditContentVerifyChain.java @@ -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 { + + @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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditDifficultyVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditDifficultyVerifyChain.java new file mode 100644 index 0000000..dd5d05b --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditDifficultyVerifyChain.java @@ -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 { + + /** + * 允许的难度等级 + */ + private static final List 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditJudgeConfigVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditJudgeConfigVerifyChain.java new file mode 100644 index 0000000..997ec5c --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditJudgeConfigVerifyChain.java @@ -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 { + + /** + * 默认时间限制(毫秒) + */ + 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditTagsVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditTagsVerifyChain.java new file mode 100644 index 0000000..086471c --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditTagsVerifyChain.java @@ -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 { + + /** + * 最大标签数量 + */ + private static final int MAX_TAGS_COUNT = 10; + + /** + * 单个标签最大长度 + */ + private static final int MAX_TAG_LENGTH = 20; + + @Override + public void handle(QuestionEditRequestDTO requestParam) { + List 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditTitleVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditTitleVerifyChain.java new file mode 100644 index 0000000..39433be --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionEditTitleVerifyChain.java @@ -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 { + + @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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionExistVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionExistVerifyChain.java new file mode 100644 index 0000000..39f4e1d --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionExistVerifyChain.java @@ -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 { + + 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionStatusVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionStatusVerifyChain.java new file mode 100644 index 0000000..2409c0a --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionStatusVerifyChain.java @@ -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 { + + 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateContentVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateContentVerifyChain.java new file mode 100644 index 0000000..9ea6215 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateContentVerifyChain.java @@ -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 { + + @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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateDifficultyVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateDifficultyVerifyChain.java new file mode 100644 index 0000000..e4fb33c --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateDifficultyVerifyChain.java @@ -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 { + + /** + * 允许的难度等级 + */ + private static final List 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateExistVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateExistVerifyChain.java new file mode 100644 index 0000000..2bfd67e --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateExistVerifyChain.java @@ -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 { + + 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateJudgeConfigVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateJudgeConfigVerifyChain.java new file mode 100644 index 0000000..bfc48e8 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateJudgeConfigVerifyChain.java @@ -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 { + + /** + * 默认时间限制(毫秒) + */ + 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateTagsVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateTagsVerifyChain.java new file mode 100644 index 0000000..683bfc4 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateTagsVerifyChain.java @@ -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 { + + /** + * 最大标签数量 + */ + private static final int MAX_TAGS_COUNT = 10; + + /** + * 单个标签最大长度 + */ + private static final int MAX_TAG_LENGTH = 20; + + @Override + public void handle(QuestionUpdateRequestDTO requestParam) { + List 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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateTitleVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateTitleVerifyChain.java new file mode 100644 index 0000000..2944cf8 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionUpdateTitleVerifyChain.java @@ -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 { + + @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; + } +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionEditRequestParamVerifyContext.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionEditRequestParamVerifyContext.java new file mode 100644 index 0000000..f9eeb4d --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionEditRequestParamVerifyContext.java @@ -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 { + +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionSubmitRequestParamVerifyContext.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionSubmitRequestParamVerifyContext.java new file mode 100644 index 0000000..864a7b3 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionSubmitRequestParamVerifyContext.java @@ -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 { + +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionUpdateRequestParamVerifyContext.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionUpdateRequestParamVerifyContext.java new file mode 100644 index 0000000..b5236b7 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionUpdateRequestParamVerifyContext.java @@ -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 { + +} diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionService.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionService.java index 657fd4b..db63565 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionService.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/QuestionService.java @@ -20,6 +20,14 @@ public interface QuestionService extends IService { */ 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 题目信息 diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java index 94debcc..14ec64f 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionServiceImpl.java @@ -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.mapper.QuestionMapper; 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.resp.QuestionResponseDTO; 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.stereotype.Service; import com.fasterxml.jackson.core.type.TypeReference; +import org.springframework.transaction.annotation.Transactional; + import java.util.List; /** @@ -28,8 +31,10 @@ import java.util.List; public class QuestionServiceImpl extends ServiceImpl implements QuestionService { private final QuestionCreateRequestParamVerifyContext questionCreateChainContext; + private final QuestionEditRequestParamVerifyContext questionEditChainContext; @Override + @Transactional(rollbackFor = Exception.class) public Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO) { // 执行责任链校验 log.info("开始执行题目创建责任链校验"); @@ -79,17 +84,86 @@ public class QuestionServiceImpl extends ServiceImpl i } @Override + @Transactional(rollbackFor = Exception.class) public Long createQuestion(Question question) { this.save(question); return question.getId(); } @Override + @Transactional(rollbackFor = Exception.class) public Boolean updateQuestion(Question question) { return this.updateById(question); } @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) { return this.removeById(questionId); } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java index a637500..14d2cd0 100644 --- a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/service/impl/QuestionSubmitServiceImpl.java @@ -1,32 +1,69 @@ 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.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 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; /** * 题目提交服务实现 */ +@Slf4j @Service @RequiredArgsConstructor public class QuestionSubmitServiceImpl extends ServiceImpl implements QuestionSubmitService { - @Override - public Long createSubmit(QuestionSubmit questionSubmit) { + private final QuestionSubmitRequestParamVerifyContext submitChainContext; + + @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); return questionSubmit.getId(); } - @Override - public Boolean updateSubmitStatus(QuestionSubmit questionSubmit) { - return this.updateById(questionSubmit); - } + @Override + public Boolean updateSubmitStatus(QuestionSubmit questionSubmit) { + return this.updateById(questionSubmit); + } - @Override - public QuestionSubmit getSubmitById(Long submitId) { - return this.getById(submitId); - } + @Override + public QuestionSubmit getSubmitById(Long submitId) { + return this.getById(submitId); + } } diff --git a/aioj-backend-question-service/src/main/resources/application-dev.yml b/aioj-backend-question-service/src/main/resources/application-dev.yml index f186dc2..b4dbdf9 100644 --- a/aioj-backend-question-service/src/main/resources/application-dev.yml +++ b/aioj-backend-question-service/src/main/resources/application-dev.yml @@ -17,3 +17,14 @@ spring: server-addr: 10.0.0.10:8848 username: 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