From c06cfc10ee807689c911a2669495847d541ce516 Mon Sep 17 00:00:00 2001 From: meowrain Date: Mon, 26 Jan 2026 21:58:06 +0800 Subject: [PATCH] refactor: use DTOs in QuestionController API responses - Change getQuestion endpoint to return QuestionResponseDTO instead of Question entity - Change listQuestions endpoint to return Page instead of Page - Simplify createQuestion endpoint by using createQuestionWithChain method directly - Add chain-related DTOs for question processing pipeline Co-Authored-By: Claude --- .../controller/QuestionController.java | 13 +-- .../chains/QuestionContentVerifyChain.java | 50 ++++++++ .../chains/QuestionDifficultyVerifyChain.java | 57 ++++++++++ .../QuestionJudgeConfigVerifyChain.java | 107 ++++++++++++++++++ .../dto/chains/QuestionTagsVerifyChain.java | 90 +++++++++++++++ .../dto/chains/QuestionTitleVerifyChain.java | 54 +++++++++ ...estionCreateRequestParamVerifyContext.java | 13 +++ 7 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionDifficultyVerifyChain.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionJudgeConfigVerifyChain.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionTagsVerifyChain.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionTitleVerifyChain.java create mode 100644 aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionCreateRequestParamVerifyContext.java 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 3c4deff..88936b3 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 @@ -6,6 +6,7 @@ import cn.meowrain.aioj.backend.question.dao.entity.Question; import cn.meowrain.aioj.backend.question.dto.req.QuestionCreateRequestDTO; import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO; import cn.meowrain.aioj.backend.question.dto.req.QuestionQueryRequestDTO; +import cn.meowrain.aioj.backend.question.dto.resp.QuestionResponseDTO; import cn.meowrain.aioj.backend.question.service.QuestionService; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; @@ -36,9 +37,7 @@ public class QuestionController { public Result createQuestion( @Parameter(description = "题目信息", required = true) @RequestBody @Valid QuestionCreateRequestDTO request) { - Question question = new Question(); - BeanUtils.copyProperties(request, question); - Long questionId = questionService.createQuestion(question); + Long questionId = questionService.createQuestionWithChain(request); return Results.success(questionId); } @@ -79,10 +78,10 @@ public class QuestionController { */ @GetMapping("/{id}") @Operation(summary = "获取题目详情", description = "根据ID获取题目详情") - public Result getQuestion( + public Result getQuestion( @Parameter(description = "题目ID", required = true) @PathVariable("id") Long id) { - Question question = questionService.getQuestionById(id); + QuestionResponseDTO question = questionService.getQuestionById(id); return Results.success(question); } @@ -92,9 +91,9 @@ public class QuestionController { */ @GetMapping @Operation(summary = "分页查询题目列表", description = "支持按标题、难度、标签等条件查询") - public Result> listQuestions( + public Result> listQuestions( @Parameter(description = "查询条件") QuestionQueryRequestDTO request) { - Page page = questionService.listQuestions(request); + Page page = questionService.listQuestions(request); return Results.success(page); } diff --git a/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java new file mode 100644 index 0000000..f1ee56d --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionContentVerifyChain.java @@ -0,0 +1,50 @@ +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.QuestionCreateRequestDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +/** + * 题目内容校验责任链处理器 + */ +@Component +@Slf4j +public class QuestionContentVerifyChain implements AbstractChianHandler { + + @Override + public void handle(QuestionCreateRequestDTO requestParam) { + String content = requestParam.getContent(); + + // 校验内容不为空 + if (StringUtils.isBlank(content)) { + throw new ClientException("题目内容不能为空", ErrorCode.PARAMS_ERROR); + } + + // 校验内容长度(至少20个字符) + if (content.length() < 20) { + throw new ClientException("题目内容过短,至少需要20个字符", ErrorCode.PARAMS_ERROR); + } + + // 校验内容长度(最多10000个字符) + if (content.length() > 10000) { + throw new ClientException("题目内容过长,最多支持10000个字符", ErrorCode.PARAMS_ERROR); + } + + log.debug("题目内容校验通过"); + } + + @Override + public String mark() { + return ChainMarkEnums.QUESTION_CREATE_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/QuestionDifficultyVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionDifficultyVerifyChain.java new file mode 100644 index 0000000..3dfaa9a --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionDifficultyVerifyChain.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.QuestionCreateRequestDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +/** + * 题目难度校验责任链处理器 + */ +@Component +@Slf4j +public class QuestionDifficultyVerifyChain implements AbstractChianHandler { + + /** + * 允许的难度等级 + */ + private static final List ALLOWED_DIFFICULTIES = Arrays.asList("easy", "medium", "hard"); + + @Override + public void handle(QuestionCreateRequestDTO requestParam) { + String difficulty = requestParam.getDifficulty(); + + // 校验难度不为空 + if (StringUtils.isBlank(difficulty)) { + throw new ClientException("题目难度不能为空", ErrorCode.PARAMS_ERROR); + } + + // 校验难度是否为允许的值(不区分大小写) + 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_CREATE_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/QuestionJudgeConfigVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionJudgeConfigVerifyChain.java new file mode 100644 index 0000000..77521a0 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionJudgeConfigVerifyChain.java @@ -0,0 +1,107 @@ +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.QuestionCreateRequestDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 题目判题配置校验责任链处理器 + */ +@Component +@Slf4j +public class QuestionJudgeConfigVerifyChain 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(QuestionCreateRequestDTO requestParam) { + JudgeConfig judgeConfig = requestParam.getJudgeConfig(); + + // 判题配置可以为空,创建时使用默认值 + if (judgeConfig == null) { + log.debug("判题配置为空,将使用默认值"); + 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_CREATE_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/QuestionTagsVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionTagsVerifyChain.java new file mode 100644 index 0000000..b769320 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionTagsVerifyChain.java @@ -0,0 +1,90 @@ +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.QuestionCreateRequestDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 题目标签校验责任链处理器 + */ +@Component +@Slf4j +public class QuestionTagsVerifyChain implements AbstractChianHandler { + + /** + * 最大标签数量 + */ + private static final int MAX_TAGS_COUNT = 10; + + /** + * 最小标签数量 + */ + private static final int MIN_TAGS_COUNT = 1; + + /** + * 单个标签最大长度 + */ + private static final int MAX_TAG_LENGTH = 20; + + @Override + public void handle(QuestionCreateRequestDTO requestParam) { + List tags = requestParam.getTags(); + + // 标签可以为空,但如果提供了则进行校验 + if (tags == null || tags.isEmpty()) { + log.debug("题目标签为空,跳过校验"); + 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_CREATE_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/QuestionTitleVerifyChain.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionTitleVerifyChain.java new file mode 100644 index 0000000..a032c8a --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/QuestionTitleVerifyChain.java @@ -0,0 +1,54 @@ +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.QuestionCreateRequestDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +/** + * 题目标题校验责任链处理器 + */ +@Component +@Slf4j +public class QuestionTitleVerifyChain implements AbstractChianHandler { + + @Override + public void handle(QuestionCreateRequestDTO requestParam) { + String title = requestParam.getTitle(); + + // 校验标题不为空(虽然 @NotBlank 已经处理,但责任链可以提供更详细的业务校验) + if (StringUtils.isBlank(title)) { + throw new ClientException("题目标题不能为空", ErrorCode.PARAMS_ERROR); + } + + // 校验标题长度 + if (title.length() < 2) { + throw new ClientException("题目标题长度不能少于2个字符", ErrorCode.PARAMS_ERROR); + } + + if (title.length() > 100) { + throw new ClientException("题目标题长度不能超过100个字符", ErrorCode.PARAMS_ERROR); + } + + // 可以添加更多业务规则,比如不允许包含特殊字符等 + // if (title.contains("[deleted]")) { + // throw new ClientException("题目标题包含非法字符", ErrorCode.PARAMS_ERROR); + // } + + log.debug("题目标题校验通过: {}", title); + } + + @Override + public String mark() { + return ChainMarkEnums.QUESTION_CREATE_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/context/QuestionCreateRequestParamVerifyContext.java b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionCreateRequestParamVerifyContext.java new file mode 100644 index 0000000..2f4be42 --- /dev/null +++ b/aioj-backend-question-service/src/main/java/cn/meowrain/aioj/backend/question/dto/chains/context/QuestionCreateRequestParamVerifyContext.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.QuestionCreateRequestDTO; +import org.springframework.stereotype.Component; + +/** + * 题目创建参数校验责任链上下文 + */ +@Component +public class QuestionCreateRequestParamVerifyContext extends CommonChainContext { + +}