feat: parse uploaded review mind maps

This commit is contained in:
2026-06-20 10:33:14 +08:00
parent f170e85297
commit 5f3f35c53b
9 changed files with 570 additions and 0 deletions
@@ -1,6 +1,7 @@
package com.guo.learningprogresstracker.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
import com.guo.learningprogresstracker.dto.request.CreateReviewApplicationRequest;
@@ -13,6 +14,8 @@ import com.guo.learningprogresstracker.entity.StudyReportsEntity;
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
import com.guo.learningprogresstracker.entity.TaskEntity;
import com.guo.learningprogresstracker.enums.ReviewApplicationStatusEnum;
import com.guo.learningprogresstracker.enums.ReviewMindMapFileFormatEnum;
import com.guo.learningprogresstracker.enums.ReviewMindMapParseStatusEnum;
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
import com.guo.learningprogresstracker.mapper.ReviewApplicationMapper;
import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
@@ -24,7 +27,14 @@ import com.guo.learningprogresstracker.service.ReviewService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -43,6 +53,8 @@ public class ReviewServiceImpl implements ReviewService {
private static final int MAX_LIMIT = 100;
private static final String RANDOM_MODE = "random";
private static final String DEFAULT_MIND_MAP_FORMAT = "TEXT";
private static final String MANUAL_SOURCE_TYPE = "MANUAL";
private static final String FILE_SOURCE_TYPE = "FILE";
private final StudyReportsMapper studyReportsMapper;
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
@@ -50,6 +62,7 @@ public class ReviewServiceImpl implements ReviewService {
private final TasksMapper tasksMapper;
private final ReviewApplicationMapper reviewApplicationMapper;
private final ReviewMindMapMapper reviewMindMapMapper;
private final MindMapFileParser mindMapFileParser = new MindMapFileParser(new ObjectMapper());
@Override
public List<ReviewFeedItem> getReviewFeed(int limit, String mode) {
@@ -208,6 +221,66 @@ public class ReviewServiceImpl implements ReviewService {
entity.setTitle(request.getTitle());
entity.setContent(request.getContent());
entity.setContentFormat(normalizeMindMapFormat(request.getContentFormat()));
entity.setSourceType(MANUAL_SOURCE_TYPE);
entity.setFileName(null);
entity.setFilePath(null);
entity.setFileFormat(null);
entity.setParsedContent(null);
entity.setSummary(null);
entity.setLastParsedTime(null);
entity.setParseStatus(ReviewMindMapParseStatusEnum.SUCCESS.getCode());
entity.setParseError(null);
if (create) {
reviewMindMapMapper.insert(entity);
} else {
reviewMindMapMapper.updateById(entity);
}
return entity;
}
@Override
public ReviewMindMapEntity uploadMindMap(String taskNum, MultipartFile file) throws NotFindEntitiesException, IOException {
ensureTaskExists(taskNum);
if (file == null || file.isEmpty()) {
throw new IOException("思维导图文件不可为空");
}
String originalFileName = StringUtils.cleanPath(
Optional.ofNullable(file.getOriginalFilename()).orElse("mind-map.txt"));
ReviewMindMapFileFormatEnum format = ReviewMindMapFileFormatEnum.fromFileName(originalFileName);
Path storedPath = storeMindMapFile(taskNum, originalFileName, file);
ReviewMindMapEntity entity = getMindMap(taskNum);
boolean create = entity == null;
if (create) {
entity = new ReviewMindMapEntity();
entity.setTaskNum(taskNum);
}
entity.setSourceType(FILE_SOURCE_TYPE);
entity.setFileName(originalFileName);
entity.setFilePath(storedPath.toString());
entity.setFileFormat(format.getCode());
entity.setContentFormat("JSON");
entity.setLastParsedTime(LocalDateTime.now());
try {
MindMapFileParser.ParseResult parseResult = mindMapFileParser.parse(file, format);
entity.setTitle(StringUtils.hasText(parseResult.title()) ? parseResult.title() : originalFileName);
entity.setContent(parseResult.outline());
entity.setParsedContent(parseResult.parsedContent());
entity.setSummary(parseResult.summary());
entity.setParseStatus(ReviewMindMapParseStatusEnum.SUCCESS.getCode());
entity.setParseError(null);
} catch (IOException e) {
entity.setTitle(originalFileName);
entity.setContent("");
entity.setParsedContent(null);
entity.setSummary(null);
entity.setParseStatus(ReviewMindMapParseStatusEnum.FAILED.getCode());
entity.setParseError(e.getMessage());
}
if (create) {
reviewMindMapMapper.insert(entity);
} else {
@@ -323,6 +396,17 @@ public class ReviewServiceImpl implements ReviewService {
return normalized;
}
private Path storeMindMapFile(String taskNum, String originalFileName, MultipartFile file) throws IOException {
Path directory = Paths.get("uploads", "review-mind-maps", taskNum);
Files.createDirectories(directory);
String storedFileName = UUID.randomUUID() + "-" + originalFileName.replaceAll("[\\\\/:*?\"<>|]", "_");
Path target = directory.resolve(storedFileName);
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
}
return target;
}
private void fillReviewTaskStats(List<ReviewTaskStats> statsList) {
Map<String, ReviewTaskStats> statsByTaskNum = statsList.stream()
.filter(item -> StringUtils.hasText(item.getTaskNum()))