diff --git a/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java index 2e113b1..70ba9fa 100644 --- a/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java +++ b/src/main/java/com/codzilla/backend/PreMatch/MatchRoom/MatchService.java @@ -95,8 +95,6 @@ public boolean finishMatch(UUID matchId, UUID winnerId) { } public Problem pickProblemOfOptions(Map options) { - // SQL в доменной модели — это ЯЗЫК (Category.Language == SQL), а не ProblemType. - // Поэтому SQL-задачу определяем по выбранному языку, а не по ProblemType. String language = options.get(Category.Language); String problemType = options.get(Category.ProblemType); String problemLevel = options.getOrDefault(Category.ProblemLevel, "EASY").toUpperCase(); @@ -110,8 +108,7 @@ public Problem pickProblemOfOptions(Map options) { return sqlProblem; } - // Алгоритмическая задача: тип берём из ProblemType (ALGORITHM/MATH/DATA_STRUCTURES), - // по умолчанию ALGORITHM. + String algoType = (problemType != null) ? problemType : "ALGORITHM"; Problem algoProblem = problemService.getOrCreateRandomAlgoProblem(algoType, problemLevel); log.info("Selected ALGO problem: id={}, type={}", algoProblem.getId(), algoProblem.getType()); diff --git a/src/main/java/com/codzilla/backend/judge/client/Artefactik0Client.java b/src/main/java/com/codzilla/backend/judge/client/Artefactik0Client.java index 022115f..f0388a0 100644 --- a/src/main/java/com/codzilla/backend/judge/client/Artefactik0Client.java +++ b/src/main/java/com/codzilla/backend/judge/client/Artefactik0Client.java @@ -28,7 +28,6 @@ public Artefactik0Client(@Value("${artefactik0.base-url}") String baseUrl) { .build(); } - // Создать задачу в Artefactik0, вернуть её id public Long createProblem(CreateProblemRequest request) { try { String body = objectMapper.writeValueAsString(request); @@ -47,10 +46,9 @@ public Long createProblem(CreateProblemRequest request) { } } - // Получить тесты задачи по её id в Artefactik0 public List getTests(Long problemId) { try { - // Используем ParameterizedTypeReference для получения списка + List tests = restClient.get() .uri("/api/problems/" + problemId + "/tests") .retrieve() @@ -73,7 +71,7 @@ public List getTests(Long problemId) { */ public RandomProblemResponse getRandomProblem(String type, String level) { try { - // Пока type не поддерживается, но передаём для будущей совместимости + String uri = UriComponentsBuilder.fromPath("/api/problems/random") .queryParam("type", type.toUpperCase()) .queryParam("complexity", level.toUpperCase()) @@ -89,16 +87,13 @@ public RandomProblemResponse getRandomProblem(String type, String level) { } } - // ── DTOs ────────────────────────────────────────────────────── - - // DTO для ответа (только нужные поля) @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class RandomProblemResponse { private Long id; private String name; - private String level; // EASY, MEDIUM, HARD + private String level; } @Data diff --git a/src/main/java/com/codzilla/backend/judge/client/SqlServiceClient.java b/src/main/java/com/codzilla/backend/judge/client/SqlServiceClient.java index 3c667e4..4686aeb 100644 --- a/src/main/java/com/codzilla/backend/judge/client/SqlServiceClient.java +++ b/src/main/java/com/codzilla/backend/judge/client/SqlServiceClient.java @@ -27,6 +27,8 @@ public SqlServiceClient(@Value("${sqlservice.base-url}") String baseUrl) { .build(); } + + public Long submitSolution(Long taskId, String userId, String query, UUID matchId) { try { SubmitRequest request = new SubmitRequest(); @@ -43,13 +45,15 @@ public Long submitSolution(Long taskId, String userId, String query, UUID matchI .retrieve() .body(String.class); - // SqlService возвращает ApiResponse, где data — это сразу submissionId + log.info("SqlService submit raw response: {}", raw); + + JsonNode root = objectMapper.readTree(raw); if (!root.has("success") || !root.get("success").asBoolean()) { String error = root.has("error") ? root.get("error").asText() : "Unknown error"; throw new RuntimeException("SqlService error: " + error); } - long id = root.get("data").asLong(); // <-- чисто число, а не объект + long id = root.get("data").asLong(); log.info("SqlService accepted submission id={}", id); return id; } catch (Exception e) { @@ -57,6 +61,8 @@ public Long submitSolution(Long taskId, String userId, String query, UUID matchI } } + + public SubmissionStatus getSubmissionStatus(Long submissionId) { try { String raw = restClient.get() @@ -64,7 +70,9 @@ public SubmissionStatus getSubmissionStatus(Long submissionId) { .retrieve() .body(String.class); - // SqlService возвращает ApiResponse, где data — объект + log.debug("SqlService status raw response: {}", raw); + + JsonNode root = objectMapper.readTree(raw); if (!root.has("success") || !root.get("success").asBoolean()) { String error = root.has("error") ? root.get("error").asText() : "Unknown error"; @@ -77,6 +85,8 @@ public SubmissionStatus getSubmissionStatus(Long submissionId) { } } + + public RandomSqlTaskResponse getRandomTask(String level) { try { String raw = restClient.get() @@ -101,6 +111,8 @@ public RandomSqlTaskResponse getRandomTask(String level) { } } + + @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class RandomSqlTaskResponse { @@ -123,12 +135,10 @@ public static class SubmitResponse { private Long id; } - - @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class SubmissionStatus { - private Long id; + private Long submissionId; private String status; private String verdict; private Long executionTimeMs; diff --git a/src/main/java/com/codzilla/backend/judge/problem/ProblemController.java b/src/main/java/com/codzilla/backend/judge/problem/ProblemController.java index 8bf81af..6bdba6d 100644 --- a/src/main/java/com/codzilla/backend/judge/problem/ProblemController.java +++ b/src/main/java/com/codzilla/backend/judge/problem/ProblemController.java @@ -33,52 +33,18 @@ public class ProblemController { private final SqlServiceClient sqlServiceClient; private final MatchService matchService; - // ── Создание задач ──────────────────────────────────────────── - - // POST /problems/algo — создать алго-задачу (регистрирует в Artefactik0) @PostMapping("/algo") public ResponseEntity createAlgoProblem( @RequestBody CreateAlgoProblemRequest request) { return ResponseEntity.ok(problemService.createAlgoProblem(request)); } - // POST /problems/sql — зарегистрировать SQL-задачу (уже созданную в SqlService) @PostMapping("/sql") public ResponseEntity registerSqlProblem( @RequestBody RegisterSqlProblemRequest request) { return ResponseEntity.ok(problemService.registerSqlProblem(request)); } - // ── Отправка решений ────────────────────────────────────────── - - // POST /problems/{id}/submit — отправить код (тип задачи определяется автоматически) -// @PostMapping("/{id}/submit") -// public ResponseEntity submit( -// @AuthenticationPrincipal User user, -// @PathVariable Long id, -// @RequestParam(defaultValue = "62") int languageId, -// @RequestBody String sourceCode) { -// UUID userId = user != null -// ? userService.getIdByEmail(user.getEmail()) -// : UUID.randomUUID(); -// return ResponseEntity.ok(problemService.submitSolution(userId, id, sourceCode, languageId)); -// } - - // POST /problems/{id}/submit/file — отправить файл с кодом -// @PostMapping(value = "/{id}/submit/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) -// public ResponseEntity submitFile( -// @AuthenticationPrincipal User user, -// @PathVariable Long id, -// @RequestParam int languageId, -// @RequestParam MultipartFile file -// -// ) throws IOException { -// String sourceCode = new String(file.getBytes(), StandardCharsets.UTF_8); -// UUID userId = userService.getIdByEmail(user.getEmail()); -// return ResponseEntity.ok( -// problemService.submitSolution(userId, id, sourceCode, languageId)); -// } - @PostMapping(value = "/submit/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity submitFileByMatch( @AuthenticationPrincipal User user, @@ -90,7 +56,6 @@ public ResponseEntity submitFileByMatch( return ResponseEntity.badRequest().body("Match not found"); } - // Читаем файл String sourceCode = new String(file.getBytes(), StandardCharsets.UTF_8); log.info("Received file for match {}: size={}, content (first 100 chars): {}", matchId, sourceCode.length(), sourceCode.substring(0, Math.min(100, sourceCode.length()))); @@ -108,11 +73,6 @@ public ResponseEntity submitFileByMatch( return ResponseEntity.ok(result); } - // ── Статус посылки ──────────────────────────────────────────── - - // GET /problems/submissions/{ref}/status - // ref = "123" → ALGO-посылка, смотрим в локальной БД - // ref = "sql:123" → SQL-посылка, проксируем в SqlService @GetMapping("/submissions/{submissionRef}/status") public ResponseEntity getStatus(@PathVariable String submissionRef) { diff --git a/src/main/java/com/codzilla/backend/judge/problem/ProblemService.java b/src/main/java/com/codzilla/backend/judge/problem/ProblemService.java index 1a09f90..6e0eae1 100644 --- a/src/main/java/com/codzilla/backend/judge/problem/ProblemService.java +++ b/src/main/java/com/codzilla/backend/judge/problem/ProblemService.java @@ -30,7 +30,7 @@ public class ProblemService { private final SubmissionTestRepository submissionTestRepository; private final S3Repository s3Repository; - // Создать ALGO-задачу: регистрируем в Artefactik0, сохраняем externalId + public Problem createAlgoProblem(CreateAlgoProblemRequest request) { Artefactik0Client.CreateProblemRequest artefaktRequest = new Artefactik0Client.CreateProblemRequest(); @@ -51,7 +51,7 @@ public Problem createAlgoProblem(CreateAlgoProblemRequest request) { return problemRepository.save(problem); } - // Зарегистрировать SQL-задачу: задача уже есть в SqlService, просто сохраняем ссылку + public Problem registerSqlProblem(RegisterSqlProblemRequest request) { Problem problem = new Problem(); problem.setName(request.getName()); @@ -61,7 +61,7 @@ public Problem registerSqlProblem(RegisterSqlProblemRequest request) { return problemRepository.save(problem); } - // Отправить решение — ветвление по типу задачи + public String submitSolution(UUID userId, UUID matchId , Long problemId, String sourceCode, int languageId) { Problem problem = problemRepository.findById(problemId) .orElseThrow(() -> new RuntimeException("Problem not found: " + problemId)); @@ -69,13 +69,13 @@ public String submitSolution(UUID userId, UUID matchId , Long problemId, String return switch (problem.getType()) { case ALGORITHM -> submitAlgo(userId, problem, sourceCode, languageId , matchId); case SQL -> submitSql(userId, problem, sourceCode , matchId); - //TODO ( не понятно что к чему ) + case MATH -> null; case DATA_STRUCTURES -> null; }; } - // ALGO: тесты из Artefactik0 → каждый тест в Judge0 + private String submitAlgo(UUID userId, Problem problem, String sourceCode, int languageId , UUID matchId) { List tests = artefactik0Client.getTests(problem.getExternalId()); @@ -118,7 +118,7 @@ private String submitAlgo(UUID userId, Problem problem, String sourceCode, int l return saved.getId().toString(); } - // SQL: полностью делегируем в SqlService + private String submitSql(UUID userId, Problem problem, String query, UUID matchId) { if (query == null || query.isBlank()) { throw new RuntimeException("SQL query cannot be empty"); @@ -132,18 +132,17 @@ private String submitSql(UUID userId, Problem problem, String query, UUID matchI ); log.info("SQL submission delegated, sqlSubmissionId={}", sqlSubmissionId); - // Создаём локальную запись — чтобы polling и SubmissionController работали Submission sub = new Submission(); sub.setProblemId(problem.getId()); sub.setUserId(userId); sub.setMatchId(matchId); - sub.setLanguageId(0); // SQL не имеет languageId + sub.setLanguageId(0); sub.setStatus(Submission.Status.IN_QUEUE); - sub.setSqlSubmissionId(sqlSubmissionId); // новое поле — см. ниже + sub.setSqlSubmissionId(sqlSubmissionId); Submission saved = submissionRepository.save(sub); log.info("Local SQL submission {} created, sqlSubmissionId={}", saved.getId(), sqlSubmissionId); - return saved.getId().toString(); // возвращаем локальный id, не "sql:123" + return saved.getId().toString(); } /** @@ -153,11 +152,9 @@ private String submitSql(UUID userId, Problem problem, String query, UUID matchI */ @Transactional public Problem getOrCreateRandomAlgoProblem(String type, String level) { - // 1. Запросить случайную задачу из Artefactik0 Artefactik0Client.RandomProblemResponse external = artefactik0Client.getRandomProblem(type, level); - // 2. Преобразовать строковый тип в enum (пока заглушка -> ALGORITHM) ProblemType problemType; try { problemType = ProblemType.valueOf(type.toUpperCase()); @@ -165,7 +162,6 @@ public Problem getOrCreateRandomAlgoProblem(String type, String level) { problemType = ProblemType.ALGORITHM; } - // 3. Поиск в локальной БД ProblemType finalProblemType = problemType; return problemRepository.findByExternalIdAndType(external.getId(), problemType) .orElseGet(() -> { @@ -185,7 +181,7 @@ public Problem getOrCreateRandomAlgoProblem(String type, String level) { */ public Problem getOrCreateRandomSqlProblem(String level) { List levelsToTry = Arrays.asList(level.toUpperCase(), "MEDIUM", "EASY"); - // используем LinkedHashSet для уникальности и сохранения порядка + Set uniqueLevels = new LinkedHashSet<>(levelsToTry); for (String lvl : uniqueLevels) { diff --git a/src/main/java/com/codzilla/backend/judge/submission/SubmissionPollingService.java b/src/main/java/com/codzilla/backend/judge/submission/SubmissionPollingService.java index 968bc82..7873dde 100644 --- a/src/main/java/com/codzilla/backend/judge/submission/SubmissionPollingService.java +++ b/src/main/java/com/codzilla/backend/judge/submission/SubmissionPollingService.java @@ -5,9 +5,7 @@ import com.codzilla.backend.PreMatch.MatchRoom.MatchService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import org.springframework.context.ApplicationEventPublisher; - import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,9 +22,10 @@ public class SubmissionPollingService { private final SubmissionRepository submissionRepository; private final SubmissionTestRepository submissionTestRepository; private final Judge0Client judge0Client; - private final SqlServiceClient sqlServiceClient; private final ApplicationEventPublisher eventPublisher; private final MatchService matchService; + private final SqlServiceClient sqlServiceClient; + @Transactional(readOnly = true) public List getPendingSubmissions() { @@ -41,7 +40,7 @@ public void pollStatuses() { for (SubmissionTest subTest : pending) { var response = judge0Client.getSubmissionStatus(subTest.getJudge0Token()); if (response == null || response.getStatus() == null) continue; - if (response.getStatus().getId() <= 2) continue; // ещё обрабатывается + if (response.getStatus().getId() <= 2) continue; updateTestStatus(subTest, response); updateSubmissionStatus(subTest.getSubmissionId()); @@ -51,14 +50,12 @@ public void pollStatuses() { private void updateTestStatus(SubmissionTest subTest, Judge0Client.SubmissionResponse response) { int statusId = response.getStatus().getId(); - // Декодируем stdout, если он пришёл в Base64 String actual = response.getStdout(); if (actual != null && !actual.isBlank()) { try { byte[] decoded = Base64.getDecoder().decode(actual); actual = new String(decoded, StandardCharsets.UTF_8).trim(); } catch (IllegalArgumentException e) { - // Не Base64 — оставляем как есть actual = actual.trim(); } } else { @@ -73,17 +70,16 @@ private void updateTestStatus(SubmissionTest subTest, Judge0Client.SubmissionRes subTest.setStatus(SubmissionTest.Status.RUNTIME_ERROR); } else if (statusId == 3) { String expected = subTest.getExpectedOutput() == null ? "" : subTest.getExpectedOutput().trim(); - if (actual.equals(expected)) { - subTest.setStatus(SubmissionTest.Status.ACCEPTED); - } else { - subTest.setStatus(SubmissionTest.Status.WRONG_ANSWER); - } + subTest.setStatus(actual.equals(expected) + ? SubmissionTest.Status.ACCEPTED + : SubmissionTest.Status.WRONG_ANSWER); } else { subTest.setStatus(SubmissionTest.Status.RUNTIME_ERROR); } submissionTestRepository.save(subTest); - log.info("Test {} of submission {} → {}", subTest.getTestIndex(), subTest.getSubmissionId(), subTest.getStatus()); + log.info("Test {} of submission {} → {}", + subTest.getTestIndex(), subTest.getSubmissionId(), subTest.getStatus()); } private void updateSubmissionStatus(Long submissionId) { @@ -92,7 +88,6 @@ private void updateSubmissionStatus(Long submissionId) { boolean allDone = allTests.stream() .allMatch(t -> t.getStatus() != SubmissionTest.Status.IN_QUEUE); - if (!allDone) return; SubmissionTest firstFailed = allTests.stream() @@ -140,10 +135,7 @@ public void pollSqlStatuses() { if (sqlStatus == null || sqlStatus.getStatus() == null) continue; - String status = sqlStatus.getStatus(); - - // SqlService возвращает PENDING пока обрабатывает - if ("PENDING".equals(status)) continue; + if ("PENDING".equals(sqlStatus.getStatus())) continue; updateSqlSubmissionStatus(sub, sqlStatus); } catch (Exception e) { @@ -152,33 +144,31 @@ public void pollSqlStatuses() { } } - private void updateSqlSubmissionStatus(Submission sub, SqlServiceClient.SubmissionStatus sqlStatus) { - String status = sqlStatus.getStatus(); + private void updateSqlSubmissionStatus(Submission sub, + SqlServiceClient.SubmissionStatus sqlStatus) { String verdict = sqlStatus.getVerdict(); + boolean accepted = "ACCEPTED".equals(verdict); - if ("DONE".equals(status) || "ACCEPTED".equals(status) || "WRONG_ANSWER".equals(status) || "ERROR".equals(status)) { - boolean accepted = "ACCEPTED".equals(verdict); - - if (accepted) { - sub.setStatus(Submission.Status.ACCEPTED); - sub.setResultDetails("Accepted"); - if (sub.getMatchId() != null) { - matchService.finishMatch(sub.getMatchId(), sub.getUserId()); - } - } else { - Submission.Status localStatus = switch (verdict != null ? verdict : "") { - case "WRONG_ANSWER" -> Submission.Status.WRONG_ANSWER; - case "COMPILATION_ERROR" -> Submission.Status.COMPILE_ERROR; - case "TIME_LIMIT_EXCEEDED" -> Submission.Status.RUNTIME_ERROR; - default -> Submission.Status.RUNTIME_ERROR; - }; - sub.setStatus(localStatus); - sub.setResultDetails(sqlStatus.getVerdict() != null ? sqlStatus.getVerdict() : "Error"); + if (accepted) { + sub.setStatus(Submission.Status.ACCEPTED); + sub.setResultDetails("Accepted"); + if (sub.getMatchId() != null) { + matchService.finishMatch(sub.getMatchId(), sub.getUserId()); } - - submissionRepository.save(sub); - log.info("SQL Submission {} final verdict: {}", sub.getId(), sub.getStatus()); - eventPublisher.publishEvent(new SubmissionUpdatedEvent(sub.getUserId())); + } else { + Submission.Status localStatus = switch (verdict != null ? verdict : "") { + case "WRONG_ANSWER" -> Submission.Status.WRONG_ANSWER; + case "COMPILATION_ERROR" -> Submission.Status.COMPILE_ERROR; + case "TIME_LIMIT_EXCEEDED" -> Submission.Status.RUNTIME_ERROR; + case "SECURITY_VIOLATION" -> Submission.Status.RUNTIME_ERROR; + default -> Submission.Status.RUNTIME_ERROR; + }; + sub.setStatus(localStatus); + sub.setResultDetails(verdict != null ? verdict : "Error"); } + + submissionRepository.save(sub); + log.info("SQL Submission {} final verdict: {}", sub.getId(), sub.getStatus()); + eventPublisher.publishEvent(new SubmissionUpdatedEvent(sub.getUserId())); } } \ No newline at end of file