feat(导图): 防并发生成 + 增量合并 + mergeTrees 工具

- MindMapTreeTool.mergeTrees() 按标准化标题合并新旧树节点
- StandardMindServiceImpl 防并发生成(ConcurrentHashMap 锁)
- incrementalGenerate() 保留用户编辑节点 + 追加新节点
- regenerate 端点接受 mode 参数(full/incremental)
This commit is contained in:
2026-07-05 15:19:03 +08:00
parent a636e516d1
commit 3c3f682b2b
4 changed files with 132 additions and 2 deletions
@@ -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<MindMapNode> mergeChildren(List<MindMapNode> oldChildren, List<MindMapNode> newChildren) {
Map<String, MindMapNode> oldByNormalized = new LinkedHashMap<>();
if (oldChildren != null) {
for (MindMapNode child : oldChildren) {
oldByNormalized.put(normalizeForMerge(child.getTitle()), child);
}
}
List<MindMapNode> merged = new ArrayList<>();
Set<String> 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 {