Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76742ed65a | |||
| c8c0773a6a | |||
| 5794bddc6c | |||
| b3c7d52174 | |||
| a9a5c888e3 | |||
| 4cf2827a74 |
@@ -4,9 +4,7 @@ import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
|||||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||||
import com.guo.learningprogresstracker.dto.request.RecallCompareRequest;
|
import com.guo.learningprogresstracker.dto.request.RecallCompareRequest;
|
||||||
import com.guo.learningprogresstracker.dto.request.UpdateStandardMindMapRequest;
|
import com.guo.learningprogresstracker.dto.request.UpdateStandardMindMapRequest;
|
||||||
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
|
||||||
import com.guo.learningprogresstracker.entity.CommonResult;
|
import com.guo.learningprogresstracker.entity.CommonResult;
|
||||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
||||||
import com.guo.learningprogresstracker.entity.ReviewStandardMindMapEntity;
|
import com.guo.learningprogresstracker.entity.ReviewStandardMindMapEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||||
@@ -17,12 +15,9 @@ import com.guo.learningprogresstracker.service.ReviewService;
|
|||||||
import com.guo.learningprogresstracker.service.StandardMindMapService;
|
import com.guo.learningprogresstracker.service.StandardMindMapService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -89,34 +84,6 @@ public class ReviewController {
|
|||||||
return CommonResult.success(reviewService.getFragmentDetail(id));
|
return CommonResult.success(reviewService.getFragmentDetail(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定任务的思维导图
|
|
||||||
*/
|
|
||||||
@GetMapping("/mind-map/{taskNum}")
|
|
||||||
public CommonResult<ReviewMindMapEntity> getMindMap(@PathVariable String taskNum) throws NotFindEntitiesException {
|
|
||||||
return CommonResult.success(reviewService.getMindMap(taskNum));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建或更新指定任务的思维导图
|
|
||||||
*/
|
|
||||||
@PutMapping("/mind-map/{taskNum}")
|
|
||||||
public CommonResult<ReviewMindMapEntity> upsertMindMap(
|
|
||||||
@PathVariable String taskNum,
|
|
||||||
@Valid @RequestBody UpsertReviewMindMapRequest request) throws NotFindEntitiesException {
|
|
||||||
return CommonResult.success(reviewService.upsertMindMap(taskNum, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传并解析指定任务的思维导图文件
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/mind-map/{taskNum}/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
||||||
public CommonResult<ReviewMindMapEntity> uploadMindMap(
|
|
||||||
@PathVariable String taskNum,
|
|
||||||
@RequestParam("file") MultipartFile file) throws NotFindEntitiesException, IOException {
|
|
||||||
return CommonResult.success(reviewService.uploadMindMap(taskNum, file));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 标准思维导图与回忆对比 ============
|
// ============ 标准思维导图与回忆对比 ============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
package com.guo.learningprogresstracker.dto.request;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class UpsertReviewMindMapRequest {
|
|
||||||
|
|
||||||
@NotBlank(message = "思维导图标题不可为空")
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
@NotBlank(message = "思维导图内容不可为空")
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
private String contentFormat;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.guo.learningprogresstracker.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@TableName(value = "review_mind_maps")
|
|
||||||
@Data
|
|
||||||
public class ReviewMindMapEntity extends BaseEntity implements Serializable {
|
|
||||||
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Integer id;
|
|
||||||
|
|
||||||
@TableField(value = "task_num")
|
|
||||||
private String taskNum;
|
|
||||||
|
|
||||||
@TableField(value = "title")
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
@TableField(value = "content")
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
@TableField(value = "content_format")
|
|
||||||
private String contentFormat;
|
|
||||||
|
|
||||||
@TableField(value = "source_type")
|
|
||||||
private String sourceType;
|
|
||||||
|
|
||||||
@TableField(value = "file_name")
|
|
||||||
private String fileName;
|
|
||||||
|
|
||||||
@TableField(value = "file_path")
|
|
||||||
private String filePath;
|
|
||||||
|
|
||||||
@TableField(value = "file_format")
|
|
||||||
private String fileFormat;
|
|
||||||
|
|
||||||
@TableField(value = "parsed_content")
|
|
||||||
private String parsedContent;
|
|
||||||
|
|
||||||
@TableField(value = "parse_status")
|
|
||||||
private String parseStatus;
|
|
||||||
|
|
||||||
@TableField(value = "parse_error")
|
|
||||||
private String parseError;
|
|
||||||
|
|
||||||
@TableField(value = "summary")
|
|
||||||
private String summary;
|
|
||||||
|
|
||||||
@TableField(value = "last_parsed_time")
|
|
||||||
private LocalDateTime lastParsedTime;
|
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.guo.learningprogresstracker.enums;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
public enum ReviewMindMapFileFormatEnum {
|
|
||||||
XMIND("XMIND"),
|
|
||||||
MARKDOWN("MARKDOWN"),
|
|
||||||
OPML("OPML"),
|
|
||||||
FREEMIND("FREEMIND"),
|
|
||||||
TEXT("TEXT");
|
|
||||||
|
|
||||||
private final String code;
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ReviewMindMapFileFormatEnum fromFileName(String fileName) {
|
|
||||||
String normalized = fileName == null ? "" : fileName.toLowerCase(Locale.ROOT);
|
|
||||||
if (normalized.endsWith(".xmind")) {
|
|
||||||
return XMIND;
|
|
||||||
}
|
|
||||||
if (normalized.endsWith(".md") || normalized.endsWith(".markdown")) {
|
|
||||||
return MARKDOWN;
|
|
||||||
}
|
|
||||||
if (normalized.endsWith(".opml")) {
|
|
||||||
return OPML;
|
|
||||||
}
|
|
||||||
if (normalized.endsWith(".mm")) {
|
|
||||||
return FREEMIND;
|
|
||||||
}
|
|
||||||
if (normalized.endsWith(".txt")) {
|
|
||||||
return TEXT;
|
|
||||||
}
|
|
||||||
return TEXT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.guo.learningprogresstracker.enums;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
public enum ReviewMindMapParseStatusEnum {
|
|
||||||
SUCCESS("SUCCESS"),
|
|
||||||
FAILED("FAILED");
|
|
||||||
|
|
||||||
private final String code;
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.guo.learningprogresstracker.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface ReviewMindMapMapper extends BaseMapper<ReviewMindMapEntity> {
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,9 @@ package com.guo.learningprogresstracker.service;
|
|||||||
|
|
||||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||||
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -47,19 +42,4 @@ public interface ReviewService {
|
|||||||
* 获取残片详情
|
* 获取残片详情
|
||||||
*/
|
*/
|
||||||
StudyReportFragmentsEntity getFragmentDetail(int id) throws NotFindEntitiesException;
|
StudyReportFragmentsEntity getFragmentDetail(int id) throws NotFindEntitiesException;
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定任务的思维导图
|
|
||||||
*/
|
|
||||||
ReviewMindMapEntity getMindMap(String taskNum) throws NotFindEntitiesException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建或更新指定任务的思维导图
|
|
||||||
*/
|
|
||||||
ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传并解析指定任务的思维导图文件
|
|
||||||
*/
|
|
||||||
ReviewMindMapEntity uploadMindMap(String taskNum, MultipartFile file) throws NotFindEntitiesException, IOException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
package com.guo.learningprogresstracker.service.impl;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.guo.learningprogresstracker.enums.ReviewMindMapFileFormatEnum;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
import org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Element;
|
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
|
|
||||||
class MindMapFileParser {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
MindMapFileParser(ObjectMapper objectMapper) {
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
ParseResult parse(MultipartFile file, ReviewMindMapFileFormatEnum format) throws IOException {
|
|
||||||
byte[] bytes = file.getBytes();
|
|
||||||
Map<String, Object> root = switch (format) {
|
|
||||||
case XMIND -> parseXMind(bytes);
|
|
||||||
case MARKDOWN -> parseMarkdown(new String(bytes, StandardCharsets.UTF_8), format);
|
|
||||||
case OPML -> parseOutlineXml(bytes, "text", format);
|
|
||||||
case FREEMIND -> parseOutlineXml(bytes, "TEXT", format);
|
|
||||||
case TEXT -> parseMarkdown(new String(bytes, StandardCharsets.UTF_8), format);
|
|
||||||
};
|
|
||||||
String title = String.valueOf(root.getOrDefault("title", file.getOriginalFilename()));
|
|
||||||
String outline = toOutline(root);
|
|
||||||
int nodeCount = countNodes(root);
|
|
||||||
int depth = maxDepth(root);
|
|
||||||
String summary = "共解析 " + nodeCount + " 个节点,最大层级 " + depth + "。";
|
|
||||||
return new ParseResult(title, format.getCode(), objectMapper.writeValueAsString(root), outline, summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseXMind(byte[] bytes) throws IOException {
|
|
||||||
byte[] contentJson = null;
|
|
||||||
byte[] contentXml = null;
|
|
||||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(bytes))) {
|
|
||||||
ZipEntry entry;
|
|
||||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
|
||||||
if ("content.json".equals(entry.getName())) {
|
|
||||||
contentJson = zipInputStream.readAllBytes();
|
|
||||||
} else if ("content.xml".equals(entry.getName())) {
|
|
||||||
contentXml = zipInputStream.readAllBytes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (contentJson != null) {
|
|
||||||
return parseXMindJson(contentJson);
|
|
||||||
}
|
|
||||||
if (contentXml != null) {
|
|
||||||
return parseXMindXml(contentXml);
|
|
||||||
}
|
|
||||||
throw new IOException("未找到 XMind 内容文件");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseXMindJson(byte[] bytes) throws IOException {
|
|
||||||
JsonNode sheets = objectMapper.readTree(bytes);
|
|
||||||
JsonNode sheet = sheets.isArray() && !sheets.isEmpty() ? sheets.get(0) : sheets;
|
|
||||||
JsonNode rootTopic = sheet.path("rootTopic");
|
|
||||||
Map<String, Object> root = node(rootTopic.path("title").asText("XMind 导图"));
|
|
||||||
root.put("sourceFormat", ReviewMindMapFileFormatEnum.XMIND.getCode());
|
|
||||||
root.put("nodes", parseXMindTopicChildren(rootTopic));
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Map<String, Object>> parseXMindTopicChildren(JsonNode topic) {
|
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
|
||||||
JsonNode attached = topic.path("children").path("attached");
|
|
||||||
if (!attached.isArray()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
for (JsonNode child : attached) {
|
|
||||||
Map<String, Object> item = node(child.path("title").asText("未命名节点"));
|
|
||||||
item.put("children", parseXMindTopicChildren(child));
|
|
||||||
result.add(item);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseXMindXml(byte[] bytes) throws IOException {
|
|
||||||
Document document = xmlDocument(bytes);
|
|
||||||
Element sheet = firstElementByName(document.getDocumentElement(), "sheet");
|
|
||||||
Element topic = sheet == null ? firstElementByName(document.getDocumentElement(), "topic") : firstElementByName(sheet, "topic");
|
|
||||||
if (topic == null) {
|
|
||||||
throw new IOException("未找到 XMind 根节点");
|
|
||||||
}
|
|
||||||
Map<String, Object> root = parseTopicElement(topic);
|
|
||||||
root.put("sourceFormat", ReviewMindMapFileFormatEnum.XMIND.getCode());
|
|
||||||
root.put("nodes", root.remove("children"));
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseMarkdown(String content, ReviewMindMapFileFormatEnum format) {
|
|
||||||
Map<String, Object> root = node("导图大纲");
|
|
||||||
root.put("sourceFormat", format.getCode());
|
|
||||||
List<Map<String, Object>> roots = new ArrayList<>();
|
|
||||||
root.put("nodes", roots);
|
|
||||||
|
|
||||||
ArrayDeque<StackItem> stack = new ArrayDeque<>();
|
|
||||||
stack.push(new StackItem(0, roots));
|
|
||||||
for (String rawLine : content.split("\\R")) {
|
|
||||||
String line = rawLine.stripTrailing();
|
|
||||||
if (line.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int level = markdownLevel(line);
|
|
||||||
String title = markdownTitle(line);
|
|
||||||
Map<String, Object> item = node(title);
|
|
||||||
while (stack.peek().level >= level) {
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
stack.peek().children.add(item);
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Map<String, Object>> children = (List<Map<String, Object>>) item.get("children");
|
|
||||||
stack.push(new StackItem(level, children));
|
|
||||||
}
|
|
||||||
if (!roots.isEmpty()) {
|
|
||||||
root.put("title", roots.get(0).get("title"));
|
|
||||||
}
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseOutlineXml(byte[] bytes, String titleAttribute, ReviewMindMapFileFormatEnum format) throws IOException {
|
|
||||||
Document document = xmlDocument(bytes);
|
|
||||||
Element rootElement = document.getDocumentElement();
|
|
||||||
Element first = firstElementByName(rootElement, "outline");
|
|
||||||
if (first == null) {
|
|
||||||
first = firstElementByName(rootElement, "node");
|
|
||||||
titleAttribute = "TEXT";
|
|
||||||
}
|
|
||||||
if (first == null) {
|
|
||||||
throw new IOException("未找到导图节点");
|
|
||||||
}
|
|
||||||
Map<String, Object> root = parseOutlineElement(first, titleAttribute);
|
|
||||||
root.put("sourceFormat", format.getCode());
|
|
||||||
root.put("nodes", root.remove("children"));
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Document xmlDocument(byte[] bytes) throws IOException {
|
|
||||||
try {
|
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|
||||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
|
||||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
|
||||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
|
||||||
factory.setNamespaceAware(false);
|
|
||||||
return factory.newDocumentBuilder().parse(new ByteArrayInputStream(bytes));
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IOException("导图 XML 解析失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseTopicElement(Element element) {
|
|
||||||
Map<String, Object> item = node(textOfFirstChild(element, "title", "未命名节点"));
|
|
||||||
item.put("children", childTopicElements(element).stream()
|
|
||||||
.map(this::parseTopicElement)
|
|
||||||
.toList());
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> parseOutlineElement(Element element, String titleAttribute) {
|
|
||||||
String title = element.getAttribute(titleAttribute);
|
|
||||||
if (title == null || title.isBlank()) {
|
|
||||||
title = element.getAttribute("text");
|
|
||||||
}
|
|
||||||
if (title == null || title.isBlank()) {
|
|
||||||
title = "未命名节点";
|
|
||||||
}
|
|
||||||
Map<String, Object> item = node(title);
|
|
||||||
List<Map<String, Object>> children = directChildElements(element).stream()
|
|
||||||
.filter(child -> "outline".equalsIgnoreCase(child.getTagName()) || "node".equalsIgnoreCase(child.getTagName()))
|
|
||||||
.map(child -> parseOutlineElement(child, titleAttribute))
|
|
||||||
.toList();
|
|
||||||
item.put("children", children);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> node(String title) {
|
|
||||||
Map<String, Object> node = new LinkedHashMap<>();
|
|
||||||
node.put("title", title);
|
|
||||||
node.put("notes", "");
|
|
||||||
node.put("children", new ArrayList<Map<String, Object>>());
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int markdownLevel(String line) {
|
|
||||||
String trimmed = line.stripLeading();
|
|
||||||
if (trimmed.startsWith("#")) {
|
|
||||||
int count = 0;
|
|
||||||
while (count < trimmed.length() && trimmed.charAt(count) == '#') {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
int indent = line.length() - trimmed.length();
|
|
||||||
return 1 + indent / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String markdownTitle(String line) {
|
|
||||||
return line.strip()
|
|
||||||
.replaceFirst("^#{1,6}\\s*", "")
|
|
||||||
.replaceFirst("^[-*+]\\s*", "")
|
|
||||||
.replaceFirst("^\\d+\\.\\s*", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Element> childTopicElements(Element element) {
|
|
||||||
return directChildElements(element).stream()
|
|
||||||
.flatMap(child -> {
|
|
||||||
if ("topic".equalsIgnoreCase(child.getTagName())) {
|
|
||||||
return List.of(child).stream();
|
|
||||||
}
|
|
||||||
return childTopicElements(child).stream();
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Element firstElementByName(Element element, String name) {
|
|
||||||
if (name.equalsIgnoreCase(element.getTagName())) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
NodeList nodes = element.getChildNodes();
|
|
||||||
for (int i = 0; i < nodes.getLength(); i++) {
|
|
||||||
if (nodes.item(i) instanceof Element child) {
|
|
||||||
Element found = firstElementByName(child, name);
|
|
||||||
if (found != null) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Element> directChildElements(Element element) {
|
|
||||||
List<Element> result = new ArrayList<>();
|
|
||||||
NodeList nodes = element.getChildNodes();
|
|
||||||
for (int i = 0; i < nodes.getLength(); i++) {
|
|
||||||
if (nodes.item(i) instanceof Element child) {
|
|
||||||
result.add(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String textOfFirstChild(Element element, String name, String fallback) {
|
|
||||||
for (Element child : directChildElements(element)) {
|
|
||||||
if (name.equalsIgnoreCase(child.getTagName())) {
|
|
||||||
return child.getTextContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toOutline(Map<String, Object> root) {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
appendOutline(builder, root, 0);
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void appendOutline(StringBuilder builder, Map<String, Object> node, int level) {
|
|
||||||
builder.append(" ".repeat(level)).append("- ").append(node.get("title")).append('\n');
|
|
||||||
Object children = node.get(level == 0 && node.containsKey("nodes") ? "nodes" : "children");
|
|
||||||
if (children instanceof List<?> list) {
|
|
||||||
for (Object child : list) {
|
|
||||||
appendOutline(builder, (Map<String, Object>) child, level + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private int countNodes(Map<String, Object> node) {
|
|
||||||
int count = 1;
|
|
||||||
Object children = node.get(node.containsKey("nodes") ? "nodes" : "children");
|
|
||||||
if (children instanceof List<?> list) {
|
|
||||||
for (Object child : list) {
|
|
||||||
count += countNodes((Map<String, Object>) child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private int maxDepth(Map<String, Object> node) {
|
|
||||||
Object children = node.get(node.containsKey("nodes") ? "nodes" : "children");
|
|
||||||
if (!(children instanceof List<?> list) || list.isEmpty()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
int max = 1;
|
|
||||||
for (Object child : list) {
|
|
||||||
max = Math.max(max, 1 + maxDepth((Map<String, Object>) child));
|
|
||||||
}
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
record ParseResult(String title, String fileFormat, String parsedContent, String outline, String summary) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record StackItem(int level, List<Map<String, Object>> children) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
package com.guo.learningprogresstracker.service.impl;
|
package com.guo.learningprogresstracker.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||||
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||||
import com.guo.learningprogresstracker.enums.ReviewMindMapFileFormatEnum;
|
|
||||||
import com.guo.learningprogresstracker.enums.ReviewMindMapParseStatusEnum;
|
|
||||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||||
import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
|
|
||||||
import com.guo.learningprogresstracker.mapper.ReviewRecallRecordMapper;
|
import com.guo.learningprogresstracker.mapper.ReviewRecallRecordMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||||
@@ -24,14 +18,7 @@ import com.guo.learningprogresstracker.service.ReviewService;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
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.DayOfWeek;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -51,17 +38,12 @@ public class ReviewServiceImpl implements ReviewService {
|
|||||||
private static final String RANDOM_MODE = "random";
|
private static final String RANDOM_MODE = "random";
|
||||||
private static final String SMART_MODE = "smart";
|
private static final String SMART_MODE = "smart";
|
||||||
private static final int SMART_CANDIDATE_MULTIPLIER = 5;
|
private static final int SMART_CANDIDATE_MULTIPLIER = 5;
|
||||||
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 StudyReportsMapper studyReportsMapper;
|
||||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||||
private final StudySessionsMapper studySessionsMapper;
|
private final StudySessionsMapper studySessionsMapper;
|
||||||
private final TasksMapper tasksMapper;
|
private final TasksMapper tasksMapper;
|
||||||
private final ReviewMindMapMapper reviewMindMapMapper;
|
|
||||||
private final ReviewRecallRecordMapper reviewRecallRecordMapper;
|
private final ReviewRecallRecordMapper reviewRecallRecordMapper;
|
||||||
private final MindMapFileParser mindMapFileParser = new MindMapFileParser(new ObjectMapper());
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ReviewFeedItem> getReviewFeed(int limit, String mode) {
|
public List<ReviewFeedItem> getReviewFeed(int limit, String mode) {
|
||||||
@@ -251,95 +233,6 @@ public class ReviewServiceImpl implements ReviewService {
|
|||||||
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
|
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReviewMindMapEntity getMindMap(String taskNum) throws NotFindEntitiesException {
|
|
||||||
ensureTaskExists(taskNum);
|
|
||||||
return reviewMindMapMapper.selectOne(
|
|
||||||
Wrappers.<ReviewMindMapEntity>lambdaQuery()
|
|
||||||
.eq(ReviewMindMapEntity::getTaskNum, taskNum)
|
|
||||||
.last("LIMIT 1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException {
|
|
||||||
ensureTaskExists(taskNum);
|
|
||||||
ReviewMindMapEntity entity = getMindMap(taskNum);
|
|
||||||
boolean create = entity == null;
|
|
||||||
if (create) {
|
|
||||||
entity = new ReviewMindMapEntity();
|
|
||||||
entity.setTaskNum(taskNum);
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
reviewMindMapMapper.updateById(entity);
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将报告和残片合并转换为 ReviewFeedItem 列表,按创建时间倒序排列
|
* 将报告和残片合并转换为 ReviewFeedItem 列表,按创建时间倒序排列
|
||||||
*/
|
*/
|
||||||
@@ -432,28 +325,6 @@ public class ReviewServiceImpl implements ReviewService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeMindMapFormat(String contentFormat) {
|
|
||||||
if (!StringUtils.hasText(contentFormat)) {
|
|
||||||
return DEFAULT_MIND_MAP_FORMAT;
|
|
||||||
}
|
|
||||||
String normalized = contentFormat.trim().toUpperCase(Locale.ROOT);
|
|
||||||
if (!Set.of("TEXT", "MERMAID", "JSON").contains(normalized)) {
|
|
||||||
return DEFAULT_MIND_MAP_FORMAT;
|
|
||||||
}
|
|
||||||
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) {
|
private void fillReviewTaskStats(List<ReviewTaskStats> statsList) {
|
||||||
Map<String, ReviewTaskStats> statsByTaskNum = statsList.stream()
|
Map<String, ReviewTaskStats> statsByTaskNum = statsList.stream()
|
||||||
.filter(item -> StringUtils.hasText(item.getTaskNum()))
|
.filter(item -> StringUtils.hasText(item.getTaskNum()))
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ package com.guo.learningprogresstracker.service.impl;
|
|||||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||||
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
|
||||||
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||||
import com.guo.learningprogresstracker.enums.ReviewMindMapFileFormatEnum;
|
|
||||||
import com.guo.learningprogresstracker.enums.ReviewMindMapParseStatusEnum;
|
|
||||||
import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
|
|
||||||
import com.guo.learningprogresstracker.mapper.ReviewRecallRecordMapper;
|
import com.guo.learningprogresstracker.mapper.ReviewRecallRecordMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||||
@@ -25,17 +20,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -48,7 +38,6 @@ class ReviewServiceImplTest {
|
|||||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudyReportFragmentsEntity.class);
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudyReportFragmentsEntity.class);
|
||||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudySessionsEntity.class);
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudySessionsEntity.class);
|
||||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), TaskEntity.class);
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), TaskEntity.class);
|
||||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), ReviewMindMapEntity.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
@@ -66,9 +55,6 @@ class ReviewServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private TasksMapper tasksMapper;
|
private TasksMapper tasksMapper;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ReviewMindMapMapper reviewMindMapMapper;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ReviewRecallRecordMapper reviewRecallRecordMapper;
|
private ReviewRecallRecordMapper reviewRecallRecordMapper;
|
||||||
|
|
||||||
@@ -163,88 +149,4 @@ class ReviewServiceImplTest {
|
|||||||
assertEquals("Task", items.get(0).getTaskName());
|
assertEquals("Task", items.get(0).getTaskName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void upsertMindMap_shouldCreateWhenMissing() throws Exception {
|
|
||||||
when(tasksMapper.exists(any())).thenReturn(true);
|
|
||||||
when(reviewMindMapMapper.selectOne(any())).thenReturn(null);
|
|
||||||
when(reviewMindMapMapper.insert(any())).thenAnswer(invocation -> {
|
|
||||||
ReviewMindMapEntity entity = invocation.getArgument(0);
|
|
||||||
entity.setId(3);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
UpsertReviewMindMapRequest request = new UpsertReviewMindMapRequest();
|
|
||||||
request.setTitle("Map");
|
|
||||||
request.setContent("root");
|
|
||||||
request.setContentFormat("mermaid");
|
|
||||||
|
|
||||||
ReviewMindMapEntity entity = reviewService.upsertMindMap("T1", request);
|
|
||||||
|
|
||||||
assertNotNull(entity.getId());
|
|
||||||
assertEquals("T1", entity.getTaskNum());
|
|
||||||
assertEquals("MERMAID", entity.getContentFormat());
|
|
||||||
verify(reviewMindMapMapper).insert(any(ReviewMindMapEntity.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void upsertMindMap_shouldUpdateExisting() throws Exception {
|
|
||||||
ReviewMindMapEntity existing = new ReviewMindMapEntity();
|
|
||||||
existing.setId(5);
|
|
||||||
existing.setTaskNum("T1");
|
|
||||||
existing.setTitle("Old");
|
|
||||||
existing.setContent("old");
|
|
||||||
existing.setContentFormat("TEXT");
|
|
||||||
|
|
||||||
when(tasksMapper.exists(any())).thenReturn(true);
|
|
||||||
when(reviewMindMapMapper.selectOne(any())).thenReturn(existing);
|
|
||||||
|
|
||||||
UpsertReviewMindMapRequest request = new UpsertReviewMindMapRequest();
|
|
||||||
request.setTitle("New");
|
|
||||||
request.setContent("new");
|
|
||||||
request.setContentFormat("json");
|
|
||||||
|
|
||||||
ReviewMindMapEntity entity = reviewService.upsertMindMap("T1", request);
|
|
||||||
|
|
||||||
assertEquals(5, entity.getId());
|
|
||||||
assertEquals("New", entity.getTitle());
|
|
||||||
assertEquals("JSON", entity.getContentFormat());
|
|
||||||
verify(reviewMindMapMapper).updateById(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void uploadMindMap_shouldParseMarkdownFile() throws Exception {
|
|
||||||
when(tasksMapper.exists(any())).thenReturn(true);
|
|
||||||
when(reviewMindMapMapper.selectOne(any())).thenReturn(null);
|
|
||||||
when(reviewMindMapMapper.insert(any())).thenAnswer(invocation -> {
|
|
||||||
ReviewMindMapEntity entity = invocation.getArgument(0);
|
|
||||||
entity.setId(9);
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
MockMultipartFile file = new MockMultipartFile(
|
|
||||||
"file",
|
|
||||||
"java-review.md",
|
|
||||||
"text/markdown",
|
|
||||||
"# Java 并发\n## 线程基础\n- RUNNABLE\n".getBytes());
|
|
||||||
|
|
||||||
ReviewMindMapEntity entity = reviewService.uploadMindMap("T1", file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
assertEquals(9, entity.getId());
|
|
||||||
assertEquals("FILE", entity.getSourceType());
|
|
||||||
assertEquals(ReviewMindMapFileFormatEnum.MARKDOWN.getCode(), entity.getFileFormat());
|
|
||||||
assertEquals(ReviewMindMapParseStatusEnum.SUCCESS.getCode(), entity.getParseStatus());
|
|
||||||
assertEquals("JSON", entity.getContentFormat());
|
|
||||||
assertNotNull(entity.getParsedContent());
|
|
||||||
assertEquals(true, entity.getParsedContent().contains("线程基础"));
|
|
||||||
assertEquals(true, entity.getContent().contains("RUNNABLE"));
|
|
||||||
} finally {
|
|
||||||
if (entity.getFilePath() != null) {
|
|
||||||
Path storedFile = Path.of(entity.getFilePath());
|
|
||||||
Files.deleteIfExists(storedFile);
|
|
||||||
Files.deleteIfExists(storedFile.getParent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verify(reviewMindMapMapper).insert(any(ReviewMindMapEntity.class));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user