Browse Source

数据服务对接修改

master
luoweijian 2 months ago
parent
commit
6efd14e3c4
  1. 11
      src/main/java/com/project/classicpaper/application/ClassicPaperSetApplicationService.java
  2. 11
      src/main/java/com/project/classicpaper/application/impl/ClassicPaperSetApplicationServiceImpl.java
  3. 17
      src/main/java/com/project/classicpaper/controller/ClassicPaperSetController.java
  4. 5
      src/main/java/com/project/classicpaper/domain/dto/ClassicPaperQuestionDTO.java
  5. 3
      src/main/java/com/project/classicpaper/domain/dto/ClassicPaperSetDTO.java
  6. 4
      src/main/java/com/project/classicpaper/domain/entity/ClassicPaperSetEntity.java
  7. 8
      src/main/java/com/project/classicpaper/domain/service/ImportClassicPaperDomainService.java
  8. 145
      src/main/java/com/project/classicpaper/domain/service/impl/ImportClassicPaperDomainServiceImpl.java
  9. 143
      src/main/java/com/project/classicpaper/domain/service/parser/WordQuestionParser.java
  10. 13
      src/main/java/com/project/information/utils/MinIoUtils.java
  11. 4
      src/main/java/com/project/interaction/domain/service/impl/SaveDataExtractResultDomainServiceImpl.java
  12. 7
      src/main/java/com/project/operation/config/RedisKeyConstant.java
  13. 22
      src/main/java/com/project/operation/controller/OperationLogController.java

11
src/main/java/com/project/classicpaper/application/ClassicPaperSetApplicationService.java

