feat(复习):标准思维导图生成与回忆对比
- 新增 review_standard_mind_maps / review_recall_records 数据表 - BuiltinMindMapGenerator:从报告/残片/应用场景规则生成标准导图 - MindMapAiClient 接口 + RemoteAiMindMapClient 预留 - StandardMindMapService:生成/编辑/重新生成/回忆对比 - 对比算法:标题归一化 + Bigram Jaccard 模糊匹配 - ReviewController 新增 6 个端点 - MindMapTreeTool / MindMapNode / CompareResult 工具类 - 完整单元测试(10 个,全部通过) - 更新 docs/review-module-design.md
This commit is contained in:
+195
@@ -0,0 +1,195 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.guo.learningprogresstracker.entity.ReviewStandardMindMapEntity;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.mapper.*;
|
||||
import com.guo.learningprogresstracker.service.MindMapAiClient;
|
||||
import com.guo.learningprogresstracker.utils.CompareResult;
|
||||
import com.guo.learningprogresstracker.utils.MindMapNode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardMindMapServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private StandardMindMapServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private ReviewStandardMindMapMapper standardMindMapMapper;
|
||||
@Mock
|
||||
private ReviewRecallRecordMapper recallRecordMapper;
|
||||
@Mock
|
||||
private TasksMapper tasksMapper;
|
||||
@Mock
|
||||
private StudyReportsMapper studyReportsMapper;
|
||||
@Mock
|
||||
private StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
@Mock
|
||||
private TaskApplicationMapper taskApplicationMapper;
|
||||
@Mock
|
||||
private StudySessionsMapper studySessionsMapper;
|
||||
@Mock
|
||||
private List<MindMapAiClient> aiClients;
|
||||
@Mock
|
||||
private MindMapAiClient mockAiClient;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// ============ normalize / fuzzyMatch / bigramSet ============
|
||||
|
||||
@Test
|
||||
void normalize_shouldStripPunctuationAndLowercase() {
|
||||
assertEquals("javaconcurrency", StandardMindMapServiceImpl.normalize("Java Concurrency"));
|
||||
assertEquals("测试abc", StandardMindMapServiceImpl.normalize("测试,ABC!?"));
|
||||
assertEquals("helloworld", StandardMindMapServiceImpl.normalize(" Hello, World! "));
|
||||
assertEquals("", StandardMindMapServiceImpl.normalize(null));
|
||||
assertEquals("", StandardMindMapServiceImpl.normalize(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalize_shouldStripCompareMarkers() {
|
||||
assertEquals("thread", StandardMindMapServiceImpl.normalize("MATCHED|Thread"));
|
||||
assertEquals("thread", StandardMindMapServiceImpl.normalize("MISSED|Thread"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bigramSet_shouldExtractCharacterPairs() {
|
||||
java.util.Set<String> set = StandardMindMapServiceImpl.bigramSet("hello");
|
||||
assertTrue(set.contains("he"));
|
||||
assertTrue(set.contains("el"));
|
||||
assertTrue(set.contains("ll"));
|
||||
assertTrue(set.contains("lo"));
|
||||
assertEquals(4, set.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void bigramSet_shouldReturnEmptyForShortStrings() {
|
||||
assertTrue(StandardMindMapServiceImpl.bigramSet("a").isEmpty());
|
||||
assertTrue(StandardMindMapServiceImpl.bigramSet("").isEmpty());
|
||||
}
|
||||
|
||||
// ============ compareTrees ============
|
||||
|
||||
@Test
|
||||
void compareTrees_perfectMatch() {
|
||||
service = createService();
|
||||
|
||||
MindMapNode standard = new MindMapNode("学习Java");
|
||||
standard.setChildren(List.of(new MindMapNode("线程基础"), new MindMapNode("锁机制")));
|
||||
|
||||
MindMapNode recall = new MindMapNode("学习Java");
|
||||
recall.setChildren(List.of(new MindMapNode("线程基础"), new MindMapNode("锁机制")));
|
||||
|
||||
CompareResult result = service.compareTrees(standard, recall);
|
||||
|
||||
assertEquals(2, result.getMatchedCount());
|
||||
assertEquals(0, result.getMissedCount());
|
||||
assertEquals(0, result.getExtraCount());
|
||||
assertEquals(1.0, result.getRecallRatio(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTrees_partialMatch() {
|
||||
service = createService();
|
||||
|
||||
MindMapNode standard = new MindMapNode("学习Java");
|
||||
standard.setChildren(List.of(
|
||||
new MindMapNode("线程基础"),
|
||||
new MindMapNode("锁机制"),
|
||||
new MindMapNode("JVM内存模型")
|
||||
));
|
||||
|
||||
MindMapNode recall = new MindMapNode("学习Java");
|
||||
recall.setChildren(List.of(
|
||||
new MindMapNode("线程基础"),
|
||||
new MindMapNode("SpringBoot")
|
||||
));
|
||||
|
||||
CompareResult result = service.compareTrees(standard, recall);
|
||||
|
||||
assertEquals(1, result.getMatchedCount());
|
||||
assertEquals(2, result.getMissedCount());
|
||||
assertEquals(1, result.getExtraCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTrees_noMatch() {
|
||||
service = createService();
|
||||
|
||||
MindMapNode standard = new MindMapNode("学习Java");
|
||||
standard.setChildren(List.of(new MindMapNode("线程基础"), new MindMapNode("锁机制")));
|
||||
|
||||
MindMapNode recall = new MindMapNode("完全不同");
|
||||
recall.setChildren(List.of(new MindMapNode("前端开发")));
|
||||
|
||||
CompareResult result = service.compareTrees(standard, recall);
|
||||
|
||||
assertEquals(0, result.getMatchedCount());
|
||||
assertEquals(2, result.getMissedCount());
|
||||
assertEquals(1, result.getExtraCount());
|
||||
assertEquals(0.0, result.getRecallRatio(), 0.001);
|
||||
}
|
||||
|
||||
// ============ 边缘情况 ============
|
||||
|
||||
@Test
|
||||
void compareTrees_emptyStandardLeaves() {
|
||||
service = createService();
|
||||
|
||||
MindMapNode standard = new MindMapNode("根"); // 无子节点
|
||||
MindMapNode recall = new MindMapNode("根");
|
||||
recall.setChildren(List.of(new MindMapNode("额外节点")));
|
||||
|
||||
CompareResult result = service.compareTrees(standard, recall);
|
||||
|
||||
assertEquals(0, result.getMatchedCount());
|
||||
assertEquals(0, result.getMissedCount());
|
||||
assertEquals(1, result.getExtraCount());
|
||||
assertEquals(0.0, result.getRecallRatio(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerate_shouldThrowWhenTaskNotFound() {
|
||||
when(tasksMapper.exists(any())).thenReturn(false);
|
||||
assertThrows(NotFindEntitiesException.class, () -> service.getOrGenerate("NOT_EXIST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerate_shouldReturnExisting() throws Exception {
|
||||
when(tasksMapper.exists(any())).thenReturn(true);
|
||||
|
||||
ReviewStandardMindMapEntity existing = new ReviewStandardMindMapEntity();
|
||||
existing.setId(1);
|
||||
existing.setTaskNum("T1");
|
||||
existing.setTitle("已有导图");
|
||||
when(standardMindMapMapper.selectOne(any())).thenReturn(existing);
|
||||
|
||||
ReviewStandardMindMapEntity result = service.getOrGenerate("T1");
|
||||
assertEquals("已有导图", result.getTitle());
|
||||
verify(standardMindMapMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
// ============ 辅助 ============
|
||||
|
||||
private StandardMindMapServiceImpl createService() {
|
||||
StandardMindMapServiceImpl s = new StandardMindMapServiceImpl(
|
||||
standardMindMapMapper, recallRecordMapper,
|
||||
tasksMapper, studyReportsMapper, studyReportFragmentsMapper,
|
||||
taskApplicationMapper, studySessionsMapper,
|
||||
List.of(mockAiClient), objectMapper);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user