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