refactor: use DTOs in QuestionController API responses

- Change getQuestion endpoint to return QuestionResponseDTO instead of Question entity
- Change listQuestions endpoint to return Page<QuestionResponseDTO> instead of Page<Question>
- Simplify createQuestion endpoint by using createQuestionWithChain method directly
- Add chain-related DTOs for question processing pipeline

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-26 21:58:06 +08:00
parent be709efa2e
commit c06cfc10ee
7 changed files with 377 additions and 7 deletions

View File

@@ -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<Long> 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<Question> getQuestion(
public Result<QuestionResponseDTO> 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<Page<Question>> listQuestions(
public Result<Page<QuestionResponseDTO>> listQuestions(
@Parameter(description = "查询条件") QuestionQueryRequestDTO request) {
Page<Question> page = questionService.listQuestions(request);
Page<QuestionResponseDTO> page = questionService.listQuestions(request);
return Results.success(page);
}

View File

@@ -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<QuestionCreateRequestDTO> {
@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;
}
}

View File

@@ -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<QuestionCreateRequestDTO> {
/**
* 允许的难度等级
*/
private static final List<String> 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;
}
}

View File

@@ -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<QuestionCreateRequestDTO> {
/**
* 默认时间限制(毫秒)
*/
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;
}
}

View File

@@ -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<QuestionCreateRequestDTO> {
/**
* 最大标签数量
*/
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<String> 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;
}
}

View File

@@ -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<QuestionCreateRequestDTO> {
@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;
}
}

View File

@@ -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<QuestionCreateRequestDTO> {
}