From a636e516d1c944e3c6f09d0dad133f2d1636c018 Mon Sep 17 00:00:00 2001 From: cat_shark <1716967236@qq.com> Date: Sun, 5 Jul 2026 14:40:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=A4=8D=E4=B9=A0):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=BB=B4=E5=BA=A6=E5=A4=8D=E4=B9=A0=20+=20?= =?UTF-8?q?=E6=A0=87=E5=87=86=E5=AF=BC=E5=9B=BE=E6=8C=89=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=EF=BC=88=E5=90=8E=E7=AB=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MindMapNode 新增 sessionNum 字段 - BuiltinMindMapGenerator 生成节点时填充 sessionNum - MindMapTreeTool.filterBySession() 按会话过滤标准导图 - recallCompare 接受 sessionNum 参数 - ReviewRecallRecordEntity 新增 sessionNum 字段 - Flyway V20260705_1: review_recall_records 加 session_num 列 - 新增 GET /study-sessions/tasks/{taskNum}/sessions 端点 --- .../controller/ReviewController.java | 2 +- .../controller/StudySessionController.java | 8 +++ .../dto/request/RecallCompareRequest.java | 3 + .../dto/response/SessionBriefVO.java | 15 +++++ .../entity/ReviewRecallRecordEntity.java | 6 ++ .../service/StandardMindMapService.java | 3 +- .../service/StudySessionsService.java | 5 ++ .../service/impl/BuiltinMindMapGenerator.java | 3 + .../impl/StandardMindMapServiceImpl.java | 8 ++- .../impl/StudySessionsServiceImpl.java | 30 ++++++++++ .../utils/MindMapNode.java | 4 ++ .../utils/MindMapTreeTool.java | 55 +++++++++++++++++++ ...5_1__add_session_num_to_recall_records.sql | 5 ++ 13 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/guo/learningprogresstracker/dto/response/SessionBriefVO.java create mode 100644 src/main/resources/db/migration/V20260705_1__add_session_num_to_recall_records.sql diff --git a/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java b/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java index b5ba48b..c602266 100644 --- a/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java +++ b/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java @@ -154,7 +154,7 @@ public class ReviewController { public CommonResult recallCompare( @PathVariable String taskNum, @Valid @RequestBody RecallCompareRequest request) throws NotFindEntitiesException, OperationFailedException { - return CommonResult.success(standardMindMapService.recallCompare(taskNum, request.getRecallOutline())); + return CommonResult.success(standardMindMapService.recallCompare(taskNum, request.getRecallOutline(), request.getSessionNum())); } /** diff --git a/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java b/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java index d0b5bda..c913f35 100644 --- a/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java +++ b/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java @@ -18,7 +18,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import com.guo.learningprogresstracker.dto.response.SessionBriefVO; import java.util.ArrayList; +import java.util.List; import java.time.LocalDateTime; /** @@ -137,4 +139,10 @@ public class StudySessionController { @RequestParam(required = false) String excludeTaskNum) { return CommonResult.success(studySessionsServiceImpl.getActiveSession(excludeTaskNum)); } + + /** 获取任务的所有会话简要列表(用于复习维度选择) */ + @GetMapping("/tasks/{taskNum}/sessions") + public CommonResult> getTaskSessions(@PathVariable String taskNum) { + return CommonResult.success(studySessionsServiceImpl.getTaskSessions(taskNum)); + } } diff --git a/src/main/java/com/guo/learningprogresstracker/dto/request/RecallCompareRequest.java b/src/main/java/com/guo/learningprogresstracker/dto/request/RecallCompareRequest.java index b5ac15c..d5f055c 100644 --- a/src/main/java/com/guo/learningprogresstracker/dto/request/RecallCompareRequest.java +++ b/src/main/java/com/guo/learningprogresstracker/dto/request/RecallCompareRequest.java @@ -11,4 +11,7 @@ public class RecallCompareRequest { @NotBlank(message = "回忆大纲不可为空") private String recallOutline; + + /** 复习维度:会话编码,null 为任务维度 */ + private String sessionNum; } diff --git a/src/main/java/com/guo/learningprogresstracker/dto/response/SessionBriefVO.java b/src/main/java/com/guo/learningprogresstracker/dto/response/SessionBriefVO.java new file mode 100644 index 0000000..75c2576 --- /dev/null +++ b/src/main/java/com/guo/learningprogresstracker/dto/response/SessionBriefVO.java @@ -0,0 +1,15 @@ +package com.guo.learningprogresstracker.dto.response; + +import lombok.Data; + +/** + * 会话简要信息(用于复习维度选择列表) + */ +@Data +public class SessionBriefVO { + private String sessionNum; + private String startTime; + private String expectation; + private int reportCount; + private int fragmentCount; +} diff --git a/src/main/java/com/guo/learningprogresstracker/entity/ReviewRecallRecordEntity.java b/src/main/java/com/guo/learningprogresstracker/entity/ReviewRecallRecordEntity.java index 329fea4..a0f704a 100644 --- a/src/main/java/com/guo/learningprogresstracker/entity/ReviewRecallRecordEntity.java +++ b/src/main/java/com/guo/learningprogresstracker/entity/ReviewRecallRecordEntity.java @@ -24,6 +24,12 @@ public class ReviewRecallRecordEntity extends BaseEntity implements Serializable @TableField(value = "standard_map_id") private Integer standardMapId; + /** + * 复习维度:会话编码(null 表示任务维度复习) + */ + @TableField(value = "session_num") + private String sessionNum; + /** * 用户回忆绘制的导图大纲文本 */ diff --git a/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java b/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java index 78ae950..c5b3039 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java +++ b/src/main/java/com/guo/learningprogresstracker/service/StandardMindMapService.java @@ -32,9 +32,10 @@ public interface StandardMindMapService { * 用户提交回忆大纲,与标准导图对比并返回结果。 * * @param recallOutline 用户回忆的缩进大纲文本 + * @param sessionNum 可选会话编码,指定时仅对比该会话相关的标准导图节点 * @return 对比记录实体(compareResult 字段包含对比树 JSON) */ - ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline) throws NotFindEntitiesException, OperationFailedException; + ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline, String sessionNum) throws NotFindEntitiesException, OperationFailedException; /** * 获取该任务的所有回忆对比记录。 diff --git a/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java b/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java index 9932da1..76630fb 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java +++ b/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java @@ -11,7 +11,9 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.guo.learningprogresstracker.exception.ErrorParameterException; import java.time.LocalDateTime; +import com.guo.learningprogresstracker.dto.response.SessionBriefVO; import java.util.ArrayList; +import java.util.List; /** * @author guo @@ -35,4 +37,7 @@ public interface StudySessionsService extends IService { Page getTaskReports(String taskNum, int page, int size, String keyword); StudySessionResponse getActiveSession(String excludeTaskNum); + + /** 获取任务的所有会话简要信息(复习维度选择用) */ + List getTaskSessions(String taskNum); } diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/BuiltinMindMapGenerator.java b/src/main/java/com/guo/learningprogresstracker/service/impl/BuiltinMindMapGenerator.java index e6be4a9..2c208af 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/BuiltinMindMapGenerator.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/BuiltinMindMapGenerator.java @@ -84,6 +84,7 @@ public class BuiltinMindMapGenerator implements MindMapAiClient { } MindMapNode sessionNode = new MindMapNode(sessionTitle); + sessionNode.setSessionNum(sessionNum); // 报告作为子节点 for (StudyReportsEntity report : sessReports) { @@ -93,6 +94,7 @@ public class BuiltinMindMapGenerator implements MindMapAiClient { reportNode.setNotes(content); reportNode.setSourceType("REPORT"); reportNode.setSourceId(report.getId()); + reportNode.setSessionNum(sessionNum); sessionNode.getChildren().add(reportNode); } @@ -104,6 +106,7 @@ public class BuiltinMindMapGenerator implements MindMapAiClient { fragNode.setNotes(content); fragNode.setSourceType("FRAGMENT"); fragNode.setSourceId(frag.getId()); + fragNode.setSessionNum(sessionNum); sessionNode.getChildren().add(fragNode); } 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 aac66f0..8a44e82 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java @@ -96,7 +96,7 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { @Override @Transactional - public ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline) + public ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline, String sessionNum) throws NotFindEntitiesException, OperationFailedException { ensureTaskExists(taskNum); @@ -107,6 +107,11 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { MindMapNode standardRoot = MindMapTreeTool.fromJson(standard.getContent(), objectMapper); MindMapNode recallRoot = MindMapTreeTool.parseOutline(recallOutline); + // 2b. 若指定了会话维度,过滤标准导图只保留该会话相关节点 + if (sessionNum != null && !sessionNum.isBlank()) { + standardRoot = MindMapTreeTool.filterBySession(standardRoot, sessionNum); + } + // 3. 执行对比(优先 AI 语义对比,失败时降级为内置算法) CompareResult result; if (aiServiceClient.isConfigured()) { @@ -142,6 +147,7 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { ReviewRecallRecordEntity record = new ReviewRecallRecordEntity(); record.setTaskNum(taskNum); record.setStandardMapId(standard.getId()); + record.setSessionNum(sessionNum != null && !sessionNum.isBlank() ? sessionNum : null); record.setRecallContent(recallOutline); record.setCompareResult(resultJson); record.setRecallRatio(result.getRecallRatio()); diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java index fcc93d6..b7495fc 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.protobuf.ServiceException; import com.guo.learningprogresstracker.dto.StudySessionsDto; +import com.guo.learningprogresstracker.dto.response.SessionBriefVO; import com.guo.learningprogresstracker.dto.response.StudySessionResponse; import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity; import com.guo.learningprogresstracker.entity.StudyReportsEntity; @@ -26,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -225,6 +227,34 @@ public class StudySessionsServiceImpl extends ServiceImpl getTaskSessions(String taskNum) { + var sessions = this.list(Wrappers.lambdaQuery(StudySessionsEntity.class) + .eq(StudySessionsEntity::getTaskNum, taskNum) + .orderByDesc(StudySessionsEntity::getStartTime)); + + return sessions.stream().map(s -> { + SessionBriefVO vo = new SessionBriefVO(); + vo.setSessionNum(s.getSessionNum()); + vo.setStartTime(s.getStartTime() != null ? s.getStartTime().toString() : null); + // 查询该会话的预期 + Optional.ofNullable(studyExpectationsService.getBySessionNum(s.getSessionNum())) + .ifPresent(e -> vo.setExpectation(e.getDescription())); + // 统计报告和残片数量 + long reportCount = studyReportsMapper.selectCount( + Wrappers.lambdaQuery(StudyReportsEntity.class) + .eq(StudyReportsEntity::getSessionNum, s.getSessionNum())); + long fragmentCount = studyReportFragmentsMapper.selectCount( + Wrappers.lambdaQuery(StudyReportFragmentsEntity.class) + .eq(StudyReportFragmentsEntity::getSessionNum, s.getSessionNum())); + vo.setReportCount((int) reportCount); + vo.setFragmentCount((int) fragmentCount); + return vo; + }).collect(Collectors.toList()); + } + // ============ 活跃会话查询 ============ /** diff --git a/src/main/java/com/guo/learningprogresstracker/utils/MindMapNode.java b/src/main/java/com/guo/learningprogresstracker/utils/MindMapNode.java index 78d126a..53d285c 100644 --- a/src/main/java/com/guo/learningprogresstracker/utils/MindMapNode.java +++ b/src/main/java/com/guo/learningprogresstracker/utils/MindMapNode.java @@ -35,6 +35,8 @@ public class MindMapNode { private String notes; private String sourceType; private Integer sourceId; + /** 可选:节点来源会话编码(REPORT/FRAGMENT 节点使用) */ + private String sessionNum; private List children; public MindMapNode(String title) { @@ -51,6 +53,7 @@ public class MindMapNode { map.put("notes", notes != null ? notes : ""); if (sourceType != null) map.put("sourceType", sourceType); if (sourceId != null) map.put("sourceId", sourceId); + if (sessionNum != null) map.put("sessionNum", sessionNum); List childMaps = new ArrayList<>(); if (children != null) { for (MindMapNode c : children) { @@ -70,6 +73,7 @@ public class MindMapNode { if (map.containsKey("sourceId") && map.get("sourceId") != null) { node.setSourceId(((Number) map.get("sourceId")).intValue()); } + node.setSessionNum((String) map.get("sessionNum")); Object raw = map.getOrDefault("children", map.get("nodes")); List children = new ArrayList<>(); if (raw instanceof List list) { diff --git a/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java b/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java index 2474682..5514d89 100644 --- a/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java +++ b/src/main/java/com/guo/learningprogresstracker/utils/MindMapTreeTool.java @@ -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 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 { diff --git a/src/main/resources/db/migration/V20260705_1__add_session_num_to_recall_records.sql b/src/main/resources/db/migration/V20260705_1__add_session_num_to_recall_records.sql new file mode 100644 index 0000000..5fd8677 --- /dev/null +++ b/src/main/resources/db/migration/V20260705_1__add_session_num_to_recall_records.sql @@ -0,0 +1,5 @@ +-- 为回忆对比记录添加 session_num 字段,支持会话维度的复习过滤 +ALTER TABLE review_recall_records + ADD COLUMN session_num VARCHAR(255) NULL AFTER standard_map_id; + +CREATE INDEX idx_recall_records_session_num ON review_recall_records (session_num);