Browse Source

历史题库导入

master
luoweijian 3 months ago
parent
commit
3b9b56f392
  1. 23
      src/main/java/com/project/classicpaper/domain/service/ImportClassicPaperDomainService.java
  2. 123
      src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java
  3. 236
      src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java
  4. 28
      src/main/java/com/project/information/application/impl/GenerateFromFilesApplicationServiceImpl.java
  5. 3
      src/main/java/com/project/information/application/impl/UploadFilesApplicationServiceImpl.java
  6. 1
      src/main/java/com/project/information/controller/InformationController.java
  7. 30
      src/main/java/com/project/information/domain/param/GenerateFromFilesParam.java
  8. 21
      src/main/java/com/project/information/domain/service/ConfirmKnowledgePointDraftDomainService.java
  9. 103
      src/main/java/com/project/information/domain/service/impl/ConfirmKnowledgePointDraftDomainServiceImpl.java
  10. 10
      src/main/java/com/project/interaction/application/AiExtractCallbackApplicationService.java
  11. 10
      src/main/java/com/project/interaction/application/DataExtractCallbackApplicationService.java
  12. 22
      src/main/java/com/project/interaction/application/impl/AiExtractCallbackApplicationServiceImpl.java
  13. 22
      src/main/java/com/project/interaction/application/impl/DataExtractCallbackApplicationServiceImpl.java
  14. 11
      src/main/java/com/project/interaction/domain/service/SaveAiExtractDraftDomainService.java
  15. 11
      src/main/java/com/project/interaction/domain/service/SaveDataExtractResultDomainService.java
  16. 68
      src/main/java/com/project/interaction/domain/service/impl/SaveAiExtractDraftDomainServiceImpl.java
  17. 96
      src/main/java/com/project/interaction/domain/service/impl/SaveDataExtractResultDomainServiceImpl.java
  18. 97
      src/main/resources/application-prod.yml
  19. 6
      src/main/resources/application-test.yml

23
src/main/java/com/project/classicpaper/domain/service/ImportClassicPaperDomainService.java

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

123
src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java

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

236
src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java

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

28
src/main/java/com/project/information/application/impl/GenerateFromFilesApplicationServiceImpl.java

@ -7,6 +7,7 @@ import com.project.information.application.GenerateFromFilesApplicationService;
import com.project.information.domain.dto.UploadedFileDTO;
import com.project.information.domain.entity.InformationEntity;
import com.project.information.domain.entity.InformationFileEntity;
import com.project.information.domain.enums.ContentCategoryEnum;
import com.project.information.domain.enums.DraftStatusEnum;
import com.project.information.domain.enums.FileParseStatusEnum;
import com.project.information.domain.enums.MaterialTypeEnum;
@ -18,6 +19,7 @@ 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.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
@ -49,10 +51,32 @@ public class GenerateFromFilesApplicationServiceImpl implements GenerateFromFile
if (param.getSubLineId() == null) {
throw new BusinessErrorException("子产品线不能为空");
}
if (param.getFiles() == null || param.getFiles().isEmpty()) {
if (!StringUtils.hasText(param.getFileName())) {
throw new BusinessErrorException("文件名不能为空");
}
// 解析 files JSON 字符串
List<UploadedFileDTO> files = param.parseFiles();
if (files.isEmpty()) {
throw new BusinessErrorException("请先上传文件");
}
// 校验内容分类是否合法
for (int i = 0; i < files.size(); i++) {
if (!ContentCategoryEnum.isValid(files.get(i).getContentCategory())) {
throw new BusinessErrorException("第" + (i + 1) + "个文件的内容分类不合法");
}
}
// 检查同子产品线下是否有重复文件名
boolean exists = informationBaseService.lambdaQuery()
.eq(InformationEntity::getSubLineId, param.getSubLineId())
.eq(InformationEntity::getName, param.getFileName())
.exists();
if (exists) {
throw new BusinessErrorException("该子产品线下已存在同名资料「" + param.getFileName() + "」,请修改文件名");
}
// 1. 创建虚拟 InformationEntity(materialType=原始资料, draftStatus=PENDING)
InformationEntity virtualInfo = new InformationEntity();
virtualInfo.setSubLineId(param.getSubLineId());
@ -68,7 +92,7 @@ public class GenerateFromFilesApplicationServiceImpl implements GenerateFromFile
// 2. 创建子文件记录并关联虚拟资料
List<InformationFileEntity> fileEntities = new ArrayList<>();
for (UploadedFileDTO file : param.getFiles()) {
for (UploadedFileDTO file : files) {
InformationFileEntity fileEntity = new InformationFileEntity();
fileEntity.setInformationId(informationId);
fileEntity.setFileName(file.getFileName());

3
src/main/java/com/project/information/application/impl/UploadFilesApplicationServiceImpl.java

@ -1,6 +1,7 @@
package com.project.information.application.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.project.base.config.CustomIdGenerator;
import com.project.base.domain.exception.BusinessErrorException;
import com.project.base.domain.result.Result;
@ -73,7 +74,7 @@ public class UploadFilesApplicationServiceImpl implements UploadFilesApplication
// MinIO 路径规范:classicpaper/{yyyyMMdd}/{uuid}.{ext}
String filePath = String.format("classicpaper/%s/%s.%s",
new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()),
customIdGenerator.nextId(null),
IdUtil.fastSimpleUUID(),
suffix);
// 上传到 MinIO

1
src/main/java/com/project/information/controller/InformationController.java

@ -77,6 +77,7 @@ public class InformationController {
/**
* Step2提交表单创建虚拟资料 + 关联子文件 + 触发解析
* form-data 参数subLineId, fileName, fileNames[], filePaths[], fileSuffixes[], contentCategories[]
*/
@PostMapping("/generateFromFiles")
public Result<String> generateFromFiles(GenerateFromFilesParam param) throws Exception {

30
src/main/java/com/project/information/domain/param/GenerateFromFilesParam.java

@ -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创建虚拟资料 + 子文件记录 + 触发数据服务解析
* subLineIdfileName 用普通 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("文件信息解析失败");
}
}
}

21
src/main/java/com/project/information/domain/service/ConfirmKnowledgePointDraftDomainService.java

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

103
src/main/java/com/project/information/domain/service/impl/ConfirmKnowledgePointDraftDomainServiceImpl.java

@ -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("确认成功");
}
}

