feat: 添加管理员权限检查功能和Maven打包配置优化
主要更新: 1. 新增管理员权限检查功能 - 添加 UserRoleEnum 枚举类统一管理用户角色(USER, ADMIN, BAN) - 改进 ContextHolderUtils.isAdmin() 方法,支持不区分大小写的角色比较 - 更新 UserServiceImpl 使用枚举常量代替硬编码字符串 - 新增管理员权限使用指南文档 (docs/admin-permission-guide.md) 2. 修复Maven打包配置 - 配置根POM的spring-boot-maven-plugin默认跳过repackage - 为所有服务模块启用repackage,确保可以打包为可执行JAR - 修复公共库模块打包失败的问题 - 涉及服务:gateway, auth, user-service, question-service, file-service, blog-service, upms-biz, ai-service 3. 更新项目文档 - README.md:添加详细的打包说明、首次克隆准备工作、服务启动顺序等 - CLAUDE.md:更新项目架构说明和开发指南 4. 重构题目服务责任链结构 - 将责任链类按功能分类到 question/ 和 submit/ 子目录 - 新增 QuestionSubmitJudgeInfoEnum 和相关查询功能 - 改进题目提交服务的实现 5. 其他改进 - 添加 Feign Token 中继拦截器 - 更新 AsyncConfig 配置 - 优化 Jackson 和 Security 配置
This commit is contained in:
@@ -30,6 +30,11 @@ public class RedisKeyConstants {
|
||||
*/
|
||||
public static final String QUESTION_SUBMIT_CACHE_KEY_PREFIX = "question_submit:";
|
||||
|
||||
/**
|
||||
* 题目提交并发锁 Key 前缀(userId + questionId)
|
||||
*/
|
||||
public static final String QUESTION_SUBMIT_LOCK_KEY_PREFIX = "question_submit_lock:";
|
||||
|
||||
private RedisKeyConstants() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.meowrain.aioj.backend.question.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 判题信息消息枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum QuestionSubmitJudgeInfoEnum {
|
||||
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
ACCEPTED("Accepted", "成功"),
|
||||
|
||||
/**
|
||||
* 答案错误
|
||||
*/
|
||||
WRONG_ANSWER("Wrong Answer", "答案错误"),
|
||||
|
||||
/**
|
||||
* 编译错误
|
||||
*/
|
||||
COMPILE_ERROR("Compile Error", "编译错误"),
|
||||
|
||||
/**
|
||||
* 内存溢出
|
||||
*/
|
||||
MEMORY_LIMIT_EXCEEDED("Memory Limit Exceeded", "内存溢出"),
|
||||
|
||||
/**
|
||||
* 超时
|
||||
*/
|
||||
TIME_LIMIT_EXCEEDED("Time Limit Exceeded", "超时"),
|
||||
|
||||
/**
|
||||
* 展示错误
|
||||
*/
|
||||
PRESENTATION_ERROR("Presentation Error", "展示错误"),
|
||||
|
||||
/**
|
||||
* 输出溢出
|
||||
*/
|
||||
OUTPUT_LIMIT_EXCEEDED("Output Limit Exceeded", "输出溢出"),
|
||||
|
||||
/**
|
||||
* 等待中
|
||||
*/
|
||||
WAITING("Waiting", "等待中"),
|
||||
|
||||
/**
|
||||
* 危险操作
|
||||
*/
|
||||
DANGEROUS_OPERATION("Dangerous Operation", "危险操作"),
|
||||
|
||||
/**
|
||||
* 运行错误(用户程序的问题)
|
||||
*/
|
||||
RUNTIME_ERROR("Runtime Error", "运行错误(用户程序的问题)"),
|
||||
|
||||
/**
|
||||
* 系统错误(做系统人的问题)
|
||||
*/
|
||||
SYSTEM_ERROR("System Error", "系统错误(做系统人的问题)"),
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* 判题信息值
|
||||
*/
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String desc;
|
||||
|
||||
QuestionSubmitJudgeInfoEnum(String value, String desc) {
|
||||
this.value = value;
|
||||
this.desc = desc;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package cn.meowrain.aioj.backend.question.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 题目提交状态枚举
|
||||
*/
|
||||
@@ -44,4 +46,20 @@ public enum QuestionSubmitStatusEnum {
|
||||
this.value = value;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据value获取desc
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static String getDescByValue(Integer value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(QuestionSubmitStatusEnum.values())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.map(QuestionSubmitStatusEnum::getDesc)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
import cn.meowrain.aioj.backend.framework.core.web.Result;
|
||||
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.QuestionSubmitQueryRequestDTO;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||
import cn.meowrain.aioj.backend.question.dto.resp.QuestionSubmitResponseDTO;
|
||||
import cn.meowrain.aioj.backend.question.service.QuestionSubmitService;
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import com.alibaba.csp.sentinel.slots.block.BlockException;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -15,6 +19,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 题目提交管理控制器 - RESTful API
|
||||
@@ -46,8 +51,6 @@ public class QuestionSubmitController {
|
||||
public Result<Void> handleException(QuestionSubmitRequestDTO request, BlockException ex) {
|
||||
System.out.println("被限流了: " + ex.getClass().getCanonicalName());
|
||||
|
||||
// 假设你的 Results 工具类支持返回错误信息
|
||||
// 这里的 code (比如 429) 和 message 根据你的 Result 结构来定
|
||||
return Results.failure(ErrorCode.API_REQUEST_ERROR.code(),"系统繁忙,请稍后再试!(这是自定义的限流提示)");
|
||||
}
|
||||
|
||||
@@ -64,6 +67,14 @@ public class QuestionSubmitController {
|
||||
return Results.success(submit);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询",description = "根据用户id,题目id,查找提交记录")
|
||||
// 根据用户id,题目id,编程语言,题目状态,查找提交记录
|
||||
public Result<Page<QuestionSubmitResponseDTO>> getSubmitPage(
|
||||
@Parameter(description = "查询条件") QuestionSubmitQueryRequestDTO request) {
|
||||
Page<QuestionSubmitResponseDTO> dtoPage = questionSubmitService.listQuestionSubmits(request);
|
||||
return Results.success(dtoPage);
|
||||
}
|
||||
/**
|
||||
* 内部接口:更新提交状态
|
||||
* PATCH /v1/question-submits/{id}/status
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -48,3 +48,4 @@ public class QuestionContentVerifyChain implements AbstractChianHandler<Question
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -55,3 +55,4 @@ public class QuestionDifficultyVerifyChain implements AbstractChianHandler<Quest
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -47,3 +47,4 @@ public class QuestionEditContentVerifyChain implements AbstractChianHandler<Ques
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -55,3 +55,4 @@ public class QuestionEditDifficultyVerifyChain implements AbstractChianHandler<Q
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -103,3 +103,4 @@ public class QuestionEditJudgeConfigVerifyChain implements AbstractChianHandler<
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -78,3 +78,4 @@ public class QuestionEditTagsVerifyChain implements AbstractChianHandler<Questio
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -47,3 +47,4 @@ public class QuestionEditTitleVerifyChain implements AbstractChianHandler<Questi
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -105,3 +105,4 @@ public class QuestionJudgeConfigVerifyChain implements AbstractChianHandler<Ques
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -88,3 +88,4 @@ public class QuestionTagsVerifyChain implements AbstractChianHandler<QuestionCre
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -52,3 +52,4 @@ public class QuestionTitleVerifyChain implements AbstractChianHandler<QuestionCr
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -47,3 +47,4 @@ public class QuestionUpdateContentVerifyChain implements AbstractChianHandler<Qu
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -55,3 +55,4 @@ public class QuestionUpdateDifficultyVerifyChain implements AbstractChianHandler
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -51,3 +51,4 @@ public class QuestionUpdateExistVerifyChain implements AbstractChianHandler<Ques
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -103,3 +103,4 @@ public class QuestionUpdateJudgeConfigVerifyChain implements AbstractChianHandle
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -78,3 +78,4 @@ public class QuestionUpdateTagsVerifyChain implements AbstractChianHandler<Quest
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.question;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -47,3 +47,4 @@ public class QuestionUpdateTitleVerifyChain implements AbstractChianHandler<Ques
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -87,3 +87,4 @@ public class CodeVerifyChain implements AbstractChianHandler<QuestionSubmitReque
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
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.common.enums.LanguageEnum;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -11,6 +12,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 编程语言校验责任链处理器
|
||||
@@ -22,23 +24,9 @@ public class LanguageVerifyChain implements AbstractChianHandler<QuestionSubmitR
|
||||
/**
|
||||
* 支持的编程语言列表
|
||||
*/
|
||||
private static final List<String> SUPPORTED_LANGUAGES = Arrays.asList(
|
||||
"java",
|
||||
"cpp",
|
||||
"python",
|
||||
"go",
|
||||
"javascript",
|
||||
"c",
|
||||
"csharp",
|
||||
"rust",
|
||||
"php",
|
||||
"swift",
|
||||
"kotlin",
|
||||
"typescript",
|
||||
"ruby",
|
||||
"shell"
|
||||
);
|
||||
|
||||
private static final List<String> SUPPORTED_LANGUAGES = Arrays.stream(LanguageEnum.values())
|
||||
.map(LanguageEnum::getValue)
|
||||
.toList();
|
||||
@Override
|
||||
public void handle(QuestionSubmitRequestDTO requestParam) {
|
||||
String language = requestParam.getLanguage();
|
||||
@@ -72,3 +60,4 @@ public class LanguageVerifyChain implements AbstractChianHandler<QuestionSubmitR
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
import cn.meowrain.aioj.backend.framework.core.designpattern.chains.AbstractChianHandler;
|
||||
import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode;
|
||||
@@ -46,3 +46,4 @@ public class QuestionExistVerifyChain implements AbstractChianHandler<QuestionSu
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains;
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
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.common.enums.QuestionSubmitStatusEnum;
|
||||
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;
|
||||
@@ -55,3 +56,4 @@ public class QuestionStatusVerifyChain implements AbstractChianHandler<QuestionS
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.chains.submit;
|
||||
|
||||
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.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 题目提交 - 题目ID校验责任链处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class QuestionSubmitIdVerifyChain implements AbstractChianHandler<QuestionSubmitRequestDTO> {
|
||||
|
||||
@Override
|
||||
public void handle(QuestionSubmitRequestDTO requestParam) {
|
||||
Long questionId = requestParam.getQuestionId();
|
||||
if (questionId == null || questionId <= 0) {
|
||||
throw new ClientException("题目ID不能为空", ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
log.debug("题目ID校验通过,题目ID: {}", questionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String mark() {
|
||||
return ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.req;
|
||||
|
||||
import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 题目提交查询请求 DTO
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "题目提交查询请求")
|
||||
public class QuestionSubmitQueryRequestDTO extends Page<QuestionSubmit> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "题目ID", example = "1")
|
||||
private Long questionId;
|
||||
|
||||
@Schema(description = "编程语言",example = "go")
|
||||
private String language;
|
||||
|
||||
@Schema(description = "提交状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
|
||||
@Schema(description = "排序字段", example = "createTime")
|
||||
private String sortField;
|
||||
|
||||
@Schema(description = "排序方向", example = "desc", allowableValues = {"asc", "desc"})
|
||||
private String sortOrder;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package cn.meowrain.aioj.backend.question.dto.resp;
|
||||
|
||||
import cn.meowrain.aioj.backend.question.dto.req.JudgeInfo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 题目提交响应 DTO
|
||||
@@ -12,6 +14,31 @@ import java.io.Serializable;
|
||||
@Schema(description = "题目提交响应")
|
||||
public class QuestionSubmitResponseDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "提交ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "代码",example = "public class Main {}")
|
||||
private String code;
|
||||
@Schema(description = "编程语言", example = "java")
|
||||
private String language;
|
||||
|
||||
@Schema(description = "判题信息(JSON)")
|
||||
private JudgeInfo judgeInfo;
|
||||
|
||||
@Schema(description = "判题状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "题目ID", example = "1")
|
||||
private Long questionId;
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updateTime;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cn.meowrain.aioj.backend.question.service;
|
||||
|
||||
import cn.meowrain.aioj.backend.question.dao.entity.QuestionSubmit;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitQueryRequestDTO;
|
||||
import cn.meowrain.aioj.backend.question.dto.resp.QuestionSubmitResponseDTO;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
@@ -28,4 +31,11 @@ public interface QuestionSubmitService extends IService<QuestionSubmit> {
|
||||
* @return 提交记录
|
||||
*/
|
||||
QuestionSubmit getSubmitById(Long submitId);
|
||||
|
||||
/**
|
||||
* 分页查询题目提交记录
|
||||
* @param request 请求体
|
||||
* @return
|
||||
*/
|
||||
Page<QuestionSubmitResponseDTO> listQuestionSubmits(QuestionSubmitQueryRequestDTO request);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
||||
|
||||
private final QuestionCreateRequestParamVerifyContext questionCreateChainContext;
|
||||
private final QuestionEditRequestParamVerifyContext questionEditChainContext;
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createQuestionWithChain(QuestionCreateRequestDTO requestDTO) {
|
||||
@@ -136,7 +136,6 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
||||
}
|
||||
|
||||
// 处理复杂字段
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
if (requestDTO.getTags() != null && !requestDTO.getTags().isEmpty()) {
|
||||
try {
|
||||
questionToUpdate.setTags(mapper.writeValueAsString(requestDTO.getTags()));
|
||||
|
||||
@@ -2,19 +2,34 @@ 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.framework.core.exception.ServiceException;
|
||||
import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils;
|
||||
import cn.meowrain.aioj.backend.question.common.constants.RedisKeyConstants;
|
||||
import cn.meowrain.aioj.backend.question.common.enums.ChainMarkEnums;
|
||||
import cn.meowrain.aioj.backend.question.common.enums.QuestionSubmitStatusEnum;
|
||||
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.JudgeInfo;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitQueryRequestDTO;
|
||||
import cn.meowrain.aioj.backend.question.dto.req.QuestionSubmitRequestDTO;
|
||||
import cn.meowrain.aioj.backend.question.dto.resp.QuestionSubmitResponseDTO;
|
||||
import cn.meowrain.aioj.backend.question.service.QuestionSubmitService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 题目提交服务实现
|
||||
*/
|
||||
@@ -23,47 +38,183 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionSubmitServiceImpl extends ServiceImpl<QuestionSubmitMapper, QuestionSubmit> implements QuestionSubmitService {
|
||||
|
||||
private final QuestionSubmitRequestParamVerifyContext submitChainContext;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final QuestionSubmitRequestParamVerifyContext submitChainContext;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
private static final Duration SUBMIT_LOCK_TTL = Duration.ofMinutes(30);
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public Long createSubmit(QuestionSubmit questionSubmit) {
|
||||
return createSubmitWithChain(questionSubmit);
|
||||
// TODO: 接入微服务后从请求头中获取
|
||||
Long userId = 1L;
|
||||
// Long userId = ContextHolderUtils.getCurrentUserId();
|
||||
// questionSubmit.setUserId(userId);
|
||||
|
||||
Long questionId = questionSubmit.getQuestionId();
|
||||
questionSubmit.setUserId(userId);
|
||||
if (!tryAcquireSubmitLock(userId, questionId)) {
|
||||
throw new ClientException("当前有判题进行中,请稍后再提交", ErrorCode.OPERATION_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
ensureNoInProgressSubmit(userId, questionId);
|
||||
return createSubmitWithChain(questionSubmit);
|
||||
} catch (Exception ex) {
|
||||
releaseSubmitLock(userId, questionId);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用责任链模式创建题目提交
|
||||
*/
|
||||
private Long createSubmitWithChain(QuestionSubmit questionSubmit) {
|
||||
// 将 QuestionSubmit 转换为 QuestionSubmitRequestDTO 用于责任链校验
|
||||
QuestionSubmitRequestDTO requestDTO = new QuestionSubmitRequestDTO();
|
||||
requestDTO.setQuestionId(questionSubmit.getQuestionId());
|
||||
requestDTO.setLanguage(questionSubmit.getLanguage());
|
||||
requestDTO.setCode(questionSubmit.getCode());
|
||||
/**
|
||||
* 使用责任链模式创建题目提交
|
||||
*/
|
||||
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("题目提交责任链校验通过");
|
||||
// 执行责任链校验
|
||||
log.info("开始执行题目提交责任链校验,题目ID: {}", questionSubmit.getQuestionId());
|
||||
submitChainContext.handler(
|
||||
ChainMarkEnums.QUESTION_SUBMIT_REQ_PARAM_VERIFY_CHAIN.getMark(),
|
||||
requestDTO
|
||||
);
|
||||
log.info("题目提交责任链校验通过");
|
||||
|
||||
// 校验通过,保存提交记录
|
||||
// 设置初始状态:0 - 待判题
|
||||
questionSubmit.setStatus(0);
|
||||
// TODO: 判题机设置judgeInfo
|
||||
JudgeInfo judgeInfo = new JudgeInfo();
|
||||
String judgeInfos;
|
||||
try {
|
||||
judgeInfos = objectMapper.writeValueAsString(judgeInfo);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new ServiceException("判题信息序列化失败", e, ErrorCode.SYSTEM_ERROR);
|
||||
}
|
||||
questionSubmit.setJudgeInfo(judgeInfos);
|
||||
|
||||
this.save(questionSubmit);
|
||||
return questionSubmit.getId();
|
||||
}
|
||||
// 校验通过,保存提交记录
|
||||
// 设置初始状态:0 - 待判题
|
||||
questionSubmit.setStatus(QuestionSubmitStatusEnum.WAITING.getValue());
|
||||
|
||||
this.save(questionSubmit);
|
||||
return questionSubmit.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean updateSubmitStatus(QuestionSubmit questionSubmit) {
|
||||
return this.updateById(questionSubmit);
|
||||
Boolean updated = this.updateById(questionSubmit);
|
||||
if (Boolean.TRUE.equals(updated) && shouldReleaseLock(questionSubmit.getStatus())) {
|
||||
Long userId = questionSubmit.getUserId();
|
||||
Long questionId = questionSubmit.getQuestionId();
|
||||
if (userId == null && questionSubmit.getId() != null) {
|
||||
QuestionSubmit existing = this.getById(questionSubmit.getId());
|
||||
if (existing != null) {
|
||||
userId = existing.getUserId();
|
||||
questionId = existing.getQuestionId();
|
||||
}
|
||||
}
|
||||
if (userId != null && questionId != null) {
|
||||
releaseSubmitLock(userId, questionId);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuestionSubmit getSubmitById(Long submitId) {
|
||||
return this.getById(submitId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<QuestionSubmitResponseDTO> listQuestionSubmits(QuestionSubmitQueryRequestDTO request) {
|
||||
LambdaQueryWrapper<QuestionSubmit> wrapper = new LambdaQueryWrapper<>();
|
||||
if (request.getUserId() != null) {
|
||||
wrapper.eq(QuestionSubmit::getUserId, request.getUserId());
|
||||
}
|
||||
if (request.getQuestionId() != null) {
|
||||
wrapper.eq(QuestionSubmit::getQuestionId, request.getQuestionId());
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
wrapper.eq(QuestionSubmit::getStatus, request.getStatus());
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.getLanguage())) {
|
||||
wrapper.eq(QuestionSubmit::getLanguage, request.getLanguage());
|
||||
}
|
||||
|
||||
|
||||
// 排序字段
|
||||
String sortField = request.getSortField();
|
||||
if (StringUtils.isNotBlank(sortField)) {
|
||||
boolean asc = "asc".equalsIgnoreCase(request.getSortOrder());
|
||||
if ("createTime".equals(sortField)) {
|
||||
wrapper.orderBy(true, asc, QuestionSubmit::getCreateTime);
|
||||
} else if ("updateTime".equals(sortField)) {
|
||||
wrapper.orderBy(true, asc, QuestionSubmit::getUpdateTime);
|
||||
}
|
||||
} else {
|
||||
wrapper.orderByDesc(QuestionSubmit::getCreateTime);
|
||||
}
|
||||
|
||||
Page<QuestionSubmit> page = this.page(request, wrapper);
|
||||
Page<QuestionSubmitResponseDTO> dtoPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
dtoPage.setRecords(page.getRecords().stream()
|
||||
.map(this::convertToDTO)
|
||||
.toList());
|
||||
return dtoPage;
|
||||
}
|
||||
|
||||
private QuestionSubmitResponseDTO convertToDTO(QuestionSubmit submit) {
|
||||
if (submit == null) {
|
||||
return null;
|
||||
}
|
||||
QuestionSubmitResponseDTO dto = new QuestionSubmitResponseDTO();
|
||||
BeanUtils.copyProperties(submit, dto);
|
||||
if (StringUtils.isBlank(submit.getJudgeInfo())) {
|
||||
return dto;
|
||||
}
|
||||
try {
|
||||
JudgeInfo judgeInfo = objectMapper.readValue(submit.getJudgeInfo(), JudgeInfo.class);
|
||||
dto.setJudgeInfo(judgeInfo);
|
||||
|
||||
}catch (JsonProcessingException e) {
|
||||
throw new ServiceException("判题信息解析失败", e, ErrorCode.SYSTEM_ERROR);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private boolean tryAcquireSubmitLock(Long userId, Long questionId) {
|
||||
String lockKey = getSubmitLockKey(userId, questionId);
|
||||
return Boolean.TRUE.equals(
|
||||
stringRedisTemplate.opsForValue().setIfAbsent(lockKey, String.valueOf(System.currentTimeMillis()), SUBMIT_LOCK_TTL)
|
||||
);
|
||||
}
|
||||
|
||||
private void releaseSubmitLock(Long userId, Long questionId) {
|
||||
stringRedisTemplate.delete(getSubmitLockKey(userId, questionId));
|
||||
}
|
||||
|
||||
private String getSubmitLockKey(Long userId, Long questionId) {
|
||||
return RedisKeyConstants.QUESTION_SUBMIT_LOCK_KEY_PREFIX + userId + ":" + questionId;
|
||||
}
|
||||
|
||||
private void ensureNoInProgressSubmit(Long userId, Long questionId) {
|
||||
Long count = this.lambdaQuery()
|
||||
.eq(QuestionSubmit::getUserId, userId)
|
||||
.eq(QuestionSubmit::getQuestionId, questionId)
|
||||
.in(QuestionSubmit::getStatus,
|
||||
QuestionSubmitStatusEnum.WAITING.getValue(),
|
||||
QuestionSubmitStatusEnum.JUDGING.getValue())
|
||||
.count();
|
||||
if (count != null && count > 0) {
|
||||
throw new ClientException("当前有判题进行中,请稍后再提交", ErrorCode.OPERATION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldReleaseLock(Integer status) {
|
||||
return status != null && (status.equals(QuestionSubmitStatusEnum.SUCCESS.getValue())
|
||||
|| status.equals(QuestionSubmitStatusEnum.FAILED.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ spring:
|
||||
name: aioj-question-service
|
||||
profiles:
|
||||
active: @env@
|
||||
devtools:
|
||||
livereload:
|
||||
enabled: true
|
||||
restart:
|
||||
enabled: true
|
||||
server:
|
||||
port: 18083
|
||||
servlet:
|
||||
|
||||
Reference in New Issue
Block a user