13 changed files with 470 additions and 65 deletions
@ -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<ExamRecordEntity> records = examRecordMapper.selectList( |
|||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ExamRecordEntity>() |
|||
.isNotNull(ExamRecordEntity::getSubmitTime) |
|||
.orderByAsc(ExamRecordEntity::getSubmitTime)); |
|||
|
|||
if (records.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
int processed = 0; |
|||
|
|||
for (ExamRecordEntity record : records) { |
|||
if (processed >= batchSize) break; |
|||
|
|||
List<ExamRecordEntity.QuestionSnapshot> 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<String> 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<Integer> 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<ExamRecordEntity.QuestionSnapshot> 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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue