32 changed files with 697 additions and 80 deletions
@ -0,0 +1,40 @@ |
|||
package com.project.base.domain.service; |
|||
|
|||
import com.baomidou.mybatisplus.extension.service.IService; |
|||
import com.project.base.domain.dto.BaseDTO; |
|||
import com.project.base.domain.entity.BaseEntity; |
|||
import java.util.function.Supplier; |
|||
|
|||
/** |
|||
* 增强版通用 Service 接口 |
|||
*/ |
|||
public interface IBaseService<T extends BaseEntity> extends IService<T> { |
|||
|
|||
/** |
|||
* 通用保存 DTO 的方法:转换 -> 保存 -> 回传带ID的DTO |
|||
*/ |
|||
default <D extends BaseDTO> D saveDTO(D dto, Supplier<T> entitySupplier, Supplier<D> dtoSupplier) { |
|||
if (dto == null) return null; |
|||
|
|||
// 1. DTO 转 Entity
|
|||
T entity = dto.toEntity(entitySupplier); |
|||
|
|||
// 2. 调用 MyBatis-Plus 的 save 方法
|
|||
// 由于 IBaseService 继承了 IService,所以这里可以直接调 this.save
|
|||
this.save(entity); |
|||
|
|||
// 3. 将持久化后(带ID和自动填充字段)的 Entity 转回 DTO 并返回
|
|||
return entity.toDTO(dtoSupplier); |
|||
} |
|||
|
|||
/** |
|||
* 【扩展】通用更新 DTO 的方法 |
|||
*/ |
|||
default <D extends BaseDTO> boolean updateDTO(D dto, Supplier<T> entitySupplier) { |
|||
if (dto == null) { |
|||
return false; |
|||
} |
|||
T entity = dto.toEntity(entitySupplier); |
|||
return this.updateById(entity); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
package com.project.ding.utils; |
|||
|
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
|
|||
import java.util.Optional; |
|||
|
|||
public class SecurityUtils { |
|||
/** |
|||
* 获取当前登录用户的 ID |
|||
*/ |
|||
public static String getUserId() { |
|||
return getAuthentication() |
|||
.map(auth -> (String) auth.getPrincipal()) |
|||
.orElse(null); |
|||
} |
|||
|
|||
/** |
|||
* 获取当前认证信息 |
|||
*/ |
|||
private static Optional<Authentication> getAuthentication() { |
|||
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
package com.project.exam.application; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.exam.domain.dto.ExamRecordDTO; |
|||
|
|||
public interface ExamRecordApplicationService { |
|||
|
|||
Result<ExamRecordDTO> assemblePaper(Long taskId) throws Exception; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.project.exam.application.impl; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.ding.utils.SecurityUtils; |
|||
import com.project.exam.application.ExamRecordApplicationService; |
|||
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.Service; |
|||
|
|||
@Service |
|||
public class ExamRecordApplicationServiceImpl implements ExamRecordApplicationService { |
|||
@Autowired |
|||
private AssemblePaperDomainService assemblePaperDomainService; |
|||
@Override |
|||
public Result<ExamRecordDTO> assemblePaper(Long taskId) throws Exception { |
|||
return Result.success(assemblePaperDomainService.assemblePaper(taskId , SecurityUtils.getUserId())); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.project.exam.controller; |
|||
|
|||
|
|||
import com.project.base.domain.result.Result; |
|||
import com.project.exam.application.ExamRecordApplicationService; |
|||
import com.project.exam.domain.dto.ExamRecordDTO; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/examRecord") |
|||
public class ExamRecordController { |
|||
@Autowired |
|||
private ExamRecordApplicationService examRecordApplicationService; |
|||
@PostMapping("/assemblePaper") |
|||
public Result<ExamRecordDTO> assemblePaper(Long taskId) throws Exception{ |
|||
return examRecordApplicationService.assemblePaper(taskId); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
package com.project.exam.domain.dto; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import com.project.base.domain.dto.BaseDTO; |
|||
import com.project.exam.domain.entity.ExamRecordEntity; |
|||
import com.project.task.domain.dto.TaskDTO; |
|||
import lombok.Data; |
|||
import org.springframework.beans.BeanUtils; |
|||
|
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.function.Supplier; |
|||
|
|||
@Data |
|||
public class ExamRecordDTO extends BaseDTO { |
|||
private Long id; |
|||
private Long taskUserId; |
|||
private Double score; |
|||
private Boolean pass; |
|||
private Date startTime; |
|||
private Date submitTime; |
|||
private List<QuestionSnapshotDTO> answerSnapshotDTOList; |
|||
|
|||
private Long taskId; |
|||
private TaskDTO taskDTO; |
|||
@Data |
|||
public static class QuestionSnapshotDTO { |
|||
private Long questionId; |
|||
// 题干原文
|
|||
private String questionContent; |
|||
// 题型
|
|||
private Integer type; |
|||
// 用户看的选项:{"A":"xxx", "B":"yyy"}
|
|||
private Map<String, String> options; |
|||
// 正确项
|
|||
private String rightAnswer; |
|||
// 用户选的
|
|||
private String userAnswer; |
|||
// 判定正确
|
|||
private Boolean isRight; |
|||
// AI解析
|
|||
private String analysis; |
|||
// 该题是否已发起申诉
|
|||
private Boolean hasAppealed; |
|||
private Double score; |
|||
} |
|||
|
|||
@Override |
|||
public <T> T toEntity(Supplier<T> supplier) { |
|||
T result = super.toEntity(supplier); |
|||
if (result instanceof ExamRecordEntity entity) { |
|||
// 处理内部列表快照的转换
|
|||
if (CollUtil.isNotEmpty(this.answerSnapshotDTOList)) { |
|||
List<ExamRecordEntity.QuestionSnapshot> snapshotList = this.answerSnapshotDTOList.stream().map(dto -> { |
|||
ExamRecordEntity.QuestionSnapshot snapshot = new ExamRecordEntity.QuestionSnapshot(); |
|||
BeanUtils.copyProperties(dto, snapshot); |
|||
return snapshot; |
|||
}).toList(); |
|||
entity.setAnswerSnapshot(snapshotList); |
|||
} |
|||
entity.setId(this.getId()); |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
package com.project.exam.domain.service; |
|||
|
|||
import com.project.exam.domain.dto.ExamRecordDTO; |
|||
|
|||
public interface AssemblePaperDomainService { |
|||
|
|||
|
|||
ExamRecordDTO assemblePaper(Long taskId , String userId) throws Exception; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
package com.project.exam.domain.service; |
|||
|
|||
import com.project.exam.domain.dto.ExamRecordDTO; |
|||
|
|||
public interface BuildExamRecordDomainService { |
|||
ExamRecordDTO buildDTO(ExamRecordDTO dto) throws Exception; |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
package com.project.exam.domain.service; |
|||
|
|||
import com.project.base.domain.service.IBaseService; |
|||
import com.project.exam.domain.entity.ExamRecordEntity; |
|||
|
|||
public interface ExamRecordBaseService extends IBaseService<ExamRecordEntity> { |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
package com.project.exam.domain.service; |
|||
|
|||
public interface StepSaveExamRecordDomainService { |
|||
|
|||
void stepSave(Long recordId , int index , String answer) throws Exception; |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
package com.project.exam.domain.service; |
|||
|
|||
public interface SummitPaperDomainService { |
|||
} |
|||
@ -0,0 +1,245 @@ |
|||
package com.project.exam.domain.service.impl; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import com.project.exam.domain.service.AssemblePaperDomainService; |
|||
import com.project.exam.domain.service.BuildExamRecordDomainService; |
|||
import com.project.question.domain.dto.QuestionDTO; |
|||
import com.project.question.domain.dto.TaskKnowledgePointDTO; |
|||
import com.project.question.domain.entity.QuestionEntity; |
|||
import com.project.question.domain.entity.TaskKnowledgePointEntity; |
|||
import com.project.question.domain.enums.QuestionUseStatusEnum; |
|||
import com.project.question.domain.service.TaskKnowledgePointBaseService; |
|||
import com.project.question.mapper.QuestionKpRelMapper; |
|||
import com.project.question.mapper.QuestionMapper; |
|||
import com.project.exam.domain.dto.ExamRecordDTO; |
|||
import com.project.task.domain.dto.TaskDTO; |
|||
import com.project.task.domain.dto.TaskUserDTO; |
|||
import com.project.exam.domain.entity.ExamRecordEntity; |
|||
import com.project.task.domain.entity.TaskUserEntity; |
|||
import com.project.task.domain.enums.QuestionTypeEnum; |
|||
import com.project.task.domain.enums.TaskUserStatusEnum; |
|||
import com.project.exam.domain.service.ExamRecordBaseService; |
|||
import com.project.task.domain.service.TaskBaseService; |
|||
import com.project.task.domain.service.TaskUserBaseService; |
|||
import io.vavr.control.Try; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.BeanUtils; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.redis.core.StringRedisTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import java.util.*; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ThreadLocalRandom; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.locks.ReentrantLock; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
public class AssemblePaperDomainServiceImpl implements AssemblePaperDomainService { |
|||
// 簇锁(针对多选题)
|
|||
private final ConcurrentHashMap<Long, ReentrantLock> clusterLocks = new ConcurrentHashMap<>(); |
|||
// 知识点锁(针对单选和判断)
|
|||
private final ConcurrentHashMap<Long, ReentrantLock> kpLocks = new ConcurrentHashMap<>(); |
|||
|
|||
@Autowired |
|||
private TaskKnowledgePointBaseService taskKnowledgePointBaseService; |
|||
|
|||
@Autowired |
|||
private TaskBaseService taskBaseService; |
|||
|
|||
@Autowired |
|||
private TaskUserBaseService taskUserBaseService; |
|||
|
|||
@Autowired |
|||
private ExamRecordBaseService examRecordBaseService; |
|||
|
|||
@Autowired |
|||
private StringRedisTemplate redisTemplate; |
|||
|
|||
@Autowired |
|||
private BuildExamRecordDomainService buildExamRecordDomainService; |
|||
|
|||
@Autowired |
|||
private QuestionMapper questionMapper; |
|||
|
|||
@Autowired |
|||
private QuestionKpRelMapper questionKpRelMapper; |
|||
|
|||
private static final String EXAM_START_LOCK = "lock:exam:start:%s:%s"; |
|||
|
|||
|
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public ExamRecordDTO assemblePaper(Long taskId, String userId) throws Exception { |
|||
// 校验是否已通过考核
|
|||
TaskUserDTO taskUserDTO = Try.of(() -> taskUserBaseService.getOne(new LambdaQueryWrapper<TaskUserEntity>() |
|||
.eq(TaskUserEntity::getTaskId, taskId).eq(TaskUserEntity::getUserId, userId)) |
|||
.toDTO(TaskUserDTO::new)).getOrNull(); |
|||
if (Objects.nonNull(taskUserDTO)) { |
|||
throw new BusinessErrorException("您无需参与本场考核"); |
|||
} |
|||
if (TaskUserStatusEnum.Pass.getValue().equals(taskUserDTO.getStatus())) { |
|||
throw new BusinessErrorException("您已通过考核,无需重复考试"); |
|||
} |
|||
// 防抖
|
|||
String lockKey = String.format(EXAM_START_LOCK , taskId , userId); |
|||
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "processing", 30, TimeUnit.SECONDS); |
|||
if (Boolean.FALSE.equals(isLock)) { |
|||
log.warn(">>> [考试限制] 用户{} 尝试短时间内重复参加任务{}", userId, taskId); |
|||
throw new BusinessErrorException("请勿短时间内重复参加考试"); |
|||
} |
|||
|
|||
// 获取任务配置
|
|||
TaskDTO taskDTO = taskBaseService.getById(taskId).toDTO(TaskDTO::new); |
|||
int totalQuestionNum = taskDTO.getSingleChoiceNum() + taskDTO.getMultipleChoiceNum() + taskDTO.getTrueFalseNum(); |
|||
// 权重乱序种子选取
|
|||
List<TaskKnowledgePointDTO> seedKpList = selectWeightedSeedKpList(taskDTO, totalQuestionNum); |
|||
// 本张试卷已覆盖的知识点ID集合
|
|||
Set<Long> coveredKpPool = new HashSet<>(); |
|||
// 最终选定题目
|
|||
List<QuestionDTO> selectedQuestionList = new ArrayList<>(); |
|||
// 分配种子:前N3个多选,中间N1个单选,最后N2个判断
|
|||
List<TaskKnowledgePointDTO> mcSeeds = seedKpList.subList(0, taskDTO.getMultipleChoiceNum()); |
|||
List<TaskKnowledgePointDTO> scSeeds = seedKpList.subList(taskDTO.getMultipleChoiceNum() , taskDTO.getMultipleChoiceNum() + taskDTO.getSingleChoiceNum()); |
|||
List<TaskKnowledgePointDTO> tfSeeds = seedKpList.subList(taskDTO.getMultipleChoiceNum() + taskDTO.getSingleChoiceNum() , totalQuestionNum); |
|||
// 选择单选题
|
|||
pickWithLock(scSeeds, QuestionTypeEnum.SINGLE_CHOICE , selectedQuestionList, coveredKpPool); |
|||
// 选择判断题
|
|||
pickWithLock(tfSeeds, QuestionTypeEnum.TRUE_FALSE , selectedQuestionList, coveredKpPool); |
|||
|
|||
pickMultipleQuestionsWithGreedy(mcSeeds , selectedQuestionList, coveredKpPool); |
|||
selectedQuestionList.sort(Comparator.comparing(QuestionDTO::getQuestionType)); |
|||
// 校验题数
|
|||
if (selectedQuestionList.size() < totalQuestionNum) { |
|||
throw new BusinessErrorException("当前题库可用题目不足,请联系管理员补货"); |
|||
} |
|||
// todo 异步起一个检测库存
|
|||
|
|||
// 持久化记录
|
|||
ExamRecordDTO recordDTO = new ExamRecordDTO(); |
|||
recordDTO.setTaskDTO(taskDTO); |
|||
recordDTO.setTaskUserId(taskUserDTO.getId()); |
|||
recordDTO.setStartTime(new Date()); |
|||
recordDTO.setAnswerSnapshotDTOList(buildPaperSnapshot(selectedQuestionList)); |
|||
ExamRecordDTO examRecordDTO = examRecordBaseService.saveDTO(recordDTO, ExamRecordEntity::new, ExamRecordDTO::new); |
|||
return buildExamRecordDomainService.buildDTO(examRecordDTO); |
|||
} |
|||
|
|||
private List<ExamRecordDTO.QuestionSnapshotDTO> buildPaperSnapshot(List<QuestionDTO> questionDTOList) { |
|||
return questionDTOList.stream().map(questionDTO -> { |
|||
QuestionDTO.QuestionDetailDTO detail = questionDTO.getQuestionDetailDTO(); |
|||
ExamRecordDTO.QuestionSnapshotDTO snapshot = new ExamRecordDTO.QuestionSnapshotDTO(); |
|||
BeanUtils.copyProperties(detail , snapshot); |
|||
return snapshot; |
|||
}).toList(); |
|||
} |
|||
|
|||
/** |
|||
* 算权重,带权乱序抽取种子知识点 |
|||
* @param dto |
|||
* @param totalNum |
|||
* @return |
|||
*/ |
|||
private List<TaskKnowledgePointDTO> selectWeightedSeedKpList(TaskDTO dto , int totalNum) { |
|||
List<TaskKnowledgePointDTO> kpList = taskKnowledgePointBaseService.lambdaQuery() |
|||
.eq(TaskKnowledgePointEntity::getTaskId, dto.getId()).list().stream() |
|||
.map(entity -> entity.toDTO(TaskKnowledgePointDTO::new)).toList(); |
|||
// 按簇分组
|
|||
Map<Long, List<TaskKnowledgePointDTO>> clusterGroup = kpList.stream() |
|||
.collect(Collectors.groupingBy(TaskKnowledgePointDTO::getClusterId)); |
|||
// 计算分层常量 C = 当前任务中最大簇的规模 + 1
|
|||
int maxClusterSize = clusterGroup.values().stream() |
|||
.mapToInt(List::size) |
|||
.max() |
|||
.orElse(0); |
|||
int c = maxClusterSize + 1; |
|||
// 簇内洗牌并计算 S
|
|||
clusterGroup.forEach((clusterId, kpsInCluster) -> { |
|||
// 每次组卷都随机洗牌,保证 IDx 动态变化
|
|||
Collections.shuffle(kpsInCluster); |
|||
int w = kpsInCluster.size(); // 固定簇大小
|
|||
for (int i = 0; i < kpsInCluster.size(); i++) { |
|||
TaskKnowledgePointDTO kp = kpsInCluster.get(i); |
|||
// 序号从 1 开始
|
|||
int idx = i + 1; |
|||
// 计算动态分值:S = W + (IDx * C)
|
|||
Integer dynamicS = w + (idx * c); |
|||
kp.setWeightScore(dynamicS); |
|||
} |
|||
}); |
|||
// 带权乱序(A-Res算法)
|
|||
return kpList.stream() |
|||
.sorted(Comparator.comparingDouble(kp -> -Math.pow(ThreadLocalRandom.current().nextDouble(), 1.0 / kp.getWeightScore()))) |
|||
.limit(totalNum) |
|||
.toList(); |
|||
} |
|||
|
|||
/** |
|||
* 通用的普通题抽取逻辑(锁 KP 级别) |
|||
*/ |
|||
private void pickWithLock(List<TaskKnowledgePointDTO> seeds, QuestionTypeEnum type, |
|||
List<QuestionDTO> result, Set<Long> coveredPool) { |
|||
for (TaskKnowledgePointDTO seed : seeds) { |
|||
// 锁住当前知识点,不阻塞其他知识点的考生
|
|||
ReentrantLock lock = kpLocks.computeIfAbsent(seed.getId(), k -> new ReentrantLock()); |
|||
lock.lock(); |
|||
try { |
|||
// 查可用的题
|
|||
List<Long> qIds = questionKpRelMapper.findAvailableIdsByKpAndType(seed.getId(), type.getValue()); |
|||
if (CollUtil.isNotEmpty(qIds)) { |
|||
// 更新数据库状态
|
|||
QuestionEntity questionEntity = questionMapper.selectById(qIds.stream().findAny().get()); |
|||
questionEntity.setUseStatus(QuestionUseStatusEnum.Used.getValue()); |
|||
questionMapper.updateById(questionEntity); |
|||
QuestionDTO questionDTO = questionEntity.toDTO(QuestionDTO::new); |
|||
result.add(questionDTO); |
|||
coveredPool.addAll(questionDTO.getKpIdList()); |
|||
} |
|||
} finally { |
|||
lock.unlock(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 抽取多选题:执行贪婪算法 |
|||
*/ |
|||
private void pickMultipleQuestionsWithGreedy(List<TaskKnowledgePointDTO> seedKpList, |
|||
List<QuestionDTO> result, Set<Long> coveredKpPool) { |
|||
for (TaskKnowledgePointDTO seedKp : seedKpList) { |
|||
// 锁定当前种子所属的簇,保证抽题互斥
|
|||
ReentrantLock lock = clusterLocks.computeIfAbsent(seedKp.getClusterId(), k -> new ReentrantLock()); |
|||
lock.lock(); |
|||
try { |
|||
// 候选题目id
|
|||
List<Long> candidateIds = questionKpRelMapper.findAvailableIdsByKpAndType(seedKp.getId(), QuestionTypeEnum.MULTIPLE_CHOICE.getValue()); |
|||
if (CollUtil.isEmpty(candidateIds)){ |
|||
continue; |
|||
} |
|||
|
|||
QuestionDTO bestQuestionDTO = questionMapper.selectBatchIds(candidateIds).stream() |
|||
.max(Comparator.comparingInt(q -> { |
|||
int kp1 = CollUtil.count(q.getKpIdList(), k -> !coveredKpPool.contains(k)); |
|||
int kp2 = q.getKpIdList().size() - kp1; |
|||
return kp1 - kp2; |
|||
})).map(entity -> entity.toDTO(QuestionDTO::new)) |
|||
.orElse(null); |
|||
// 选题成功
|
|||
if (Objects.nonNull(bestQuestionDTO)) { |
|||
// c. 占位:立即更新数据库 is_used 状态,实现全系统排他
|
|||
bestQuestionDTO.setUseStatus(QuestionUseStatusEnum.Used.getValue()); |
|||
questionMapper.updateById(bestQuestionDTO.toEntity(QuestionEntity::new)); |
|||
result.add(bestQuestionDTO); |
|||
coveredKpPool.addAll(bestQuestionDTO.getKpIdList()); // 更新已覆盖池
|
|||
} |
|||
} finally { |
|||
lock.unlock(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
package com.project.exam.domain.service.impl; |
|||
|
|||
import com.project.exam.domain.dto.ExamRecordDTO; |
|||
import com.project.exam.domain.service.BuildExamRecordDomainService; |
|||
import com.project.task.domain.enums.QuestionTypeEnum; |
|||
import com.project.task.domain.service.TaskBaseService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
|
|||
@Service |
|||
public class BuildExamRecordDomainServiceImpl implements BuildExamRecordDomainService { |
|||
@Autowired |
|||
private TaskBaseService taskBaseService; |
|||
@Override |
|||
public ExamRecordDTO buildDTO(ExamRecordDTO dto) throws Exception { |
|||
// 补全题目分数
|
|||
if (Objects.nonNull(dto.getTaskDTO())) { |
|||
dto.getAnswerSnapshotDTOList().stream().map(answerSnapshotDTO -> { |
|||
if (QuestionTypeEnum.SINGLE_CHOICE.getValue().equals(answerSnapshotDTO.getType())) { |
|||
answerSnapshotDTO.setScore(dto.getTaskDTO().getSingleChoiceScore()); |
|||
} else if (QuestionTypeEnum.TRUE_FALSE.getValue().equals(answerSnapshotDTO.getType())) { |
|||
answerSnapshotDTO.setScore(dto.getTaskDTO().getTrueFalseScore()); |
|||
} else { |
|||
answerSnapshotDTO.setScore(dto.getTaskDTO().getMultipleChoiceScore()); |
|||
} |
|||
return answerSnapshotDTO; |
|||
}); |
|||
} |
|||
return dto; |
|||
} |
|||
} |
|||
@ -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.ExamRecordEntity; |
|||
import com.project.exam.domain.service.ExamRecordBaseService; |
|||
import com.project.exam.mapper.ExamRecordMapper; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
@Service |
|||
public class ExamRecordBaseServiceImpl extends ServiceImpl<ExamRecordMapper, ExamRecordEntity> implements ExamRecordBaseService { |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
package com.project.exam.domain.service.impl; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import com.project.exam.domain.service.StepSaveExamRecordDomainService; |
|||
import com.project.exam.mapper.ExamRecordMapper; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
@Service |
|||
public class StepSaveExamRecordDomainServiceImpl implements StepSaveExamRecordDomainService { |
|||
@Autowired |
|||
private ExamRecordMapper examRecordMapper; |
|||
|
|||
@Override |
|||
public void stepSave(Long recordId, int index, String answer) throws Exception { |
|||
if (index < 1) { |
|||
throw new BusinessErrorException("非法题号"); |
|||
} |
|||
if (StrUtil.isNotBlank(answer)) { |
|||
throw new BusinessErrorException("答案不能为空"); |
|||
} |
|||
int rows = examRecordMapper.updateAnswerIncremental(recordId, index - 1, answer); |
|||
|
|||
if (rows == 0) { |
|||
throw new BusinessErrorException("保存进度失败,请确认考试是否有效"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.project.exam.mapper; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.project.exam.domain.entity.ExamRecordEntity; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.apache.ibatis.annotations.Update; |
|||
import org.springframework.data.repository.query.Param; |
|||
|
|||
@Mapper |
|||
public interface ExamRecordMapper extends BaseMapper<ExamRecordEntity> { |
|||
/** |
|||
* 原子更新 JSON 数组中指定索引的用户答案 |
|||
* SQL 解释:$[index].userAnswer 对应 JSON 数组中第 index 个对象的字段 |
|||
*/ |
|||
@Update("UPDATE evaluator_exam_record SET " + |
|||
"answer_snapshot = JSON_SET(paper_snapshot, '$[${idx}].userAnswer', #{answer}), " + |
|||
"update_time = NOW() " + |
|||
"WHERE id = #{recordId}") |
|||
int updateAnswerIncremental(@Param("recordId") Long recordId, |
|||
@Param("idx") int idx, |
|||
@Param("answer") String answer); |
|||
} |
|||
@ -1,54 +0,0 @@ |
|||
package com.project.question.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.project.base.domain.entity.BaseEntity; |
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.Entity; |
|||
import jakarta.persistence.Id; |
|||
import jakarta.persistence.Table; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.hibernate.annotations.Comment; |
|||
|
|||
@Data |
|||
@Table(name = "evaluator_question_answer" ) |
|||
@Entity |
|||
@TableName(value = "evaluator_question_answer", autoResultMap = true) |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class QuestionAnswerEntity extends BaseEntity { |
|||
@TableId(type = IdType.ASSIGN_ID) |
|||
@Id |
|||
private Long id; |
|||
|
|||
@Column(name = "exam_record_id") |
|||
@TableField("exam_record_id") |
|||
@Comment("所属考试记录ID") |
|||
private Long examRecordId; |
|||
|
|||
@Column(name = "user_id") |
|||
@TableField("user_id") |
|||
@Comment("所属用户ID") |
|||
private Long userId; |
|||
|
|||
@Column(name = "question_id") |
|||
@TableField("question_id") |
|||
@Comment("问题ID") |
|||
private Long questionId; |
|||
|
|||
@Column(name = "user_answer" , columnDefinition="varchar(255) comment '用户回答答案'") |
|||
@TableField("user_answer") |
|||
private String userAnswer; |
|||
|
|||
@Column(name = "is_right") |
|||
@TableField("is_right") |
|||
@Comment("是否正确") |
|||
private Boolean isRight; |
|||
|
|||
@Column(name = "score") |
|||
@TableField("score") |
|||
@Comment("得分") |
|||
private Double score; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
package com.project.task.domain.dto; |
|||
|
|||
import com.project.base.domain.dto.BaseDTO; |
|||
import com.project.task.domain.enums.TaskUserStatusEnum; |
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class TaskUserDTO extends BaseDTO { |
|||
private Long id; |
|||
private Long taskId; |
|||
private String userId; |
|||
private Integer status; |
|||
private Long lastRecordId; |
|||
private Integer attemptNum = 0; |
|||
|
|||
|
|||
} |
|||
Loading…
Reference in new issue