Browse Source

# feat: 提交模块DTO/VO与SubmissionService实现

yangyi 1 month ago
parent
commit
71ea71bee3

+ 161 - 0
src/main/java/space/anyi/serve/entity/contest/SubmissionVo.java

@@ -0,0 +1,161 @@
+package space.anyi.serve.entity.contest;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+@Schema(description = "提交记录响应展示对象")
+public class SubmissionVo {
+
+    @Schema(description = "提交ID")
+    private Long id;
+    @Schema(description = "题目ID")
+    private Long problemId;
+    @Schema(description = "比赛ID,练习提交为null")
+    private Long contestId;
+    @Schema(description = "提交语言")
+    private String language;
+    @Schema(description = "判题状态:SUBMITTED-已提交,JUDGING-判题中,AC-通过,WA-答案错误,TLE-超时,MLE-内存超限,RE-运行错误,CE-编译失败,SE-系统错误")
+    private String status;
+    @Schema(description = "执行时间(毫秒)")
+    private Integer timeUsedMs;
+    @Schema(description = "内存使用(KB)")
+    private Integer memoryUsedKb;
+    @Schema(description = "得分")
+    private Integer score;
+    @Schema(description = "总测试用例数")
+    private Integer totalTestCases;
+    @Schema(description = "通过测试用例数")
+    private Integer passedTestCases;
+    @Schema(description = "判题详情(CE/SE时展示错误信息)")
+    private String judgeDetail;
+    @Schema(description = "提交时间")
+    private LocalDateTime createdAt;
+
+    public static SubmissionVo from(Submission submission) {
+        SubmissionVo vo = new SubmissionVo();
+        if (Objects.isNull(submission)) {
+            return vo;
+        }
+        vo.setId(submission.getId());
+        vo.setProblemId(submission.getProblemId());
+        vo.setContestId(submission.getContestId());
+        vo.setLanguage(submission.getLanguage());
+        vo.setStatus(submission.getStatus());
+        vo.setTimeUsedMs(submission.getTimeUsedMs());
+        vo.setMemoryUsedKb(submission.getMemoryUsedKb());
+        vo.setScore(submission.getScore());
+        vo.setTotalTestCases(submission.getTotalTestCases());
+        vo.setPassedTestCases(submission.getPassedTestCases());
+        vo.setJudgeDetail(submission.getJudgeDetail());
+        vo.setCreatedAt(submission.getCreatedAt());
+        return vo;
+    }
+
+    public static List<SubmissionVo> from(List<Submission> list) {
+        List<SubmissionVo> result = new ArrayList<>();
+        for (Submission submission : list) {
+            result.add(SubmissionVo.from(submission));
+        }
+        return result;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getProblemId() {
+        return problemId;
+    }
+
+    public void setProblemId(Long problemId) {
+        this.problemId = problemId;
+    }
+
+    public Long getContestId() {
+        return contestId;
+    }
+
+    public void setContestId(Long contestId) {
+        this.contestId = contestId;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public Integer getTimeUsedMs() {
+        return timeUsedMs;
+    }
+
+    public void setTimeUsedMs(Integer timeUsedMs) {
+        this.timeUsedMs = timeUsedMs;
+    }
+
+    public Integer getMemoryUsedKb() {
+        return memoryUsedKb;
+    }
+
+    public void setMemoryUsedKb(Integer memoryUsedKb) {
+        this.memoryUsedKb = memoryUsedKb;
+    }
+
+    public Integer getScore() {
+        return score;
+    }
+
+    public void setScore(Integer score) {
+        this.score = score;
+    }
+
+    public Integer getTotalTestCases() {
+        return totalTestCases;
+    }
+
+    public void setTotalTestCases(Integer totalTestCases) {
+        this.totalTestCases = totalTestCases;
+    }
+
+    public Integer getPassedTestCases() {
+        return passedTestCases;
+    }
+
+    public void setPassedTestCases(Integer passedTestCases) {
+        this.passedTestCases = passedTestCases;
+    }
+
+    public String getJudgeDetail() {
+        return judgeDetail;
+    }
+
+    public void setJudgeDetail(String judgeDetail) {
+        this.judgeDetail = judgeDetail;
+    }
+
+    public LocalDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(LocalDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+}

+ 56 - 0
src/main/java/space/anyi/serve/entity/contest/SubmitDto.java

@@ -0,0 +1,56 @@
+package space.anyi.serve.entity.contest;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+@Schema(description = "代码提交DTO")
+public class SubmitDto {
+
+    @NotNull(message = "题目ID不能为空")
+    @Schema(description = "题目ID")
+    private Long problemId;
+
+    @Schema(description = "比赛ID,为空表示日常练习提交")
+    private Long contestId;
+
+    @NotBlank(message = "提交代码不能为空")
+    @Schema(description = "提交代码")
+    private String code;
+
+    @NotBlank(message = "语言不能为空")
+    @Schema(description = "提交语言,当前仅支持java", allowableValues = {"java"}, example = "java")
+    private String language;
+
+    public Long getProblemId() {
+        return problemId;
+    }
+
+    public void setProblemId(Long problemId) {
+        this.problemId = problemId;
+    }
+
+    public Long getContestId() {
+        return contestId;
+    }
+
+    public void setContestId(Long contestId) {
+        this.contestId = contestId;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+}

+ 38 - 0
src/main/java/space/anyi/serve/service/SubmissionService.java

@@ -0,0 +1,38 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.contest.Submission;
+import space.anyi.serve.entity.contest.SubmissionVo;
+import space.anyi.serve.entity.contest.SubmitDto;
+
+public interface SubmissionService extends IService<Submission> {
+
+    /**
+     * 提交代码并同步判题,返回完整结果
+     *
+     * @param dto    提交参数
+     * @param userId 提交用户ID
+     * @return 判题结果
+     */
+    SubmissionVo submit(SubmitDto dto, Long userId);
+
+    /**
+     * 查询提交记录详情(仅本人或 coach/admin 可查看)
+     *
+     * @param submissionId 提交ID
+     * @param userId       当前用户ID
+     * @return 提交详情
+     */
+    SubmissionVo getDetail(Long submissionId, Long userId);
+
+    /**
+     * 分页查询当前用户的提交记录
+     */
+    Page<SubmissionVo> queryMySubmissions(Long userId, Integer pageNum, Integer pageSize);
+
+    /**
+     * 分页查询某题的所有提交记录(coach/admin 管理用)
+     */
+    Page<SubmissionVo> queryByProblemId(Long problemId, Integer pageNum, Integer pageSize);
+}

+ 177 - 0
src/main/java/space/anyi/serve/service/impl/SubmissionServiceImpl.java

@@ -0,0 +1,177 @@
+package space.anyi.serve.service.impl;
+
+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 org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.codesandbox.CodeContext;
+import space.anyi.codesandbox.CodeResponse;
+import space.anyi.codesandbox.CodeSandBox;
+import space.anyi.codesandbox.TestCase;
+import space.anyi.serve.entity.contest.Contest;
+import space.anyi.serve.entity.contest.ContestProblem;
+import space.anyi.serve.entity.contest.Submission;
+import space.anyi.serve.entity.contest.SubmissionVo;
+import space.anyi.serve.entity.contest.SubmitDto;
+import space.anyi.serve.entity.problem.Problem;
+import space.anyi.serve.entity.problem.ProblemTest;
+import space.anyi.serve.entity.user.User;
+import space.anyi.serve.mapper.ContestProblemMapper;
+import space.anyi.serve.mapper.ProblemTestMapper;
+import space.anyi.serve.mapper.SubmissionMapper;
+import space.anyi.serve.service.ContestService;
+import space.anyi.serve.service.ProblemService;
+import space.anyi.serve.service.SubmissionService;
+import space.anyi.serve.util.SecurityUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+@Service
+public class SubmissionServiceImpl extends ServiceImpl<SubmissionMapper, Submission> implements SubmissionService {
+
+    private final SubmissionMapper submissionMapper;
+    private final ProblemTestMapper problemTestMapper;
+    private final ContestProblemMapper contestProblemMapper;
+    private final ProblemService problemService;
+    private final ContestService contestService;
+    private final CodeSandBox codeSandBox;
+
+    public SubmissionServiceImpl(SubmissionMapper submissionMapper,
+                                 ProblemTestMapper problemTestMapper,
+                                 ContestProblemMapper contestProblemMapper,
+                                 ProblemService problemService,
+                                 ContestService contestService,
+                                 CodeSandBox codeSandBox) {
+        this.submissionMapper = submissionMapper;
+        this.problemTestMapper = problemTestMapper;
+        this.contestProblemMapper = contestProblemMapper;
+        this.problemService = problemService;
+        this.contestService = contestService;
+        this.codeSandBox = codeSandBox;
+    }
+
+    @Override
+    @Transactional
+    public SubmissionVo submit(SubmitDto dto, Long userId) {
+        Problem problem = problemService.getById(dto.getProblemId());
+        if (problem == null) {
+            throw new IllegalArgumentException("题目不存在");
+        }
+        if (dto.getContestId() != null) {
+            validateContestSubmission(dto.getContestId(), userId);
+        }
+        List<ProblemTest> problemTests = problemTestMapper.selectList(
+                new LambdaQueryWrapper<ProblemTest>()
+                        .eq(ProblemTest::getProblemId, dto.getProblemId())
+                        .orderByAsc(ProblemTest::getSortOrder));
+        if (problemTests.isEmpty()) {
+            throw new IllegalArgumentException("该题目还没有评测用例,无法提交");
+        }
+        Submission submission = new Submission();
+        submission.setProblemId(dto.getProblemId());
+        submission.setUserId(userId);
+        submission.setContestId(dto.getContestId());
+        submission.setCode(dto.getCode());
+        submission.setLanguage(dto.getLanguage());
+        submission.setStatus(Submission.STATUS_JUDGING);
+        submission.setTotalTestCases(problemTests.size());
+        submission.setPassedTestCases(0);
+        submissionMapper.insert(submission);
+
+        List<TestCase> testCases = new ArrayList<>();
+        for (ProblemTest problemTest : problemTests) {
+            testCases.add(new TestCase(problemTest.getId(), problemTest.getInput(), problemTest.getOutput()));
+        }
+        CodeContext context = new CodeContext(submission.getId(), dto.getCode(), dto.getLanguage(),
+                problem.getTimeLimitMs().longValue(), problem.getMemoryLimitMb() * 1024L, testCases);
+        CodeResponse response = codeSandBox.process(context);
+
+        submission.setStatus(response.status());
+        submission.setTimeUsedMs(response.usedTime() == null ? 0 : response.usedTime().intValue());
+        submission.setMemoryUsedKb(response.usedMemory() == null ? 0 : response.usedMemory().intValue());
+        submission.setJudgeDetail(response.message());
+        if (response.testCaseResponses() != null) {
+            long passed = response.testCaseResponses().stream()
+                    .filter(c -> Submission.STATUS_AC.equals(c.status()))
+                    .count();
+            submission.setPassedTestCases((int) passed);
+        }
+        submission.setScore(resolveScore(dto.getContestId(), submission, problem));
+        submissionMapper.updateById(submission);
+
+        problem.setTryCount(problem.getTryCount() == null ? 1 : problem.getTryCount() + 1);
+        if (Submission.STATUS_AC.equals(submission.getStatus())) {
+            problem.setAcceptedCount(problem.getAcceptedCount() == null ? 1 : problem.getAcceptedCount() + 1);
+        }
+        problemService.updateById(problem);
+
+        return SubmissionVo.from(submission);
+    }
+
+    @Override
+    public SubmissionVo getDetail(Long submissionId, Long userId) {
+        Submission submission = submissionMapper.selectById(submissionId);
+        if (submission == null) {
+            throw new IllegalArgumentException("提交记录不存在");
+        }
+        String role = SecurityUtils.getCurrentRole();
+        if (!User.ROLE_ADMIN.equals(role) && !User.ROLE_COACH.equals(role)
+                && !Objects.equals(submission.getUserId(), userId)) {
+            throw new IllegalArgumentException("无权限查看该提交记录");
+        }
+        return SubmissionVo.from(submission);
+    }
+
+    @Override
+    public Page<SubmissionVo> queryMySubmissions(Long userId, Integer pageNum, Integer pageSize) {
+        Page<Submission> page = page(new Page<>(pageNum, pageSize),
+                new LambdaQueryWrapper<Submission>()
+                        .eq(Submission::getUserId, userId)
+                        .orderByDesc(Submission::getCreatedAt));
+        return convertToVoPage(page);
+    }
+
+    @Override
+    public Page<SubmissionVo> queryByProblemId(Long problemId, Integer pageNum, Integer pageSize) {
+        Page<Submission> page = page(new Page<>(pageNum, pageSize),
+                new LambdaQueryWrapper<Submission>()
+                        .eq(Submission::getProblemId, problemId)
+                        .orderByDesc(Submission::getCreatedAt));
+        return convertToVoPage(page);
+    }
+
+    private void validateContestSubmission(Long contestId, Long userId) {
+        Contest contest = contestService.getById(contestId);
+        if (contest == null) {
+            throw new IllegalArgumentException("比赛不存在");
+        }
+        if (!Contest.STATUS_RUNNING.equals(contest.getStatus())) {
+            throw new IllegalArgumentException("比赛未在进行中");
+        }
+        if (!contestService.isRegistered(contestId, userId)) {
+            throw new IllegalArgumentException("未报名该比赛");
+        }
+    }
+
+    private Integer resolveScore(Long contestId, Submission submission, Problem problem) {
+        if (!Submission.STATUS_AC.equals(submission.getStatus())) {
+            return 0;
+        }
+        if (contestId == null) {
+            return 0;
+        }
+        ContestProblem contestProblem = contestProblemMapper.selectOne(new LambdaQueryWrapper<ContestProblem>()
+                .eq(ContestProblem::getContestId, contestId)
+                .eq(ContestProblem::getProblemId, submission.getProblemId()));
+        return contestProblem == null || contestProblem.getScore() == null ? 0 : contestProblem.getScore();
+    }
+
+    private Page<SubmissionVo> convertToVoPage(Page<Submission> page) {
+        Page<SubmissionVo> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+        result.setRecords(SubmissionVo.from(page.getRecords()));
+        return result;
+    }
+}