@ -4,6 +4,7 @@ import com.project.base.domain.result.PageResult;
import com.project.base.domain.result.Result;
import com.project.classicpaper.domain.dto.ClassicPaperSetDTO;
import com.project.classicpaper.domain.param.ClassicPaperSetParam;
import org.springframework.web.multipart.MultipartFile;
public interface ClassicPaperSetApplicationService {
@ -33,4 +34,14 @@ public interface ClassicPaperSetApplicationService {
* 更新分值比例同时计算每道题的分值
*/
Result<ClassicPaperSetDTO> save(ClassicPaperSetDTO request) throws Exception;
/**
* Word 文件导入历史套卷
*
* @param file Word 文档
* @param classicCategoryId 套题二级分类ID
* @param paperName 套卷名称可选为空时取 Word 标题
* @return 套题组详情
*/
Result<ClassicPaperSetDTO> importFromWord(MultipartFile file, Long classicCategoryId, String paperName) throws Exception;
}

11
src/main/java/com/project/classicpaper/application/impl/ClassicPaperSetApplicationServiceImpl.java

@ -21,6 +21,7 @@ import com.project.classicpaper.domain.param.ClassicPaperSetParam;
import com.project.classicpaper.domain.service.*;
import com.project.question.domain.entity.QuestionEntity;
import com.project.question.domain.service.QuestionBaseService;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -54,6 +55,9 @@ public class ClassicPaperSetApplicationServiceImpl implements ClassicPaperSetApp
@Autowired
private RegenerateQuestionTaskBaseService regenerateQuestionTaskBaseService;
@Autowired
private ImportClassicPaperDomainService importClassicPaperDomainService;
@Autowired
private com.project.task.config.ExamScoreRatioConfig examScoreRatioConfig;
@ -212,6 +216,13 @@ public class ClassicPaperSetApplicationServiceImpl implements ClassicPaperSetApp
return getDetail(request.getId(), null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<ClassicPaperSetDTO> importFromWord(MultipartFile file, Long classicCategoryId, String paperName) throws Exception {
ClassicPaperSetDTO setDTO = importClassicPaperDomainService.importFromWord(file, classicCategoryId, paperName);
return Result.success(setDTO);
}
/**
* 计算分值比例并填充到 DTO 的四个独立字段
* 优先取实体已存储的 scoreRatio JSON否则从 ExamScoreRatioConfig 计算

17
src/main/java/com/project/classicpaper/controller/ClassicPaperSetController.java

@ -9,6 +9,7 @@ import com.project.operation.annotation.OperationLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@Slf4j
@ -34,6 +35,22 @@ public class ClassicPaperSetController {
return classicPaperSetApplicationService.getDetail(param.getId(), param.getPaperId());
}
/**
* Word 文件导入历史套卷
* 解析 Word 文档 提取题目含图片上传 MinIO 创建套题组setCount=1
*
* @param file Word 文件.docx
* @param classicCategoryId 套题二级分类ID
* @param paperName 套卷名称可选为空时取 Word 文档标题
*/
@PostMapping("/importWord")
@OperationLog(module = "经典套题")
public Result<ClassicPaperSetDTO> importFromWord(@RequestParam("file") MultipartFile file,
@RequestParam("classicCategoryId") Long classicCategoryId,
@RequestParam(value = "paperName", required = false) String paperName) throws Exception {
return classicPaperSetApplicationService.importFromWord(file, classicCategoryId, paperName);
}
@PostMapping("/delete")
@OperationLog(module = "经典套题")
public Result<String> delete(Long setId) throws Exception {

5
src/main/java/com/project/classicpaper/domain/dto/ClassicPaperQuestionDTO.java

@ -3,6 +3,8 @@ package com.project.classicpaper.domain.dto;
import com.project.question.domain.entity.QuestionEntity;
import lombok.Data;
import java.util.List;
@Data
public class ClassicPaperQuestionDTO {
/** 关联记录ID(ClassicPaperQuestionEntity 主键),编辑暂存时用于定位要修改的排序/替换关系 */
@ -30,4 +32,7 @@ public class ClassicPaperQuestionDTO {
* 题目详情 QuestionEntity 关联查询
*/
private QuestionEntity.QuestionDetail questionDetail;
/** 题目中的图片URL列表(图片从题干和选项中提取,按出现顺序排列) */
private List<String> images;
}

3
src/main/java/com/project/classicpaper/domain/dto/ClassicPaperSetDTO.java

@ -50,6 +50,9 @@ public class ClassicPaperSetDTO extends BaseDTO {
private String statusText;
/** 导入原始文件MinIO路径(Word导入时记录) */
private String sourceFileName;
/** 单选题分值比例(数量为0时null,未设置时取配置默认值) */
private Integer singleChoiceScoreRatio;

4
src/main/java/com/project/classicpaper/domain/entity/ClassicPaperSetEntity.java

@ -84,4 +84,8 @@ public class ClassicPaperSetEntity extends BaseEntity {
@TableField("status")
@Comment("状态:0-生成中,1-草稿,2-已确认,3-已废弃")
private Integer status;
@Column(name = "source_file_name", columnDefinition = "varchar(500) comment '导入原始文件MinIO路径(Word导入时记录)'")
@TableField("source_file_name")
private String sourceFileName;
}

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

@ -14,10 +14,10 @@ public interface ImportClassicPaperDomainService {
* 解析 Word 提取题目含图片 创建套题组setCount=1
* 导入的题目 sourceType = Historical_Import(2)无知识点关联
*
* @param file Word 文件
* @param subLineId 子产品线ID
* @param paperName 套卷名称
* @param file Word 文件
* @param classicCategoryId 套题二级分类ID
* @param paperName 套卷名称为空时取 Word 标题
* @return 套题组详情
*/
ClassicPaperSetDTO importFromWord(MultipartFile file, Long subLineId, String paperName) throws Exception;
ClassicPaperSetDTO importFromWord(MultipartFile file, Long classicCategoryId, String paperName) throws Exception;
}

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

@ -1,6 +1,7 @@
package com.project.classicpaper.domain.service.impl;
import com.project.classicpaper.domain.dto.ClassicPaperDTO;
import com.project.classicpaper.domain.dto.ClassicPaperQuestionDTO;
import com.project.classicpaper.domain.dto.ClassicPaperSetDTO;
import com.project.classicpaper.domain.entity.ClassicPaperEntity;
import com.project.classicpaper.domain.entity.ClassicPaperQuestionEntity;
@ -14,7 +15,9 @@ 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.entity.QuestionEntity;
import com.project.question.domain.enums.QuestionSourceTypeEnum;
import com.project.question.domain.service.QuestionBaseService;
import com.project.question.domain.service.SaveQuestionDomainService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -23,9 +26,14 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* 历史套卷导入服务实现
@ -48,17 +56,32 @@ public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDo
@Autowired
private SaveQuestionDomainService saveQuestionDomainService;
@Autowired
private QuestionBaseService questionBaseService;
@Autowired
private MinIoUtils minIoUtils;
@Override
@Transactional(rollbackFor = Exception.class)
public ClassicPaperSetDTO importFromWord(MultipartFile file, Long subLineId, String paperName) throws Exception {
log.info(">>> [历史导入] 开始导入套卷, fileName={}, subLineId={}", file.getOriginalFilename(), subLineId);
public ClassicPaperSetDTO importFromWord(MultipartFile file, Long classicCategoryId, String paperName) throws Exception {
String originalFileName = file.getOriginalFilename();
log.info(">>> [历史导入] 开始导入套卷, fileName={}, classicCategoryId={}", originalFileName, classicCategoryId);
// 读取文件字节(inputStream 只能消费一次,先读入内存)
byte[] fileBytes = file.getBytes();
// 将原始 Word 文件上传到 MinIO 保存,用于后续溯源
String minioFileName = String.format("classicpaper/%s/%s.docx",
java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")),
java.util.UUID.randomUUID().toString().replace("-", ""));
minIoUtils.uploadFile(new ByteArrayInputStream(fileBytes), minioFileName);
String sourceFileName = minIoUtils.getPublicUrl(minioFileName);
log.info(">>> [历史导入] 原始文件已上传 MinIO: {}", sourceFileName);
// 1. 解析 Word 文件,提取题目列表(含图片上传 MinIO)
WordQuestionParser parser = new WordQuestionParser(minIoUtils);
WordQuestionParser.ParseResult parseResult = parser.parse(file.getInputStream());
WordQuestionParser.ParseResult parseResult = parser.parse(new ByteArrayInputStream(fileBytes));
List<WordQuestionParser.ParsedQuestion> parsedQuestions = parseResult.getQuestions();
if (parsedQuestions.isEmpty()) {
@ -67,25 +90,43 @@ public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDo
String name = paperName != null ? paperName : parseResult.getTitle();
// 统计各题型数量
int singleChoiceNum = 0, multipleChoiceNum = 0, trueFalseNum = 0, shortAnswerNum = 0;
for (WordQuestionParser.ParsedQuestion pq : parsedQuestions) {
switch (pq.getQuestionType()) {
case 0 -> singleChoiceNum++;
case 1 -> multipleChoiceNum++;
case 2 -> trueFalseNum++;
case 4 -> shortAnswerNum++;
}
}
// 2. 创建套题组(Word导入只有1个版本)
ClassicPaperSetEntity paperSet = new ClassicPaperSetEntity();
paperSet.setName(name);
paperSet.setSourceType(ClassicPaperSourceTypeEnum.WORD_IMPORT.getValue());
paperSet.setSubLineId(subLineId);
paperSet.setClassicCategoryId(classicCategoryId);
paperSet.setSourceFileName(sourceFileName);
paperSet.setDescription(name);
paperSet.setSetCount(1);
paperSet.setQuestionCount(parsedQuestions.size());
paperSet.setSingleChoiceNum(singleChoiceNum);
paperSet.setMultipleChoiceNum(multipleChoiceNum);
paperSet.setTrueFalseNum(trueFalseNum);
paperSet.setShortAnswerNum(shortAnswerNum);
paperSet.setStatus(ClassicPaperStatusEnum.DRAFT.getValue());
classicPaperSetBaseService.save(paperSet);
// 3. 创建套卷(版本1)
ClassicPaperEntity paper = new ClassicPaperEntity();
paper.setName(name);
paper.setSubLineId(subLineId);
paper.setDescription(name);
paper.setSourceType(ClassicPaperSourceTypeEnum.WORD_IMPORT.getValue());
paper.setStatus(ClassicPaperStatusEnum.DRAFT.getValue());
paper.setQuestionCount(parsedQuestions.size());
paper.setSetId(paperSet.getId());
paper.setSetIndex(1);
paper.setClassicCategoryId(classicCategoryId);
classicPaperBaseService.save(paper);
// 3. 保存题目并创建关联
@ -113,10 +154,25 @@ public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDo
detailDTO.setOptions(optionsMap.isEmpty() ? null : optionsMap);
detailDTO.setRightAnswer(parsed.getAnswer());
detailDTO.setAnalysis(parsed.getAnalysis());
// 简答题得分点
if (parsed.getScoringPoints() != null && !parsed.getScoringPoints().isEmpty()) {
List<QuestionDTO.ScoringPointDTO> scoringPoints = new ArrayList<>();
for (WordQuestionParser.ParsedScoringPoint sp : parsed.getScoringPoints()) {
QuestionDTO.ScoringPointDTO spDTO = new QuestionDTO.ScoringPointDTO();
spDTO.setIndex(sp.getIndex());
spDTO.setContent(sp.getContent());
scoringPoints.add(spDTO);
}
detailDTO.setScoringPoints(scoringPoints);
}
questionDTO.setQuestionDetailDTO(detailDTO);
// 保存题目
saveQuestionDomainService.save(questionDTO);
// 保存题目(直接调用 baseService 绕过 kpIdList 非空校验)
QuestionEntity entity = questionDTO.toEntity(QuestionEntity::new);
questionBaseService.save(entity);
questionDTO.setId(entity.getId());
// 创建套卷-题目关联
ClassicPaperQuestionEntity paperQuestion = new ClassicPaperQuestionEntity();
@ -131,7 +187,7 @@ public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDo
log.info(">>> [历史导入] 导入完成, setId={}, paperId={}, 题目数={}", paperSet.getId(), paper.getId(), paperQuestions.size());
// 5. 返回套题组详情
// 5. 构建返回 DTO(含题目列表)
ClassicPaperSetDTO setDTO = new ClassicPaperSetDTO();
setDTO.setId(paperSet.getId());
setDTO.setName(paperSet.getName());
@ -139,6 +195,79 @@ public class ImportClassicPaperDomainServiceImpl implements ImportClassicPaperDo
setDTO.setStatus(paperSet.getStatus());
setDTO.setSetCount(paperSet.getSetCount());
setDTO.setQuestionCount(paperSet.getQuestionCount());
setDTO.setSourceFileName(paperSet.getSourceFileName());
// 查询题目详情并构建 questionList
List<Long> questionIds = paperQuestions.stream()
.map(ClassicPaperQuestionEntity::getQuestionId)
.collect(java.util.stream.Collectors.toList());
List<QuestionEntity> savedQuestions = questionBaseService.listByIds(questionIds);
if (savedQuestions != null && !savedQuestions.isEmpty()) {
Map<Long, QuestionEntity> questionMap = savedQuestions.stream()
.collect(java.util.stream.Collectors.toMap(QuestionEntity::getId, q -> q));
List<ClassicPaperQuestionDTO> questionDTOList = paperQuestions.stream()
.map(pq -> {
ClassicPaperQuestionDTO qDto = new ClassicPaperQuestionDTO();
qDto.setId(pq.getId());
qDto.setPaperId(pq.getPaperId());
qDto.setQuestionId(pq.getQuestionId());
qDto.setSortOrder(pq.getSortOrder());
qDto.setQuestionType(pq.getQuestionType());
QuestionEntity qEntity = questionMap.get(pq.getQuestionId());
if (qEntity != null && qEntity.getQuestionDetail() != null) {
// 深拷贝 questionDetail,避免修改实体缓存
QuestionEntity.QuestionDetail detail = new QuestionEntity.QuestionDetail();
detail.setQuestionContent(qEntity.getQuestionDetail().getQuestionContent());
detail.setType(qEntity.getQuestionDetail().getType());
detail.setOptions(qEntity.getQuestionDetail().getOptions() != null
? new java.util.LinkedHashMap<>(qEntity.getQuestionDetail().getOptions()) : null);
detail.setRightAnswer(qEntity.getQuestionDetail().getRightAnswer());
detail.setAnalysis(qEntity.getQuestionDetail().getAnalysis());
detail.setScoringPoints(qEntity.getQuestionDetail().getScoringPoints());
// 提取图片URL并替换为占位符
List<String> images = new ArrayList<>();
if (detail.getQuestionContent() != null) {
detail.setQuestionContent(extractImages(detail.getQuestionContent(), images));
}
if (detail.getOptions() != null) {
Map<String, String> newOptions = new java.util.LinkedHashMap<>();
for (Map.Entry<String, String> entry : detail.getOptions().entrySet()) {
newOptions.put(entry.getKey(), extractImages(entry.getValue(), images));
}
detail.setOptions(newOptions);
}
qDto.setImages(images);
qDto.setQuestionDetail(detail);
}
return qDto;
})
.collect(java.util.stream.Collectors.toList());
setDTO.setQuestionList(questionDTOList);
}
return setDTO;
}
/**
* 从文本中提取 <img src='...'/> 标签的 URL替换为 img:N 占位符
* 按出现顺序收集到 images 列表中
*/
private String extractImages(String text, List<String> images) {
if (text == null || text.isBlank()) return text;
Pattern imgPattern = Pattern.compile("<img\\s+src=['\"]([^'\"]+)['\"]\\s*/>");
Matcher matcher = imgPattern.matcher(text);
StringBuilder sb = new StringBuilder();
int lastEnd = 0;
while (matcher.find()) {
sb.append(text, lastEnd, matcher.start());
String url = matcher.group(1);
int index = images.size();
images.add(url);
sb.append("【【img:").append(index).append("】】");
lastEnd = matcher.end();
}
sb.append(text.substring(lastEnd));
return sb.toString();
}
}

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

@ -20,10 +20,12 @@ import java.util.regex.Pattern;
@Slf4j
public class WordQuestionParser {
private static final Pattern QUESTION_PATTERN = Pattern.compile("^\\d+[..\\u3001]");
// 题号匹配:仅 ASCII 数字,排除 Unicode 圈码数字(①②③)
private static final Pattern QUESTION_PATTERN = Pattern.compile("^[0-9]+[..\\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 static final Pattern SCORING_POINT_PATTERN = Pattern.compile("^得分点[::\\u3001]\\s*(.*)");
private final MinIoUtils minIoUtils;
@ -31,6 +33,8 @@ public class WordQuestionParser {
private final List<ParsedQuestion> questions = new ArrayList<>();
private ParsedQuestion currentQuestion;
private int imageCounter = 0;
/** 是否正在解析得分点段落(得分点触发后,后续段落归入得分点内容) */
private boolean parsingScoringPoints = false;
public WordQuestionParser(MinIoUtils minIoUtils) {
this.minIoUtils = minIoUtils;
@ -76,10 +80,18 @@ public class WordQuestionParser {
// 题号匹配
if (QUESTION_PATTERN.matcher(text).find()) {
// 如果正在解析得分点,且题干极短,说明是得分点子项(如"1、两块纳米黑板")
// 归入最后一个得分点而非新题目
if (parsingScoringPoints && currentQuestion != null
&& !currentQuestion.scoringPoints.isEmpty()) {
accumulateScoringPointContent(text, currentQuestion);
return;
}
if (currentQuestion != null) {
validateAndSave(currentQuestion);
}
currentQuestion = new ParsedQuestion();
parsingScoringPoints = false; // 新题目,重置得分点状态
// 去掉题号
currentQuestion.stem = QUESTION_PATTERN.matcher(text).replaceFirst("").trim();
currentQuestion.questionType = detectQuestionType(currentQuestion.stem);
@ -90,6 +102,12 @@ public class WordQuestionParser {
return;
}
// 正在解析得分点段落:后续内容归入最后一个得分点
if (parsingScoringPoints) {
accumulateScoringPointContent(text, currentQuestion);
return;
}
// 答案行
Matcher answerMatcher = ANSWER_PATTERN.matcher(text);
if (answerMatcher.matches()) {
@ -107,6 +125,15 @@ public class WordQuestionParser {
return;
}
// 得分点行(格式:得分点:得分点1. xxx 得分点2. xxx)
Matcher scoringPointMatcher = SCORING_POINT_PATTERN.matcher(text);
if (scoringPointMatcher.matches()) {
parsingScoringPoints = true; // 后续段落归入得分点
String pointsContent = scoringPointMatcher.group(1).trim();
parseScoringPoints(pointsContent, currentQuestion);
return;
}
// 选项行
Matcher optionMatcher = OPTION_PATTERN.matcher(text);
if (optionMatcher.find()) {
@ -117,7 +144,7 @@ public class WordQuestionParser {
}
// 独立图片段落:合并到题干或最后一个选项
if (text.startsWith("[IMAGE:") && currentQuestion.answer == null) {
if (text.startsWith("<img") && currentQuestion.answer == null) {
if (currentQuestion.options.isEmpty()) {
currentQuestion.stem += text;
} else {
@ -158,7 +185,7 @@ public class WordQuestionParser {
// 上传到 MinIO
minIoUtils.uploadFile(new java.io.ByteArrayInputStream(imageData), fileName);
String imageUrl = minIoUtils.getPreviewUrl(fileName);
String imageUrl = minIoUtils.getPublicUrl(fileName);
// 替换为 HTML img 标签
sb.append(String.format("<img src='%s'/>", imageUrl));
@ -174,9 +201,10 @@ public class WordQuestionParser {
}
private String sanitizeText(String text) {
if (text == null) return "";
if (text == null) {
return "";
}
return text
.replace('、', ' ') // 顿号
.replace(' ', ' ') // 不间断空格
.replace('​', ' ') // 零宽空格
.replace('', ' ') // BOM
@ -188,10 +216,18 @@ public class WordQuestionParser {
* 检测题型
*/
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
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; // 默认单选
}
@ -200,11 +236,92 @@ public class WordQuestionParser {
log.warn(">>> [Word解析] 题目内容为空,跳过");
return;
}
// 简答题的得分点子段(如"智慧黑板"、"1)...")可能被误识别为题号
// 跳过题干过短的疑似子段
if (q.questionType == 4 && q.stem.length() < 10) {
log.warn(">>> [Word解析] 跳过疑似得分点子段: {}", q.stem);
return;
}
// 清理题型标签
q.stem = q.stem.replaceAll("\\[.*?题\\]", "").trim();
questions.add(q);
}
/**
* 将后续段落内容归入得分点得分点跨段落时使用
*/
private void accumulateScoringPointContent(String text, ParsedQuestion question) {
// 检查是否是新得分点标题(如:得分点3. xxx)
Matcher m = Pattern.compile("得分点([0-9]+)[..、]\\s*(.*)").matcher(text);
if (m.matches()) {
// 新得分点:通过 parseScoringPoints 处理(它会处理保存逻辑)
// 这里把单条"得分点N. 内容"包装后调用
parseScoringPoints(text, question);
return;
}
// 普通内容:追加到最后一个得分点
if (!question.scoringPoints.isEmpty()) {
ParsedScoringPoint last = question.scoringPoints.get(question.scoringPoints.size() - 1);
if (last.getContent() != null && !last.getContent().isEmpty()) {
last.setContent(last.getContent() + " " + text);
} else {
last.setContent(text);
}
}
// 如果还没有得分点(异常情况),忽略
}
/**
* 解析得分点行文本提取结构化得分点列表
* 格式得分点得分点1. 内容 得分点2. 内容
* "得分点N."作为分隔符每段内容归入对应序号
*/
private void parseScoringPoints(String text, ParsedQuestion question) {
// 以"得分点N."为分隔符分割
String[] parts = text.split("(?=得分点[0-9]+[..、])");
Integer currentIndex = null;
StringBuilder currentContent = new StringBuilder();
for (String part : parts) {
part = part.trim();
if (part.isBlank()) {
continue;
}
Matcher m = Pattern.compile("得分点([0-9]+)[..、]\\s*(.*)").matcher(part);
if (m.matches()) {
// 保存上一条得分点
if (currentIndex != null && currentContent.length() > 0) {
ParsedScoringPoint point = new ParsedScoringPoint();
point.setIndex(currentIndex);
point.setContent(currentContent.toString().trim());
question.scoringPoints.add(point);
}
currentIndex = Integer.parseInt(m.group(1));
String content = m.group(2).trim();
currentContent = new StringBuilder(content);
} else {
// 普通内容,追加到当前得分点
if (currentIndex != null) {
if (currentContent.length() > 0) {
currentContent.append(' ');
}
currentContent.append(part);
}
}
}
// 保存最后一条得分点
if (currentIndex != null && currentContent.length() > 0) {
ParsedScoringPoint point = new ParsedScoringPoint();
point.setIndex(currentIndex);
point.setContent(currentContent.toString().trim());
question.scoringPoints.add(point);
}
log.debug(">>> [Word解析] 得分点解析完成, count={}", question.scoringPoints.size());
}
// =============== 内部数据结构 ===============
@Data
@ -221,6 +338,8 @@ public class WordQuestionParser {
List<ParsedOption> options = new ArrayList<>();
String answer;
String analysis;
/** 简答题得分点列表 */
List<ParsedScoringPoint> scoringPoints = new ArrayList<>();
}
@Data
@ -233,4 +352,10 @@ public class WordQuestionParser {
this.content = content;
}
}
@Data
public static class ParsedScoringPoint {
private int index;
private String content;
}
}

13
src/main/java/com/project/information/utils/MinIoUtils.java

@ -123,6 +123,19 @@ public class MinIoUtils {
return "";
}
}
/**
* 生成公开访问链接桶为 public 时使用无需签名
* 格式{tempAccessFileUrl}/{bucket}/{objectName}
* @param objectName 对象名称含路径 classicpaper/20260629/uuid.png
* @return 公开访问URL
*/
public String getPublicUrl(String objectName) {
if (StrUtil.isBlank(minioProperties.getTempAccessFileUrl())) {
return "";
}
return minioProperties.getTempAccessFileUrl() + "/" + minioProperties.getBucket() + "/" + objectName;
}
public StreamInfoDTO mergeMultipartUpload(String objectName, String uploadId) {
try {
//单文件,不需要合并

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

@ -33,8 +33,8 @@ public class SaveDataExtractResultDomainServiceImpl implements SaveDataExtractRe
Long fileId = callback.getFileId();
Long informationId = callback.getInformationId();
log.info(">>> [数据服务回调] 收到子文件解析回调, fileId={}, informationId={}, status={}",
fileId, informationId, callback.getParseStatus());
log.info(">>> [数据服务回调] 收到子文件解析回调, fileId={}, informationId={}, status={} , parsedText={} , errorMsg={} ",
fileId, informationId, callback.getParseStatus() , callback.getParsedText() , callback.getErrorMsg());
// 1. 更新子文件解析状态
InformationFileEntity file = informationFileBaseService.getById(fileId);

7
src/main/java/com/project/operation/config/RedisKeyConstant.java

@ -0,0 +1,7 @@
package com.project.operation.config;
public class RedisKeyConstant {
public static final String OPERATION_LOG_SWITCH = "operation:log:switch";
}

22
src/main/java/com/project/operation/controller/OperationLogController.java

@ -3,18 +3,23 @@ package com.project.operation.controller;
import com.project.base.domain.result.PageResult;
import com.project.base.domain.result.Result;
import com.project.operation.application.impl.OperationLogApplicationService;
import com.project.operation.config.RedisKeyConstant;
import com.project.operation.domain.dto.OperationLogDTO;
import com.project.operation.domain.param.OperationLogParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/api/admin/operationLog")
public class OperationLogController {
@Autowired
private StringRedisTemplate redisTemplate; // 使用 RedisTemplate 操作 Redis
@Autowired
private OperationLogApplicationService operationLogApplicationService;
@ -27,4 +32,15 @@ public class OperationLogController {
public void export(HttpServletResponse response,OperationLogParam param){
operationLogApplicationService.export(response,param);
}
@PostMapping("/setSwitch")
public Result<String> setSwitch(@RequestParam Boolean isOpen) {
redisTemplate.opsForValue().set(RedisKeyConstant.OPERATION_LOG_SWITCH, String.valueOf(isOpen));
return Result.success("修改成功");
}
@GetMapping("/getSwitch")
public Result<String> getSwitch() {
return Result.success(redisTemplate.opsForValue().get(RedisKeyConstant.OPERATION_LOG_SWITCH));
}
}

Loading…
Cancel
Save