10
src/main/java/com/project/interaction/application/AiExtractCallbackApplicationService.java

@ -0,0 +1,10 @@
package com.project.interaction.application;
import com.project.interaction.domain.dto.AiExtractCallbackDTO;
/**
* 算法服务知识点提取回调应用服务
*/
public interface AiExtractCallbackApplicationService {
void handleCallback(AiExtractCallbackDTO callback);
}

10
src/main/java/com/project/interaction/application/DataExtractCallbackApplicationService.java

@ -0,0 +1,10 @@
package com.project.interaction.application;
import com.project.interaction.domain.dto.DataExtractCallbackDTO;
/**
* 数据服务解析回调应用服务
*/
public interface DataExtractCallbackApplicationService {
void handleCallback(DataExtractCallbackDTO callback);
}

22
src/main/java/com/project/interaction/application/impl/AiExtractCallbackApplicationServiceImpl.java

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

22
src/main/java/com/project/interaction/application/impl/DataExtractCallbackApplicationServiceImpl.java

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

11
src/main/java/com/project/interaction/domain/service/SaveAiExtractDraftDomainService.java

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

11
src/main/java/com/project/interaction/domain/service/SaveDataExtractResultDomainService.java

@ -0,0 +1,11 @@
package com.project.interaction.domain.service;
import com.project.interaction.domain.dto.DataExtractCallbackDTO;
/**
* 保存数据服务解析结果域服务
* 更新子文件解析状态和文本检查批次完成触发算法服务
*/
public interface SaveDataExtractResultDomainService {
void handleCallback(DataExtractCallbackDTO callback);
}

68
src/main/java/com/project/interaction/domain/service/impl/SaveAiExtractDraftDomainServiceImpl.java

@ -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());
}
}

96
src/main/java/com/project/interaction/domain/service/impl/SaveDataExtractResultDomainServiceImpl.java

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

97
src/main/resources/application-prod.yml

@ -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

6
src/main/resources/application-test.yml

@ -9,7 +9,7 @@ spring:
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
url: jdbc:mysql://8.129.84.155:3306/ai_evaluator_test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
username: root
# Redis
data:
@ -47,10 +47,10 @@ spring:
generator:
write-numbers-as-strings: true
minio:
endpoint: ${MINIO_ENDPOINT:http://localhost:9000}
endpoint: ${MINIO_ENDPOINT:http://8.129.84.155:9000}
accessKey: ${MINIO_ASSESSKEY:DTKYZDZM1i31XOvd24SP}
secretKey: ${MINIO_SECRETKEY:PnfLPcJbvaUboZIwYZAADPB0pDtPZgbi0QiLSs3C}
bucket: ${MINIO_BUCKET:ai-evaluator}
bucket: ${MINIO_BUCKET:ai-evaluator-test}
tempAccessFileUrl: http://8.129.84.155/minio-api
mybatis-plus:
configuration:

Loading…
Cancel
Save