From 838eba74717c2c699a9ed8050523723da026f636 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Thu, 2 Jul 2026 14:59:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=80=E7=AD=94=E9=A2=98=E8=AF=84=E5=88=86?= =?UTF-8?q?=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...assicPaperQuestionCallbackServiceImpl.java | 18 ++ .../RegenerateQuestionDomainServiceImpl.java | 35 +-- .../exam/domain/entity/ExamRecordEntity.java | 4 + .../domain/job/ShortAnswerScoringJob.java | 294 ++++++++++++++++++ .../impl/SubmitPaperDomainServiceImpl.java | 33 +- .../strategy/SubjectiveScoringStrategy.java | 41 ++- .../KnowledgePointApplicationServiceImpl.java | 10 +- .../domain/dto/AiScoringResponseDTO.java | 38 ++- .../PostToAiScoringDomainServiceImpl.java | 15 +- .../com/project/task/domain/dto/TaskDTO.java | 4 + .../impl/SearchTaskDomainServiceImpl.java | 28 ++ src/main/resources/application-dev.yml | 7 + src/main/resources/application-test.yml | 8 +- 13 files changed, 470 insertions(+), 65 deletions(-) create mode 100644 src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java 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 ade7093..e603ca4 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 @@ -6,9 +6,11 @@ import com.project.base.domain.result.Result; import com.project.classicpaper.domain.entity.ClassicPaperEntity; import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; import com.project.classicpaper.domain.entity.ClassicPaperSetEntity; +import com.project.classicpaper.domain.entity.RegenerateQuestionTaskEntity; import com.project.classicpaper.domain.enums.ClassicPaperStatusEnum; import com.project.classicpaper.domain.service.*; import com.project.interaction.domain.dto.QuestionCallBackDTO; +import com.project.classicpaper.domain.service.RegenerateQuestionTaskBaseService; import com.project.question.domain.dto.QuestionDTO; import com.project.question.domain.entity.QuestionEntity; import com.project.question.domain.service.SaveQuestionDomainService; @@ -40,6 +42,9 @@ public class ClassicPaperQuestionCallbackServiceImpl implements ClassicPaperQues @Autowired private ClassicPaperSetBaseService classicPaperSetBaseService; + @Autowired + private RegenerateQuestionTaskBaseService regenerateQuestionTaskBaseService; + @Override @Transactional(rollbackFor = Exception.class) public void handleCallback(QuestionCallBackDTO callback) { @@ -113,6 +118,19 @@ public class ClassicPaperQuestionCallbackServiceImpl implements ClassicPaperQues log.info(">>> [经典套题-回调] 处理完成, paperQuestionId={}, promotedQuestionId={}", paperQuestionId, promoted.getId()); + // 6. 更新重新生题任务状态(如果有进行中的) + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper taskWrapper = + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>(); + taskWrapper.eq(RegenerateQuestionTaskEntity::getPaperQuestionId, paperQuestionId) + .eq(RegenerateQuestionTaskEntity::getStatus, 0); + RegenerateQuestionTaskEntity regenTask = regenerateQuestionTaskBaseService.getOne(taskWrapper); + if (regenTask != null) { + regenTask.setStatus(1); + regenTask.setNewQuestionId(promoted.getId()); + regenerateQuestionTaskBaseService.updateById(regenTask); + log.info(">>> [经典套题-回调] regenerateTaskId={} 状态更新为成功", regenTask.getId()); + } + // 检查当前 paper 的所有题目是否都已生成完成 boolean paperAllDone = classicPaperQuestionBaseService.count( new LambdaQueryWrapper() diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java index d97e251..a844fb9 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java @@ -238,35 +238,24 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo List kpIds = kpRels.stream() .map(QuestionKpRelEntity::getKpId).collect(Collectors.toList()); List knowledgePoints = knowledgePointBaseService.listByIds(kpIds); - - // 3. Mock 延迟:模拟算法服务返回耗时 - mockSleep(); - - // 4. 调用生成器生成新题 - QuestionTypeEnum questionType = QuestionTypeEnum.findByValue(originalQuestion.getQuestionType()); - List newQuestions = classicPaperQuestionGenerator.generate( - knowledgePoints, questionType, 1); - if (newQuestions.isEmpty()) { - throw new BusinessErrorException("生成新题失败"); + if (knowledgePoints.isEmpty()) { + throw new BusinessErrorException("关联知识点数据不存在"); } - QuestionEntity newQuestion = newQuestions.get(0); - // 5. 更新关联关系 - paperQuestion.setQuestionId(newQuestion.getId()); + // 3. 清空 questionId,让回调走提拔分支 + paperQuestion.setQuestionId(null); classicPaperQuestionBaseService.updateById(paperQuestion); - // 6. 更新任务状态为成功 - RegenerateQuestionTaskEntity taskEntity = regenerateQuestionTaskBaseService.getOne( - new LambdaQueryWrapper() - .eq(RegenerateQuestionTaskEntity::getTaskId, taskId)); - taskEntity.setStatus(1); - taskEntity.setNewQuestionId(newQuestion.getId()); - regenerateQuestionTaskBaseService.updateById(taskEntity); + // 4. 异步提交生题任务(回调会提拔新题 + 更新 task 状态) + QuestionTypeEnum questionType = QuestionTypeEnum.findByValue(originalQuestion.getQuestionType()); + Long clusterId = knowledgePoints.get(0).getClusterId(); + classicPaperQuestionGenerator.generateAsync( + paperQuestionId, knowledgePoints, questionType, 1, clusterId); - log.info(">>> [经典套题-重新生题-异步] 任务成功, taskId={}, 原题{} -> 新题{}", taskId, questionId, newQuestion.getId()); + log.info(">>> [经典套题-重新生题-异步] 已提交算法, taskId={}, paperQuestionId={}, 题型={}", + taskId, paperQuestionId, questionType); } catch (Exception e) { - log.error(">>> [经典套题-重新生题-异步] 任务失败, taskId={}", taskId, e); - // 更新任务状态为失败 + log.error(">>> [经典套题-重新生题-异步] 提交失败, taskId={}", taskId, e); RegenerateQuestionTaskEntity taskEntity = regenerateQuestionTaskBaseService.getOne( new LambdaQueryWrapper() .eq(RegenerateQuestionTaskEntity::getTaskId, taskId)); diff --git a/src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java b/src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java index 0541821..b30cd07 100644 --- a/src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java +++ b/src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java @@ -112,6 +112,10 @@ public class ExamRecordEntity extends BaseEntity { private String aiComment; // V1.1 新增:命中得分点列表(简答题统计用) private List hitPoints; + // 该题分值(从 task 题型分值映射) + private Double score; + // V1.2 新增:简答题评分状态(0=待评分,1=已完成,2=失败) + private Integer scoringStatus; } @Override diff --git a/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java b/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java new file mode 100644 index 0000000..927694c --- /dev/null +++ b/src/main/java/com/project/exam/domain/job/ShortAnswerScoringJob.java @@ -0,0 +1,294 @@ +package com.project.exam.domain.job; + +import com.project.exam.domain.entity.ExamRecordEntity; +import com.project.exam.mapper.ExamRecordMapper; +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.information.domain.entity.KnowledgePointEntity; +import com.project.information.domain.service.KnowledgePointBaseService; +import com.project.question.domain.service.QuestionBaseService; +import com.project.task.domain.entity.TaskEntity; +import com.project.task.domain.enums.QuestionTypeEnum; +import com.project.task.domain.enums.TaskUserStatusEnum; +import com.project.task.mapper.TaskMapper; +import com.project.task.mapper.TaskUserMapper; +import com.project.task.domain.service.TaskBaseService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 简答题异步评分定时任务 + * 扫描已提交但简答题尚未评分的考试记录,逐题调用算法评分 + */ +@Service +@Slf4j +public class ShortAnswerScoringJob { + + @Autowired + private ExamRecordMapper examRecordMapper; + + @Autowired + private PostToAiScoringDomainService postToAiScoringDomainService; + + @Autowired + private QuestionBaseService questionBaseService; + + @Autowired + private KnowledgePointBaseService knowledgePointBaseService; + + @Autowired + private TaskMapper taskMapper; + + @Autowired + private TaskUserMapper taskUserMapper; + + @Autowired + private TaskBaseService taskBaseService; + + /** 每批最多处理多少道简答题 */ + @Value("${scoring.batch-size:5}") + private int batchSize; + + @Override + public String toString() { + return "ShortAnswerScoringJob{" + + "batchSize=" + batchSize + + '}'; + } + + /** + * 每60秒执行一次,扫描待评分的简答题 + */ + @Scheduled(fixedRateString = "${scoring.interval-ms:60000}") + public void processPendingScoring() { + log.debug(">>> [简答题评分任务] 开始扫描待评分简答题, batchSize={}", batchSize); + + try { + // 查询已提交的考试记录 + List records = examRecordMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .isNotNull(ExamRecordEntity::getSubmitTime) + .orderByAsc(ExamRecordEntity::getSubmitTime)); + + if (records.isEmpty()) { + return; + } + + int processed = 0; + + for (ExamRecordEntity record : records) { + if (processed >= batchSize) break; + + List snapshots = record.getAnswerSnapshot(); + if (snapshots == null || snapshots.isEmpty()) continue; + + boolean updated = false; + + for (int i = 0; i < snapshots.size(); i++) { + if (processed >= batchSize) break; + + ExamRecordEntity.QuestionSnapshot snapshot = snapshots.get(i); + // 只处理简答题且待评分状态 + if (!QuestionTypeEnum.SHORT_ANSWER.getValue().equals(snapshot.getType())) continue; + if (!Objects.equals(0, snapshot.getScoringStatus())) continue; + + log.info(">>> [简答题评分任务] 开始评分, recordId={}, questionId={}", + record.getId(), snapshot.getQuestionId()); + + // 调用算法评分 + scoreOne(snapshot, record); + processed++; + updated = true; + } + + if (updated) { + recalcTotalScore(record); + examRecordMapper.updateById(record); + log.info(">>> [简答题评分任务] recordId={} 快照已更新, 总分={}", record.getId(), record.getScore()); + } + } + + if (processed > 0) { + log.info(">>> [简答题评分任务] 本轮完成 {} 道简答题评分", processed); + } + + } catch (Exception e) { + log.error(">>> [简答题评分任务] 执行异常", e); + } + } + + private void scoreOne(ExamRecordEntity.QuestionSnapshot snapshot, ExamRecordEntity record) { + try { + // 查询任务配置(简答题分值、及格线) + double shortAnswerScore = 0.0; + try { + Long taskUserId = record.getTaskUserId(); + if (taskUserId != null) { + com.project.task.domain.entity.TaskUserEntity tu = taskUserMapper.selectById(taskUserId); + if (tu != null && tu.getTaskId() != null) { + TaskEntity task = taskMapper.selectById(tu.getTaskId()); + if (task != null) { + shortAnswerScore = task.getShortAnswerScore() != null ? task.getShortAnswerScore() : 0.0; + } + } + } + } catch (Exception e) { + log.warn(">>> [简答题评分任务] 查询任务配置失败, 使用0", e); + } + + // 从数据库查询题目(得分点 + 产品线名称) + QuestionEntity question = questionBaseService.getById(snapshot.getQuestionId()); + String products = ""; + if (question != null && question.getKpIdList() != null && !question.getKpIdList().isEmpty()) { + try { + KnowledgePointEntity kp = knowledgePointBaseService.getById(question.getKpIdList().get(0)); + if (kp != null && kp.getParseName() != null) { + products = kp.getParseName(); + } + } catch (Exception e) { + log.warn(">>> [简答题评分任务] 查询 products 失败", e); + } + } + + // 构建请求 + AiScoringRequestDTO request = new AiScoringRequestDTO(); + request.setProducts(products); + request.setQuestion(snapshot.getQuestionContent()); + request.setUserAnswer(snapshot.getUserAnswer()); + List options = new ArrayList<>(); + if (question != null && question.getQuestionDetail() != null + && question.getQuestionDetail().getScoringPoints() != null) { + for (QuestionEntity.ScoringPoint sp : question.getQuestionDetail().getScoringPoints()) { + options.add(sp.getContent()); + } + } + int totalPoints = options.size(); + request.setOptions(options); + + // 调用算法 + AiScoringResponseDTO response = postToAiScoringDomainService.requestAiScoring(request); + + if (response.getSuccess() != null && response.getSuccess() + && response.getCode() != null && response.getCode() == 0 + && response.getData() != null) { + + // 计算各得分点得分和命中点 + double pointsValue = totalPoints > 0 ? shortAnswerScore / totalPoints : 0.0; + double aiScore = 0.0; + List hitPoints = new ArrayList<>(); + StringBuilder comment = new StringBuilder(); + + 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; + if (algoScore > 0) { + hitPoints.add(i); + } + if (ps.getReason() != null && !ps.getReason().isEmpty()) { + if (comment.length() > 0) comment.append("; "); + comment.append(ps.getReason()); + } + } + + snapshot.setAiScore(aiScore); + snapshot.setAiComment(comment.toString()); + snapshot.setHitPoints(hitPoints); + snapshot.setScoringStatus(1); + + log.info(">>> [简答题评分任务] 评分完成, questionId={}, shortAnswerScore={}, totalPoints={}, hitPoints={}, aiScore={}", + snapshot.getQuestionId(), shortAnswerScore, totalPoints, hitPoints, aiScore); + } else { + log.warn(">>> [简答题评分任务] 评分失败, questionId={}, message={}", + snapshot.getQuestionId(), response.getMessage()); + snapshot.setScoringStatus(2); + } + + } catch (Exception e) { + log.error(">>> [简答题评分任务] 评分异常, questionId={}", snapshot.getQuestionId(), e); + snapshot.setScoringStatus(2); + } + } + + private void recalcTotalScore(ExamRecordEntity record) { + List snapshots = record.getAnswerSnapshot(); + if (snapshots == null || snapshots.isEmpty()) return; + + double total = 0.0; + boolean allShortDone = true; + + for (ExamRecordEntity.QuestionSnapshot s : snapshots) { + boolean isShort = QuestionTypeEnum.SHORT_ANSWER.getValue().equals(s.getType()); + if (isShort) { + if (!Objects.equals(1, s.getScoringStatus())) { + allShortDone = false; + break; + } + total += s.getAiScore() != null ? s.getAiScore() : 0.0; + } else { + if (s.getIsRight() != null && s.getIsRight()) { + total += s.getScore() != null ? s.getScore() : 0.0; + } + } + } + + if (allShortDone) { + record.setScore(total); + // 查询任务及格线 + double passScore = 60.0; + TaskEntity task = null; + com.project.task.domain.entity.TaskUserEntity tu = null; + try { + Long taskUserId = record.getTaskUserId(); + if (taskUserId != null) { + tu = taskUserMapper.selectById(taskUserId); + if (tu != null && tu.getTaskId() != null) { + task = taskMapper.selectById(tu.getTaskId()); + if (task != null && task.getPassScore() != null) { + passScore = task.getPassScore(); + } + } + } + } catch (Exception e) { + log.warn(">>> [简答题评分任务] 查询及格线失败,使用默认60分", e); + } + + boolean oldPass = record.getPass() != null && record.getPass(); + boolean newPass = total >= passScore; + record.setPass(newPass); + + // 如果及格状态变了,更新相关冗余字段 + if (oldPass != newPass && task != null && tu != null) { + try { + if (newPass) { + // 不及格→及格:增加 pass_num,更新用户状态 + taskBaseService.update() + .setSql("pass_num = pass_num + 1") + .eq("id", task.getId()) + .update(); + + if (!TaskUserStatusEnum.Pass.getValue().equals(tu.getStatus())) { + tu.setStatus(TaskUserStatusEnum.Pass.getValue()); + tu.setLastRecordId(record.getId()); + taskUserMapper.updateById(tu); + } + } + // 及格→不及格:理论上不会发生(简答题只加分不减分),暂不处理 + } catch (Exception e) { + log.error(">>> [简答题评分任务] 更新及格状态失败, recordId={}", record.getId(), e); + } + } + } + } +} diff --git a/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java index 262e322..82386b3 100644 --- a/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java +++ b/src/main/java/com/project/exam/domain/service/impl/SubmitPaperDomainServiceImpl.java @@ -75,6 +75,7 @@ public class SubmitPaperDomainServiceImpl implements SubmitPaperDomainService { scoringContext.setTask(task); for (ExamRecordEntity.QuestionSnapshot snapshot : snapshotList) { + double questionWeight = getQuestionWeight(snapshot.getType(), task); // 设置题目分值到快照(DTO转换用) ExamRecordDTO.QuestionSnapshotDTO tempDto = new ExamRecordDTO.QuestionSnapshotDTO(); tempDto.setQuestionId(snapshot.getQuestionId()); @@ -82,17 +83,27 @@ public class SubmitPaperDomainServiceImpl implements SubmitPaperDomainService { tempDto.setQuestionContent(snapshot.getQuestionContent()); tempDto.setUserAnswer(snapshot.getUserAnswer()); tempDto.setRightAnswer(snapshot.getRightAnswer()); - tempDto.setScore(getQuestionWeight(snapshot.getType(), task)); - - // 根据题型获取对应策略并判分 - ScoringStrategy strategy = scoringStrategyFactory.getStrategy(snapshot.getType()); - strategy.score(tempDto, scoringContext, task.getId()); - - // 回写判分结果到快照 - snapshot.setIsRight(tempDto.getIsRight()); - snapshot.setAiScore(tempDto.getAiScore()); - snapshot.setAiComment(tempDto.getAiComment()); - snapshot.setHitPoints(tempDto.getHitPoints()); + tempDto.setScore(questionWeight); + + // 客观题即时出分,简答题异步评分 + if (QuestionTypeEnum.SHORT_ANSWER.getValue().equals(snapshot.getType())) { + // 简答题:标记待评分,跳过同步调用 + snapshot.setIsRight(false); + snapshot.setAiScore(0.0); + snapshot.setScore(tempDto.getScore()); + snapshot.setScoringStatus(0); + } else { + // 客观题:策略判分(比对答案) + ScoringStrategy strategy = scoringStrategyFactory.getStrategy(snapshot.getType()); + strategy.score(tempDto, scoringContext, task.getId()); + + // 回写判分结果到快照 + snapshot.setIsRight(tempDto.getIsRight()); + snapshot.setScore(questionWeight); + snapshot.setAiScore(tempDto.getAiScore()); + snapshot.setAiComment(tempDto.getAiComment()); + snapshot.setHitPoints(tempDto.getHitPoints()); + } } // 汇总总分:客观题 + 简答题 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 94be7be..d66c7c1 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 @@ -53,7 +53,7 @@ public class SubjectiveScoringStrategy implements ScoringStrategy { List options = new ArrayList<>(); if (question != null && question.getQuestionDetail() != null && question.getQuestionDetail().getScoringPoints() != null) { - for (QuestionEntity.QuestionDetail.ScoringPoint sp : question.getQuestionDetail().getScoringPoints()) { + for (QuestionEntity.ScoringPoint sp : question.getQuestionDetail().getScoringPoints()) { options.add(sp.getContent()); } } @@ -71,17 +71,36 @@ public class SubjectiveScoringStrategy implements ScoringStrategy { // 调用AI阅卷 AiScoringResponseDTO response = postToAiScoringDomainService.requestAiScoring(request); - if (response.getStatus() == 1 && response.getHitPoints() != null) { - // 计算得分:命中数 / 总数 × 题目分值 - int hitCount = response.getHitPoints().size(); - int totalCount = response.getTotalPoints(); + if (response.getSuccess() != null && response.getSuccess() + && response.getCode() != null && response.getCode() == 0 + && response.getData() != null) { + // 计算得分 + int hitCount = 0; + int totalCount = response.getData().size(); + StringBuilder comment = new StringBuilder(); + List hitPoints = new ArrayList<>(); + for (int i = 0; i < response.getData().size(); i++) { + AiScoringResponseDTO.PointScore ps = response.getData().get(i); + Integer algoScore = ps.getScore(); + if (algoScore != null && algoScore > 0) { + hitCount++; + hitPoints.add(i); + } + if (ps.getReason() != null && !ps.getReason().isEmpty()) { + if (comment.length() > 0) { + comment.append("; "); + } + comment.append(ps.getReason()); + } + } + double questionScore = snapshot.getScore() != null ? snapshot.getScore() : 0.0; double aiScore = totalCount > 0 ? questionScore * hitCount / totalCount : 0.0; // 更新快照 snapshot.setAiScore(aiScore); - snapshot.setAiComment(response.getComment()); - snapshot.setHitPoints(response.getHitPoints()); + snapshot.setAiComment(comment.toString()); + snapshot.setHitPoints(hitPoints); snapshot.setUserScore(aiScore); // 累加简答题得分 @@ -89,14 +108,14 @@ public class SubjectiveScoringStrategy implements ScoringStrategy { // 更新日志 logEntity.setAiScore(aiScore); - logEntity.setHitPoints(response.getHitPoints()); + logEntity.setHitPoints(hitPoints); logEntity.setTotalPoints(totalCount); - logEntity.setAiComment(response.getComment()); + logEntity.setAiComment(comment.toString()); logEntity.setStatus(1); // 成功 } else { - log.error(">>> [AI阅卷] 评分失败, questionId={}, error={}", questionId, response.getErrorMsg()); + log.error(">>> [AI阅卷] 评分失败, questionId={}, message={}", questionId, response.getMessage()); logEntity.setStatus(2); // 失败 - logEntity.setErrorMsg(response.getErrorMsg()); + logEntity.setErrorMsg(response.getMessage()); } } catch (Exception e) { log.error(">>> [AI阅卷] 异常, questionId={}", questionId, e); diff --git a/src/main/java/com/project/information/application/impl/KnowledgePointApplicationServiceImpl.java b/src/main/java/com/project/information/application/impl/KnowledgePointApplicationServiceImpl.java index 6fc27d2..1e15037 100644 --- a/src/main/java/com/project/information/application/impl/KnowledgePointApplicationServiceImpl.java +++ b/src/main/java/com/project/information/application/impl/KnowledgePointApplicationServiceImpl.java @@ -218,7 +218,15 @@ public class KnowledgePointApplicationServiceImpl implements KnowledgePointAppli KnowledgePointEntity entity = new KnowledgePointEntity(); entity.setContent((String) item.get("knowledge")); - entity.setParseName(fileName); + // parseName 取资料名称(InformationEntity.name),而非原始文件名 + String infoName = fileName; + try { + InformationEntity info = informationBaseService.getById(id); + if (info != null && info.getName() != null && !info.getName().isEmpty()) { + infoName = info.getName(); + } + } catch (Exception ignored) {} + entity.setParseName(infoName); entity.setKnowledgeType(hasYellow ? 0 : 1); entity.setExamFocusList(focusList); entity.setInformationId(id); diff --git a/src/main/java/com/project/interaction/domain/dto/AiScoringResponseDTO.java b/src/main/java/com/project/interaction/domain/dto/AiScoringResponseDTO.java index 48ade5a..e26f313 100644 --- a/src/main/java/com/project/interaction/domain/dto/AiScoringResponseDTO.java +++ b/src/main/java/com/project/interaction/domain/dto/AiScoringResponseDTO.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,22 +8,39 @@ import java.util.List; /** * AI阅卷响应 DTO * 算法服务返回的评分结果 + * 接口:/v1/score/short_answer + * { + * "code": 0, + * "success": true, + * "message": "string", + * "data": [{"score_point": "string", "score": 0, "reason": "string"}] + * } */ @Data public class AiScoringResponseDTO { - /** 命中的得分点index列表 */ - private List hitPoints; + /** 状态码(0=成功) */ + private Integer code; - /** 总得分点数 */ - private Integer totalPoints; + /** 是否成功 */ + private Boolean success; - /** AI评语 */ - private String comment; + /** 消息 */ + private String message; - /** 处理状态:1-成功,2-失败 */ - private Integer status; + /** 各得分点评分结果 */ + private List data; - /** 错误信息(失败时有值) */ - private String errorMsg; + @Data + public static class PointScore { + /** 得分点内容 */ + @JsonProperty("score_point") + private String scorePoint; + + /** 该得分点得分 */ + private Integer score; + + /** 评分理由 */ + private String reason; + } } 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 f9ff592..3e3bf41 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 @@ -33,8 +33,7 @@ public class PostToAiScoringDomainServiceImpl implements PostToAiScoringDomainSe @Override public AiScoringResponseDTO requestAiScoring(AiScoringRequestDTO request) { try { - log.info(">>> [AI阅卷] 正在请求AI阅卷, questionId={}, examRecordId={}", - request.getQuestionId(), request.getExamRecordId()); + log.info(">>> [AI阅卷] 正在请求AI阅卷, question={}", request.getQuestion()); String responseBody = algorithmWebClient.post() .uri(scoringUrl) @@ -44,19 +43,19 @@ public class PostToAiScoringDomainServiceImpl implements PostToAiScoringDomainSe .timeout(Duration.ofSeconds(60)) .block(); - log.info(">>> [AI阅卷] 算法服务返回, questionId={}, response={}", - request.getQuestionId(), responseBody); + log.info(">>> [AI阅卷] 算法服务返回, question={}, response={}", + request.getQuestion(), responseBody); // 解析响应 AiScoringResponseDTO response = objectMapper.readValue(responseBody, AiScoringResponseDTO.class); - response.setStatus(1); // 成功 return response; } catch (Exception e) { - log.error(">>> [AI阅卷] 算法服务调用异常, questionId={}", request.getQuestionId(), e); + log.error(">>> [AI阅卷] 算法服务调用异常, question={}", request.getQuestion(), e); AiScoringResponseDTO errorResponse = new AiScoringResponseDTO(); - errorResponse.setStatus(2); // 失败 - errorResponse.setErrorMsg(e.getMessage()); + errorResponse.setCode(1); + errorResponse.setSuccess(false); + errorResponse.setMessage(e.getMessage()); return errorResponse; } } diff --git a/src/main/java/com/project/task/domain/dto/TaskDTO.java b/src/main/java/com/project/task/domain/dto/TaskDTO.java index dbc027f..9c424e0 100644 --- a/src/main/java/com/project/task/domain/dto/TaskDTO.java +++ b/src/main/java/com/project/task/domain/dto/TaskDTO.java @@ -100,6 +100,10 @@ public class TaskDTO extends BaseDTO { */ private Integer notParticipatedNum; + private String classificationTag; + + private String examModeTag; + // =============== V1.1 新增字段 结束 ============ } diff --git a/src/main/java/com/project/task/domain/service/impl/SearchTaskDomainServiceImpl.java b/src/main/java/com/project/task/domain/service/impl/SearchTaskDomainServiceImpl.java index 7291522..0533c00 100644 --- a/src/main/java/com/project/task/domain/service/impl/SearchTaskDomainServiceImpl.java +++ b/src/main/java/com/project/task/domain/service/impl/SearchTaskDomainServiceImpl.java @@ -2,6 +2,7 @@ package com.project.task.domain.service.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; @@ -9,6 +10,10 @@ import com.project.base.domain.exception.MissingParameterException; import com.project.base.domain.result.PageResult; import com.project.base.domain.result.Result; import com.project.base.domain.utils.PageConverter; +import com.project.classicpaper.domain.entity.ClassicCategoryEntity; +import com.project.classicpaper.domain.entity.ClassicPaperSetEntity; +import com.project.classicpaper.domain.service.ClassicCategoryBaseService; +import com.project.classicpaper.domain.service.ClassicPaperSetBaseService; import com.project.ding.domain.dto.UserDTO; import com.project.information.application.ProductLineApplicationService; import com.project.information.domain.entity.ProductLineEntity; @@ -16,11 +21,13 @@ import com.project.information.domain.service.ProductLineBaseService; import com.project.task.domain.dto.TaskDTO; import com.project.task.domain.entity.TaskEntity; import com.project.task.domain.entity.TaskUserEntity; +import com.project.task.domain.enums.ExamModeEnum; import com.project.task.domain.enums.TaskStatusEnum; import com.project.task.domain.param.TaskParam; import com.project.task.domain.service.SearchTaskDomainService; import com.project.task.domain.service.TaskUserBaseService; import com.project.task.mapper.TaskMapper; +import io.vavr.control.Try; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -43,6 +50,12 @@ public class SearchTaskDomainServiceImpl implements SearchTaskDomainService { @Autowired private ProductLineBaseService productLineBaseService; + @Autowired + private ClassicPaperSetBaseService classicPaperSetBaseService; + + @Autowired + private ClassicCategoryBaseService classicCategoryBaseService; + @Override public Result> search(TaskParam param) throws Exception { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); @@ -136,7 +149,22 @@ public class SearchTaskDomainServiceImpl implements SearchTaskDomainService { String subLineName = productLineApplicationService.searchLineNameById(dto.getSubLineId()); dto.setSubLineName(subLineName); + // 题目类型 + dto.setExamModeTag(Try.of(() -> EnumUtil.getBy(ExamModeEnum::getValue, dto.getExamMode())) + .getOrElse(ExamModeEnum.GENERATIVE) + .getDesc()); + if (ExamModeEnum.GENERATIVE.getValue().equals(dto.getExamMode())) { + // 生成式 + ProductLineEntity byId = productLineBaseService.getById(dto.getSubLineId()); + dto.setClassificationTag(String.format("%s - %s" , byId.getParentName() , byId.getName())); + } else { + // 经典 + ClassicPaperSetEntity byId = classicPaperSetBaseService.getById(dto.getClassicPaperSetId()); + ClassicCategoryEntity classicCategoryBaseServiceById = classicCategoryBaseService.getById(byId.getClassicCategoryId()); + ClassicCategoryEntity classicCategoryBaseServiceByParentId = classicCategoryBaseService.getById(classicCategoryBaseServiceById.getParentId()); + dto.setClassificationTag(String.format("%s - %s" , classicCategoryBaseServiceByParentId.getName() , byId.getName())); + } dto.setParticipantUserDTOList(participantUserDTOList); dto.setParticipantUserIdList(participantUserDTOList.stream().map(UserDTO::getId).toList()); dto.setTotalQuestionNum(dto.getSingleChoiceNum() + dto.getMultipleChoiceNum() + dto.getTrueFalseNum()); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3de7064..2755fd1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -94,6 +94,13 @@ question: queue: # 重试间隔(秒) retry-interval: 60 +# 简答题异步评分 +scoring: + # 每批最多处理多少道简答题 + batch-size: 5 + # 执行间隔(毫秒),每60秒扫描一次 + interval-ms: 60000 + scheduled-task: owner: test diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 78fa6dc..782764e 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -108,4 +108,10 @@ classicpaper: regenerate-delay-max-ms: 5000 # 是否启用真实算法(true=使用算法服务) algorithm: - enabled: true \ No newline at end of file + enabled: true +# 简答题异步评分 +scoring: + # 每批最多处理多少道简答题 + batch-size: 5 + # 执行间隔(毫秒),每60秒扫描一次 + interval-ms: 60000 \ No newline at end of file