From 3c3f682b2b1fe65f3cb3d300a3700d4ef752dcee Mon Sep 17 00:00:00 2001 From: cat_shark <1716967236@qq.com> Date: Sun, 5 Jul 2026 15:19:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AF=BC=E5=9B=BE):=20=E9=98=B2=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E7=94=9F=E6=88=90=20+=20=E5=A2=9E=E9=87=8F=E5=90=88?= =?UTF-8?q?=E5=B9=B6=20+=20mergeTrees=20=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MindMapTreeTool.mergeTrees() 按标准化标题合并新旧树节点 - StandardMindServiceImpl 防并发生成(ConcurrentHashMap 锁) - incrementalGenerate() 保留用户编辑节点 + 追加新节点 - regenerate 端点接受 mode 参数(full/incremental) --- .../controller/ReviewController.java | 7 +- .../service/StandardMindMapService.java | 5 ++ .../impl/StandardMindMapServiceImpl.java | 58 ++++++++++++++++- .../utils/MindMapTreeTool.java | 64 +++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java b/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java index c602266..a4cacb0 100644 --- a/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java +++ b/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java @@ -130,10 +130,15 @@ public class ReviewController { /** * 强制重新生成任务的标准思维导图 + * @param mode 生成模式:full(全量覆盖,默认)或 incremental(增量合并) */ @PostMapping("/standard-mind-map/{taskNum}/regenerate") public CommonResult regenerateStandardMindMap( - @PathVariable String taskNum) throws NotFindEntitiesException, OperationFailedException { + @PathVariable String taskNum, + @RequestParam(defaultValue = "full") String mode) throws NotFindEntitiesException, OperationFailedException { + if ("incremental".equals(mode)) { + return CommonResult.success(standardMindMapService.incrementalGenerate(taskNum)); + } return CommonResult.success(standardMindMapService.regenerate(taskNum)); } diff --git a/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java b/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java index c5b3039..734cf71 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java +++ b/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java @@ -23,6 +23,11 @@ public interface StandardMindMapService { */ ReviewStandardMindMapEntity regenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException; + /** + * 增量更新:保留用户编辑过的节点,合并最新生成的节点。 + */ + ReviewStandardMindMapEntity incrementalGenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException; + /** * 用户手动编辑标准思维导图(用缩进大纲文本替换)。 */ diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java index 8a44e82..e1c0c58 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java @@ -20,6 +20,8 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; /** @@ -44,9 +46,13 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { private static final String GENERATOR_BUILTIN = "BUILTIN"; private static final String GENERATOR_USER = "USER"; + private static final String GENERATOR_USER_MERGE = "USER_MERGE"; private static final String MATCH_STATUS_MATCHED = "MATCHED"; private static final String MATCH_STATUS_MISSED = "MISSED"; + /** 防并发生成:taskNum → 是否正在生成中 */ + private final ConcurrentHashMap generatingTasks = new ConcurrentHashMap<>(); + // ============ 查询与生成 ============ @Override @@ -59,7 +65,57 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { @Override public ReviewStandardMindMapEntity regenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException { ensureTaskExists(taskNum); - return doGenerate(taskNum); + // 防并发生成:同一任务正在生成时直接返回当前实体 + AtomicBoolean lock = generatingTasks.computeIfAbsent(taskNum, k -> new AtomicBoolean(false)); + if (!lock.compareAndSet(false, true)) { + ReviewStandardMindMapEntity existing = queryByTaskNum(taskNum); + log.warn("任务[{}]正在生成中,跳过重复请求", taskNum); + return existing; + } + try { + return doGenerate(taskNum); + } finally { + lock.set(false); + generatingTasks.remove(taskNum); + } + } + + @Override + @Transactional + public ReviewStandardMindMapEntity incrementalGenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException { + ensureTaskExists(taskNum); + ReviewStandardMindMapEntity existing = queryByTaskNum(taskNum); + if (existing == null) { + throw new NotFindEntitiesException("标准思维导图尚不存在,无法增量更新"); + } + // 防并发生成 + AtomicBoolean lock = generatingTasks.computeIfAbsent(taskNum, k -> new AtomicBoolean(false)); + if (!lock.compareAndSet(false, true)) { + log.warn("任务[{}]正在生成中,跳过重复增量请求", taskNum); + return existing; + } + try { + ReviewStandardMindMapEntity fresh = doGenerate(taskNum); + // 合并新旧树:保留用户编辑过的节点,追加新节点 + MindMapNode oldRoot = MindMapTreeTool.fromJson(existing.getContent(), objectMapper); + MindMapNode newRoot = MindMapTreeTool.fromJson(fresh.getContent(), objectMapper); + MindMapNode merged = MindMapTreeTool.mergeTrees(oldRoot, newRoot); + String mergedJson = MindMapTreeTool.toJson(merged, objectMapper); + String mergedOutline = MindMapTreeTool.toFullOutline(merged); + + existing.setContent(mergedJson); + existing.setOutline(mergedOutline); + existing.setTitle(merged.getTitle() != null ? merged.getTitle() : "思维导图"); + existing.setGenerator(GENERATOR_USER_MERGE); + existing.setGeneratorVersion(fresh.getGeneratorVersion()); + existing.setSummary("增量更新,共 " + MindMapTreeTool.countNodes(merged) + " 个节点"); + existing.setGeneratedTime(LocalDateTime.now()); + standardMindMapMapper.updateById(existing); + return existing; + } finally { + lock.set(false); + generatingTasks.remove(taskNum); + } } @Override diff --git a/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java b/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java index 5514d89..cd2e503 100644 --- a/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java +++ b/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java @@ -211,6 +211,70 @@ public class MindMapTreeTool { return copy; } + /** + * 合并新旧树:保留旧树中已有节点(用户编辑),追加新树中的新节点。 + * 按标准化标题匹配同级节点,旧树匹配到的节点优先保留。 + * + * @param oldRoot 旧树根(用户可能编辑过) + * @param newRoot 新树根(AI/BUILTIN 最新生成) + * @return 合并后的树 + */ + public static MindMapNode mergeTrees(MindMapNode oldRoot, MindMapNode newRoot) { + if (oldRoot == null) return newRoot; + if (newRoot == null) return oldRoot; + MindMapNode result = copyNodeShallow(oldRoot); + result.setChildren(mergeChildren(oldRoot.getChildren(), newRoot.getChildren())); + return result; + } + + /** 递归合并子节点列表 */ + private static List mergeChildren(List oldChildren, List newChildren) { + Map oldByNormalized = new LinkedHashMap<>(); + if (oldChildren != null) { + for (MindMapNode child : oldChildren) { + oldByNormalized.put(normalizeForMerge(child.getTitle()), child); + } + } + + List merged = new ArrayList<>(); + Set usedKeys = new HashSet<>(); + + if (newChildren != null) { + for (MindMapNode newNode : newChildren) { + String key = normalizeForMerge(newNode.getTitle()); + MindMapNode oldNode = oldByNormalized.get(key); + if (oldNode != null) { + // 旧节点存在:保留旧节点标题,递归合并子节点 + MindMapNode kept = copyNodeShallow(oldNode); + kept.setChildren(mergeChildren(oldNode.getChildren(), newNode.getChildren())); + merged.add(kept); + usedKeys.add(key); + } else { + // 新节点:直接添加 + merged.add(newNode); + } + } + } + + // 添加旧树中有但新树中没有的节点(用户添加的额外节点) + if (oldChildren != null) { + for (MindMapNode oldNode : oldChildren) { + if (!usedKeys.contains(normalizeForMerge(oldNode.getTitle()))) { + merged.add(oldNode); + } + } + } + + return merged; + } + + private static String normalizeForMerge(String s) { + if (s == null) return ""; + return s.replaceAll("[\\s 、,。!?:;()\\[\\]{},.!?:;()\\-—/\\\\|]", "") + .toLowerCase(Locale.ROOT) + .trim(); + } + /** 序列化整棵树为 JSON */ public static String toJson(MindMapNode root, ObjectMapper mapper) { try {