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
@@ -130,10 +130,15 @@ public class ReviewController {
/**
* 强制重新生成任务的标准思维导图
* @param mode 生成模式:full(全量覆盖,默认)或 incremental(增量合并)
*/
@PostMapping("/standard-mind-map/{taskNum}/regenerate")
public CommonResult<ReviewStandardMindMapEntity> 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));
}
@@ -23,6 +23,11 @@ public interface StandardMindMapService {
*/
ReviewStandardMindMapEntity regenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException;
/**
* 增量更新:保留用户编辑过的节点,合并最新生成的节点。
*/
ReviewStandardMindMapEntity incrementalGenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException;
/**
* 用户手动编辑标准思维导图(用缩进大纲文本替换)。
*/
@@ -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<String, AtomicBoolean> 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
@@ -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 {