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:
@@ -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.QuestionCreateRequestDTO;
|
||||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionEditRequestDTO;
|
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.req.QuestionQueryRequestDTO;
|
||||||
|
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;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -36,9 +37,7 @@ public class QuestionController {
|
|||||||
public Result<Long> createQuestion(
|
public Result<Long> createQuestion(
|
||||||
@Parameter(description = "题目信息", required = true)
|
@Parameter(description = "题目信息", required = true)
|
||||||
@RequestBody @Valid QuestionCreateRequestDTO request) {
|
@RequestBody @Valid QuestionCreateRequestDTO request) {
|
||||||
Question question = new Question();
|
Long questionId = questionService.createQuestionWithChain(request);
|
||||||
BeanUtils.copyProperties(request, question);
|
|
||||||
Long questionId = questionService.createQuestion(question);
|
|
||||||
return Results.success(questionId);
|
return Results.success(questionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +78,10 @@ public class QuestionController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "获取题目详情", description = "根据ID获取题目详情")
|
@Operation(summary = "获取题目详情", description = "根据ID获取题目详情")
|
||||||
public Result<Question> getQuestion(
|
public Result<QuestionResponseDTO> getQuestion(
|
||||||
@Parameter(description = "题目ID", required = true)
|
@Parameter(description = "题目ID", required = true)
|
||||||
@PathVariable("id") Long id) {
|
@PathVariable("id") Long id) {
|
||||||
Question question = questionService.getQuestionById(id);
|
QuestionResponseDTO question = questionService.getQuestionById(id);
|
||||||
return Results.success(question);
|
return Results.success(question);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +91,9 @@ public class QuestionController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询题目列表", description = "支持按标题、难度、标签等条件查询")
|
@Operation(summary = "分页查询题目列表", description = "支持按标题、难度、标签等条件查询")
|
||||||
public Result<Page<Question>> listQuestions(
|
public Result<Page<QuestionResponseDTO>> listQuestions(
|
||||||
@Parameter(description = "查询条件") QuestionQueryRequestDTO request) {
|
@Parameter(description = "查询条件") QuestionQueryRequestDTO request) {
|
||||||
Page<Question> page = questionService.listQuestions(request);
|
Page<QuestionResponseDTO> page = questionService.listQuestions(request);
|
||||||
return Results.success(page);
|
return Results.success(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user