diff --git a/src/main/java/com/project/base/domain/entity/BaseEntity.java b/src/main/java/com/project/base/domain/entity/BaseEntity.java index 586c81d..657c9c0 100644 --- a/src/main/java/com/project/base/domain/entity/BaseEntity.java +++ b/src/main/java/com/project/base/domain/entity/BaseEntity.java @@ -72,8 +72,7 @@ public class BaseEntity implements Serializable { try { String currentUserId = SecurityUtils.getUserId(); boolean isCreator = StrUtil.equals(currentUserId, this.creatorId); - boolean isAdmin = SecurityUtils.hasRole("ROLE_ADMIN") - || SecurityUtils.hasRole("ROLE_SUPER_ADMIN"); + boolean isAdmin = SecurityUtils.hasRole("ROLE_SUPER_ADMIN"); baseDTO.setCanEdit(isCreator || isAdmin); } catch (Exception ignored) { // 获取当前用户信息失败时默认不可编辑 diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java index 4c00ef3..ade7093 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java @@ -83,8 +83,11 @@ public class ClassicPaperQuestionCallbackServiceImpl implements ClassicPaperQues return; } - // 2. 查找 paper_question → paperId - ClassicPaperQuestionEntity paperQuestion = classicPaperQuestionBaseService.getById(paperQuestionId); + // 2. 查找 paper_question → paperId(加悲观锁,防止并发回调同时提拔) + ClassicPaperQuestionEntity paperQuestion = classicPaperQuestionBaseService.lambdaQuery() + .eq(ClassicPaperQuestionEntity::getId, paperQuestionId) + .last("FOR UPDATE") + .one(); if (paperQuestion == null) { log.warn(">>> [经典套题-回调] paper_question 不存在, id={}", paperQuestionId); return; diff --git a/src/main/java/com/project/exam/domain/service/strategy/SubjectiveScoringStrategy.java b/src/main/java/com/project/exam/domain/service/strategy/SubjectiveScoringStrategy.java index b48a3c5..94be7be 100644 --- a/src/main/java/com/project/exam/domain/service/strategy/SubjectiveScoringStrategy.java +++ b/src/main/java/com/project/exam/domain/service/strategy/SubjectiveScoringStrategy.java @@ -7,6 +7,7 @@ import com.project.interaction.domain.dto.AiScoringRequestDTO; import com.project.interaction.domain.dto.AiScoringResponseDTO; import com.project.interaction.domain.service.PostToAiScoringDomainService; import com.project.question.domain.entity.QuestionEntity; +import com.project.question.domain.service.QuestionBaseService; import com.project.task.domain.enums.QuestionTypeEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +30,9 @@ public class SubjectiveScoringStrategy implements ScoringStrategy { @Autowired private AiGradingLogBaseService aiGradingLogBaseService; + @Autowired + private QuestionBaseService questionBaseService; + @Override public boolean supports(Integer questionType) { return QuestionTypeEnum.SHORT_ANSWER.getValue().equals(questionType); @@ -38,16 +42,22 @@ public class SubjectiveScoringStrategy implements ScoringStrategy { public void score(ExamRecordDTO.QuestionSnapshotDTO snapshot, ScoringContext context, Long taskId) { Long questionId = snapshot.getQuestionId(); - // 构建AI阅卷请求 + // 构建AI阅卷请求(新接口:/v1/score/short_answer) AiScoringRequestDTO request = new AiScoringRequestDTO(); - request.setQuestionId(questionId); - request.setQuestionContent(snapshot.getQuestionContent()); + request.setProducts(context.getTask() != null ? context.getTask().getName() : ""); + request.setQuestion(snapshot.getQuestionContent()); request.setUserAnswer(snapshot.getUserAnswer()); - // 构建得分点列表(从快照的 options 中获取,简答题的 options 存储得分点) - // 注意:得分点存储在 QuestionDetail.scoringPoints 中,需要从快照中获取 - // 这里先用空列表,后续从快照中获取 - request.setScoringPoints(new ArrayList<>()); + // 从数据库查询得分点,转为字符串列表 + QuestionEntity question = questionBaseService.getById(questionId); + List options = new ArrayList<>(); + if (question != null && question.getQuestionDetail() != null + && question.getQuestionDetail().getScoringPoints() != null) { + for (QuestionEntity.QuestionDetail.ScoringPoint sp : question.getQuestionDetail().getScoringPoints()) { + options.add(sp.getContent()); + } + } + request.setOptions(options); // 创建判分日志 AiGradingLogEntity logEntity = new AiGradingLogEntity(); diff --git a/src/main/java/com/project/interaction/domain/dto/AiScoringRequestDTO.java b/src/main/java/com/project/interaction/domain/dto/AiScoringRequestDTO.java index e4b6a72..76cd941 100644 --- a/src/main/java/com/project/interaction/domain/dto/AiScoringRequestDTO.java +++ b/src/main/java/com/project/interaction/domain/dto/AiScoringRequestDTO.java @@ -1,5 +1,6 @@ package com.project.interaction.domain.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -7,30 +8,21 @@ import java.util.List; /** * AI阅卷请求 DTO * 发送给算法服务,请求对简答题进行评分 + * 接口:/v1/score/short_answer */ @Data public class AiScoringRequestDTO { - /** 考试记录ID */ - private Long examRecordId; - - /** 题目ID */ - private Long questionId; + /** 产品线/来源标识 */ + private String products; /** 题目内容 */ - private String questionContent; + private String question; /** 考生作答 */ + @JsonProperty("user_answer") private String userAnswer; - /** 得分点列表 */ - private List scoringPoints; - - @Data - public static class ScoringPoint { - /** 得分点序号 */ - private Integer index; - /** 得分点内容 */ - private String content; - } + /** 得分点内容列表(按顺序,算法按顺序匹配命中) */ + private List options; } 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 4e98d15..f9ff592 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 @@ -24,8 +24,8 @@ public class PostToAiScoringDomainServiceImpl implements PostToAiScoringDomainSe @Resource(name = "algorithmWebClient") private WebClient algorithmWebClient; - /** 算法服务阅卷路径(待确认) */ - @Value("${algo.scoringUrl:/ai-scoring}") + /** 算法服务阅卷路径 */ + @Value("${algo.scoringUrl:/v1/score/short_answer}") private String scoringUrl; private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java b/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java index 5299539..06f7d6a 100644 --- a/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java +++ b/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java @@ -1,5 +1,6 @@ package com.project.milvus.application.impl; +import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.project.base.domain.exception.MissingParameterException; import com.project.classicpaper.domain.service.ClassicPaperQuestionCallbackService; @@ -43,10 +44,10 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { public void insertTitle(TitleVector title) { // 经典套卷分支:走备用池 + 提拔,跳过 Milvus + log.info(">>> [回调处理] 经典套卷生题回调, json={}", JSONUtil.toJsonStr(title)); if (title.getBizKey() != null && title.getBizKey().startsWith("PQ:")) { log.info(">>> [回调处理] 经典套卷生题回调, bizKey={}", title.getBizKey()); - log.info(">>> [回调处理] 经典套卷生题回调, json={}", JSONUtil.toJsonStr(title)); handleClassicPaper(title); return; @@ -135,6 +136,8 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { private void handleClassicPaper(TitleVector title) { // 构造单题列表 QuestionDTO questionDTO = buildQuestionDTO(title); + // 简答题:算法把得分点放在 options 中(key="1","2","3"...),转为 scoringPoints + convertShortAnswerScoringPoints(questionDTO); List questions = Collections.singletonList(questionDTO); QuestionCallBackDTO callback = new QuestionCallBackDTO(); @@ -151,4 +154,35 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { String collect = poinIds.stream().map(String::valueOf).collect(Collectors.joining(",")); return LOCK_KEY + DigestUtils.md5Hex(collect); } + + /** + * 简答题转换:算法把得分点放在 options(key="1","2","3"...)中, + * 转为 scoringPoints 格式,清空 options/rightAnswer + */ + private void convertShortAnswerScoringPoints(QuestionDTO dto) { + QuestionDTO.QuestionDetailDTO detail = dto.getQuestionDetailDTO(); + if (detail == null || detail.getType() != 4) { + return; + } + if (detail.getOptions() == null || detail.getOptions().isEmpty()) { + return; + } + if (detail.getScoringPoints() != null && !detail.getScoringPoints().isEmpty()) { + return; + } + + List spList = new ArrayList<>(); + for (Map.Entry entry : detail.getOptions().entrySet()) { + String content = entry.getValue(); + if (StrUtil.isNotBlank(content)) { + QuestionDTO.ScoringPointDTO sp = new QuestionDTO.ScoringPointDTO(); + sp.setIndex(Integer.parseInt(entry.getKey())); + sp.setContent(content); + spList.add(sp); + } + } + detail.setScoringPoints(spList); + detail.setOptions(null); + detail.setRightAnswer(null); + } } diff --git a/src/main/java/com/project/question/domain/dto/QuestionDTO.java b/src/main/java/com/project/question/domain/dto/QuestionDTO.java index dc76d75..3844f41 100644 --- a/src/main/java/com/project/question/domain/dto/QuestionDTO.java +++ b/src/main/java/com/project/question/domain/dto/QuestionDTO.java @@ -56,6 +56,21 @@ public class QuestionDTO extends BaseDTO { if (this.questionDetailDTO != null) { QuestionEntity.QuestionDetail detail = new QuestionEntity.QuestionDetail(); BeanUtils.copyProperties(this.questionDetailDTO, detail); + // BeanUtils 无法处理 List → List + // 手动复制得分点 + if (this.questionDetailDTO.getScoringPoints() != null) { + List spList = + this.questionDetailDTO.getScoringPoints().stream() + .map(sp -> { + QuestionEntity.ScoringPoint target = + new QuestionEntity.ScoringPoint(); + target.setIndex(sp.getIndex()); + target.setContent(sp.getContent()); + return target; + }) + .collect(java.util.stream.Collectors.toList()); + detail.setScoringPoints(spList); + } entity.setQuestionDetail(detail); }