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
@@ -14,9 +14,12 @@ import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
import com.guo.learningprogresstracker.service.ReviewService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
@@ -133,4 +136,14 @@ public class ReviewController {
@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));
}
}
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@TableName(value = "review_mind_maps")
@Data
@@ -27,6 +28,33 @@ public class ReviewMindMapEntity extends BaseEntity implements Serializable {
@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;
}
@@ -0,0 +1,40 @@
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;
}
}
@@ -0,0 +1,15 @@
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;
}
}
@@ -10,6 +10,9 @@ import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
@@ -77,4 +80,9 @@ public interface ReviewService {
* 创建或更新指定任务的思维导图
*/
ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException;
/**
* 上传并解析指定任务的思维导图文件
*/
ReviewMindMapEntity uploadMindMap(String taskNum, MultipartFile file) throws NotFindEntitiesException, IOException;
}
@@ -0,0 +1,314 @@
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,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()))
@@ -0,0 +1,26 @@
alter table review_mind_maps
add column source_type varchar(30) not null default 'MANUAL' comment '来源类型:MANUAL/FILE';
alter table review_mind_maps
add column file_name varchar(255) null comment '原始文件名';
alter table review_mind_maps
add column file_path varchar(1024) null comment '服务端文件路径';
alter table review_mind_maps
add column file_format varchar(30) null comment '文件格式:XMIND/MARKDOWN/OPML/FREEMIND/TEXT';
alter table review_mind_maps
add column parsed_content mediumtext null comment '解析后的统一结构JSON';
alter table review_mind_maps
add column parse_status varchar(30) not null default 'SUCCESS' comment '解析状态:SUCCESS/FAILED';
alter table review_mind_maps
add column parse_error text null comment '解析错误';
alter table review_mind_maps
add column summary text null comment '解析摘要';
alter table review_mind_maps
add column last_parsed_time datetime null comment '最近解析时间';
@@ -12,6 +12,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.mapper.ReviewApplicationMapper;
import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
@@ -25,7 +27,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
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.util.List;
@@ -172,4 +177,41 @@ class ReviewServiceImplTest {
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));
}
}