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:
2026-01-28 23:01:48 +08:00
parent 67825a8c5c
commit 1945cc2fb1
52 changed files with 1561 additions and 89 deletions

View File

@@ -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() {
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

@@ -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()));

View File

@@ -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()));
}
}

View File

@@ -3,6 +3,11 @@ spring:
name: aioj-question-service
profiles:
active: @env@
devtools:
livereload:
enabled: true
restart:
enabled: true
server:
port: 18083
servlet: