Browse Source

简答题阅卷

master
luoweijian 3 months ago
parent
commit
6bd84028e9
  1. 1
      src/main/java/com/project/exam/application/impl/ExamRecordApplicationServiceImpl.java
  2. 99
      src/main/java/com/project/exam/domain/entity/AiGradingLogEntity.java
  3. 7
      src/main/java/com/project/exam/domain/service/AiGradingLogBaseService.java
  4. 11
      src/main/java/com/project/exam/domain/service/impl/AiGradingLogBaseServiceImpl.java
  5. 134
      src/main/java/com/project/exam/domain/service/strategy/ClassicPaperStrategy.java
  6. 22
      src/main/java/com/project/exam/domain/service/strategy/GenerativePaperStrategy.java
  7. 51
      src/main/java/com/project/exam/domain/service/strategy/ObjectiveScoringStrategy.java
  8. 19
      src/main/java/com/project/exam/domain/service/strategy/PaperAssemblyStrategy.java
  9. 31
      src/main/java/com/project/exam/domain/service/strategy/PaperAssemblyStrategyFactory.java
  10. 32
      src/main/java/com/project/exam/domain/service/strategy/ScoringContext.java
  11. 26
      src/main/java/com/project/exam/domain/service/strategy/ScoringStrategy.java
  12. 29
      src/main/java/com/project/exam/domain/service/strategy/ScoringStrategyFactory.java
  13. 100
      src/main/java/com/project/exam/domain/service/strategy/SubjectiveScoringStrategy.java
  14. 9
      src/main/java/com/project/exam/mapper/AiGradingLogMapper.java
  15. 36
      src/main/java/com/project/interaction/domain/dto/AiScoringRequestDTO.java
  16. 28
      src/main/java/com/project/interaction/domain/dto/AiScoringResponseDTO.java
  17. 18
      src/main/java/com/project/interaction/domain/service/PostToAiScoringDomainService.java
  18. 63
      src/main/java/com/project/interaction/domain/service/impl/PostToAiScoringDomainServiceImpl.java

1
src/main/java/com/project/exam/application/impl/ExamRecordApplicationServiceImpl.java

@ -10,6 +10,7 @@ import com.project.exam.domain.param.ExamRecordParam;
import com.project.exam.domain.service.*;
import com.project.exam.domain.service.strategy.PaperAssemblyStrategy;
import com.project.exam.domain.service.strategy.PaperAssemblyStrategyFactory;
import com.project.task.domain.entity.TaskEntity;
import com.project.task.domain.service.TaskBaseService;
import jakarta.servlet.http.HttpServletResponse;

99
src/main/java/com/project/exam/domain/entity/AiGradingLogEntity.java

@ -0,0 +1,99 @@
package com.project.exam.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.project.base.domain.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.util.List;
/**
* AI判分日志实体
* 记录每次AI阅卷的请求/响应用于审计和统计
*/
@Data
@Table(name = "evaluator_ai_grading_log",
indexes = {@Index(name = "Idx_exam_record_id", columnList = "exam_record_id"),
@Index(name = "Idx_question_id", columnList = "question_id")})
@Entity
@TableName(value = "evaluator_ai_grading_log", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)
public class AiGradingLogEntity extends BaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
@Id
private Long id;
@Column(name = "exam_record_id")
@TableField("exam_record_id")
@Comment("考试记录ID")
private Long examRecordId;
@Column(name = "question_id")
@TableField("question_id")
@Comment("题目ID")
private Long questionId;
@Column(name = "question_content", columnDefinition = "TEXT comment '题目内容快照'")
@TableField("question_content")
private String questionContent;
@Column(name = "user_answer", columnDefinition = "TEXT comment '考生作答'")
@TableField("user_answer")
private String userAnswer;
@Column(name = "standard_answer", columnDefinition = "TEXT comment '标准答案/得分点'")
@TableField("standard_answer")
private String standardAnswer;
/** AI评分(实际得分) */
@Column(name = "ai_score")
@TableField("ai_score")
@Comment("AI评分")
private Double aiScore;
/** 命中的得分点index列表 */
@TableField(value = "hit_points", typeHandler = JacksonTypeHandler.class)
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "hit_points", columnDefinition = "json comment '命中得分点列表'")
private List<Integer> hitPoints;
/** 总得分点数 */
@Column(name = "total_points")
@TableField("total_points")
@Comment("总得分点数")
private Integer totalPoints;
/** AI评语 */
@Column(name = "ai_comment", columnDefinition = "varchar(1000) comment 'AI评语'")
@TableField("ai_comment")
private String aiComment;
/** 请求报文(排查用) */
@Column(name = "request_body", columnDefinition = "TEXT comment '请求报文'")
@TableField("request_body")
private String requestBody;
/** 响应报文(排查用) */
@Column(name = "response_body", columnDefinition = "TEXT comment '响应报文'")
@TableField("response_body")
private String responseBody;
/** 处理状态:0-处理中,1-成功,2-失败 */
@Column(name = "status")
@TableField("status")
@Comment("处理状态:0-处理中,1-成功,2-失败")
private Integer status = 0;
/** 错误信息 */
@Column(name = "error_msg", columnDefinition = "varchar(500) comment '错误信息'")
@TableField("error_msg")
private String errorMsg;
}

7
src/main/java/com/project/exam/domain/service/AiGradingLogBaseService.java

@ -0,0 +1,7 @@
package com.project.exam.domain.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.project.exam.domain.entity.AiGradingLogEntity;
public interface AiGradingLogBaseService extends IService<AiGradingLogEntity> {
}

11
src/main/java/com/project/exam/domain/service/impl/AiGradingLogBaseServiceImpl.java

@ -0,0 +1,11 @@
package com.project.exam.domain.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.project.exam.domain.entity.AiGradingLogEntity;
import com.project.exam.domain.service.AiGradingLogBaseService;
import com.project.exam.mapper.AiGradingLogMapper;
import org.springframework.stereotype.Service;
@Service
public class AiGradingLogBaseServiceImpl extends ServiceImpl<AiGradingLogMapper, AiGradingLogEntity> implements AiGradingLogBaseService {
}

134
src/main/java/com/project/exam/domain/service/strategy/ClassicPaperStrategy.java

@ -0,0 +1,134 @@
package com.project.exam.domain.service.strategy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.project.base.domain.exception.BusinessErrorException;
import com.project.exam.domain.dto.ExamRecordDTO;
import com.project.exam.domain.entity.ExamRecordEntity;
import com.project.exam.domain.service.ExamRecordBaseService;
import com.project.question.domain.entity.QuestionEntity;
import com.project.task.domain.entity.TaskEntity;
import com.project.task.domain.entity.TaskPaperSnapshotEntity;
import com.project.task.domain.entity.TaskUserEntity;
import com.project.task.domain.service.TaskBaseService;
import com.project.task.domain.service.TaskPaperSnapshotBaseService;
import com.project.task.domain.service.TaskUserBaseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 经典套卷组卷策略
* 从快照表随机抽一个版本不锁题不补库
*/
@Component
@Slf4j
public class ClassicPaperStrategy implements PaperAssemblyStrategy {
@Autowired
private TaskBaseService taskBaseService;
@Autowired
private TaskUserBaseService taskUserBaseService;
@Autowired
private TaskPaperSnapshotBaseService taskPaperSnapshotBaseService;
@Autowired
private ExamRecordBaseService examRecordBaseService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public ExamRecordDTO assemblePaper(Long taskId, String userId) throws Exception {
// 1. 校验用户资格
TaskUserEntity taskUser = taskUserBaseService.lambdaQuery()
.eq(TaskUserEntity::getTaskId, taskId)
.eq(TaskUserEntity::getUserId, userId)
.one();
if (taskUser == null) {
throw new BusinessErrorException("您无需参与本场考核");
}
if (taskUser.getStatus() != null && taskUser.getStatus() == 2) {
throw new BusinessErrorException("您已通过考核,无需重复考试");
}
// 2. Redis 防抖(30 秒 TTL)
String lockKey = String.format("lock:exam:start:%s:%s", taskId, userId);
Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
throw new BusinessErrorException("请勿短时间内重复参加考试");
}
// 3. 查询任务配置
TaskEntity task = taskBaseService.getById(taskId);
// 4. 查询该任务的所有快照
List<TaskPaperSnapshotEntity> allSnapshots = taskPaperSnapshotBaseService.list(
new LambdaQueryWrapper<TaskPaperSnapshotEntity>()
.eq(TaskPaperSnapshotEntity::getTaskId, taskId));
if (allSnapshots.isEmpty()) {
throw new BusinessErrorException("该任务暂无可用套卷快照");
}
// 5. 获取所有版本的 setIndex,随机选一个
List<Integer> setIndexes = allSnapshots.stream()
.map(TaskPaperSnapshotEntity::getSetIndex)
.distinct()
.sorted()
.collect(Collectors.toList());
Integer selectedSetIndex = setIndexes.get(new Random().nextInt(setIndexes.size()));
// 6. 过滤出该版本的快照,按 sortOrder 排序
List<TaskPaperSnapshotEntity> selectedSnapshots = allSnapshots.stream()
.filter(s -> selectedSetIndex.equals(s.getSetIndex()))
.sorted(Comparator.comparing(TaskPaperSnapshotEntity::getSortOrder))
.collect(Collectors.toList());
// 7. 构建 QuestionSnapshot 列表
List<ExamRecordDTO.QuestionSnapshotDTO> snapshotDTOList = new ArrayList<>();
for (int i = 0; i < selectedSnapshots.size(); i++) {
TaskPaperSnapshotEntity snapshot = selectedSnapshots.get(i);
ExamRecordDTO.QuestionSnapshotDTO dto = new ExamRecordDTO.QuestionSnapshotDTO();
dto.setQuestionId(snapshot.getQuestionId());
dto.setIndex(i + 1);
// 从快照的 questionDetail 中拷贝题目内容
if (snapshot.getQuestionDetail() != null) {
QuestionEntity.QuestionDetail detail = snapshot.getQuestionDetail();
dto.setQuestionContent(detail.getQuestionContent());
dto.setType(detail.getType());
dto.setOptions(detail.getOptions());
dto.setRightAnswer(detail.getRightAnswer());
dto.setAnalysis(detail.getAnalysis());
}
snapshotDTOList.add(dto);
}
// 8. 构建 ExamRecordDTO(通过 taskUserId 关联)
ExamRecordDTO examRecordDTO = new ExamRecordDTO();
examRecordDTO.setTaskUserId(taskUser.getId());
examRecordDTO.setTaskId(taskId);
examRecordDTO.setTaskName(task.getName());
examRecordDTO.setAnswerSnapshotDTOList(snapshotDTOList);
// 9. 保存考试记录(通过 taskUserId 关联)
ExamRecordEntity recordEntity = new ExamRecordEntity();
recordEntity.setTaskUserId(taskUser.getId());
recordEntity.setTaskName(task.getName());
recordEntity.setStartTime(new Date());
examRecordBaseService.save(recordEntity);
examRecordDTO.setId(recordEntity.getId());
log.info(">>> [经典组卷] 组卷完成, taskId={}, userId={}, selectedSetIndex={}, 题目数={}",
taskId, userId, selectedSetIndex, snapshotDTOList.size());
return examRecordDTO;
}
}

22
src/main/java/com/project/exam/domain/service/strategy/GenerativePaperStrategy.java

@ -0,0 +1,22 @@
package com.project.exam.domain.service.strategy;
import com.project.exam.domain.dto.ExamRecordDTO;
import com.project.exam.domain.service.AssemblePaperDomainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 生成式组卷策略
* 直接委托给现有 AssemblePaperDomainServiceImpl不迁移原有 388 行逻辑
*/
@Component
public class GenerativePaperStrategy implements PaperAssemblyStrategy {
@Autowired
private AssemblePaperDomainService assemblePaperDomainService;
@Override
public ExamRecordDTO assemblePaper(Long taskId, String userId) throws Exception {
return assemblePaperDomainService.assemblePaper(taskId, userId);
}
}

51
src/main/java/com/project/exam/domain/service/strategy/ObjectiveScoringStrategy.java

@ -0,0 +1,51 @@
package com.project.exam.domain.service.strategy;
import com.project.exam.domain.dto.ExamRecordDTO;
import com.project.task.domain.enums.QuestionTypeEnum;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 客观题判分策略
* 单选题多选题判断题比对答案
*/
@Component
public class ObjectiveScoringStrategy implements ScoringStrategy {
@Override
public boolean supports(Integer questionType) {
// 支持单选、多选、判断
return QuestionTypeEnum.SINGLE_CHOICE.getValue().equals(questionType)
|| QuestionTypeEnum.MULTIPLE_CHOICE.getValue().equals(questionType)
|| QuestionTypeEnum.TRUE_FALSE.getValue().equals(questionType);
}
@Override
public void score(ExamRecordDTO.QuestionSnapshotDTO snapshot, ScoringContext context, Long taskId) {
context.setObjectiveTotal(context.getObjectiveTotal() + 1);
String userAnswer = snapshot.getUserAnswer();
String rightAnswer = snapshot.getRightAnswer();
boolean isRight = false;
if (userAnswer != null && rightAnswer != null) {
if (QuestionTypeEnum.MULTIPLE_CHOICE.getValue().equals(snapshot.getType())) {
// 多选题:集合比较(忽略顺序)
Set<String> userSet = Arrays.stream(userAnswer.split(",")).map(String::trim).collect(Collectors.toSet());
Set<String> rightSet = Arrays.stream(rightAnswer.split(",")).map(String::trim).collect(Collectors.toSet());
isRight = userSet.equals(rightSet);
} else {
// 单选题、判断题:直接比较
isRight = userAnswer.trim().equalsIgnoreCase(rightAnswer.trim());
}
}
snapshot.setIsRight(isRight);
if (isRight) {
context.setRightCount(context.getRightCount() + 1);
}
}
}

19
src/main/java/com/project/exam/domain/service/strategy/PaperAssemblyStrategy.java

@ -0,0 +1,19 @@
package com.project.exam.domain.service.strategy;
import com.project.exam.domain.dto.ExamRecordDTO;
/**
* 组卷策略接口
* 生成式模式A-Res 算法动态抽卷 + 锁题 + 水位线补库
* 经典模式从快照表随机抽一个版本不锁题不补库
*/
public interface PaperAssemblyStrategy {
/**
* 组卷
* @param taskId 考试任务ID
* @param userId 考生ID
* @return 考试记录DTO含题目快照
*/
ExamRecordDTO assemblePaper(Long taskId, String userId) throws Exception;
}

31
src/main/java/com/project/exam/domain/service/strategy/PaperAssemblyStrategyFactory.java

@ -0,0 +1,31 @@
package com.project.exam.domain.service.strategy;
import com.project.task.domain.enums.ExamModeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 组卷策略工厂
* 根据 examMode 分发到对应的策略实现
*/
@Component
public class PaperAssemblyStrategyFactory {
@Autowired
private GenerativePaperStrategy generativePaperStrategy;
@Autowired
private ClassicPaperStrategy classicPaperStrategy;
/**
* 获取组卷策略
* @param examMode 考试模式0-生成式1-经典套题
* @return 对应的策略实现
*/
public PaperAssemblyStrategy getStrategy(Integer examMode) {
if (ExamModeEnum.CLASSIC.getValue().equals(examMode)) {
return classicPaperStrategy;
}
return generativePaperStrategy;
}
}

32
src/main/java/com/project/exam/domain/service/strategy/ScoringContext.java

@ -0,0 +1,32 @@
package com.project.exam.domain.service.strategy;
import com.project.task.domain.entity.TaskEntity;
import lombok.Data;
/**
* 判分上下文
* 在判分过程中累积客观题得分最终与简答题得分汇总
*/
@Data
public class ScoringContext {
/** 任务配置 */
private TaskEntity task;
/** 客观题累计得分 */
private Double objectiveScore = 0.0;
/** 客观题答对数 */
private int rightCount = 0;
/** 客观题总数 */
private int objectiveTotal = 0;
/** 简答题累计得分 */
private Double subjectiveScore = 0.0;
/** 最终总分 */
public Double getTotalScore() {
return objectiveScore + subjectiveScore;
}
}

26
src/main/java/com/project/exam/domain/service/strategy/ScoringStrategy.java

@ -0,0 +1,26 @@
package com.project.exam.domain.service.strategy;
import com.project.exam.domain.dto.ExamRecordDTO;
/**
* 判分策略接口
* 客观题比对答案算分
* 主观题简答题调AI阅卷
*/
public interface ScoringStrategy {
/**
* 判断该策略是否支持此题型
* @param questionType 题型
* @return 是否支持
*/
boolean supports(Integer questionType);
/**
* 对单题判分
* @param snapshot 题目快照含用户答案
* @param context 判分上下文累积得分
* @param taskId 任务ID用于获取分值配置
*/
void score(ExamRecordDTO.QuestionSnapshotDTO snapshot, ScoringContext context, Long taskId);
}

