diff --git a/pom.xml b/pom.xml
index 37468fc..a5d0793 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,17 @@
org.springframework.boot
spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.apache.commons
+ commons-pool2
+
org.springframework.boot
spring-boot-starter-aop
diff --git a/src/main/java/com/project/base/domain/service/IBaseService.java b/src/main/java/com/project/base/domain/service/IBaseService.java
new file mode 100644
index 0000000..930740d
--- /dev/null
+++ b/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 extends IService {
+
+ /**
+ * 通用保存 DTO 的方法:转换 -> 保存 -> 回传带ID的DTO
+ */
+ default D saveDTO(D dto, Supplier entitySupplier, Supplier 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 boolean updateDTO(D dto, Supplier entitySupplier) {
+ if (dto == null) {
+ return false;
+ }
+ T entity = dto.toEntity(entitySupplier);
+ return this.updateById(entity);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/project/ding/controller/LoginController.java b/src/main/java/com/project/ding/controller/LoginController.java
index 746bd39..5cc0f17 100644
--- a/src/main/java/com/project/ding/controller/LoginController.java
+++ b/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 login(@RequestParam("authCode") String authCode) {
- // 逻辑全部下沉到 Service
return Result.success(authDomainService.loginByDing(authCode));
}
+
+ /**
+ * 钉钉免登
+ */
+ @GetMapping("/getToken")
+ public Result getToken(String userId) {
+ return Result.success(authDomainService.getToken(userId));
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/project/ding/domain/service/AuthDomainService.java b/src/main/java/com/project/ding/domain/service/AuthDomainService.java
index 9b6fe60..26d0ef0 100644
--- a/src/main/java/com/project/ding/domain/service/AuthDomainService.java
+++ b/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);
}
\ No newline at end of file
diff --git a/src/main/java/com/project/ding/domain/service/impl/AuthDomainServiceImpl.java b/src/main/java/com/project/ding/domain/service/impl/AuthDomainServiceImpl.java
index 11b5634..4ea3d80 100644
--- a/src/main/java/com/project/ding/domain/service/impl/AuthDomainServiceImpl.java
+++ b/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 roles = new ArrayList<>();
+ roles.add("ROLE_CANDIDATE"); // 所有人都是考生
+
+ // 检查是否在管理员白名单
+ AdminWhiteListEntity adminWhiteListEntity = adminWhiteListMapper.selectOne(new LambdaQueryWrapper()
+ .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();
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/project/ding/utils/JwtUtils.java b/src/main/java/com/project/ding/utils/JwtUtils.java
index d1757e3..c4a484d 100644
--- a/src/main/java/com/project/ding/utils/JwtUtils.java
+++ b/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();
diff --git a/src/main/java/com/project/ding/utils/SecurityUtils.java b/src/main/java/com/project/ding/utils/SecurityUtils.java
new file mode 100644
index 0000000..885a9ee
--- /dev/null
+++ b/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 getAuthentication() {
+ return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication());
+ }
+}
diff --git a/src/main/java/com/project/exam/application/ExamRecordApplicationService.java b/src/main/java/com/project/exam/application/ExamRecordApplicationService.java
new file mode 100644
index 0000000..3f1ec35
--- /dev/null
+++ b/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 assemblePaper(Long taskId) throws Exception;
+}
diff --git a/src/main/java/com/project/exam/application/impl/ExamRecordApplicationServiceImpl.java b/src/main/java/com/project/exam/application/impl/ExamRecordApplicationServiceImpl.java
new file mode 100644
index 0000000..63e77c5
--- /dev/null
+++ b/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 assemblePaper(Long taskId) throws Exception {
+ return Result.success(assemblePaperDomainService.assemblePaper(taskId , SecurityUtils.getUserId()));
+ }
+}
diff --git a/src/main/java/com/project/exam/controller/ExamRecordController.java b/src/main/java/com/project/exam/controller/ExamRecordController.java
new file mode 100644
index 0000000..84b1f02
--- /dev/null
+++ b/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 assemblePaper(Long taskId) throws Exception{
+ return examRecordApplicationService.assemblePaper(taskId);
+ }
+}
diff --git a/src/main/java/com/project/exam/domain/dto/ExamRecordDTO.java b/src/main/java/com/project/exam/domain/dto/ExamRecordDTO.java
new file mode 100644
index 0000000..5e1ecc5
--- /dev/null
+++ b/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 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 options;
+ // 正确项
+ private String rightAnswer;
+ // 用户选的
+ private String userAnswer;
+ // 判定正确
+ private Boolean isRight;
+ // AI解析
+ private String analysis;
+ // 该题是否已发起申诉
+ private Boolean hasAppealed;
+ private Double score;
+ }
+
+ @Override
+ public T toEntity(Supplier supplier) {
+ T result = super.toEntity(supplier);
+ if (result instanceof ExamRecordEntity entity) {
+ // 处理内部列表快照的转换
+ if (CollUtil.isNotEmpty(this.answerSnapshotDTOList)) {
+ List 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;
+ }
+}
diff --git a/src/main/java/com/project/task/domain/entity/ExamRecordEntity.java b/src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java
similarity index 71%
rename from src/main/java/com/project/task/domain/entity/ExamRecordEntity.java
rename to src/main/java/com/project/exam/domain/entity/ExamRecordEntity.java
index d59f061..91cce56 100644
--- a/src/main/java/com/project/task/domain/entity/ExamRecordEntity.java
+++ b/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 toDTO(Supplier supplier) {
+ T result = super.toDTO(supplier);
+ if (result instanceof ExamRecordDTO dto) {
+ // 处理内部快照列表从 Entity 到 DTO 的转换
+ if (CollUtil.isNotEmpty(this.answerSnapshot)) {
+ List dtoList = this.answerSnapshot.stream().map(entitySnapshot -> {
+ ExamRecordDTO.QuestionSnapshotDTO snapshotDTO = new ExamRecordDTO.QuestionSnapshotDTO();
+ BeanUtils.copyProperties(entitySnapshot, snapshotDTO);
+ return snapshotDTO;
+ }).toList();
+ dto.setAnswerSnapshotDTOList(dtoList);
+ }
+ }
+ return result;
+ }
+
}
diff --git a/src/main/java/com/project/exam/domain/service/AssemblePaperDomainService.java b/src/main/java/com/project/exam/domain/service/AssemblePaperDomainService.java
new file mode 100644
index 0000000..21276e7
--- /dev/null
+++ b/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;
+
+
+}
diff --git a/src/main/java/com/project/exam/domain/service/BuildExamRecordDomainService.java b/src/main/java/com/project/exam/domain/service/BuildExamRecordDomainService.java
new file mode 100644
index 0000000..0af62ed
--- /dev/null
+++ b/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;
+}
diff --git a/src/main/java/com/project/exam/domain/service/ExamRecordBaseService.java b/src/main/java/com/project/exam/domain/service/ExamRecordBaseService.java
new file mode 100644
index 0000000..045b322
--- /dev/null
+++ b/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 {
+}
diff --git a/src/main/java/com/project/exam/domain/service/StepSaveExamRecordDomainService.java b/src/main/java/com/project/exam/domain/service/StepSaveExamRecordDomainService.java
new file mode 100644
index 0000000..64dd765
--- /dev/null
+++ b/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;
+}
diff --git a/src/main/java/com/project/exam/domain/service/SummitPaperDomainService.java b/src/main/java/com/project/exam/domain/service/SummitPaperDomainService.java
new file mode 100644
index 0000000..fb4bc40
--- /dev/null
+++ b/src/main/java/com/project/exam/domain/service/SummitPaperDomainService.java
@@ -0,0 +1,4 @@
+package com.project.exam.domain.service;
+
+public interface SummitPaperDomainService {
+}
diff --git a/src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java
new file mode 100644
index 0000000..20fda3e
--- /dev/null
+++ b/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 clusterLocks = new ConcurrentHashMap<>();
+ // 知识点锁(针对单选和判断)
+ private final ConcurrentHashMap 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()
+ .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 seedKpList = selectWeightedSeedKpList(taskDTO, totalQuestionNum);
+ // 本张试卷已覆盖的知识点ID集合
+ Set coveredKpPool = new HashSet<>();
+ // 最终选定题目
+ List selectedQuestionList = new ArrayList<>();
+ // 分配种子:前N3个多选,中间N1个单选,最后N2个判断
+ List mcSeeds = seedKpList.subList(0, taskDTO.getMultipleChoiceNum());
+ List scSeeds = seedKpList.subList(taskDTO.getMultipleChoiceNum() , taskDTO.getMultipleChoiceNum() + taskDTO.getSingleChoiceNum());
+ List 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 buildPaperSnapshot(List 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 selectWeightedSeedKpList(TaskDTO dto , int totalNum) {
+ List kpList = taskKnowledgePointBaseService.lambdaQuery()
+ .eq(TaskKnowledgePointEntity::getTaskId, dto.getId()).list().stream()
+ .map(entity -> entity.toDTO(TaskKnowledgePointDTO::new)).toList();
+ // 按簇分组
+ Map> 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 seeds, QuestionTypeEnum type,
+ List result, Set coveredPool) {
+ for (TaskKnowledgePointDTO seed : seeds) {
+ // 锁住当前知识点,不阻塞其他知识点的考生
+ ReentrantLock lock = kpLocks.computeIfAbsent(seed.getId(), k -> new ReentrantLock());
+ lock.lock();
+ try {
+ // 查可用的题
+ List 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 seedKpList,
+ List result, Set coveredKpPool) {
+ for (TaskKnowledgePointDTO seedKp : seedKpList) {
+ // 锁定当前种子所属的簇,保证抽题互斥
+ ReentrantLock lock = clusterLocks.computeIfAbsent(seedKp.getClusterId(), k -> new ReentrantLock());
+ lock.lock();
+ try {
+ // 候选题目id
+ List 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();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/project/exam/domain/service/impl/BuildExamRecordDomainServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/BuildExamRecordDomainServiceImpl.java
new file mode 100644
index 0000000..302f1e4
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/main/java/com/project/exam/domain/service/impl/ExamRecordBaseServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/ExamRecordBaseServiceImpl.java
new file mode 100644
index 0000000..61328fa
--- /dev/null
+++ b/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 implements ExamRecordBaseService {
+}
diff --git a/src/main/java/com/project/exam/domain/service/impl/StepSaveExamRecordDomainServiceImpl.java b/src/main/java/com/project/exam/domain/service/impl/StepSaveExamRecordDomainServiceImpl.java
new file mode 100644
index 0000000..3394c40
--- /dev/null
+++ b/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("保存进度失败,请确认考试是否有效");
+ }
+ }
+}
diff --git a/src/main/java/com/project/exam/mapper/ExamRecordMapper.java b/src/main/java/com/project/exam/mapper/ExamRecordMapper.java
new file mode 100644
index 0000000..ff1da9b
--- /dev/null
+++ b/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 {
+ /**
+ * 原子更新 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);
+}
diff --git a/src/main/java/com/project/interaction/domain/service/impl/SaveClusterDomainServiceImpl.java b/src/main/java/com/project/interaction/domain/service/impl/SaveClusterDomainServiceImpl.java
index 2303d66..0d2f371 100644
--- a/src/main/java/com/project/interaction/domain/service/impl/SaveClusterDomainServiceImpl.java
+++ b/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();
// 批量插入
diff --git a/src/main/java/com/project/question/domain/dto/QuestionDTO.java b/src/main/java/com/project/question/domain/dto/QuestionDTO.java
index 1d4579a..e661e5a 100644
--- a/src/main/java/com/project/question/domain/dto/QuestionDTO.java
+++ b/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;
}
}
diff --git a/src/main/java/com/project/question/domain/dto/TaskKnowledgePointDTO.java b/src/main/java/com/project/question/domain/dto/TaskKnowledgePointDTO.java
index 5b078f7..dc4f144 100644
--- a/src/main/java/com/project/question/domain/dto/TaskKnowledgePointDTO.java
+++ b/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;
}
diff --git a/src/main/java/com/project/question/domain/entity/QuestionAnswerEntity.java b/src/main/java/com/project/question/domain/entity/QuestionAnswerEntity.java
deleted file mode 100644
index 2298ad3..0000000
--- a/src/main/java/com/project/question/domain/entity/QuestionAnswerEntity.java
+++ /dev/null
@@ -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;
-}
diff --git a/src/main/java/com/project/question/domain/entity/QuestionEntity.java b/src/main/java/com/project/question/domain/entity/QuestionEntity.java
index 2279ebd..7378b44 100644
--- a/src/main/java/com/project/question/domain/entity/QuestionEntity.java
+++ b/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)
diff --git a/src/main/java/com/project/question/domain/entity/TaskKnowledgePointEntity.java b/src/main/java/com/project/question/domain/entity/TaskKnowledgePointEntity.java
index be4d448..5ea4e03 100644
--- a/src/main/java/com/project/question/domain/entity/TaskKnowledgePointEntity.java
+++ b/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;
}
diff --git a/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java b/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java
index 821b9f3..312d4a1 100644
--- a/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java
+++ b/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 {
+ /**
+ * 根据知识点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 findAvailableIdsByKpAndType(@Param("kpId") Long kpId, @Param("type") Integer type);
}
diff --git a/src/main/java/com/project/task/domain/dto/TaskUserDTO.java b/src/main/java/com/project/task/domain/dto/TaskUserDTO.java
new file mode 100644
index 0000000..ad301d4
--- /dev/null
+++ b/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;
+
+
+}
diff --git a/src/main/java/com/project/task/domain/enums/QuestionTypeEnum.java b/src/main/java/com/project/task/domain/enums/QuestionTypeEnum.java
index d179289..9f9bd90 100644
--- a/src/main/java/com/project/task/domain/enums/QuestionTypeEnum.java
+++ b/src/main/java/com/project/task/domain/enums/QuestionTypeEnum.java
@@ -8,9 +8,9 @@ import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum QuestionTypeEnum implements HasValueEnum {
- 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;
diff --git a/src/main/java/com/project/task/domain/service/impl/DeleteTaskDomainServiceImpl.java b/src/main/java/com/project/task/domain/service/impl/DeleteTaskDomainServiceImpl.java
index 7d2c41c..c14ca4c 100644
--- a/src/main/java/com/project/task/domain/service/impl/DeleteTaskDomainServiceImpl.java
+++ b/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 idList = Arrays.asList(ids.split(","));
-
- long count = taskBaseService.count(new LambdaQueryWrapper()
+ 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("已开考 、 已截止的考试任务不允许删除");
}