From 45b497a39df38ec08cd2e67ee119d4c6cb9afd27 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Wed, 1 Jul 2026 15:27:42 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=97=E6=B3=95=E7=94=9F=E9=A2=98=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ClassicCategoryApplicationServiceImpl.java | 31 ++++++-- .../domain/dto/ClassicCategoryDTO.java | 3 + .../ClassicPaperQuestionGenerator.java | 4 +- ...lgorithmClassicPaperQuestionGenerator.java | 10 ++- ...assicPaperQuestionCallbackServiceImpl.java | 12 ++- ...FallbackClassicPaperQuestionGenerator.java | 3 +- ...GenerateClassicPaperDomainServiceImpl.java | 18 +++-- .../RegenerateQuestionDomainServiceImpl.java | 76 ++++++++++++++++++- .../dto/GenerateQuestionRequestDTO.java | 12 +-- .../impl/MilvusApplicationServiceImpl.java | 9 ++- src/main/resources/application-dev.yml | 5 +- src/main/resources/application-test.yml | 7 +- 12 files changed, 155 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/project/classicpaper/application/impl/ClassicCategoryApplicationServiceImpl.java b/src/main/java/com/project/classicpaper/application/impl/ClassicCategoryApplicationServiceImpl.java index 159a64a..94109b0 100644 --- a/src/main/java/com/project/classicpaper/application/impl/ClassicCategoryApplicationServiceImpl.java +++ b/src/main/java/com/project/classicpaper/application/impl/ClassicCategoryApplicationServiceImpl.java @@ -95,12 +95,31 @@ public class ClassicCategoryApplicationServiceImpl implements ClassicCategoryApp throw new BusinessErrorException("层级参数错误"); } if (dto.getLevel() == 2) { - if (dto.getParentId() == null || dto.getParentId() == 0) { - throw new BusinessErrorException("二级分类必须选择父级"); - } - ClassicCategoryEntity parent = classicCategoryBaseService.getById(dto.getParentId()); - if (parent == null || !Objects.equals(parent.getLevel(), 1)) { - throw new BusinessErrorException("父级分类不存在或不是一级分类"); + if (StrUtil.isNotBlank(dto.getParentName())) { + // 按一级分类名称查找,已存在则报错 + ClassicCategoryEntity existingParent = classicCategoryBaseService.getOne( + new LambdaQueryWrapper() + .eq(ClassicCategoryEntity::getName, dto.getParentName()) + .eq(ClassicCategoryEntity::getLevel, 1)); + if (existingParent != null) { + throw new BusinessErrorException("一级分类已存在"); + } + // 自动创建一级分类 + ClassicCategoryEntity newParent = new ClassicCategoryEntity(); + newParent.setName(dto.getParentName()); + newParent.setLevel(1); + newParent.setParentId(0L); + newParent.setSort(0); + classicCategoryBaseService.save(newParent); + dto.setParentId(newParent.getId()); + } else { + if (dto.getParentId() == null || dto.getParentId() == 0) { + throw new BusinessErrorException("二级分类必须选择父级或传入parentName"); + } + ClassicCategoryEntity parent = classicCategoryBaseService.getById(dto.getParentId()); + if (parent == null || !Objects.equals(parent.getLevel(), 1)) { + throw new BusinessErrorException("父级分类不存在或不是一级分类"); + } } } else { dto.setParentId(0L); diff --git a/src/main/java/com/project/classicpaper/domain/dto/ClassicCategoryDTO.java b/src/main/java/com/project/classicpaper/domain/dto/ClassicCategoryDTO.java index 94a9b59..3ac90bc 100644 --- a/src/main/java/com/project/classicpaper/domain/dto/ClassicCategoryDTO.java +++ b/src/main/java/com/project/classicpaper/domain/dto/ClassicCategoryDTO.java @@ -18,6 +18,9 @@ public class ClassicCategoryDTO extends BaseDTO { private Integer sort = 0; + /** 一级分类名称(创建二级分类时传入,自动创建该一级分类并绑定) */ + private String parentName; + /** 该分类下的套题组数量(一级=下属二级总和,二级=直接数量) */ private Integer paperSetCount = 0; diff --git a/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java index 56a7f6f..c5aeb04 100644 --- a/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java +++ b/src/main/java/com/project/classicpaper/domain/service/ClassicPaperQuestionGenerator.java @@ -25,10 +25,12 @@ public interface ClassicPaperQuestionGenerator { * @param knowledgePoints 知识点素材 * @param questionType 题型 * @param count 生成数量(包含展示题 + 备用题) + * @param clusterId 知识点簇ID(单题/判断题传 null) * @return taskId 算法任务ID(用于跟踪) */ String generateAsync(Long paperQuestionId, List knowledgePoints, QuestionTypeEnum questionType, - int count) throws Exception; + int count, + Long clusterId) throws Exception; } diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/AlgorithmClassicPaperQuestionGenerator.java b/src/main/java/com/project/classicpaper/domain/service/impl/AlgorithmClassicPaperQuestionGenerator.java index ae5107f..03b8131 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/AlgorithmClassicPaperQuestionGenerator.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/AlgorithmClassicPaperQuestionGenerator.java @@ -39,6 +39,9 @@ public class AlgorithmClassicPaperQuestionGenerator implements ClassicPaperQuest @Value("${algo.apiUrl:http://172.16.25.174:8000}") private String apiUrl; + @Value("${algo.generateQuestionCallbackUrl:http://172.16.204.50/evaluator-api}") + private String callbackUrl; + @Override public List generate(List knowledgePoints, QuestionTypeEnum questionType, int count) throws Exception { @@ -50,7 +53,8 @@ public class AlgorithmClassicPaperQuestionGenerator implements ClassicPaperQuest public String generateAsync(Long paperQuestionId, List knowledgePoints, QuestionTypeEnum questionType, - int count) throws Exception { + int count, + Long clusterId) throws Exception { List sourceTexts = knowledgePoints.stream() .map(kp -> kp.getContent() != null ? kp.getContent() : "") .collect(Collectors.toList()); @@ -59,13 +63,15 @@ public class AlgorithmClassicPaperQuestionGenerator implements ClassicPaperQuest .collect(Collectors.toList()); GenerateQuestionRequestDTO request = GenerateQuestionRequestDTO.builder() - .bizKey("PQ:" + paperQuestionId) + .callbackUrl(callbackUrl) .numQuestions(count) .questionTypes(Collections.singletonList(questionType.name().toLowerCase())) .cluster(GenerateQuestionRequestDTO.ClusterInfo.builder() + .clusterId(clusterId) .sourceText(sourceTexts) .sourceId(sourceIds) .products(knowledgePoints.get(0).getParseName()) + .bizKey("PQ:" + paperQuestionId) .build()) .build(); diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java index ad5761b..4c00ef3 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/ClassicPaperQuestionCallbackServiceImpl.java @@ -91,11 +91,19 @@ public class ClassicPaperQuestionCallbackServiceImpl implements ClassicPaperQues } Long paperId = paperQuestion.getPaperId(); - // 3. 写入 spare 表 + 提拔第 1 道 + // 3. 判断:如果是补充备用(questionId 已存在),只存不提拔,跳过状态更新 + if (paperQuestion.getQuestionId() != null) { + spareQuestionService.storeSpares(paperQuestionId, paperId, null, savedQuestions); + log.info(">>> [经典套题-回调] 备用题补充完成, paperQuestionId={}, 补充{}道", + paperQuestionId, savedQuestions.size()); + return; + } + + // 4. 写入 spare 表 + 提拔第 1 道 QuestionEntity promoted = spareQuestionService.promoteFirstAndStoreSpares( paperQuestionId, paperId, null, savedQuestions); - // 4. 更新 paper_question 指向被提拔的题 + // 5. 更新 paper_question 指向被提拔的题 paperQuestion.setQuestionId(promoted.getId()); classicPaperQuestionBaseService.updateById(paperQuestion); diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java b/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java index 406234b..295f985 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/FallbackClassicPaperQuestionGenerator.java @@ -200,7 +200,8 @@ public class FallbackClassicPaperQuestionGenerator implements ClassicPaperQuesti public String generateAsync(Long paperQuestionId, List knowledgePoints, QuestionTypeEnum questionType, - int count) throws Exception { + int count, + Long clusterId) throws Exception { String taskId = UUID.randomUUID().toString().replace("-", ""); log.info(">>> [经典套题-Mock] 模拟提交算法, paperQuestionId={}, taskId={}, 题型={}, 数量={}", diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java index a63484a..5d97ca5 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/GenerateClassicPaperDomainServiceImpl.java @@ -269,7 +269,7 @@ public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPap taskPointEntities.add(createTaskKp(setId, kp, null, 1)); paperQuestionTasks.add(new PaperQuestionTask( paper.getId(), sortOrder++, QuestionTypeEnum.SINGLE_CHOICE, - Collections.singletonList(kp))); + Collections.singletonList(kp), null)); } } @@ -287,9 +287,9 @@ public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPap taskPointEntities.add(createTaskKp(setId, kp, null, 1)); paperQuestionTasks.add(new PaperQuestionTask( paper.getId(), sortOrder++, QuestionTypeEnum.TRUE_FALSE, - Collections.singletonList(kp))); + Collections.singletonList(kp), null)); + } } - } // 3. 简答题(抽簇) int shortAnswerNum = Optional.ofNullable(request.getShortAnswerNum()).orElse(0); @@ -307,7 +307,7 @@ public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPap } paperQuestionTasks.add(new PaperQuestionTask( - paper.getId(), sortOrder++, QuestionTypeEnum.SHORT_ANSWER, clusterKps)); + paper.getId(), sortOrder++, QuestionTypeEnum.SHORT_ANSWER, clusterKps, cluster.getId())); } } @@ -330,7 +330,7 @@ public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPap } paperQuestionTasks.add(new PaperQuestionTask( - paper.getId(), sortOrder++, QuestionTypeEnum.MULTIPLE_CHOICE, subSampledKps)); + paper.getId(), sortOrder++, QuestionTypeEnum.MULTIPLE_CHOICE, subSampledKps, cluster.getId())); } } @@ -358,9 +358,11 @@ public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPap // 逐个提交异步任务(使用 Phase 1 采样好的 KPs) for (PaperQuestionTask task : paperQuestionTasks) { + Long clusterId = task.knowledgePoints != null && !task.knowledgePoints.isEmpty() + ? task.knowledgePoints.get(0).getClusterId() : null; classicPaperQuestionGenerator.generateAsync( task.paperQuestionId, task.knowledgePoints, task.questionType, - 1 + sparePerQuestion); + 1 + sparePerQuestion, clusterId); } // ============ Phase 3: 立即返回 ============ @@ -412,13 +414,15 @@ public class GenerateClassicPaperDomainServiceImpl implements GenerateClassicPap final int sortOrder; final QuestionTypeEnum questionType; final List knowledgePoints; + final Long clusterId; PaperQuestionTask(Long paperId, int sortOrder, QuestionTypeEnum questionType, - List knowledgePoints) { + List knowledgePoints, Long clusterId) { this.paperId = paperId; this.sortOrder = sortOrder; this.questionType = questionType; this.knowledgePoints = knowledgePoints; + this.clusterId = clusterId; } } diff --git a/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java b/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java index cfa4fdb..d97e251 100644 --- a/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java +++ b/src/main/java/com/project/classicpaper/domain/service/impl/RegenerateQuestionDomainServiceImpl.java @@ -73,6 +73,12 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo @Value("${classicpaper.spare.replenish-threshold:1}") private int replenishThreshold; + @Value("${classicpaper.spare.replenish-count:2}") + private int replenishCount; + + @Value("${classicpaper.spare.replenish-delay-ms:10000}") + private long replenishDelayMs; + @Override @Transactional(rollbackFor = Exception.class) public Result regenerate(Long paperId, Long paperQuestionId) throws Exception { @@ -115,6 +121,7 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo if (knowledgePoints.isEmpty()) { throw new BusinessErrorException("关联知识点数据不存在"); } + QuestionTypeEnum questionType = QuestionTypeEnum.findByValue(originalQuestion.getQuestionType()); // 6. 优先从备用池取题 QuestionEntity spareQuestion = spareQuestionService.takeOneSpare(paperQuestionId); @@ -128,7 +135,7 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo if (remaining <= replenishThreshold) { log.info(">>> [经典套题-重新生题] 备用池不足(剩余{}),异步补充 paperQuestionId={}", remaining, paperQuestionId); - // 异步补充由回调流程处理,这里不阻塞 + triggerReplenish(paperQuestionId, knowledgePoints, questionType); } log.info(">>> [经典套题-重新生题][备用池] 套卷{} 关联记录{} 原题{} -> 新题{}", @@ -143,7 +150,6 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo mockSleep(); - QuestionTypeEnum questionType = QuestionTypeEnum.findByValue(originalQuestion.getQuestionType()); List newQuestions = classicPaperQuestionGenerator.generate( knowledgePoints, questionType, 1); if (newQuestions.isEmpty()) { @@ -174,6 +180,7 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo int remaining = spareQuestionService.getRemainingSpares(paperQuestionId); if (remaining <= replenishThreshold) { log.info(">>> [经典套题-重新生题-异步][备用池] 备用池不足,异步补充 paperQuestionId={}", paperQuestionId); + triggerReplenish(paperQuestionId); } log.info(">>> [经典套题-重新生题-异步][备用池] 直接替换成功, paperQuestionId={}, newQuestionId={}", @@ -319,4 +326,69 @@ public class RegenerateQuestionDomainServiceImpl implements RegenerateQuestionDo log.warn(">>> [经典套题-重新生题] Mock 延迟被中断"); } } + + // ==================== 备用题补充 ==================== + + /** + * 异步补充备用题(已知知识点的场景,regenerate 使用) + */ + private void triggerReplenish(Long paperQuestionId, + List knowledgePoints, + QuestionTypeEnum questionType) { + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(replenishDelayMs); + classicPaperQuestionGenerator.generateAsync( + paperQuestionId, knowledgePoints, questionType, replenishCount, null); + log.info(">>> [备用补充] 已提交补充任务, paperQuestionId={}, 数量={}", + paperQuestionId, replenishCount); + } catch (Exception e) { + log.error(">>> [备用补充] 失败, paperQuestionId={}", paperQuestionId, e); + } + }, regenerateExecutor); + } + + /** + * 异步补充备用题(需查询知识点的场景,regenerateAsync 使用) + */ + private void triggerReplenish(Long paperQuestionId) { + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(replenishDelayMs); + + ClassicPaperQuestionEntity pq = classicPaperQuestionBaseService.getById(paperQuestionId); + if (pq == null || pq.getQuestionId() == null) { + return; + } + + QuestionEntity original = questionBaseService.getById(pq.getQuestionId()); + if (original == null) { + return; + } + + List kpRels = questionKpRelBaseService.list( + new LambdaQueryWrapper() + .eq(QuestionKpRelEntity::getQuestionId, original.getId())); + if (kpRels.isEmpty()) { + return; + } + + List kpIds = kpRels.stream() + .map(QuestionKpRelEntity::getKpId).collect(Collectors.toList()); + List kps = knowledgePointBaseService.listByIds(kpIds); + if (kps.isEmpty()) { + return; + } + + QuestionTypeEnum questionType = QuestionTypeEnum.findByValue(original.getQuestionType()); + + classicPaperQuestionGenerator.generateAsync( + paperQuestionId, kps, questionType, replenishCount, null); + log.info(">>> [备用补充] 已提交补充任务, paperQuestionId={}, 数量={}", + paperQuestionId, replenishCount); + } catch (Exception e) { + log.error(">>> [备用补充] 失败, paperQuestionId={}", paperQuestionId, e); + } + }, regenerateExecutor); + } } diff --git a/src/main/java/com/project/interaction/domain/dto/GenerateQuestionRequestDTO.java b/src/main/java/com/project/interaction/domain/dto/GenerateQuestionRequestDTO.java index fffebe9..fdd406b 100644 --- a/src/main/java/com/project/interaction/domain/dto/GenerateQuestionRequestDTO.java +++ b/src/main/java/com/project/interaction/domain/dto/GenerateQuestionRequestDTO.java @@ -29,11 +29,7 @@ public class GenerateQuestionRequestDTO { @JsonProperty("num_questions") private Integer numQuestions; - /** - * 业务标识:PQ:paperQuestionId,回调时原样返回 - */ - @JsonProperty("biz_key") - private String bizKey; + /** * 题目类型列表 @@ -68,6 +64,12 @@ public class GenerateQuestionRequestDTO { @JsonProperty("task_id") private Long taskId; + /** + * 业务标识:PQ:paperQuestionId,回调时原样返回 + */ + @JsonProperty("biz_key") + private String bizKey; + /** * 文件名称 */ diff --git a/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java b/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java index 3fda5a2..5299539 100644 --- a/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java +++ b/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java @@ -1,21 +1,18 @@ package com.project.milvus.application.impl; -import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; import com.project.base.domain.exception.MissingParameterException; import com.project.classicpaper.domain.service.ClassicPaperQuestionCallbackService; import com.project.interaction.domain.dto.QuestionCallBackDTO; import com.project.milvus.application.MilvusApplicationService; import com.project.milvus.domain.dto.TitleVector; import com.project.milvus.domain.service.CheckMilvusDomainService; -import com.project.milvus.domain.service.MilvusDemoService; import com.project.question.domain.dto.QuestionDTO; import com.project.question.domain.service.SaveQuestionDomainService; -import io.milvus.v2.service.vector.response.SearchResp; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -45,8 +42,12 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { @Transactional(rollbackFor = Exception.class) public void insertTitle(TitleVector title) { // 经典套卷分支:走备用池 + 提拔,跳过 Milvus + + if (title.getBizKey() != null && title.getBizKey().startsWith("PQ:")) { log.info(">>> [回调处理] 经典套卷生题回调, bizKey={}", title.getBizKey()); + log.info(">>> [回调处理] 经典套卷生题回调, json={}", JSONUtil.toJsonStr(title)); + handleClassicPaper(title); return; } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3f16376..3de7064 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -106,10 +106,9 @@ classicpaper: regenerate-delay-max-ms: 5000 # 生成题目模拟延迟(毫秒,Mock 模拟算法处理耗时) generate-delay-ms: 2000 - - # 是否启用真实算法(false=使用 Mock) + # 是否启用真实算法(true=使用算法服务) algorithm: - enabled: false + enabled: true spare: # 每道题预生成的备用题数量 diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index e30f88c..78fa6dc 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -83,7 +83,7 @@ algo: clusterUrl: /semantic-cluster baseUrl: http://172.16.204.50/cluster generateQuestionUrl: /v1/generate/questions_from_cluster - apiUrl: http://127.0.0.1:8000 + apiUrl: http://172.16.204.50:8000/ jwt: secret: "my-very-fixed-and-secure-secret-key-1234567890" @@ -105,4 +105,7 @@ scheduled-task: classicpaper: mock: regenerate-delay-ms: 4000 - regenerate-delay-max-ms: 5000 \ No newline at end of file + regenerate-delay-max-ms: 5000 + # 是否启用真实算法(true=使用算法服务) + algorithm: + enabled: true \ No newline at end of file