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