feat(复习): 支持会话维度复习 + 标准导图按会话过滤(后端)

- MindMapNode 新增 sessionNum 字段
- BuiltinMindMapGenerator 生成节点时填充 sessionNum
- MindMapTreeTool.filterBySession() 按会话过滤标准导图
- recallCompare 接受 sessionNum 参数
- ReviewRecallRecordEntity 新增 sessionNum 字段
- Flyway V20260705_1: review_recall_records 加 session_num 列
- 新增 GET /study-sessions/tasks/{taskNum}/sessions 端点
This commit is contained in:
2026-07-05 14:40:38 +08:00
parent 22cc5dc871
commit a636e516d1
13 changed files with 144 additions and 3 deletions
@@ -156,6 +156,61 @@ public class MindMapTreeTool {
return 1 + node.getChildren().stream().mapToInt(MindMapTreeTool::maxDepth).max().orElse(0);
}
/**
* 过滤树:只保留与指定 sessionNum 相关的分支。
* - 节点有 sessionNum 且匹配 → 保留
* - 节点无 sessionNum(如 APPLICATION 或 AI 生成的节点) → 保留(视为任务级通用知识)
* - 有子节点被保留时,父节点也保留
*
* @param root 树根
* @param sessionNum 目标会话编码
* @return 过滤后的树,若无相关节点则返回仅含根的空树
*/
public static MindMapNode filterBySession(MindMapNode root, String sessionNum) {
if (root == null || sessionNum == null || sessionNum.isBlank()) return root;
MindMapNode filtered = filterRecursive(root, sessionNum);
return filtered != null ? filtered : new MindMapNode(root.getTitle() != null ? root.getTitle() : "");
}
private static MindMapNode filterRecursive(MindMapNode node, String sessionNum) {
if (node == null) return null;
// 先过滤子节点
List<MindMapNode> keptChildren = new ArrayList<>();
if (node.getChildren() != null) {
for (MindMapNode child : node.getChildren()) {
MindMapNode kept = filterRecursive(child, sessionNum);
if (kept != null) {
keptChildren.add(kept);
}
}
}
// 判断当前节点是否应该保留:
// 1. 有 sessionNum 且匹配 → 保留
// 2. 无 sessionNum → 保留(任务级节点,如 APPLICATION 或 AI 生成)
// 3. 有子节点保留 → 保留
boolean selfMatch = (node.getSessionNum() == null) || sessionNum.equals(node.getSessionNum());
if (selfMatch || !keptChildren.isEmpty()) {
MindMapNode result = copyNodeShallow(node);
result.setChildren(keptChildren.isEmpty() ? node.getChildren() : keptChildren);
return result;
}
return null;
}
/** 浅拷贝节点(仅拷贝标量字段,不拷贝 children) */
private static MindMapNode copyNodeShallow(MindMapNode node) {
MindMapNode copy = new MindMapNode();
copy.setTitle(node.getTitle());
copy.setNotes(node.getNotes());
copy.setSourceType(node.getSourceType());
copy.setSourceId(node.getSourceId());
copy.setSessionNum(node.getSessionNum());
return copy;
}
/** 序列化整棵树为 JSON */
public static String toJson(MindMapNode root, ObjectMapper mapper) {
try {