diff --git a/server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java b/server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java index 8e2fad3..1840474 100644 --- a/server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java +++ b/server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java @@ -50,6 +50,14 @@ public class ShortNovelServiceImpl implements ShortNovelService { private static final java.util.concurrent.ConcurrentHashMap SESSION_ORIGINAL_QUERY_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); private static final long SESSION_TTL_MS = 30 * 60 * 1000L; + /** + * sessionId -> 中间步骤累积列表(跨 SSE 流保存 clarification_question/answer/outline) + * 用于在 novel_done 时统一创建 message,还原完整生成流程 + * 会话超时 30 分钟(与 SESSION_ORIGINAL_QUERY_CACHE 对齐) + */ + private static final java.util.concurrent.ConcurrentHashMap> SESSION_STAGES_ACCUMULATOR = + new java.util.concurrent.ConcurrentHashMap<>(); + /** * session 元数据(含 originalQuery 和缓存时间) */ @@ -99,6 +107,36 @@ public class ShortNovelServiceImpl implements ShortNovelService { return meta.originalQuery; } + /** + * 累积中间步骤到 session 缓存 + * 包级可见,便于单元测试 + * + * @param sessionId 会话ID + * @param stageEntry 步骤条目(含 kind 字段:clarification_question/clarification_answer/outline) + */ + static void accumulateStage(String sessionId, JSONObject stageEntry) { + if (sessionId == null || sessionId.isEmpty() || stageEntry == null) { + return; + } + SESSION_STAGES_ACCUMULATOR.computeIfAbsent(sessionId, + k -> new java.util.concurrent.CopyOnWriteArrayList<>()).add(stageEntry); + } + + /** + * 读取并清理 session 累积的中间步骤 + * 包级可见,便于单元测试 + * + * @param sessionId 会话ID + * @return 步骤列表(按累积顺序),无则返回空列表 + */ + static java.util.List drainStages(String sessionId) { + if (sessionId == null || sessionId.isEmpty()) { + return java.util.Collections.emptyList(); + } + java.util.List stages = SESSION_STAGES_ACCUMULATOR.remove(sessionId); + return stages != null ? stages : java.util.Collections.emptyList(); + } + @Autowired private ShortNovelConfig config; diff --git a/server/src/test/java/com/emotion/service/impl/ShortNovelServiceImplTest.java b/server/src/test/java/com/emotion/service/impl/ShortNovelServiceImplTest.java new file mode 100644 index 0000000..e14058e --- /dev/null +++ b/server/src/test/java/com/emotion/service/impl/ShortNovelServiceImplTest.java @@ -0,0 +1,75 @@ +package com.emotion.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * ShortNovelServiceImpl 静态缓存累积器单元测试 + * + * @author huazhongmin + * @date 2026-07-26 + */ +public class ShortNovelServiceImplTest { + + @Test + public void testAccumulateStage_singleEntry() { + String sessionId = "test-session-1"; + JSONObject entry = new JSONObject(); + entry.put("kind", "clarification_question"); + entry.put("card", new JSONObject().fluentPut("question", "你的咖啡馆特色?")); + + ShortNovelServiceImpl.accumulateStage(sessionId, entry); + + List stages = ShortNovelServiceImpl.drainStages(sessionId); + assertEquals(1, stages.size()); + assertEquals("clarification_question", stages.get(0).getString("kind")); + } + + @Test + public void testAccumulateStage_multipleEntries_preservesOrder() { + String sessionId = "test-session-2"; + ShortNovelServiceImpl.accumulateStage(sessionId, + new JSONObject().fluentPut("kind", "clarification_question").fluentPut("question", "Q1")); + ShortNovelServiceImpl.accumulateStage(sessionId, + new JSONObject().fluentPut("kind", "clarification_answer").fluentPut("answer", "A1")); + ShortNovelServiceImpl.accumulateStage(sessionId, + new JSONObject().fluentPut("kind", "outline").fluentPut("outline", new JSONObject())); + + List stages = ShortNovelServiceImpl.drainStages(sessionId); + assertEquals(3, stages.size()); + assertEquals("clarification_question", stages.get(0).getString("kind")); + assertEquals("clarification_answer", stages.get(1).getString("kind")); + assertEquals("outline", stages.get(2).getString("kind")); + } + + @Test + public void testDrainStages_clearsCache() { + String sessionId = "test-session-3"; + ShortNovelServiceImpl.accumulateStage(sessionId, + new JSONObject().fluentPut("kind", "outline")); + + List first = ShortNovelServiceImpl.drainStages(sessionId); + assertEquals(1, first.size()); + + // 二次读取应为空(drain 后已清理) + List second = ShortNovelServiceImpl.drainStages(sessionId); + assertTrue(second.isEmpty()); + } + + @Test + public void testAccumulateStage_nullSessionId_ignored() { + ShortNovelServiceImpl.accumulateStage(null, + new JSONObject().fluentPut("kind", "outline")); + assertTrue(ShortNovelServiceImpl.drainStages(null).isEmpty()); + } + + @Test + public void testDrainStages_unknownSession_returnsEmpty() { + List stages = ShortNovelServiceImpl.drainStages("nonexistent-session"); + assertTrue(stages.isEmpty()); + } +}