feat(导图): 防并发生成 + 增量合并 + mergeTrees 工具
- MindMapTreeTool.mergeTrees() 按标准化标题合并新旧树节点 - StandardMindServiceImpl 防并发生成(ConcurrentHashMap 锁) - incrementalGenerate() 保留用户编辑节点 + 追加新节点 - regenerate 端点接受 mode 参数(full/incremental)
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
* 用户手动编辑标准思维导图(用缩进大纲文本替换)。
|
||||
*/
|
||||
|
||||
+56
@@ -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);
|
||||
// 防并发生成:同一任务正在生成时直接返回当前实体
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user