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; |
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 com.project.information.domain.dto.UploadedFileDTO; |
||||
import lombok.Data; |
import lombok.Data; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
|
||||
|
import java.util.Collections; |
||||
import java.util.List; |
import java.util.List; |
||||
|
|
||||
/** |
/** |
||||
* 原始资料表单提交请求参数 |
* 原始资料表单提交请求参数 |
||||
* Step2:创建虚拟资料 + 子文件记录 + 触发解析 |
* Step2:创建虚拟资料 + 子文件记录 + 触发数据服务解析 |
||||
|
* subLineId、fileName 用普通 form-data 传参 |
||||
|
* files 用 JSON 字符串传参(仿照 ClusterCallbackDTO 模式) |
||||
*/ |
*/ |
||||
@Data |
@Data |
||||
public class GenerateFromFilesParam { |
public class GenerateFromFilesParam { |
||||
|
|
||||
|
private static final ObjectMapper MAPPER = new ObjectMapper(); |
||||
|
|
||||
/** 子产品线ID */ |
/** 子产品线ID */ |
||||
private Long subLineId; |
private Long subLineId; |
||||
|
|
||||
/** 虚拟资料文件名(用户填写的占位名称) */ |
/** 虚拟资料文件名(用户填写的占位名称) */ |
||||
private String fileName; |
private String fileName; |
||||
|
|
||||
/** Step1 上传的文件信息列表 */ |
/** Step1 上传的文件信息(JSON 字符串) */ |
||||
private List<UploadedFileDTO> files; |
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