29
src/main/java/com/project/exam/domain/service/strategy/ScoringStrategyFactory.java

@ -0,0 +1,29 @@
package com.project.exam.domain.service.strategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 判分策略工厂
* 根据题型分发到对应的策略实现
*/
@Component
public class ScoringStrategyFactory {
@Autowired
private List<ScoringStrategy> strategies;
/**
* 获取判分策略
* @param questionType 题型
* @return 对应的策略实现
*/
public ScoringStrategy getStrategy(Integer questionType) {
return strategies.stream()
.filter(s -> s.supports(questionType))
.findFirst()
.orElseThrow(() -> new RuntimeException("不支持的题型: " + questionType));
}
}

100
src/main/java/com/project/exam/domain/service/strategy/SubjectiveScoringStrategy.java

@ -0,0 +1,100 @@
package com.project.exam.domain.service.strategy;
import com.project.exam.domain.dto.ExamRecordDTO;
import com.project.exam.domain.entity.AiGradingLogEntity;
import com.project.exam.domain.service.AiGradingLogBaseService;
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.task.domain.enums.QuestionTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 简答题判分策略
* 调用AI阅卷多点得分制命中得分点数/总得分点数 × 题目分值
*/
@Component
@Slf4j
public class SubjectiveScoringStrategy implements ScoringStrategy {
@Autowired
private PostToAiScoringDomainService postToAiScoringDomainService;
@Autowired
private AiGradingLogBaseService aiGradingLogBaseService;
@Override
public boolean supports(Integer questionType) {
return QuestionTypeEnum.SHORT_ANSWER.getValue().equals(questionType);
}
@Override
public void score(ExamRecordDTO.QuestionSnapshotDTO snapshot, ScoringContext context, Long taskId) {
Long questionId = snapshot.getQuestionId();
// 构建AI阅卷请求
AiScoringRequestDTO request = new AiScoringRequestDTO();
request.setQuestionId(questionId);
request.setQuestionContent(snapshot.getQuestionContent());
request.setUserAnswer(snapshot.getUserAnswer());
// 构建得分点列表(从快照的 options 中获取,简答题的 options 存储得分点)
// 注意:得分点存储在 QuestionDetail.scoringPoints 中,需要从快照中获取
// 这里先用空列表,后续从快照中获取
request.setScoringPoints(new ArrayList<>());
// 创建判分日志
AiGradingLogEntity logEntity = new AiGradingLogEntity();
logEntity.setExamRecordId(context.getTask().getId()); // 临时,后续需要传入真正的 examRecordId
logEntity.setQuestionId(questionId);
logEntity.setQuestionContent(snapshot.getQuestionContent());
logEntity.setUserAnswer(snapshot.getUserAnswer());
logEntity.setStatus(0); // 处理中
try {
// 调用AI阅卷
AiScoringResponseDTO response = postToAiScoringDomainService.requestAiScoring(request);
if (response.getStatus() == 1 && response.getHitPoints() != null) {
// 计算得分:命中数 / 总数 × 题目分值
int hitCount = response.getHitPoints().size();
int totalCount = response.getTotalPoints();
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.setUserScore(aiScore);
// 累加简答题得分
context.setSubjectiveScore(context.getSubjectiveScore() + aiScore);
// 更新日志
logEntity.setAiScore(aiScore);
logEntity.setHitPoints(response.getHitPoints());
logEntity.setTotalPoints(totalCount);
logEntity.setAiComment(response.getComment());
logEntity.setStatus(1); // 成功
} else {
log.error(">>> [AI阅卷] 评分失败, questionId={}, error={}", questionId, response.getErrorMsg());
logEntity.setStatus(2); // 失败
logEntity.setErrorMsg(response.getErrorMsg());
}
} catch (Exception e) {
log.error(">>> [AI阅卷] 异常, questionId={}", questionId, e);
logEntity.setStatus(2); // 失败
logEntity.setErrorMsg(e.getMessage());
}
// 保存日志
aiGradingLogBaseService.save(logEntity);
}
}

