From 823ab5631593e80c01926dc8478a7d2d352ad14c Mon Sep 17 00:00:00 2001 From: luogw <3132758203@qq.com> Date: Tue, 3 Mar 2026 11:06:16 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/MilvusApplicationServiceImpl.java | 53 +++++++-- .../milvus/domain/dto/TitleVector.java | 7 +- .../service/CheckMilvusDomainService.java | 10 +- .../impl/CheckMilvusDomainServiceImpl.java | 102 +++++++++++++++++- .../service/impl/MilvusDemoServiceImpl.java | 4 +- .../SaveOrUpdateTaskDomainServiceImpl.java | 31 +++++- 6 files changed, 183 insertions(+), 24 deletions(-) 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 107f578..ea61455 100644 --- a/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java +++ b/src/main/java/com/project/milvus/application/impl/MilvusApplicationServiceImpl.java @@ -8,14 +8,18 @@ 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.entity.QuestionEntity; +import com.project.question.domain.service.QuestionBaseService; 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.web.client.RestTemplate; +import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.concurrent.TimeUnit; @@ -29,9 +33,9 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { @Autowired private CheckMilvusDomainService checkMilvusDomainService; @Autowired - private CustomIdGenerator customIdGenerator; - @Autowired private RedissonClient redissonClient; + @Autowired + private QuestionBaseService questionBaseService; //相似度阈值 @@ -39,22 +43,25 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { private static final String LOCK_KEY = "lock:title:pointIds:"; @Override + @Transactional(rollbackFor = Exception.class) public void insertTitle(TitleVector title) { - //参数校验 - checkMilvusDomainService.check(title); + // 1. 基础参数校验(在锁外,快速失败) + checkMilvusDomainService.checkBasic(title); String lockKey = buildLockKey(title.getPointIdsList()); RLock lock = redissonClient.getLock(lockKey); boolean locked = false; + //重试 3 次 int retry = 3; try { while (retry > 0 && !locked) { - // 等待 2s,拿到锁后10s自动释放 + // 等待 2s,拿到锁后 10s 自动释放 locked = lock.tryLock(2, 10, TimeUnit.SECONDS); if (!locked) { //休眠等待重试 Thread.sleep(100 + new Random().nextInt(100)); + retry--; } } @@ -62,18 +69,25 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { throw new RuntimeException("当前知识点正在处理,请稍后再试"); } + //唯一性校验 + checkMilvusDomainService.checkUnique(title); + //比较相似度 List> query = milvusDemoService.query(title); if (CollectionUtil.isNotEmpty(query) && CollectionUtil.isNotEmpty(query.get(0))) { SearchResp.SearchResult searchResult = query.get(0).get(0); Float score = searchResult.getScore(); - if(score.compareTo(SIMILARITY_THRESHOLD) == 1){ + if(score.compareTo(SIMILARITY_THRESHOLD) > 0){ throw new MissingParameterException("题目相似度"+ score +",超过阈值"); } } - //新增数据 - title.setId(customIdGenerator.nextId(title)); + //保存题目到数据库 + QuestionEntity questionEntity = buildQuestionEntity(title); + questionBaseService.save(questionEntity); + + //保存向量数据到 Milvus + title.setId(questionEntity.getId()); milvusDemoService.insertRecord(title); }catch (InterruptedException e) { @@ -87,7 +101,26 @@ public class MilvusApplicationServiceImpl implements MilvusApplicationService { } /** - * 构建锁的key + * 构建题目实体 + */ + private QuestionEntity buildQuestionEntity(TitleVector title) { + QuestionEntity questionEntity = new QuestionEntity(); + questionEntity.setSourceType(0); // 0-单 KP 出题 + questionEntity.setUseStatus(false); // 未使用 + questionEntity.setKpIdList(title.getPointIdsList()); + questionEntity.setUniqueHash(title.getUniqueHash()); + questionEntity.setQuestionType(title.QuestionDetailDTO.getType()); + + // 构建题目详情 + QuestionEntity.QuestionDetail detail = new QuestionEntity.QuestionDetail(); + BeanUtils.copyProperties(title.QuestionDetailDTO, detail); + questionEntity.setQuestionDetail(detail); + + return questionEntity; + } + + /** + * 构建锁的 key */ private String buildLockKey(List poinIds){ String collect = poinIds.stream().map(String::valueOf).collect(Collectors.joining(",")); diff --git a/src/main/java/com/project/milvus/domain/dto/TitleVector.java b/src/main/java/com/project/milvus/domain/dto/TitleVector.java index edd1c04..3293f0c 100644 --- a/src/main/java/com/project/milvus/domain/dto/TitleVector.java +++ b/src/main/java/com/project/milvus/domain/dto/TitleVector.java @@ -2,6 +2,7 @@ package com.project.milvus.domain.dto; import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.project.question.domain.dto.QuestionDTO; import lombok.Data; import org.apache.commons.codec.digest.DigestUtils; @@ -17,8 +18,10 @@ public class TitleVector { public List pointIds; //题目向量 public List titleVector; - //题目类型 - public String type; + //题目唯一标识 + private String uniqueHash; + //题目内容 + public QuestionDTO.QuestionDetailDTO QuestionDetailDTO; public String getPointIdsHash() { List sorted = new ArrayList<>(pointIds); diff --git a/src/main/java/com/project/milvus/domain/service/CheckMilvusDomainService.java b/src/main/java/com/project/milvus/domain/service/CheckMilvusDomainService.java index e1500fe..6b57730 100644 --- a/src/main/java/com/project/milvus/domain/service/CheckMilvusDomainService.java +++ b/src/main/java/com/project/milvus/domain/service/CheckMilvusDomainService.java @@ -3,5 +3,13 @@ package com.project.milvus.domain.service; import com.project.milvus.domain.dto.TitleVector; public interface CheckMilvusDomainService { - void check(TitleVector title); + /** + * 基础参数校验 + */ + void checkBasic(TitleVector title); + + /** + * 唯一性校验 + */ + void checkUnique(TitleVector title); } diff --git a/src/main/java/com/project/milvus/domain/service/impl/CheckMilvusDomainServiceImpl.java b/src/main/java/com/project/milvus/domain/service/impl/CheckMilvusDomainServiceImpl.java index 3c06459..423342a 100644 --- a/src/main/java/com/project/milvus/domain/service/impl/CheckMilvusDomainServiceImpl.java +++ b/src/main/java/com/project/milvus/domain/service/impl/CheckMilvusDomainServiceImpl.java @@ -2,21 +2,38 @@ package com.project.milvus.domain.service.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.EnumUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.project.base.domain.exception.MissingParameterException; +import com.project.information.domain.entity.KnowledgePointEntity; +import com.project.information.domain.service.KnowledgePointBaseService; import com.project.milvus.domain.dto.TitleVector; import com.project.milvus.domain.service.CheckMilvusDomainService; +import com.project.question.domain.dto.QuestionDTO; +import com.project.question.domain.entity.QuestionEntity; +import com.project.question.domain.service.QuestionBaseService; import com.project.task.domain.enums.QuestionTypeEnum; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; /** * 校验扩展 */ @Service public class CheckMilvusDomainServiceImpl implements CheckMilvusDomainService { + @Autowired + private KnowledgePointBaseService knowledgePointBaseService; + @Autowired + private QuestionBaseService questionBaseService; + + @Override - public void check(TitleVector title) { + public void checkBasic(TitleVector title) { if (title == null) { throw new MissingParameterException("请求参数缺失或格式错误"); } @@ -32,8 +49,85 @@ public class CheckMilvusDomainServiceImpl implements CheckMilvusDomainService { throw new MissingParameterException("向量格式错误"); } - if(!EnumUtil.contains(QuestionTypeEnum.class,title.getType())){ - throw new MissingParameterException("题目类型不存在"); + // 校验 QuestionDetailDTO 及其内部参数 + QuestionDTO.QuestionDetailDTO detailDTO = title.QuestionDetailDTO; + if (detailDTO == null) { + throw new MissingParameterException("题目详情不能为空"); + } + + if (StringUtils.isBlank(detailDTO.getQuestionContent())) { + throw new MissingParameterException("题干不能为空"); + } + + if (detailDTO.getType() == null) { + throw new MissingParameterException("题目类型不能为空"); + } + + // 校验题目类型值是否有效(1=单选题,2=多选题,3=判断题) + if (QuestionTypeEnum.findByValue(detailDTO.getType()) == null) { + throw new MissingParameterException("题目类型值无效"); + } + + if (CollectionUtil.isEmpty(detailDTO.getOptions())) { + throw new MissingParameterException("选项不能为空"); + } + + // 校验 options 的每个键值对都不为空 + for (Map.Entry entry : detailDTO.getOptions().entrySet()) { + if (StringUtils.isBlank(entry.getKey())) { + throw new MissingParameterException("选项键不能为空"); + } + if (StringUtils.isBlank(entry.getValue())) { + throw new MissingParameterException("选项值不能为空"); + } + } + + if (StringUtils.isBlank(detailDTO.getRightAnswer())) { + throw new MissingParameterException("正确答案不能为空"); + } + + if (StringUtils.isBlank(detailDTO.getAnalysis())) { + throw new MissingParameterException("题目解析不能为空"); + } + } + + @Override + public void checkUnique(TitleVector title) { + // 题目的 HASH 校验 + String questionHash = buildQuestionHash(title.QuestionDetailDTO, title); + QuestionEntity questionEntity = questionBaseService.getOne(new QueryWrapper().eq("unique_hash", questionHash)); + if (questionEntity != null) { + throw new IllegalArgumentException("题目已存在"); // 空值防护 } + title.setUniqueHash(questionHash); + } + public String buildQuestionHash(QuestionDTO.QuestionDetailDTO detailDTO, TitleVector title){ + //查询知识点并提取非空的 parseName + List knowledgePointEntities = knowledgePointBaseService.listByIds(title.getPointIdsList()); + if (CollectionUtils.isEmpty(knowledgePointEntities)) { + throw new IllegalArgumentException("知识点不存在"); // 空值防护 + } + + //提取非空 parseName 并去重,避免重复拼接 + Set parseNameSet = knowledgePointEntities.stream() + .filter(entity -> StringUtils.isNotBlank(entity.getParseName())) // 更严谨的非空判断 + .map(KnowledgePointEntity::getParseName) + .collect(Collectors.toSet()); + + //知识点名称排序(保证拼接顺序固定,避免"a,b"和"b,a"生成不同 hash) + List sortedParseNames = new ArrayList<>(parseNameSet); + Collections.sort(sortedParseNames); // 修正原代码"排序 Set"的语法错误 + + //规范化拼接知识点名称(固定分隔符,避免歧义) + String canonicalKnowledgePoints = sortedParseNames.stream() + .collect(Collectors.joining(",")); + + //生成最终哈希(拆分拼接逻辑,提升可读性;增加分隔符避免字符串拼接歧义) + String rawContent = String.format("%s|%s|%s", + StringUtils.isNotBlank(detailDTO.getQuestionContent()) ? detailDTO.getQuestionContent() : "", + StringUtils.isNotBlank(title.getPointIdsHash()) ? title.getPointIdsHash() : "", + DigestUtils.md5Hex(canonicalKnowledgePoints) + ); + return DigestUtils.md5Hex(rawContent); } } diff --git a/src/main/java/com/project/milvus/domain/service/impl/MilvusDemoServiceImpl.java b/src/main/java/com/project/milvus/domain/service/impl/MilvusDemoServiceImpl.java index 66f97f3..c1fceb7 100644 --- a/src/main/java/com/project/milvus/domain/service/impl/MilvusDemoServiceImpl.java +++ b/src/main/java/com/project/milvus/domain/service/impl/MilvusDemoServiceImpl.java @@ -79,7 +79,7 @@ public class MilvusDemoServiceImpl implements MilvusDemoService { * 往collection中插入一条数据 */ public void insertRecord(TitleVector title) { - QuestionTypeEnum questionTypeEnum = QuestionTypeEnum.valueOf(title.getType()); + QuestionTypeEnum questionTypeEnum = QuestionTypeEnum.findByValue(title.getQuestionDetailDTO().getType()); JsonObject vector = new JsonObject(); vector.addProperty("id", title.getId()); @@ -111,7 +111,7 @@ public class MilvusDemoServiceImpl implements MilvusDemoService { */ @Override public List> query(TitleVector title){ - QuestionTypeEnum questionTypeEnum = QuestionTypeEnum.valueOf(title.getType()); + QuestionTypeEnum questionTypeEnum = QuestionTypeEnum.findByValue(title.getQuestionDetailDTO().getType()); String expr = String.format("point_ids == '%s'", title.getPointIdsHash()); diff --git a/src/main/java/com/project/task/domain/service/impl/SaveOrUpdateTaskDomainServiceImpl.java b/src/main/java/com/project/task/domain/service/impl/SaveOrUpdateTaskDomainServiceImpl.java index 4290714..f6f8d4a 100644 --- a/src/main/java/com/project/task/domain/service/impl/SaveOrUpdateTaskDomainServiceImpl.java +++ b/src/main/java/com/project/task/domain/service/impl/SaveOrUpdateTaskDomainServiceImpl.java @@ -10,7 +10,9 @@ import com.project.base.domain.exception.BusinessErrorException; import com.project.base.domain.result.Result; import com.project.ding.domain.entity.UserEntity; import com.project.ding.domain.service.UserBaseService; +import com.project.information.domain.dto.KnowledgePointStatisticsDTO; import com.project.information.domain.entity.ProductLineEntity; +import com.project.information.domain.service.GetStatisticsKnowledgePointDomainService; import com.project.information.domain.service.ProductLineBaseService; import com.project.task.config.ExamScoreRatioConfig; import com.project.task.domain.dto.TaskDTO; @@ -51,6 +53,10 @@ public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomain @Autowired private InitTaskDomainService initTaskDomainService; + @Autowired + private GetStatisticsKnowledgePointDomainService getStatisticsKnowledgePointDomainService; + + @Override @Transactional(rollbackFor = Exception.class) @@ -156,11 +162,6 @@ public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomain if (Objects.isNull(dto.getTrueFalseNum())) { throw new BusinessErrorException("判断题数量不能为空"); } - if (!Validator.isBetween(dto.getSingleChoiceNum() + - dto.getMultipleChoiceNum() + - dto.getTrueFalseNum() , 1, 100)) { - throw new BusinessErrorException("题目数量设置错误"); - } if (CollUtil.isEmpty(dto.getParticipantUserIdList())) { throw new BusinessErrorException("参与用户不能为空"); } @@ -173,6 +174,26 @@ public class SaveOrUpdateTaskDomainServiceImpl implements SaveOrUpdateTaskDomain if (Objects.isNull(dto.getVagueGraspNum())) { throw new BusinessErrorException("模糊掌握知识点数量不能为空"); } + //校验知识点数是否有变化 + Result statistics = getStatisticsKnowledgePointDomainService.getStatistics(dto.getSubLineId()); + if (!Objects.equals(statistics.getData().getAccurateGraspNum() , dto.getAccurateGraspNum()) || + !Objects.equals(statistics.getData().getVagueGraspNum() , dto.getVagueGraspNum())) { + throw new BusinessErrorException("知识点数有变化,请重新选择关联产品线"); + } + + // 题目总数 + int totalQuestions = dto.getSingleChoiceNum() + dto.getMultipleChoiceNum() + dto.getTrueFalseNum(); + // 获取知识点总数用于校验 + int totalKnowledgePoints = dto.getAccurateGraspNum() + dto.getVagueGraspNum(); + if (totalQuestions > totalKnowledgePoints) { + throw new BusinessErrorException("题目总数(" + totalQuestions + ")不能超过知识点总数(" + totalKnowledgePoints + ")"); + } + if (totalQuestions > 100) { + throw new BusinessErrorException("题目总数不能超过 100 道,当前为:" + totalQuestions); + } + if (totalQuestions < 1) { + throw new BusinessErrorException("题目总数不能少于 1 道"); + } }