Browse Source

答题相关逻辑

master
luoweijian 1 month ago
parent
commit
4dd27e3dfb
  1. 11
      pom.xml
  2. 40
      src/main/java/com/project/base/domain/service/IBaseService.java
  3. 14
      src/main/java/com/project/ding/controller/LoginController.java
  4. 2
      src/main/java/com/project/ding/domain/service/AuthDomainService.java
  5. 43
      src/main/java/com/project/ding/domain/service/impl/AuthDomainServiceImpl.java
  6. 2
      src/main/java/com/project/ding/utils/JwtUtils.java
  7. 24
      src/main/java/com/project/ding/utils/SecurityUtils.java
  8. 9
      src/main/java/com/project/exam/application/ExamRecordApplicationService.java
  9. 19
      src/main/java/com/project/exam/application/impl/ExamRecordApplicationServiceImpl.java
  10. 21
      src/main/java/com/project/exam/controller/ExamRecordController.java
  11. 66
      src/main/java/com/project/exam/domain/dto/ExamRecordDTO.java
  12. 23
      src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java
  13. 11
      src/main/java/com/project/exam/domain/service/AssemblePaperDomainService.java
  14. 7
      src/main/java/com/project/exam/domain/service/BuildExamRecordDomainService.java
  15. 7
      src/main/java/com/project/exam/domain/service/ExamRecordBaseService.java
  16. 6
      src/main/java/com/project/exam/domain/service/StepSaveExamRecordDomainService.java
  17. 4
      src/main/java/com/project/exam/domain/service/SummitPaperDomainService.java
  18. 245
      src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java
  19. 34
      src/main/java/com/project/exam/domain/service/impl/BuildExamRecordDomainServiceImpl.java
  20. 11
      src/main/java/com/project/exam/domain/service/impl/ExamRecordBaseServiceImpl.java
  21. 29
      src/main/java/com/project/exam/domain/service/impl/StepSaveExamRecordDomainServiceImpl.java
  22. 22
      src/main/java/com/project/exam/mapper/ExamRecordMapper.java
  23. 1
      src/main/java/com/project/interaction/domain/service/impl/SaveClusterDomainServiceImpl.java
  24. 1
      src/main/java/com/project/question/domain/dto/QuestionDTO.java
  25. 3
      src/main/java/com/project/question/domain/dto/TaskKnowledgePointDTO.java
  26. 54
      src/main/java/com/project/question/domain/entity/QuestionAnswerEntity.java
  27. 9
      src/main/java/com/project/question/domain/entity/QuestionEntity.java
  28. 14
      src/main/java/com/project/question/domain/entity/TaskKnowledgePointEntity.java
  29. 16
      src/main/java/com/project/question/mapper/QuestionKpRelMapper.java
  30. 17
      src/main/java/com/project/task/domain/dto/TaskUserDTO.java
  31. 6
      src/main/java/com/project/task/domain/enums/QuestionTypeEnum.java
  32. 6
      src/main/java/com/project/task/domain/service/impl/DeleteTaskDomainServiceImpl.java

11
pom.xml

@ -76,6 +76,17 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Redis 核心启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池依赖 (Spring Boot 3.x 默认使用 Lettuce,必须加这个才能开启连接池) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>

40
src/main/java/com/project/base/domain/service/IBaseService.java

@ -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);
}
}

14
src/main/java/com/project/ding/controller/LoginController.java

@ -4,10 +4,7 @@ import com.project.base.domain.result.Result;
import com.project.ding.domain.dto.LoginDTO;
import com.project.ding.domain.service.AuthDomainService;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/login")
@ -21,7 +18,14 @@ public class LoginController {
*/
@PostMapping("/ding")
public Result<LoginDTO> login(@RequestParam("authCode") String authCode) {
// 逻辑全部下沉到 Service
return Result.success(authDomainService.loginByDing(authCode));
}
/**
* 钉钉免登
*/
@GetMapping("/getToken")
public Result<LoginDTO> getToken(String userId) {
return Result.success(authDomainService.getToken(userId));
}
}

2
src/main/java/com/project/ding/domain/service/AuthDomainService.java

@ -7,4 +7,6 @@ public interface AuthDomainService {
* 钉钉免登逻辑
*/
LoginDTO loginByDing(String authCode);
LoginDTO getToken(String userId);
}

43
src/main/java/com/project/ding/domain/service/impl/AuthDomainServiceImpl.java

