From 480b7da2a88a6f7a394b135303c64c4379be46ce Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 3 Feb 2026 19:36:10 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=98=E7=9B=AE=E5=BA=93=E5=AD=98=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/AssemblePaperDomainServiceImpl.java | 29 ++++- .../impl/SaveClusterDomainServiceImpl.java | 2 +- .../QuestionInventoryDomainService.java | 5 + .../QuestionInventoryDomainServiceImpl.java | 105 ++++++++++++++++++ .../question/mapper/QuestionKpRelMapper.java | 28 ++++- .../config/TaskAsyncThreadPoolConfig.java | 2 +- 6 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/project/question/domain/service/QuestionInventoryDomainService.java create mode 100644 src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java 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 index 20fda3e..65be34f 100644 --- a/src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java +++ b/src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java @@ -10,6 +10,7 @@ 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.QuestionInventoryDomainService; import com.project.question.domain.service.TaskKnowledgePointBaseService; import com.project.question.mapper.QuestionKpRelMapper; import com.project.question.mapper.QuestionMapper; @@ -24,16 +25,18 @@ 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 jakarta.annotation.Resource; 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 org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -71,11 +74,17 @@ public class AssemblePaperDomainServiceImpl implements AssemblePaperDomainServic private static final String EXAM_START_LOCK = "lock:exam:start:%s:%s"; + @Resource(name = "taskInternalExecutor") + private Executor taskInternalExecutor; + @Autowired + private QuestionInventoryDomainService questionInventoryDomainService; + @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)) @@ -118,7 +127,19 @@ public class AssemblePaperDomainServiceImpl implements AssemblePaperDomainServic if (selectedQuestionList.size() < totalQuestionNum) { throw new BusinessErrorException("当前题库可用题目不足,请联系管理员补货"); } - // todo 异步起一个检测库存 + // 注册一个回调:在当前事务 commit 成功后,再触发异步补库 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + CompletableFuture.runAsync(() -> { + try { + questionInventoryDomainService.checkAndReplenish(taskId); + } catch (Exception e) { + log.error(">>> [自动补库] 异步检查失败, taskId: {}", taskId, e); + } + }, taskInternalExecutor); + } + }); // 持久化记录 ExamRecordDTO recordDTO = new ExamRecordDTO(); 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 0d2f371..e265eff 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 @@ -84,7 +84,7 @@ public class SaveClusterDomainServiceImpl implements SaveClusterDomainService { if (clusterEntity.getClusterSize() > 1) { // 簇生成目标量 int clusterTargetNum = (int) Math.ceil(1.2 * u * clusterEntity.getClusterSize()); - generateQuestionDomainService.produce(clusterEntity.getId(), QuestionTypeEnum.MULTIPLE_CHOICE ,QuestionSourceTypeEnum.Multi_Concept , clusterTargetNum); + generateQuestionDomainService.produce(clusterEntity.getId(), QuestionTypeEnum.MULTIPLE_CHOICE , QuestionSourceTypeEnum.Multi_Concept , clusterTargetNum); } } } diff --git a/src/main/java/com/project/question/domain/service/QuestionInventoryDomainService.java b/src/main/java/com/project/question/domain/service/QuestionInventoryDomainService.java new file mode 100644 index 0000000..f740349 --- /dev/null +++ b/src/main/java/com/project/question/domain/service/QuestionInventoryDomainService.java @@ -0,0 +1,5 @@ +package com.project.question.domain.service; + +public interface QuestionInventoryDomainService { + void checkAndReplenish(Long taskId) throws Exception; +} diff --git a/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java b/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java new file mode 100644 index 0000000..23a9be0 --- /dev/null +++ b/src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java @@ -0,0 +1,105 @@ +package com.project.question.domain.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.project.question.domain.entity.TaskKnowledgeClusterEntity; +import com.project.question.domain.entity.TaskKnowledgePointEntity; +import com.project.question.domain.enums.QuestionSourceTypeEnum; +import com.project.question.domain.service.GenerateQuestionDomainService; +import com.project.question.domain.service.QuestionInventoryDomainService; +import com.project.question.mapper.QuestionKpRelMapper; +import com.project.question.mapper.TaskKnowledgeClusterMapper; +import com.project.question.mapper.TaskKnowledgePointMapper; +import com.project.task.domain.entity.TaskEntity; +import com.project.task.domain.enums.QuestionTypeEnum; +import com.project.task.mapper.TaskMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +@Service +@Slf4j +public class QuestionInventoryDomainServiceImpl implements QuestionInventoryDomainService { + @Autowired + private TaskKnowledgePointMapper taskKpMapper; + + @Autowired + private TaskKnowledgeClusterMapper clusterMapper; + + @Autowired + private QuestionKpRelMapper questionKpMapper; + + @Autowired + private TaskMapper taskMapper; + + @Autowired + private GenerateQuestionDomainService generateQuestionDomainService; + + @Override + public void checkAndReplenish(Long taskId) { + TaskEntity task = taskMapper.selectById(taskId); + if (task == null || task.getEndTime().before(new Date())) { + return; + } + int u = (task.getParticipantNum() == null ? 0 : task.getParticipantNum()) + - (task.getPassNum() == null ? 0 : task.getPassNum()); + if (u <= 0) { + return; + } + int watermark = (int) Math.ceil(1.0 * u); + int targetLine = (int) Math.ceil(1.2 * u); + // 针对每个知识点遍历每种题型 + List kpList = taskKpMapper.selectList( + new LambdaQueryWrapper().eq(TaskKnowledgePointEntity::getTaskId , taskId)); + for (TaskKnowledgePointEntity kp : kpList) { + for (QuestionTypeEnum questionType : QuestionTypeEnum.values()) { + checkAndProduce(kp.getId() , questionType , watermark , targetLine); + } + } + // 针对每一个簇 + List clusterList = clusterMapper.selectList( + new LambdaQueryWrapper().eq(TaskKnowledgeClusterEntity::getTaskId, taskId)); + + for (TaskKnowledgeClusterEntity cluster : clusterList) { + int w = cluster.getClusterSize(); + // 簇水位线: 1.0 * U * W + int clusterWatermark = watermark * w; + // 簇目标线: 1.2 * U * W + int clusterTargetLine = targetLine * w; + + checkClusterProduce(cluster.getId(), clusterWatermark, clusterTargetLine); + } + + } + + /** + * 单知识点补货判定 + */ + private void checkAndProduce(Long kpId, QuestionTypeEnum type, int watermark , int targetLine) { + int currentStock = questionKpMapper.countAvailableByKp(kpId, type.getValue()); + + if (currentStock < watermark) { + int gap = targetLine - currentStock; + log.info(">>> [自动补库] KP: {}, 题型: {}, 当前库存: {}, 需补货: {}", kpId, type.getDescription(), currentStock, gap); + // 触发异步生产逻辑 (内部带 30 分钟冷却锁) + generateQuestionDomainService.produce(kpId , type , QuestionSourceTypeEnum.Single_Concept , gap); + } + } + + /** + * 知识点簇补货判定 + */ + private void checkClusterProduce(Long clusterId, int watermark, int targetLine) { + int currentStock = questionKpMapper.countAvailableByCluster(clusterId); + + if (currentStock < watermark) { + int gap = targetLine - currentStock; + log.info(">>> [自动补库] 簇: {}, 类型: 复合多选, 当前库存: {}, 需补货: {}", clusterId, currentStock, gap); + // 簇维度触发,触发异步生产逻辑 + generateQuestionDomainService.produce(clusterId , QuestionTypeEnum.MULTIPLE_CHOICE , QuestionSourceTypeEnum.Single_Concept , gap); + } + } + +} diff --git a/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java b/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java index 312d4a1..f5f279e 100644 --- a/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java +++ b/src/main/java/com/project/question/mapper/QuestionKpRelMapper.java @@ -18,8 +18,34 @@ public interface QuestionKpRelMapper extends BaseMapper { "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.question_type = #{type} " + " AND q.use_status = 0 " + " AND q.deleted = 0") List findAvailableIdsByKpAndType(@Param("kpId") Long kpId, @Param("type") Integer type); + + /** + * 情况 A:统计单知识点(KP)下的可用库存 + * 用于单选题、判断题、单点多选题的补货判定 + */ + @Select("SELECT COUNT(DISTINCT 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") + int countAvailableByKp(@Param("kpId") Long kpId, @Param("type") Integer type); + + /** + * 情况 B:统计知识点簇(Cluster)下的可用库存 + * 用于复合多选题(Cluster 维度)的补货判定 + */ + @Select("SELECT COUNT(DISTINCT r.question_id) " + + "FROM evaluator_question_kp_rel r " + + "INNER JOIN evaluator_question q ON r.question_id = q.id " + + "WHERE r.cluster_id = #{clusterId} " + + " AND q.question_type = 2 " + // 2 代表多选题 QuestionTypeEnum.MULTIPLE_CHOICE + " AND q.use_status = 0 " + + " AND q.deleted = 0") + int countAvailableByCluster(@Param("clusterId") Long clusterId); } diff --git a/src/main/java/com/project/task/config/TaskAsyncThreadPoolConfig.java b/src/main/java/com/project/task/config/TaskAsyncThreadPoolConfig.java index b441813..765f8e0 100644 --- a/src/main/java/com/project/task/config/TaskAsyncThreadPoolConfig.java +++ b/src/main/java/com/project/task/config/TaskAsyncThreadPoolConfig.java @@ -17,7 +17,7 @@ public class TaskAsyncThreadPoolConfig { public Executor taskInternalExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(1); - executor.setMaxPoolSize(4); + executor.setMaxPoolSize(5); executor.setQueueCapacity(20); executor.setThreadNamePrefix("task-init-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());