diff --git a/src/main/java/com/project/classicpaper/application/impl/ClassicPaperSetApplicationServiceImpl.java b/src/main/java/com/project/classicpaper/application/impl/ClassicPaperSetApplicationServiceImpl.java index 6f5a0d7..206d49b 100644 --- a/src/main/java/com/project/classicpaper/application/impl/ClassicPaperSetApplicationServiceImpl.java +++ b/src/main/java/com/project/classicpaper/application/impl/ClassicPaperSetApplicationServiceImpl.java @@ -265,18 +265,27 @@ public class ClassicPaperSetApplicationServiceImpl implements ClassicPaperSetApp if (StrUtil.isBlank(paperSet.getScoreRatio())) { return; } - Map ratioMap; - try { - String scoreRatio = paperSet.getScoreRatio(); - // DB 中存的是带转义符的 JSON 字符串,需先还原 - scoreRatio = scoreRatio.replace("\\\"", "\""); - // 可能有外层引号,去掉 - if (scoreRatio.startsWith("\"") && scoreRatio.endsWith("\"")) { - scoreRatio = scoreRatio.substring(1, scoreRatio.length() - 1); + ObjectMapper mapper = new ObjectMapper(); + String scoreRatio = paperSet.getScoreRatio(); + + // JacksonTypeHandler 会在 String 字段上额外包裹一层 JSON 字符串, + // 导致实际值有多层转义。循环剥离直到能解析为 Map + Map ratioMap = null; + for (int i = 0; i < 5; i++) { + if (StrUtil.isBlank(scoreRatio)) break; + try { + ratioMap = mapper.readValue(scoreRatio, Map.class); + break; + } catch (Exception e) { + if (scoreRatio.startsWith("\"") && scoreRatio.endsWith("\"")) { + scoreRatio = scoreRatio.substring(1, scoreRatio.length() - 1); + } + scoreRatio = scoreRatio.replace("\\\"", "\"").replace("\\\\", "\\"); } - ratioMap = new ObjectMapper().readValue(scoreRatio, Map.class); - } catch (Exception e) { - log.warn(">>> [经典套题] scoreRatio 解析失败, setId={}", paperSet.getId(), e); + } + + if (ratioMap == null) { + log.warn(">>> [经典套题] scoreRatio 解析失败, setId={}", paperSet.getId()); return; } diff --git a/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java b/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java index d3be37b..d71cad9 100644 --- a/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java +++ b/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java @@ -26,6 +26,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -210,19 +212,22 @@ public class ShortAnswerScoringJob { && response.getData() != null) { // 计算各得分点得分和命中点 - double pointsValue = totalPoints > 0 ? shortAnswerScore / totalPoints : 0.0; double aiScore = 0.0; List hitPoints = new ArrayList<>(); StringBuilder comment = new StringBuilder(); + boolean allFullScore = true; + double pointsValue = totalPoints > 0 ? shortAnswerScore / totalPoints : 0.0; for (int i = 0; i < response.getData().size(); i++) { AiScoringResponseDTO.PointScore ps = response.getData().get(i); - int algoScore = ps.getScore() != null ? ps.getScore() : 0; - double pointScore = pointsValue * algoScore; - aiScore += pointScore; + double algoScore = ps.getScore() != null ? ps.getScore() : 0D; + aiScore += pointsValue * algoScore; if (algoScore > 0) { hitPoints.add(i); } + if (algoScore < 1.0) { + allFullScore = false; + } if (ps.getReason() != null && !ps.getReason().isEmpty()) { if (!comment.isEmpty()) { comment.append("; "); @@ -231,6 +236,10 @@ public class ShortAnswerScoringJob { } } + // 全部答对直接给满分,否则按比例计算并四舍五入保留两位小数 + aiScore = allFullScore ? shortAnswerScore + : BigDecimal.valueOf(aiScore).setScale(2, RoundingMode.HALF_UP).doubleValue(); + snapshot.setAiScore(aiScore); snapshot.setAiComment(comment.toString()); snapshot.setHitPoints(hitPoints); diff --git a/src/main/java/com/project/interaction/config/WebClientConfig.java b/src/main/java/com/project/interaction/config/WebClientConfig.java index 026baa1..8cb9135 100644 --- a/src/main/java/com/project/interaction/config/WebClientConfig.java +++ b/src/main/java/com/project/interaction/config/WebClientConfig.java @@ -22,13 +22,13 @@ public class WebClientConfig { public WebClient algorithmWebClient() { // 配置HTTP客户端,设置长超时以支持流式响应 HttpClient httpClient = HttpClient.create() - .responseTimeout(Duration.ofSeconds(60)) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .responseTimeout(Duration.ofSeconds(180)) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 180000) .doOnConnected(conn -> conn // 读超时 - .addHandlerLast(new ReadTimeoutHandler(60, TimeUnit.SECONDS)) + .addHandlerLast(new ReadTimeoutHandler(180, TimeUnit.SECONDS)) // 写超时 - .addHandlerLast(new WriteTimeoutHandler(30, TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler(180, TimeUnit.SECONDS)) ); return WebClient.builder() diff --git a/src/main/java/com/project/interaction/domain/service/impl/PostToAiScoringDomainServiceImpl.java b/src/main/java/com/project/interaction/domain/service/impl/PostToAiScoringDomainServiceImpl.java index 442a1de..a4a6a84 100644 --- a/src/main/java/com/project/interaction/domain/service/impl/PostToAiScoringDomainServiceImpl.java +++ b/src/main/java/com/project/interaction/domain/service/impl/PostToAiScoringDomainServiceImpl.java @@ -25,12 +25,9 @@ public class PostToAiScoringDomainServiceImpl implements PostToAiScoringDomainSe private WebClient algorithmWebClient; /** 算法服务阅卷路径 */ - @Value("${algo.scoringUrl:/v1/score/short_answer}") + @Value("${algo.scoringUrl:http://172.16.204.50:8010/v1/score/short_answer}") private String scoringUrl; - @Value("${algo.apiUrl:http://172.16.25.174:8000}") - private String apiUrl; - private final ObjectMapper objectMapper = new ObjectMapper(); @Override @@ -39,11 +36,11 @@ public class PostToAiScoringDomainServiceImpl implements PostToAiScoringDomainSe log.info(">>> [AI阅卷] 正在请求AI阅卷, question={}", request.getQuestion()); String responseBody = algorithmWebClient.post() - .uri(apiUrl+scoringUrl) + .uri(scoringUrl) .bodyValue(request) .retrieve() .bodyToMono(String.class) - .timeout(Duration.ofSeconds(60)) + .timeout(Duration.ofSeconds(180)) .block(); log.info(">>> [AI阅卷] 算法服务返回, question={}, response={}", diff --git a/src/main/java/com/project/statistics/domain/service/impl/ExamTaskStatisticsDomainServiceImpl.java b/src/main/java/com/project/statistics/domain/service/impl/ExamTaskStatisticsDomainServiceImpl.java index 4257176..47fbe94 100644 --- a/src/main/java/com/project/statistics/domain/service/impl/ExamTaskStatisticsDomainServiceImpl.java +++ b/src/main/java/com/project/statistics/domain/service/impl/ExamTaskStatisticsDomainServiceImpl.java @@ -75,21 +75,31 @@ public class ExamTaskStatisticsDomainServiceImpl implements ExamTaskStatisticsDo .divide(BigDecimal.valueOf(totalUserCount), 2, RoundingMode.HALF_UP); dto.setParticipationRate(formatValue(participationRate.doubleValue())); - // 6. 计算平均得分 = 累加考试得分 / 已提交记录数 - double totalScore = submittedRecordList.stream() + // 6. 每人只取最新一次考试记录,用于计算平均分和通过率 + List latestRecordPerUser = submittedRecordList.stream() + .collect(Collectors.toMap( + ExamRecordEntity::getTaskUserId, + record -> record, + (r1, r2) -> r1.getSubmitTime().compareTo(r2.getSubmitTime()) >= 0 ? r1 : r2 + )) + .values().stream() + .toList(); + + // 7. 计算平均得分 = 每人最新一次得分的累加 / 人数 + double totalScore = latestRecordPerUser.stream() .mapToDouble(record -> record.getScore() != null ? record.getScore() : 0.0) .sum(); BigDecimal averageScore = BigDecimal.valueOf(totalScore) - .divide(BigDecimal.valueOf(submittedRecordList.size()), 2, RoundingMode.HALF_UP); + .divide(BigDecimal.valueOf(latestRecordPerUser.size()), 2, RoundingMode.HALF_UP); dto.setAverageScore(formatValue(averageScore.doubleValue())); - // 7. 计算通过率 = 通过的记录数 / 已提交记录数 * 100 - long passCount = submittedRecordList.stream() + // 8. 计算通过率 = 最新一次通过的记录数 / 人数 * 100 + long passCount = latestRecordPerUser.stream() .filter(record -> Boolean.TRUE.equals(record.getPass())) .count(); BigDecimal passRate = BigDecimal.valueOf(passCount) .multiply(BigDecimal.valueOf(100)) - .divide(BigDecimal.valueOf(submittedRecordList.size()), 2, RoundingMode.HALF_UP); + .divide(BigDecimal.valueOf(latestRecordPerUser.size()), 2, RoundingMode.HALF_UP); dto.setPassRate(formatValue(passRate.doubleValue())); return Result.success(dto); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index a829c56..12808d5 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -54,6 +54,7 @@ minio: accessKey: ${MINIO_ASSESSKEY:DTKYZDZM1i31XOvd24SP} secretKey: ${MINIO_SECRETKEY:PnfLPcJbvaUboZIwYZAADPB0pDtPZgbi0QiLSs3C} bucket: ${MINIO_BUCKET:ai-evaluator} + tempAccessFileUrl: http://172.16.204.50/minio-api mybatis-plus: configuration: map-underscore-to-camel-case: true @@ -85,6 +86,7 @@ algo: apiUrl: http://172.16.204.50:8000 extractUrl: /v1/key_points/extract callbackUrl: http://172.16.204.50/evaluator-api + scoringUrl: http://172.16.204.50:8000/v1/score/short_answer jwt: secret: "my-very-fixed-and-secure-secret-key-1234567890" diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index a217f61..e1ec3dd 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -84,6 +84,7 @@ algo: apiUrl: http://127.0.0.1:8000 extractUrl: /v1/key_points/extract callbackUrl: http://8.129.84.155/evaluator-api + scoringUrl: http://127.0.0.1:8010/v1/score/short_answer jwt: secret: "my-very-fixed-and-secure-secret-key-1234567890" diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index f98aa7c..b4703e0 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -81,6 +81,7 @@ algo: apiUrl: http://127.0.0.1:8000/ extractUrl: /v1/key_points/extract callbackUrl: http://47.119.114.204/evaluator-api + scoringUrl: http://47.119.114.204:8010/v1/score/short_answer jwt: secret: "my-very-fixed-and-secure-secret-key-1234567890"