@ -1,10 +1,14 @@
package com.project.ding.domain.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.project.base.domain.exception.BusinessErrorException;
import com.project.ding.auth.DingTalkAuthenticationToken;
import com.project.ding.domain.dto.LoginDTO;
import com.project.ding.domain.entity.AdminWhiteListEntity;
import com.project.ding.domain.entity.UserEntity;
import com.project.ding.domain.service.AuthDomainService;
import com.project.ding.mapper.AdminWhiteListMapper;
import com.project.ding.mapper.UserMapper;
import com.project.ding.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -14,7 +18,9 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;
import org.springframework.security.core.GrantedAuthority;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Service
@Slf4j
@ -26,6 +32,12 @@ public class AuthDomainServiceImpl implements AuthDomainService {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private AdminWhiteListMapper adminWhiteListMapper;
@Autowired
private UserMapper userMapper;
@Override
public LoginDTO loginByDing(String authCode) {
// 1. 构建未认证的内部 Token
@ -57,4 +69,35 @@ public class AuthDomainServiceImpl implements AuthDomainService {
.roles(roles)
.build();
}
@Override
public LoginDTO getToken(String userId) {
// 1. 模拟从数据库获取用户信息(跳过钉钉 API 校验)
UserEntity user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessErrorException("调试失败:本地数据库不存在该用户,请先执行同步");
}
// 2. 模拟权限分配逻辑(与 Provider 中的逻辑保持一致)
List<String> roles = new ArrayList<>();
roles.add("ROLE_CANDIDATE"); // 所有人都是考生
// 检查是否在管理员白名单
AdminWhiteListEntity adminWhiteListEntity = adminWhiteListMapper.selectOne(new LambdaQueryWrapper<AdminWhiteListEntity>()
.eq(AdminWhiteListEntity::getUserId, userId));
if (Objects.nonNull(adminWhiteListEntity)) {
roles.add("ROLE_ADMIN");
}
// 3. 生成 JWT
String token = jwtUtils.createToken(user.getId(), roles);
log.info(">>> [DEBUG] 为用户 {} 手动签发了调试 Token", userId);
return LoginDTO.builder()
.token(token)
.name(user.getName())
.roles(roles)
.build();
}
}

2
src/main/java/com/project/ding/utils/JwtUtils.java

@ -21,7 +21,7 @@ public class JwtUtils {
.claim("roles", roles)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.signWith(KEY).compact();
}
}
public Claims parseToken(String token) {
return Jwts.parserBuilder().setSigningKey(KEY).build().parseClaimsJws(token).getBody();

24
src/main/java/com/project/ding/utils/SecurityUtils.java

@ -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());
}
}

9
src/main/java/com/project/exam/application/ExamRecordApplicationService.java

@ -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;
}

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

@ -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()));
}
}

21
src/main/java/com/project/exam/controller/ExamRecordController.java

@ -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);
}
}

66
src/main/java/com/project/exam/domain/dto/ExamRecordDTO.java

@ -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;
}
}

23
src/main/java/com/project/task/domain/entity/ExamRecordEntity.java → src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java

@ -1,22 +1,26 @@
package com.project.task.domain.entity;
package com.project.exam.domain.entity;
import cn.hutool.core.collection.CollUtil;
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 com.project.exam.domain.dto.ExamRecordDTO;
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 org.springframework.beans.BeanUtils;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
@Data
@Table(name = "evaluator_exam_record")
@ -79,5 +83,22 @@ public class ExamRecordEntity extends BaseEntity {
private Boolean hasAppealed = false;
}
@Override
public <T> T toDTO(Supplier<T> supplier) {
T result = super.toDTO(supplier);
if (result instanceof ExamRecordDTO dto) {
// 处理内部快照列表从 Entity 到 DTO 的转换
if (CollUtil.isNotEmpty(this.answerSnapshot)) {
List<ExamRecordDTO.QuestionSnapshotDTO> dtoList = this.answerSnapshot.stream().map(entitySnapshot -> {
ExamRecordDTO.QuestionSnapshotDTO snapshotDTO = new ExamRecordDTO.QuestionSnapshotDTO();
BeanUtils.copyProperties(entitySnapshot, snapshotDTO);
return snapshotDTO;
}).toList();
dto.setAnswerSnapshotDTOList(dtoList);
}
}
return result;
}
}

11
src/main/java/com/project/exam/domain/service/AssemblePaperDomainService.java

@ -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;
}

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

@ -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;
}

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

@ -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> {
}

6
src/main/java/com/project/exam/domain/service/StepSaveExamRecordDomainService.java

@ -0,0 +1,6 @@
package com.project.exam.domain.service;
public interface StepSaveExamRecordDomainService {
void stepSave(Long recordId , int index , String answer) throws Exception;
}

4
src/main/java/com/project/exam/domain/service/SummitPaperDomainService.java

@ -0,0 +1,4 @@
package com.project.exam.domain.service;
public interface SummitPaperDomainService {
}

245
src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java

@ -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();
}
}
}
}

34
src/main/java/com/project/exam/domain/service/impl/BuildExamRecordDomainServiceImpl.java

@ -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;
}
}

11
src/main/java/com/project/exam/domain/service/impl/ExamRecordBaseServiceImpl.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.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 {
}

29
src/main/java/com/project/exam/domain/service/impl/StepSaveExamRecordDomainServiceImpl.java

@ -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("保存进度失败,请确认考试是否有效");
}
}
}

22
src/main/java/com/project/exam/mapper/ExamRecordMapper.java

@ -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
src/main/java/com/project/interaction/domain/service/impl/SaveClusterDomainServiceImpl.java

@ -68,6 +68,7 @@ public class SaveClusterDomainServiceImpl implements SaveClusterDomainService {
taskKp.setClusterId(clusterEntity.getId());
taskKp.setAtomId(atomKp.getId());
taskKp.setContent(atomKp.getContent());
taskKp.setClusterSize(clusterEntity.getClusterSize());
return taskKp;
}).toList();
// 批量插入

