19 changed files with 915 additions and 12 deletions
@ -0,0 +1,23 @@ |
|||
package com.project.classicpaper.domain.service; |
|||
|
|||
import com.project.classicpaper.domain.dto.ClassicPaperDTO; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
/** |
|||
* 历史套卷导入服务 |
|||
* 支持导入旧系统的 Word 历史套卷直接入库 |
|||
*/ |
|||
public interface ImportClassicPaperDomainService { |
|||
|
|||
/** |
|||
* 从 Word 文件导入经典套卷 |
|||
* 解析 Word → 提取题目(含图片) → 创建套卷关联 |
|||
* 导入的题目 sourceType = Historical_Import(2),无知识点关联 |
|||
* |
|||
* @param file Word 文件 |
|||
* @param subLineId 子产品线ID |
|||
* @param paperName 套卷名称 |
|||
* @return 套卷详情 |
|||
*/ |
|||
ClassicPaperDTO importFromWord(MultipartFile file, Long subLineId, String paperName) throws Exception; |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
package com.project.classicpaper.domain.service.impl; |
|||
|
|||
import com.project.classicpaper.domain.dto.ClassicPaperDTO; |
|||
import com.project.classicpaper.domain.entity.ClassicPaperEntity; |
|||
import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity; |
|||
import com.project.classicpaper.domain.enums.ClassicPaperSourceTypeEnum; |
|||
import com.project.classicpaper.domain.enums.ClassicPaperStatusEnum; |
|||
import com.project.classicpaper.domain.service.ClassicPaperBaseService; |
|||
import com.project.classicpaper.domain.service.ClassicPaperQuestionBaseService; |
|||
import com.project.classicpaper.domain.service.ImportClassicPaperDomainService; |
|||
import com.project.classicpaper.domain.service.parser.WordQuestionParser; |
|||
import com.project.information.utils.MinIoUtils; |
|||
import com.project.question.domain.dto.QuestionDTO; |
|||
import com.project.question.domain.enums.QuestionSourceTypeEnum; |
|||
import com.project.question.domain.service.SaveQuestionDomainService; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.TreeMap; |
|||
|
|||
/** |
|||
* 历史套卷导入服务实现 |
|||
* 解析 Word → 提取题目(含图片上传 MinIO) → 创建套卷关联 |
|||
* 导入的题目 sourceType = Historical_Import(2),无知识点关联 |
|||
*/ |
|||
@Service |
|||
@Slf4j |
|||
public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDomainService { |
|||
|
|||
@Autowired |
|||
private ClassicPaperBaseService classicPaperBaseService; |
|||
|
|||
@Autowired |
|||
private ClassicPaperQuestionBaseService classicPaperQuestionBaseService; |
|||
|
|||
@Autowired |
|||
private SaveQuestionDomainService saveQuestionDomainService; |
|||
|
|||
@Autowired |
|||
private MinIoUtils minIoUtils; |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public ClassicPaperDTO importFromWord(MultipartFile file, Long subLineId, String paperName) throws Exception { |
|||
log.info(">>> [历史导入] 开始导入套卷, fileName={}, subLineId={}", file.getOriginalFilename(), subLineId); |
|||
|
|||
// 1. 解析 Word 文件,提取题目列表(含图片上传 MinIO)
|
|||
WordQuestionParser parser = new WordQuestionParser(minIoUtils); |
|||
WordQuestionParser.ParseResult parseResult = parser.parse(file.getInputStream()); |
|||
List<WordQuestionParser.ParsedQuestion> parsedQuestions = parseResult.getQuestions(); |
|||
|
|||
if (parsedQuestions.isEmpty()) { |
|||
log.warn(">>> [历史导入] Word 文件中未提取到题目"); |
|||
} |
|||
|
|||
// 2. 创建套卷
|
|||
ClassicPaperEntity paper = new ClassicPaperEntity(); |
|||
paper.setName(paperName != null ? paperName : parseResult.getTitle()); |
|||
paper.setSubLineId(subLineId); |
|||
paper.setSourceType(ClassicPaperSourceTypeEnum.WORD_IMPORT.getValue()); |
|||
paper.setStatus(ClassicPaperStatusEnum.DRAFT.getValue()); |
|||
paper.setQuestionCount(parsedQuestions.size()); |
|||
classicPaperBaseService.save(paper); |
|||
|
|||
// 3. 保存题目并创建关联
|
|||
List<ClassicPaperQuestionEntity> paperQuestions = new ArrayList<>(); |
|||
for (int i = 0; i < parsedQuestions.size(); i++) { |
|||
WordQuestionParser.ParsedQuestion parsed = parsedQuestions.get(i); |
|||
|
|||
// 构建选项 Map
|
|||
Map<String, String> optionsMap = new TreeMap<>(); |
|||
if (parsed.getOptions() != null) { |
|||
for (WordQuestionParser.ParsedOption opt : parsed.getOptions()) { |
|||
optionsMap.put(opt.getLabel(), opt.getContent()); |
|||
} |
|||
} |
|||
|
|||
// 创建题目(sourceType = Historical_Import,无知识点关联)
|
|||
QuestionDTO questionDTO = new QuestionDTO(); |
|||
questionDTO.setSourceType(QuestionSourceTypeEnum.Historical_Import.getValue()); |
|||
questionDTO.setQuestionType(parsed.getQuestionType()); |
|||
questionDTO.setKpIdList(new ArrayList<>()); // 无知识点关联
|
|||
|
|||
QuestionDTO.QuestionDetailDTO detailDTO = new QuestionDTO.QuestionDetailDTO(); |
|||
detailDTO.setQuestionContent(parsed.getStem()); |
|||
detailDTO.setType(parsed.getQuestionType()); |
|||
detailDTO.setOptions(optionsMap.isEmpty() ? null : optionsMap); |
|||
detailDTO.setRightAnswer(parsed.getAnswer()); |
|||
detailDTO.setAnalysis(parsed.getAnalysis()); |
|||
questionDTO.setQuestionDetailDTO(detailDTO); |
|||
|
|||
// 保存题目
|
|||
saveQuestionDomainService.save(questionDTO); |
|||
|
|||
// 创建套卷-题目关联
|
|||
ClassicPaperQuestionEntity paperQuestion = new ClassicPaperQuestionEntity(); |
|||
paperQuestion.setPaperId(paper.getId()); |
|||
paperQuestion.setQuestionId(questionDTO.getId()); |
|||
paperQuestion.setSortOrder(i + 1); |
|||
paperQuestion.setQuestionType(parsed.getQuestionType()); |
|||
paperQuestions.add(paperQuestion); |
|||
} |
|||
|
|||
classicPaperQuestionBaseService.saveBatch(paperQuestions); |
|||
|
|||
log.info(">>> [历史导入] 导入完成, paperId={}, 题目数={}", paper.getId(), paperQuestions.size()); |
|||
|
|||
// 4. 返回套卷详情
|
|||
ClassicPaperDTO dto = new ClassicPaperDTO(); |
|||
dto.setId(paper.getId()); |
|||
dto.setName(paper.getName()); |
|||
dto.setSourceType(paper.getSourceType()); |
|||
dto.setStatus(paper.getStatus()); |
|||
dto.setQuestionCount(paper.getQuestionCount()); |
|||
return dto; |
|||
} |
|||
} |
|||
@ -0,0 +1,236 @@ |
|||
package com.project.classicpaper.domain.service.parser; |
|||
|
|||
import com.project.information.utils.MinIoUtils; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.poi.xwpf.usermodel.*; |
|||
|
|||
import java.io.InputStream; |
|||
import java.util.*; |
|||
import java.util.regex.Matcher; |
|||
import java.util.regex.Pattern; |
|||
|
|||
/** |
|||
* Word 文档题目解析器 |
|||
* 逐段落状态机解析,提取题目、选项、答案、解析、图片 |
|||
* 图片上传 MinIO,占位符替换为 HTML img 标签 |
|||
*/ |
|||
@Slf4j |
|||
public class WordQuestionParser { |
|||
|
|||
private static final Pattern QUESTION_PATTERN = Pattern.compile("^\\d+[..\\u3001]"); |
|||
private static final Pattern OPTION_PATTERN = Pattern.compile("^[A-Z][..\\u3001::、]\\s*"); |
|||
private static final Pattern ANSWER_PATTERN = Pattern.compile("^答案[::\\u3001]\\s*(.*)"); |
|||
private static final Pattern ANALYSIS_PATTERN = Pattern.compile("^解析[::\\u3001]\\s*(.*)"); |
|||
|
|||
private final MinIoUtils minIoUtils; |
|||
|
|||
private String title; |
|||
private final List<ParsedQuestion> questions = new ArrayList<>(); |
|||
private ParsedQuestion currentQuestion; |
|||
private int imageCounter = 0; |
|||
|
|||
public WordQuestionParser(MinIoUtils minIoUtils) { |
|||
this.minIoUtils = minIoUtils; |
|||
} |
|||
|
|||
/** |
|||
* 解析 Word 文档 |
|||
* @param inputStream Word 文件流 |
|||
* @return 解析结果 |
|||
*/ |
|||
public ParseResult parse(InputStream inputStream) throws Exception { |
|||
try (XWPFDocument document = new XWPFDocument(inputStream)) { |
|||
for (XWPFParagraph paragraph : document.getParagraphs()) { |
|||
processParagraph(paragraph); |
|||
} |
|||
// 保存最后一道题
|
|||
if (currentQuestion != null) { |
|||
validateAndSave(currentQuestion); |
|||
} |
|||
} |
|||
|
|||
ParseResult result = new ParseResult(); |
|||
result.setTitle(title); |
|||
result.setQuestions(questions); |
|||
return result; |
|||
} |
|||
|
|||
private void processParagraph(XWPFParagraph paragraph) { |
|||
// 提取图片并上传 MinIO,返回替换后的文本
|
|||
String text = extractImagesAndUpload(paragraph); |
|||
|
|||
if (text == null || text.isBlank()) { |
|||
return; |
|||
} |
|||
|
|||
text = text.trim(); |
|||
|
|||
// 第一个非空段落作为标题
|
|||
if (title == null) { |
|||
title = text; |
|||
return; |
|||
} |
|||
|
|||
// 题号匹配
|
|||
if (QUESTION_PATTERN.matcher(text).find()) { |
|||
if (currentQuestion != null) { |
|||
validateAndSave(currentQuestion); |
|||
} |
|||
currentQuestion = new ParsedQuestion(); |
|||
// 去掉题号
|
|||
currentQuestion.stem = QUESTION_PATTERN.matcher(text).replaceFirst("").trim(); |
|||
currentQuestion.questionType = detectQuestionType(currentQuestion.stem); |
|||
return; |
|||
} |
|||
|
|||
if (currentQuestion == null) { |
|||
return; |
|||
} |
|||
|
|||
// 答案行
|
|||
Matcher answerMatcher = ANSWER_PATTERN.matcher(text); |
|||
if (answerMatcher.matches()) { |
|||
currentQuestion.answer = answerMatcher.group(1).trim(); |
|||
if (currentQuestion.answer.length() > 1 && currentQuestion.questionType == 0) { |
|||
currentQuestion.questionType = 1; // 升级为多选
|
|||
} |
|||
return; |
|||
} |
|||
|
|||
// 解析行
|
|||
Matcher analysisMatcher = ANALYSIS_PATTERN.matcher(text); |
|||
if (analysisMatcher.matches()) { |
|||
currentQuestion.analysis = analysisMatcher.group(1).trim(); |
|||
return; |
|||
} |
|||
|
|||
// 选项行
|
|||
Matcher optionMatcher = OPTION_PATTERN.matcher(text); |
|||
if (optionMatcher.find()) { |
|||
String label = text.substring(0, 1); |
|||
String content = optionMatcher.replaceFirst("").trim(); |
|||
currentQuestion.options.add(new ParsedOption(label, content)); |
|||
return; |
|||
} |
|||
|
|||
// 独立图片段落:合并到题干或最后一个选项
|
|||
if (text.startsWith("[IMAGE:") && currentQuestion.answer == null) { |
|||
if (currentQuestion.options.isEmpty()) { |
|||
currentQuestion.stem += text; |
|||
} else { |
|||
ParsedOption lastOpt = currentQuestion.options.get(currentQuestion.options.size() - 1); |
|||
lastOpt.content += text; |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// 其他文本:追加到题干
|
|||
if (currentQuestion.answer == null) { |
|||
currentQuestion.stem += "\n" + text; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 提取段落中的图片,上传 MinIO,替换为 HTML img 标签 |
|||
*/ |
|||
private String extractImagesAndUpload(XWPFParagraph paragraph) { |
|||
StringBuilder sb = new StringBuilder(); |
|||
|
|||
for (XWPFRun run : paragraph.getRuns()) { |
|||
String runText = run.text(); |
|||
if (runText != null) { |
|||
sb.append(sanitizeText(runText)); |
|||
} |
|||
|
|||
// 提取嵌入图片
|
|||
List<XWPFPicture> pictures = run.getEmbeddedPictures(); |
|||
for (XWPFPicture picture : pictures) { |
|||
try { |
|||
imageCounter++; |
|||
// MinIO 路径规范:classicpaper/{yyyyMMdd}/{uuid}.png(不依赖 subLineId)
|
|||
String fileName = String.format("classicpaper/%s/%s.png", |
|||
DateUtil.format(new java.util.Date(), "yyyyMMdd"), |
|||
IdUtil.fastSimpleUUID()); |
|||
byte[] imageData = picture.getPictureData().getData(); |
|||
|
|||
// 上传到 MinIO
|
|||
minIoUtils.uploadFile(new java.io.ByteArrayInputStream(imageData), fileName); |
|||
String imageUrl = minIoUtils.getPreviewUrl(fileName); |
|||
|
|||
// 替换为 HTML img 标签
|
|||
sb.append(String.format("<img src='%s'/>", imageUrl)); |
|||
log.debug(">>> [Word解析] 图片已上传 MinIO: {}", fileName); |
|||
} catch (Exception e) { |
|||
log.error(">>> [Word解析] 图片上传失败", e); |
|||
sb.append("[IMAGE:上传失败]"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return sb.toString().trim(); |
|||
} |
|||
|
|||
private String sanitizeText(String text) { |
|||
if (text == null) return ""; |
|||
return text |
|||
.replace('、', ' ') // 顿号
|
|||
.replace(' ', ' ') // 不间断空格
|
|||
.replace('', ' ') // 零宽空格
|
|||
.replace('', ' ') // BOM
|
|||
.replaceAll("\\s+", " ") |
|||
.trim(); |
|||
} |
|||
|
|||
/** |
|||
* 检测题型 |
|||
*/ |
|||
private int detectQuestionType(String stem) { |
|||
if (stem.contains("[判断题]")) return 2; // TRUE_FALSE
|
|||
if (stem.contains("[简答题]")) return 4; // SHORT_ANSWER
|
|||
if (stem.contains("[多选题]")) return 1; // MULTIPLE_CHOICE
|
|||
if (stem.contains("[单选题]")) return 0; // SINGLE_CHOICE
|
|||
return 0; // 默认单选
|
|||
} |
|||
|
|||
private void validateAndSave(ParsedQuestion q) { |
|||
if (q.stem == null || q.stem.isBlank()) { |
|||
log.warn(">>> [Word解析] 题目内容为空,跳过"); |
|||
return; |
|||
} |
|||
// 清理题型标签
|
|||
q.stem = q.stem.replaceAll("\\[.*?题\\]", "").trim(); |
|||
questions.add(q); |
|||
} |
|||
|
|||
// =============== 内部数据结构 ===============
|
|||
|
|||
@Data |
|||
public static class ParseResult { |
|||
private String title; |
|||
private List<ParsedQuestion> questions = new ArrayList<>(); |
|||
} |
|||
|
|||
@Data |
|||
public static class ParsedQuestion { |
|||
/** 题型:0-单选,1-多选,2-判断,4-简答 */ |
|||
int questionType = 0; |
|||
String stem = ""; |
|||
List<ParsedOption> options = new ArrayList<>(); |
|||
String answer; |
|||
String analysis; |
|||
} |
|||
|
|||
@Data |
|||
public static class ParsedOption { |
|||
String label; |
|||
String content; |
|||
|
|||
public ParsedOption(String label, String content) { |
|||
this.label = label; |
|||
this.content = content; |
|||
} |
|||
} |
|||
} |
|||
@ -1,23 +1,47 @@ |
|||
package com.project.information.domain.param; |
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import com.project.information.domain.dto.UploadedFileDTO; |
|||
import lombok.Data; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 原始资料表单提交请求参数 |
|||
* Step2:创建虚拟资料 + 子文件记录 + 触发解析 |
|||
* Step2:创建虚拟资料 + 子文件记录 + 触发数据服务解析 |
|||
* subLineId、fileName 用普通 form-data 传参 |
|||
* files 用 JSON 字符串传参(仿照 ClusterCallbackDTO 模式) |
|||
*/ |
|||
@Data |
|||
public class GenerateFromFilesParam { |
|||
|
|||
private static final ObjectMapper MAPPER = new ObjectMapper(); |
|||
|
|||
/** 子产品线ID */ |
|||
private Long subLineId; |
|||
|
|||
/** 虚拟资料文件名(用户填写的占位名称) */ |
|||
private String fileName; |
|||
|
|||
/** Step1 上传的文件信息列表 */ |
|||
private List<UploadedFileDTO> files; |
|||
/** Step1 上传的文件信息(JSON 字符串) */ |
|||
private String files; |
|||
|
|||
/** |
|||
* 解析 files JSON 字符串为列表(Lombok 不会覆盖此方法) |
|||
*/ |
|||
public List<UploadedFileDTO> parseFiles() { |
|||
if (!StringUtils.hasText(files)) { |
|||
return Collections.emptyList(); |
|||
} |
|||
try { |
|||
return MAPPER.readValue(files, new TypeReference<List<UploadedFileDTO>>() {}); |
|||
} catch (JsonProcessingException e) { |
|||
throw new BusinessErrorException("文件信息解析失败"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,21 @@ |
|||
package com.project.information.domain.service; |
|||
|
|||
import com.project.base.domain.result.Result; |
|||
|
|||
/** |
|||
* 确认固化知识点草稿域服务 |
|||
* 将草稿复制到正式表,触发聚类,不可逆 |
|||
*/ |
|||
public interface ConfirmKnowledgePointDraftDomainService { |
|||
|
|||
/** |
|||
* 确认提交(固化,不可逆) |
|||
* 1. 幂等校验 |
|||
* 2. 草稿 content → evaluator_knowledge_point |
|||
* 3. 更新 draftStatus = APPROVED |
|||
* 4. 触发聚类 |
|||
* |
|||
* @param informationId 虚拟资料ID |
|||
*/ |
|||
Result<String> confirm(Long informationId); |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
package com.project.information.domain.service.impl; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.project.base.domain.exception.BusinessErrorException; |
|||
import com.project.base.domain.result.Result; |
|||
import com.project.information.domain.dto.KnowledgePointDTO; |
|||
import com.project.information.domain.entity.InformationEntity; |
|||
import com.project.information.domain.entity.KnowledgePointDraftEntity; |
|||
import com.project.information.domain.entity.KnowledgePointEntity; |
|||
import com.project.information.domain.enums.AuditStatusEnum; |
|||
import com.project.information.domain.enums.DraftStatusEnum; |
|||
import com.project.information.domain.service.*; |
|||
import com.project.interaction.application.AlgorithmApplicationService; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 确认固化知识点草稿域服务实现 |
|||
*/ |
|||
@Service |
|||
@Slf4j |
|||
public class ConfirmKnowledgePointDraftDomainServiceImpl implements ConfirmKnowledgePointDraftDomainService { |
|||
|
|||
@Autowired |
|||
private KnowledgePointDraftBaseService knowledgePointDraftBaseService; |
|||
|
|||
@Autowired |
|||
private InformationBaseService informationBaseService; |
|||
|
|||
@Autowired |
|||
private KnowledgePointBaseService knowledgePointBaseService; |
|||
|
|||
@Autowired |
|||
private AlgorithmApplicationService algorithmApplicationService; |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public Result<String> confirm(Long informationId) { |
|||
if (informationId == null) { |
|||
throw new BusinessErrorException("资料ID不能为空"); |
|||
} |
|||
|
|||
// 1. 查询资料,幂等校验
|
|||
InformationEntity information = informationBaseService.getById(informationId); |
|||
if (information == null) { |
|||
throw new BusinessErrorException("资料不存在"); |
|||
} |
|||
if (DraftStatusEnum.APPROVED.getValue().equals(information.getDraftStatus())) { |
|||
return Result.success("已固化,不可重复操作"); |
|||
} |
|||
if (!DraftStatusEnum.PENDING.getValue().equals(information.getDraftStatus())) { |
|||
throw new BusinessErrorException("该资料不在待审核状态"); |
|||
} |
|||
|
|||
// 2. 查询所有待审核草稿
|
|||
List<KnowledgePointDraftEntity> drafts = knowledgePointDraftBaseService.list( |
|||
new LambdaQueryWrapper<KnowledgePointDraftEntity>() |
|||
.eq(KnowledgePointDraftEntity::getInformationId, informationId) |
|||
.eq(KnowledgePointDraftEntity::getAuditStatus, AuditStatusEnum.PENDING.getValue())); |
|||
if (drafts.isEmpty()) { |
|||
throw new BusinessErrorException("没有待审核的草稿"); |
|||
} |
|||
|
|||
// 3. 将草稿 content 复制到正式知识点表
|
|||
List<KnowledgePointEntity> knowledgePoints = new ArrayList<>(); |
|||
for (KnowledgePointDraftEntity draft : drafts) { |
|||
KnowledgePointEntity kp = new KnowledgePointEntity(); |
|||
kp.setContent(draft.getContent()); |
|||
kp.setKnowledgeType(draft.getKnowledgeType()); |
|||
kp.setInformationId(informationId); |
|||
kp.setParseName(draft.getParseName()); |
|||
knowledgePoints.add(kp); |
|||
} |
|||
knowledgePointBaseService.saveBatch(knowledgePoints); |
|||
|
|||
// 4. 更新草稿 auditStatus 为已通过
|
|||
List<Long> draftIds = drafts.stream().map(KnowledgePointDraftEntity::getId).toList(); |
|||
knowledgePointDraftBaseService.lambdaUpdate() |
|||
.in(KnowledgePointDraftEntity::getId, draftIds) |
|||
.set(KnowledgePointDraftEntity::getAuditStatus, AuditStatusEnum.APPROVED.getValue()) |
|||
.update(); |
|||
|
|||
// 5. 更新虚拟资料 draftStatus = APPROVED(2)
|
|||
informationBaseService.lambdaUpdate() |
|||
.eq(InformationEntity::getId, informationId) |
|||
.set(InformationEntity::getDraftStatus, DraftStatusEnum.APPROVED.getValue()) |
|||
.update(); |
|||
|
|||
// 6. 触发聚类
|
|||
List<KnowledgePointDTO> kpList = knowledgePoints.stream() |
|||
.map(entity -> entity.toDTO(KnowledgePointDTO::new)) |
|||
.toList(); |
|||
algorithmApplicationService.postToClusteringByInformationId(informationId, kpList); |
|||
|
|||
log.info(">>> [草稿审核] 确认固化完成, informationId={}, 草稿数={}", informationId, drafts.size()); |
|||
return Result.success("确认成功"); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
package com.project.interaction.application; |
|||
|
|||
import com.project.interaction.domain.dto.AiExtractCallbackDTO; |
|||
|
|||
/** |
|||
* 算法服务知识点提取回调应用服务 |
|||
*/ |
|||
public interface AiExtractCallbackApplicationService { |
|||
void handleCallback(AiExtractCallbackDTO callback); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
package com.project.interaction.application; |
|||
|
|||
import com.project.interaction.domain.dto.DataExtractCallbackDTO; |
|||
|
|||
/** |
|||
* 数据服务解析回调应用服务 |
|||
*/ |
|||
public interface DataExtractCallbackApplicationService { |
|||
void handleCallback(DataExtractCallbackDTO callback); |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.project.interaction.application.impl; |
|||
|
|||
import com.project.interaction.application.AiExtractCallbackApplicationService; |
|||
import com.project.interaction.domain.dto.AiExtractCallbackDTO; |
|||
import com.project.interaction.domain.service.SaveAiExtractDraftDomainService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 算法服务知识点提取回调应用服务实现 |
|||
*/ |
|||
@Service |
|||
public class AiExtractCallbackApplicationServiceImpl implements AiExtractCallbackApplicationService { |
|||
|
|||
@Autowired |
|||
private SaveAiExtractDraftDomainService saveAiExtractDraftDomainService; |
|||
|
|||
@Override |
|||
public void handleCallback(AiExtractCallbackDTO callback) { |
|||
saveAiExtractDraftDomainService.handleCallback(callback); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.project.interaction.application.impl; |
|||
|
|||
import com.project.interaction.application.DataExtractCallbackApplicationService; |
|||
import com.project.interaction.domain.dto.DataExtractCallbackDTO; |
|||
import com.project.interaction.domain.service.SaveDataExtractResultDomainService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 数据服务解析回调应用服务实现 |
|||
*/ |
|||
@Service |
|||
public class DataExtractCallbackApplicationServiceImpl implements DataExtractCallbackApplicationService { |
|||
|
|||
@Autowired |
|||
private SaveDataExtractResultDomainService saveDataExtractResultDomainService; |
|||
|
|||
@Override |
|||
public void handleCallback(DataExtractCallbackDTO callback) { |
|||
saveDataExtractResultDomainService.handleCallback(callback); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
package com.project.interaction.domain.service; |
|||
|
|||
import com.project.interaction.domain.dto.AiExtractCallbackDTO; |
|||
|
|||
/** |
|||
* 保存AI提取知识点草稿域服务 |
|||
* 将算法返回的草稿知识点写入草稿表 |
|||
*/ |
|||
public interface SaveAiExtractDraftDomainService { |
|||
void handleCallback(AiExtractCallbackDTO callback); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
package com.project.interaction.domain.service; |
|||
|
|||
import com.project.interaction.domain.dto.DataExtractCallbackDTO; |
|||
|
|||
/** |
|||
* 保存数据服务解析结果域服务 |
|||
* 更新子文件解析状态和文本,检查批次完成,触发算法服务 |
|||
*/ |
|||
public interface SaveDataExtractResultDomainService { |
|||
void handleCallback(DataExtractCallbackDTO callback); |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
package com.project.interaction.domain.service.impl; |
|||
|
|||
import com.project.information.domain.entity.InformationEntity; |
|||
import com.project.information.domain.entity.KnowledgePointDraftEntity; |
|||
import com.project.information.domain.enums.AuditStatusEnum; |
|||
import com.project.information.domain.enums.DraftStatusEnum; |
|||
import com.project.information.domain.service.InformationBaseService; |
|||
import com.project.information.domain.service.KnowledgePointDraftBaseService; |
|||
import com.project.interaction.domain.dto.AiExtractCallbackDTO; |
|||
import com.project.interaction.domain.service.SaveAiExtractDraftDomainService; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 保存AI提取知识点草稿域服务实现 |
|||
*/ |
|||
@Service |
|||
@Slf4j |
|||
public class SaveAiExtractDraftDomainServiceImpl implements SaveAiExtractDraftDomainService { |
|||
|
|||
@Autowired |
|||
private KnowledgePointDraftBaseService knowledgePointDraftBaseService; |
|||
|
|||
@Autowired |
|||
private InformationBaseService informationBaseService; |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public void handleCallback(AiExtractCallbackDTO callback) { |
|||
Long informationId = callback.getInformationId(); |
|||
List<AiExtractCallbackDTO.DraftItem> drafts = callback.getDrafts(); |
|||
|
|||
log.info(">>> [算法服务回调] 收到知识点提取回调, informationId={}, 草稿数={}", |
|||
informationId, drafts != null ? drafts.size() : 0); |
|||
|
|||
if (drafts == null || drafts.isEmpty()) { |
|||
log.warn(">>> [算法服务回调] 算法返回的草稿为空, informationId={}", informationId); |
|||
return; |
|||
} |
|||
|
|||
// 1. 保存草稿(aiContent = content 的副本)
|
|||
List<KnowledgePointDraftEntity> draftEntities = new ArrayList<>(); |
|||
for (AiExtractCallbackDTO.DraftItem item : drafts) { |
|||
KnowledgePointDraftEntity draft = new KnowledgePointDraftEntity(); |
|||
draft.setInformationId(informationId); |
|||
draft.setContent(item.getContent()); |
|||
draft.setAiContent(item.getContent()); |
|||
draft.setKnowledgeType(item.getKnowledgeType()); |
|||
draft.setParseName(item.getParseName()); |
|||
draft.setAuditStatus(AuditStatusEnum.PENDING.getValue()); |
|||
draftEntities.add(draft); |
|||
} |
|||
knowledgePointDraftBaseService.saveBatch(draftEntities); |
|||
|
|||
// 2. 更新虚拟资料 draftStatus = PENDING(1)
|
|||
informationBaseService.lambdaUpdate() |
|||
.eq(InformationEntity::getId, informationId) |
|||
.set(InformationEntity::getDraftStatus, DraftStatusEnum.PENDING.getValue()) |
|||
.update(); |
|||
|
|||
log.info(">>> [算法服务回调] 草稿保存完成, informationId={}, 草稿数={}", informationId, draftEntities.size()); |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
package com.project.interaction.domain.service.impl; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.project.information.domain.entity.InformationFileEntity; |
|||
import com.project.information.domain.enums.FileParseStatusEnum; |
|||
import com.project.information.domain.service.InformationFileBaseService; |
|||
import com.project.interaction.domain.dto.AiExtractRequestDTO; |
|||
import com.project.interaction.domain.dto.DataExtractCallbackDTO; |
|||
import com.project.interaction.domain.service.PostToAiExtractDomainService; |
|||
import com.project.interaction.domain.service.SaveDataExtractResultDomainService; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 保存数据服务解析结果域服务实现 |
|||
*/ |
|||
@Service |
|||
@Slf4j |
|||
public class SaveDataExtractResultDomainServiceImpl implements SaveDataExtractResultDomainService { |
|||
|
|||
@Autowired |
|||
private InformationFileBaseService informationFileBaseService; |
|||
|
|||
@Autowired |
|||
private PostToAiExtractDomainService postToAiExtractDomainService; |
|||
|
|||
@Override |
|||
public void handleCallback(DataExtractCallbackDTO callback) { |
|||
Long fileId = callback.getFileId(); |
|||
Long informationId = callback.getInformationId(); |
|||
|
|||
log.info(">>> [数据服务回调] 收到子文件解析回调, fileId={}, informationId={}, status={}", |
|||
fileId, informationId, callback.getParseStatus()); |
|||
|
|||
// 1. 更新子文件解析状态
|
|||
InformationFileEntity file = informationFileBaseService.getById(fileId); |
|||
if (file == null) { |
|||
log.error(">>> [数据服务回调] 子文件不存在, fileId={}", fileId); |
|||
return; |
|||
} |
|||
file.setParseStatus(callback.getParseStatus()); |
|||
if (FileParseStatusEnum.Success.getValue().equals(callback.getParseStatus())) { |
|||
file.setParsedText(callback.getParsedText()); |
|||
} |
|||
informationFileBaseService.updateById(file); |
|||
|
|||
// 2. 解析失败则记录日志
|
|||
if (!FileParseStatusEnum.Success.getValue().equals(callback.getParseStatus())) { |
|||
log.warn(">>> [数据服务回调] 子文件解析失败, fileId={}, error={}", fileId, callback.getErrorMsg()); |
|||
return; |
|||
} |
|||
|
|||
// 3. 检查该虚拟资料下所有子文件是否全部完成
|
|||
List<InformationFileEntity> allFiles = informationFileBaseService.list( |
|||
new LambdaQueryWrapper<InformationFileEntity>() |
|||
.eq(InformationFileEntity::getInformationId, informationId)); |
|||
|
|||
boolean allDone = allFiles.stream() |
|||
.allMatch(f -> FileParseStatusEnum.Success.getValue().equals(f.getParseStatus()) |
|||
|| FileParseStatusEnum.Failed.getValue().equals(f.getParseStatus())); |
|||
|
|||
if (!allDone) { |
|||
log.info(">>> [数据服务回调] 虚拟资料 {} 下还有子文件未完成解析,等待中", informationId); |
|||
return; |
|||
} |
|||
|
|||
// 4. 全部完成 → 调用算法服务知识点提取
|
|||
log.info(">>> [数据服务回调] 虚拟资料 {} 下所有子文件解析完成,开始调用算法服务", informationId); |
|||
|
|||
List<AiExtractRequestDTO.FileContent> fileContents = new ArrayList<>(); |
|||
for (InformationFileEntity f : allFiles) { |
|||
if (FileParseStatusEnum.Success.getValue().equals(f.getParseStatus()) && f.getParsedText() != null) { |
|||
AiExtractRequestDTO.FileContent fc = new AiExtractRequestDTO.FileContent(); |
|||
fc.setFileName(f.getFileName()); |
|||
fc.setContentCategory(String.valueOf(f.getContentCategory())); |
|||
fc.setParsedText(f.getParsedText()); |
|||
fileContents.add(fc); |
|||
} |
|||
} |
|||
|
|||
if (fileContents.isEmpty()) { |
|||
log.warn(">>> [数据服务回调] 虚拟资料 {} 下没有成功解析的子文件", informationId); |
|||
return; |
|||
} |
|||
|
|||
AiExtractRequestDTO request = new AiExtractRequestDTO(); |
|||
request.setInformationId(informationId); |
|||
request.setFiles(fileContents); |
|||
|
|||
postToAiExtractDomainService.postToAiExtract(request); |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
server: |
|||
port: 7088 |
|||
|
|||
spring: |
|||
datasource: |
|||
dynamic: |
|||
primary: master |
|||
datasource: |
|||
master: |
|||
driverClassName: com.mysql.cj.jdbc.Driver |
|||
password: Itc@123456 |
|||
url: jdbc:mysql://8.129.84.155:3306/ai_evaluator?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true |
|||
username: root |
|||
# Redis |
|||
data: |
|||
redis: |
|||
host: 8.129.84.155 |
|||
port: 6379 |
|||
password: Itc@123456 |
|||
database: 3 |
|||
timeout: 5000ms |
|||
lettuce: |
|||
pool: |
|||
max-active: 8 |
|||
max-idle: 30 |
|||
max-wait: 10000 |
|||
min-idle: 10 |
|||
jpa: |
|||
hibernate: |
|||
ddl-auto: update |
|||
naming: |
|||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl |
|||
properties: |
|||
hibernate: |
|||
dialect: org.hibernate.dialect.MySQL8Dialect |
|||
show-sql: true |
|||
# 上传下载限制 |
|||
servlet: |
|||
multipart: |
|||
max-file-size: 100MB |
|||
max-request-size: 100MB |
|||
jackson: |
|||
date-format: yyyy-MM-dd HH:mm:ss |
|||
time-zone: GMT+8 |
|||
serialization: |
|||
write-dates-as-timestamps: false |
|||
generator: |
|||
write-numbers-as-strings: true |
|||
minio: |
|||
endpoint: ${MINIO_ENDPOINT:http://localhost:9000} |
|||
accessKey: ${MINIO_ASSESSKEY:DTKYZDZM1i31XOvd24SP} |
|||
secretKey: ${MINIO_SECRETKEY:PnfLPcJbvaUboZIwYZAADPB0pDtPZgbi0QiLSs3C} |
|||
bucket: ${MINIO_BUCKET:ai-evaluator} |
|||
tempAccessFileUrl: http://8.129.84.155/minio-api |
|||
mybatis-plus: |
|||
configuration: |
|||
map-underscore-to-camel-case: true |
|||
mapper-locations: classpath*:mapper/**/*.xml |
|||
type-aliases-package: com.proposal.**.domain.entity |
|||
|
|||
milvus: |
|||
host: 127.0.0.1 |
|||
port: 19530 |
|||
analysis: |
|||
host: http://127.0.0.1 |
|||
port: 8888 |
|||
url: /word/parse |
|||
ding: |
|||
appKey: dinghgexcbos9dorl3kb |
|||
appSecret: bSNX1ZOUTk2A4FGwQ1U6fyHlFssh9IZtFO6FuKx8A97PE1VZL9lBTJapwzDGCS7v |
|||
# appKey: ding4eootvq4jzas96lr #保伦架构 |
|||
# appSecret: UI6XcD8GFPE_W_yo2eQkMuoSsTEf1whpZYEXrsDA7CV7bkJp40B3VNcETWS1aGgg |
|||
agentId: 4418045751 |
|||
corpId: ding13a2ff8718e6bcdd |
|||
# corpId: 3318151640 |
|||
noticeUrlPrefix: https://107pm707566hq.vicp.fun/ai-evaluator-test |
|||
algo: |
|||
clusterUrl: /semantic-cluster |
|||
baseUrl: http://127.0.0.1:8002 |
|||
generateQuestionUrl: /v1/generate/questions_from_cluster |
|||
apiUrl: http://127.0.0.1:8000 |
|||
jwt: |
|||
secret: "my-very-fixed-and-secure-secret-key-1234567890" |
|||
|
|||
# 题目生成相关配置 |
|||
question: |
|||
generation: |
|||
# 限流速率:每秒允许的API请求数 |
|||
rate-limit: 4 |
|||
downgrade: false |
|||
# 队列配置 |
|||
queue: |
|||
# 重试间隔(秒) |
|||
retry-interval: 60 |
|||
|
|||
scheduled-task: |
|||
owner: test |
|||
Loading…
Reference in new issue