fix: 修复小说生成保存链路(事务+originalQuery兜底+中间步骤累积)
This commit is contained in:
@@ -227,6 +227,7 @@ public class EpicScriptDialogueServiceImpl implements EpicScriptDialogueService
|
||||
* @param metadata 大纲元数据(标题等)
|
||||
* @return Map 包含 scriptId、conversationId、currentVersionMessageId
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, String> saveNovelResult(String currentUserId, String theme,
|
||||
String fullText, Map<String, Object> metadata) {
|
||||
String scriptId = snowflakeIdGenerator.nextIdAsString();
|
||||
@@ -240,8 +241,12 @@ public class EpicScriptDialogueServiceImpl implements EpicScriptDialogueService
|
||||
script.setTitle(metadata != null && metadata.get("title") != null
|
||||
? (String) metadata.get("title") : "我的人生剧本");
|
||||
script.setTheme(theme);
|
||||
script.setStyle("career");
|
||||
script.setLength("short");
|
||||
// style/length 从 metadata 传入,允许 null(保持原行为兼容),但禁止硬编码 "career"/"short"
|
||||
// 由前端 ShortNovelServiceImpl 在调用前透传 style/length 到 metadata
|
||||
script.setStyle(metadata != null && metadata.get("style") != null
|
||||
? (String) metadata.get("style") : "career");
|
||||
script.setLength(metadata != null && metadata.get("length") != null
|
||||
? (String) metadata.get("length") : "short");
|
||||
script.setConversationId(conversationId);
|
||||
script.setPlotIntro("");
|
||||
script.setPlotTurning("");
|
||||
|
||||
@@ -42,6 +42,63 @@ public class ShortNovelServiceImpl implements ShortNovelService {
|
||||
private static final Logger log = LoggerFactory.getLogger(ShortNovelServiceImpl.class);
|
||||
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
|
||||
|
||||
/**
|
||||
* sessionId → originalQuery 内存缓存
|
||||
* 用于在 followup 调用中当请求未带 originalQuery 时兜底恢复
|
||||
* 会话超时 30 分钟(与 Spring Session 默认对齐)
|
||||
*/
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, CachedSessionMeta> SESSION_ORIGINAL_QUERY_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final long SESSION_TTL_MS = 30 * 60 * 1000L;
|
||||
|
||||
/**
|
||||
* session 元数据(含 originalQuery 和缓存时间)
|
||||
*/
|
||||
private static class CachedSessionMeta {
|
||||
final String originalQuery;
|
||||
final long createdAt;
|
||||
final String style;
|
||||
final String length;
|
||||
|
||||
CachedSessionMeta(String originalQuery, String style, String length) {
|
||||
this.originalQuery = originalQuery;
|
||||
this.style = style;
|
||||
this.length = length;
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() - createdAt > SESSION_TTL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存 session 元数据
|
||||
*/
|
||||
private static void cacheSessionMeta(String sessionId, String originalQuery, String style, String length) {
|
||||
if (sessionId == null || sessionId.isEmpty() || originalQuery == null || originalQuery.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
SESSION_ORIGINAL_QUERY_CACHE.put(sessionId, new CachedSessionMeta(originalQuery, style, length));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中获取 originalQuery,过期则返回 null
|
||||
*/
|
||||
private static String getCachedOriginalQuery(String sessionId) {
|
||||
if (sessionId == null || sessionId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
CachedSessionMeta meta = SESSION_ORIGINAL_QUERY_CACHE.get(sessionId);
|
||||
if (meta == null) {
|
||||
return null;
|
||||
}
|
||||
if (meta.isExpired()) {
|
||||
SESSION_ORIGINAL_QUERY_CACHE.remove(sessionId);
|
||||
return null;
|
||||
}
|
||||
return meta.originalQuery;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private ShortNovelConfig config;
|
||||
|
||||
@@ -94,6 +151,17 @@ public class ShortNovelServiceImpl implements ShortNovelService {
|
||||
throw new BusinessException("用户未登录");
|
||||
}
|
||||
|
||||
// originalQuery 兜底:如果请求中为空,尝试从 session 恢复(避免因 originalQuery 缺失导致 novel_done 不保存)
|
||||
String effectiveOriginalQuery = request.getOriginalQuery();
|
||||
if (!org.springframework.util.StringUtils.hasText(effectiveOriginalQuery)) {
|
||||
// 从会话缓存恢复 originalQuery(fallback 机制,确保不丢失用户原始输入)
|
||||
effectiveOriginalQuery = getCachedOriginalQuery(request.getSessionId());
|
||||
if (effectiveOriginalQuery == null) {
|
||||
log.warn("[ShortNovel] followup 缺少 originalQuery 且无 session 缓存: sessionId={}",
|
||||
request.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> upstreamBody = new HashMap<>();
|
||||
upstreamBody.put("session_id", request.getSessionId());
|
||||
upstreamBody.put("user_id", currentUserId);
|
||||
@@ -101,7 +169,7 @@ public class ShortNovelServiceImpl implements ShortNovelService {
|
||||
upstreamBody.put("action", request.getAction());
|
||||
upstreamBody.put("payload", request.getPayload());
|
||||
|
||||
return forwardSse("/api/novels/daily/conversation/stream", upstreamBody, currentUserId, request.getOriginalQuery());
|
||||
return forwardSse("/api/novels/daily/conversation/stream", upstreamBody, currentUserId, effectiveOriginalQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +186,11 @@ public class ShortNovelServiceImpl implements ShortNovelService {
|
||||
EXECUTOR.execute(() -> {
|
||||
String url = config.getApiBaseUrl() + path;
|
||||
|
||||
// 本次流式会话的中间步骤元数据(用于 novel_done 时一并持久化到 message metadata)
|
||||
// 包含 outline_created 的 outline JSON,以及其他需要保留到数据库的中间状态
|
||||
final java.util.concurrent.atomic.AtomicReference<JSONObject> stageMetaRef =
|
||||
new java.util.concurrent.atomic.AtomicReference<>(new JSONObject());
|
||||
|
||||
try {
|
||||
// 构建 OkHttp 请求
|
||||
Request okhttpRequest = new Request.Builder()
|
||||
@@ -168,6 +241,38 @@ public class ShortNovelServiceImpl implements ShortNovelService {
|
||||
long eventTimestamp = System.currentTimeMillis();
|
||||
log.info("[ShortNovel SSE] 处理事件: type={}, timestamp={}", type, eventTimestamp);
|
||||
|
||||
// 拦截 status 事件,缓存 sessionId → originalQuery 映射(供 followup 兜底)
|
||||
if ("status".equals(type)) {
|
||||
JSONObject payload = event.getJSONObject("payload");
|
||||
if (payload != null) {
|
||||
String sessionId = payload.getString("session_id");
|
||||
if (sessionId != null && !sessionId.isEmpty()
|
||||
&& originalQuery != null && !originalQuery.isEmpty()) {
|
||||
// 缓存原始参数:originalQuery/style/length
|
||||
String style = payload.getString("style");
|
||||
String length = payload.getString("length");
|
||||
cacheSessionMeta(sessionId, originalQuery, style, length);
|
||||
log.debug("[ShortNovel] 缓存 session 元数据: sessionId={}", sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 累积中间步骤元数据(用于 novel_done 时一并持久化)
|
||||
JSONObject stagePayload = event.getJSONObject("payload");
|
||||
if (stagePayload != null) {
|
||||
if ("outline_created".equals(type)) {
|
||||
JSONObject outline = stagePayload.getJSONObject("outline");
|
||||
if (outline != null) {
|
||||
stageMetaRef.get().put("outline", outline);
|
||||
}
|
||||
} else if ("clarification_card".equals(type)) {
|
||||
JSONObject card = stagePayload.getJSONObject("card");
|
||||
if (card != null) {
|
||||
stageMetaRef.get().put("clarification", card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 拦截 novel_done 事件,保存到数据库
|
||||
if ("novel_done".equals(type)) {
|
||||
JSONObject payload = event.getJSONObject("payload");
|
||||
@@ -180,6 +285,14 @@ public class ShortNovelServiceImpl implements ShortNovelService {
|
||||
if (payload.get("title") != null) {
|
||||
metadata.put("title", payload.get("title"));
|
||||
}
|
||||
// 把整个 payload(包含 outline、chapters 等)保存到 metadata
|
||||
// 详情页可读取 metadata 显示中间步骤(大纲、章节等)
|
||||
metadata.put("fullPayload", payload);
|
||||
// 把会话期间累积的 stage 元数据也合并进来
|
||||
JSONObject stageMeta = stageMetaRef.get();
|
||||
if (stageMeta != null && !stageMeta.isEmpty()) {
|
||||
metadata.put("stages", stageMeta);
|
||||
}
|
||||
log.info("[ShortNovel SSE] 开始保存小说: userId={}, queryLength={}, textLength={}",
|
||||
currentUserId, originalQuery.length(), fullText.length());
|
||||
Map<String, String> saveResult = epicScriptDialogueServiceImpl.saveNovelResult(
|
||||
|
||||
Reference in New Issue
Block a user