1
src/main/java/com/project/question/domain/dto/QuestionDTO.java

@ -53,7 +53,6 @@ public class QuestionDTO extends BaseDTO {
entity.setId(this.getId());
}
return result;
}
}

3
src/main/java/com/project/question/domain/dto/TaskKnowledgePointDTO.java

@ -10,6 +10,9 @@ public class TaskKnowledgePointDTO extends BaseDTO {
private Long clusterId;
private Long atomId;
private String content;
private Integer clusterSize;
private Integer weightScore;
}

54
src/main/java/com/project/question/domain/entity/QuestionAnswerEntity.java

@ -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;
}

9
src/main/java/com/project/question/domain/entity/QuestionEntity.java

@ -8,10 +8,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.project.base.domain.entity.BaseEntity;
import com.project.question.domain.dto.QuestionDTO;
import com.project.question.domain.enums.QuestionUseStatusEnum;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
@ -26,7 +23,9 @@ import java.util.function.Supplier;
@Data
@Table(name = "evaluator_question" )
@Table(name = "evaluator_question" , indexes = {
@Index(name = "useStatus_questionType", columnList = "use_status, question_type")
})
@Entity
@TableName(value = "evaluator_question", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)

14
src/main/java/com/project/question/domain/entity/TaskKnowledgePointEntity.java

@ -5,16 +5,14 @@ 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 jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
@Data
@Table(name = "evaluator_task_knowledge_point" )
@Table(name = "evaluator_task_knowledge_point" ,
indexes = {@Index(name = "Idx_taskId", columnList = "task_id")})
@Entity
@TableName(value = "evaluator_task_knowledge_point", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)
@ -30,13 +28,11 @@ public class TaskKnowledgePointEntity extends BaseEntity {
@Column(name = "cluster_id")
@TableField("cluster_id")
@Comment("所属簇ID(关联 evaluator_task_knowledge_cluster)")
private Long clusterId;
@Column(name = "atom_id")
@TableField("atom_id")
@Comment("关联原始原子知识点ID(对应原始资料解析出的ID)")
private Long atomId;
@ -44,5 +40,9 @@ public class TaskKnowledgePointEntity extends BaseEntity {
@Comment("知识点原文文本")
private String content;
@Column(name = "cluster_size")
@TableField("cluster_size")
@Comment("所在簇大小(W):所在簇包含的知识点个数")
private Integer clusterSize;
}

16
src/main/java/com/project/question/mapper/QuestionKpRelMapper.java

@ -3,7 +3,23 @@ package com.project.question.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.project.question.domain.entity.QuestionKpRelEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.data.repository.query.Param;
import java.util.List;
@Mapper
public interface QuestionKpRelMapper extends BaseMapper<QuestionKpRelEntity> {
/**
* 根据知识点ID和题型查询该点下所有未被使用的题目ID
* 逻辑关联题目主表检查 use_status 是否为 0 (未用)
*/
@Select("SELECT r.question_id " +
"FROM evaluator_question_kp_rel r " +
"INNER JOIN evaluator_question q ON r.question_id = q.id " +
"WHERE r.kp_id = #{kpId} " +
" AND q.question_type = #{type} " + // 增加题型过滤
" AND q.use_status = 0 " +
" AND q.deleted = 0")
List<Long> findAvailableIdsByKpAndType(@Param("kpId") Long kpId, @Param("type") Integer type);
}

17
src/main/java/com/project/task/domain/dto/TaskUserDTO.java

@ -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;
}

6
src/main/java/com/project/task/domain/enums/QuestionTypeEnum.java

@ -8,9 +8,9 @@ import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum QuestionTypeEnum implements HasValueEnum<Integer> {
SINGLE_CHOICE(1, "单选题","single"), // 对应黄色背景
MULTIPLE_CHOICE(2, "多选题","multiple"), // 对应黄色背景
TRUE_FALSE(3, "判断题","judgment"); // 对应绿色背景
SINGLE_CHOICE(1, "单选题","single"),
MULTIPLE_CHOICE(2, "多选题","multiple"),
TRUE_FALSE(3, "判断题","judgment");
private final Integer value;
private final String description;

6
src/main/java/com/project/task/domain/service/impl/DeleteTaskDomainServiceImpl.java

@ -25,10 +25,10 @@ public class DeleteTaskDomainServiceImpl implements DeleteTaskDomainService {
throw new BusinessErrorException("id不能为空");
}
List<String> idList = Arrays.asList(ids.split(","));
long count = taskBaseService.count(new LambdaQueryWrapper<TaskEntity>()
long count = taskBaseService.lambdaQuery()
.in(TaskEntity::getId, idList)
.ge(TaskEntity::getStartTime, DateUtil.beginOfDay(new Date())));
.ge(TaskEntity::getStartTime, DateUtil.beginOfDay(new Date()))
.count();
if (count > 0) {
throw new BusinessErrorException("已开考 、 已截止的考试任务不允许删除");
}

Loading…
Cancel
Save