feat(复习): 节点级 focusPath + findNode,回退 session 维度
- 移除 MindMapNode.sessionNum、filterBySession、getTaskSessions
- 新增 MindMapTreeTool.extractSubtree() 按路径提取子树作为对比基准
- 新增 findClosestNode/getPath/similarityScore 支持节点匹配
- 新增 POST /review/standard-mind-map/{taskNum}/find-node
- recallCompare 使用 focusPath 替代 sessionNum
- ReviewRecallRecordEntity.focusPath 替代 sessionNum
- Flyway V20260706_1: review_recall_records 加 focus_path 列
This commit is contained in:
@@ -6,6 +6,7 @@ import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.exception.OperationFailedException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 标准思维导图服务。
|
||||
@@ -37,10 +38,16 @@ public interface StandardMindMapService {
|
||||
* 用户提交回忆大纲,与标准导图对比并返回结果。
|
||||
*
|
||||
* @param recallOutline 用户回忆的缩进大纲文本
|
||||
* @param sessionNum 可选会话编码,指定时仅对比该会话相关的标准导图节点
|
||||
* @return 对比记录实体(compareResult 字段包含对比树 JSON)
|
||||
* @param focusPath 可选:标准导图中的节点路径(/ 分隔),指定后仅对比该子树
|
||||
*/
|
||||
ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline, String sessionNum) throws NotFindEntitiesException, OperationFailedException;
|
||||
ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline, String focusPath) throws NotFindEntitiesException, OperationFailedException;
|
||||
|
||||
/**
|
||||
* 根据内容文本在标准导图中查找最匹配的节点,返回节点路径。
|
||||
*
|
||||
* @return { "path": "节点A / 节点B", "nodeTitle": "节点B", "score": 0.85 }
|
||||
*/
|
||||
Map<String, Object> findNode(String taskNum, String content) throws NotFindEntitiesException, OperationFailedException;
|
||||
|
||||
/**
|
||||
* 获取该任务的所有回忆对比记录。
|
||||
|
||||
@@ -11,9 +11,7 @@ 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
|
||||
@@ -37,7 +35,4 @@ public interface StudySessionsService extends IService<StudySessionsEntity> {
|
||||
Page<StudyReportsEntity> getTaskReports(String taskNum, int page, int size, String keyword);
|
||||
|
||||
StudySessionResponse getActiveSession(String excludeTaskNum);
|
||||
|
||||
/** 获取任务的所有会话简要信息(复习维度选择用) */
|
||||
List<SessionBriefVO> getTaskSessions(String taskNum);
|
||||
}
|
||||
|
||||
-3
@@ -84,7 +84,6 @@ public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
}
|
||||
|
||||
MindMapNode sessionNode = new MindMapNode(sessionTitle);
|
||||
sessionNode.setSessionNum(sessionNum);
|
||||
|
||||
// 报告作为子节点
|
||||
for (StudyReportsEntity report : sessReports) {
|
||||
@@ -94,7 +93,6 @@ public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
reportNode.setNotes(content);
|
||||
reportNode.setSourceType("REPORT");
|
||||
reportNode.setSourceId(report.getId());
|
||||
reportNode.setSessionNum(sessionNum);
|
||||
sessionNode.getChildren().add(reportNode);
|
||||
}
|
||||
|
||||
@@ -106,7 +104,6 @@ public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
fragNode.setNotes(content);
|
||||
fragNode.setSourceType("FRAGMENT");
|
||||
fragNode.setSourceId(frag.getId());
|
||||
fragNode.setSessionNum(sessionNum);
|
||||
sessionNode.getChildren().add(fragNode);
|
||||
}
|
||||
|
||||
|
||||
+37
-9
@@ -152,7 +152,7 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline, String sessionNum)
|
||||
public ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline, String focusPath)
|
||||
throws NotFindEntitiesException, OperationFailedException {
|
||||
ensureTaskExists(taskNum);
|
||||
|
||||
@@ -163,9 +163,14 @@ 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);
|
||||
// 2b. 若指定了起点节点路径,提取该子树作为对比基准
|
||||
MindMapNode compareRoot = standardRoot;
|
||||
if (focusPath != null && !focusPath.isBlank()) {
|
||||
compareRoot = MindMapTreeTool.extractSubtree(standardRoot, focusPath);
|
||||
if (compareRoot == null) {
|
||||
log.warn("focusPath 未匹配到节点,使用全量标准导图: path={}", focusPath);
|
||||
compareRoot = standardRoot;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 执行对比(优先 AI 语义对比,失败时降级为内置算法)
|
||||
@@ -176,19 +181,19 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
.getTaskName();
|
||||
var optJson = aiServiceClient.compareRecall(
|
||||
taskName,
|
||||
MindMapTreeTool.toFullOutline(standardRoot),
|
||||
MindMapTreeTool.toFullOutline(compareRoot),
|
||||
recallOutline
|
||||
);
|
||||
if (optJson.isPresent()) {
|
||||
result = buildCompareResultFromAI(optJson.get(), standardRoot);
|
||||
result = buildCompareResultFromAI(optJson.get(), compareRoot);
|
||||
log.info("AI 回忆对比: taskNum={}, recallRatio={}, evaluation={}",
|
||||
taskNum, result.getRecallRatio(),
|
||||
result.getEvaluation() != null ? result.getEvaluation().substring(0, Math.min(50, result.getEvaluation().length())) : "");
|
||||
} else {
|
||||
result = compareTrees(standardRoot, recallRoot);
|
||||
result = compareTrees(compareRoot, recallRoot);
|
||||
}
|
||||
} else {
|
||||
result = compareTrees(standardRoot, recallRoot);
|
||||
result = compareTrees(compareRoot, recallRoot);
|
||||
}
|
||||
|
||||
// 4. 序列化对比结果
|
||||
@@ -203,7 +208,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.setFocusPath(focusPath != null && !focusPath.isBlank() ? focusPath : null);
|
||||
record.setRecallContent(recallOutline);
|
||||
record.setCompareResult(resultJson);
|
||||
record.setRecallRatio(result.getRecallRatio());
|
||||
@@ -219,6 +224,29 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
return standard;
|
||||
}
|
||||
|
||||
// ============ 节点查找 ============
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findNode(String taskNum, String content) throws NotFindEntitiesException, OperationFailedException {
|
||||
ensureTaskExists(taskNum);
|
||||
ReviewStandardMindMapEntity standard = getOrGenerate(taskNum);
|
||||
MindMapNode root = MindMapTreeTool.fromJson(standard.getContent(), objectMapper);
|
||||
|
||||
MindMapNode closest = MindMapTreeTool.findClosestNode(root, content);
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (closest != null) {
|
||||
String path = MindMapTreeTool.getPath(root, closest.getTitle());
|
||||
result.put("path", path);
|
||||
result.put("nodeTitle", closest.getTitle());
|
||||
result.put("score", MindMapTreeTool.similarityScore(closest.getTitle(), content));
|
||||
} else {
|
||||
result.put("path", "");
|
||||
result.put("nodeTitle", "");
|
||||
result.put("score", 0.0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============ 对比算法核心 ============
|
||||
|
||||
/**
|
||||
|
||||
-30
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -27,7 +26,6 @@ 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;
|
||||
|
||||
@@ -227,34 +225,6 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============ 获取任务会话列表(复习维度选择用) ============
|
||||
|
||||
@Override
|
||||
public List<SessionBriefVO> 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());
|
||||
}
|
||||
|
||||
// ============ 活跃会话查询 ============
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user