9
src/main/java/com/project/exam/mapper/AiGradingLogMapper.java

@ -0,0 +1,9 @@
package com.project.exam.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.project.exam.domain.entity.AiGradingLogEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiGradingLogMapper extends BaseMapper<AiGradingLogEntity> {
}

36
src/main/java/com/project/interaction/domain/dto/AiScoringRequestDTO.java

@ -0,0 +1,36 @@
package com.project.interaction.domain.dto;
import lombok.Data;
import java.util.List;
/**
* AI阅卷请求 DTO
* 发送给算法服务请求对简答题进行评分
*/
@Data
public class AiScoringRequestDTO {
/** 考试记录ID */
private Long examRecordId;
/** 题目ID */
private Long questionId;
/** 题目内容 */
private String questionContent;
/** 考生作答 */
private String userAnswer;
/** 得分点列表 */
private List<ScoringPoint> scoringPoints;
@Data
public static class ScoringPoint {
/** 得分点序号 */
private Integer index;
/** 得分点内容 */
private String content;
}
}

28
src/main/java/com/project/interaction/domain/dto/AiScoringResponseDTO.java

@ -0,0 +1,28 @@
package com.project.interaction.domain.dto;
import lombok.Data;
import java.util.List;
/**
* AI阅卷响应 DTO
* 算法服务返回的评分结果
*/
@Data
public class AiScoringResponseDTO {
/** 命中的得分点index列表 */
private List<Integer> hitPoints;
/** 总得分点数 */
private Integer totalPoints;
/** AI评语 */
private String comment;
/** 处理状态:1-成功,2-失败 */
private Integer status;
/** 错误信息(失败时有值) */
private String errorMsg;
}

18
src/main/java/com/project/interaction/domain/service/PostToAiScoringDomainService.java

@ -0,0 +1,18 @@
package com.project.interaction.domain.service;
import com.project.interaction.domain.dto.AiScoringRequestDTO;
import com.project.interaction.domain.dto.AiScoringResponseDTO;
/**
* 调用算法服务进行AI阅卷同步调用
* 简答题数量少1-3道同步等待返回
*/
public interface PostToAiScoringDomainService {
/**
* 请求AI阅卷
* @param request 请求参数
* @return 评分结果
*/
AiScoringResponseDTO requestAiScoring(AiScoringRequestDTO request);
}

63
src/main/java/com/project/interaction/domain/service/impl/PostToAiScoringDomainServiceImpl.java

@ -0,0 +1,63 @@
package com.project.interaction.domain.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.interaction.domain.dto.AiScoringRequestDTO;
import com.project.interaction.domain.dto.AiScoringResponseDTO;
import com.project.interaction.domain.service.PostToAiScoringDomainService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import java.time.Duration;
/**
* 调用算法服务进行AI阅卷
* 同步调用60秒超时
* 算法服务API待确认URL为占位实现
*/
@Service
@Slf4j
public class PostToAiScoringDomainServiceImpl implements PostToAiScoringDomainService {
@Resource(name = "algorithmWebClient")
private WebClient algorithmWebClient;
/** 算法服务阅卷路径(待确认) */
@Value("${algo.scoringUrl:/ai-scoring}")
private String scoringUrl;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public AiScoringResponseDTO requestAiScoring(AiScoringRequestDTO request) {
try {
log.info(">>> [AI阅卷] 正在请求AI阅卷, questionId={}, examRecordId={}",
request.getQuestionId(), request.getExamRecordId());
String responseBody = algorithmWebClient.post()
.uri(scoringUrl)
.bodyValue(request)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(60))
.block();
log.info(">>> [AI阅卷] 算法服务返回, questionId={}, response={}",
request.getQuestionId(), responseBody);
// 解析响应
AiScoringResponseDTO response = objectMapper.readValue(responseBody, AiScoringResponseDTO.class);
response.setStatus(1); // 成功
return response;
} catch (Exception e) {
log.error(">>> [AI阅卷] 算法服务调用异常, questionId={}", request.getQuestionId(), e);
AiScoringResponseDTO errorResponse = new AiScoringResponseDTO();
errorResponse.setStatus(2); // 失败
errorResponse.setErrorMsg(e.getMessage());
return errorResponse;
}
}
}
Loading…
Cancel
Save