Browse Source

题目库存检查

master
luoweijian 1 month ago
parent
commit
480b7da2a8
  1. 29
      src/main/java/com/project/exam/domain/service/impl/AssemblePaperDomainServiceImpl.java
  2. 2
      src/main/java/com/project/interaction/domain/service/impl/SaveClusterDomainServiceImpl.java
  3. 5
      src/main/java/com/project/question/domain/service/QuestionInventoryDomainService.java
  4. 105
      src/main/java/com/project/question/domain/service/impl/QuestionInventoryDomainServiceImpl.java
  5. 28
      src/main/java/com/project/question/mapper/QuestionKpRelMapper.java
  6. 2
      src/main/java/com/project/task/config/TaskAsyncThreadPoolConfig.java

29
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<TaskUserEntity>()
.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();

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

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

105
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<TaskKnowledgePointEntity> kpList = taskKpMapper.selectList(
new LambdaQueryWrapper<TaskKnowledgePointEntity>().eq(TaskKnowledgePointEntity::getTaskId , taskId));
for (TaskKnowledgePointEntity kp : kpList) {
for (QuestionTypeEnum questionType : QuestionTypeEnum.values()) {
checkAndProduce(kp.getId() , questionType , watermark , targetLine);
}
}
// 针对每一个簇
List<TaskKnowledgeClusterEntity> clusterList = clusterMapper.selectList(
new LambdaQueryWrapper<TaskKnowledgeClusterEntity>().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);
}
}
}

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

@ -18,8 +18,34 @@ public interface QuestionKpRelMapper extends BaseMapper<QuestionKpRelEntity> {
"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<Long> 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);
}

2
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());

Loading…
Cancel
Save