diff --git a/muse-studio/src/features/editor/components/ChapterPanel.tsx b/muse-studio/src/features/editor/components/ChapterPanel.tsx index 8f938f78..f362610c 100644 --- a/muse-studio/src/features/editor/components/ChapterPanel.tsx +++ b/muse-studio/src/features/editor/components/ChapterPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { useChapterList, useChapterCreate, useChapterDelete } from '../hooks/useWorks'; import type { ChapterVO } from '@/types/openapi'; @@ -81,8 +81,12 @@ interface ChapterPanelProps { activeChapterId: string | null; /** 切换激活章节时的回调函数 */ onSelectChapter: (chapterId: string | null) => void; - /** 当前作品 revision:新建章节是作品级写命令,后端要求 expectedWorkRevision 乐观锁,缺失则 400 */ - expectedWorkRevision?: number; + /** + * 当前作品 revision:新建章节是作品级写命令,后端要求 expectedWorkRevision 乐观锁,缺失则 400。 + * 作品详情异步加载,未到位时显式为 undefined(调用方不可兜底 1,否则真实 revision>1 时乐观锁冲突); + * submitCreate 对 null/undefined 早返,挡住 work 未加载时的过早提交。 + */ + expectedWorkRevision?: number | undefined; } /** @@ -97,6 +101,11 @@ export default function ChapterPanel({ }: ChapterPanelProps) { const [isCreating, setIsCreating] = useState(false); const [newTitle, setNewTitle] = useState(''); + // 同步提交守卫:isCreatingPending(React Query isPending)是异步 state、disabled 滞后,挡不住同一事件循环内的二次重入。 + // 触发链:回车 submitCreate(第 1 次建章,reqRev=N)→ onSuccess 卸载 input / 焦点变化触发 onBlur → submitCreate(第 2 次)。 + // 此时 newTitle 未清、expectedWorkRevision 仍是旧值 N,而 work 已被第 1 次推进到 N+1 → 第 2 次乐观锁冲突 500(1041000002)。 + // useRef 在提交瞬间同步置位,挡住进行中提交的重入;只挡"同一次提交",不挡首次提交与失焦补提交的正常用途。 + const submittingRef = useRef(false); // 1. 数据流 Hooks const { data: chapters = [], isLoading } = useChapterList(workId); @@ -129,6 +138,11 @@ export default function ChapterPanel({ // 3. 提交行内新建章节表单 const submitCreate = () => { + // 同步守卫:同一次提交进行中时,挡住 onBlur 等触发的二次重入(避免复用旧 expectedWorkRevision 致乐观锁冲突 500)。 + if (submittingRef.current) { + return; + } + const trimmedTitle = newTitle.trim(); if (!trimmedTitle) { setIsCreating(false); @@ -139,6 +153,8 @@ export default function ChapterPanel({ // 作品详情尚未加载,拿不到作品乐观锁版本,不发建章请求(避免后端 400/乐观锁冲突) return; } + // 置位须在 createChapter 调用之前:阻断同一事件循环内 onBlur 的重入。 + submittingRef.current = true; createChapter( { title: trimmedTitle, expectedWorkRevision }, { @@ -150,6 +166,10 @@ export default function ChapterPanel({ onSelectChapter(newCh.id); } }, + // 无论成功失败都复位守卫,放行下一次正常新建(成功后 input 已卸载;失败时用户可重试)。 + onSettled: () => { + submittingRef.current = false; + }, } ); }; diff --git a/muse-studio/src/pages/WorkspacePage.tsx b/muse-studio/src/pages/WorkspacePage.tsx index 38ad1b29..550c434b 100644 --- a/muse-studio/src/pages/WorkspacePage.tsx +++ b/muse-studio/src/pages/WorkspacePage.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { api } from '@/api/client'; import { useEditorStore } from '@/stores/editorStore'; -import { useChapterList, useChapterDetail, workRevision } from '@/features/editor/hooks/useWorks'; +import { useChapterList, useChapterDetail } from '@/features/editor/hooks/useWorks'; import { useAcceptSuggestion } from '@/features/editor/hooks/useAcceptSuggestion'; import { blockRevision } from '@/features/editor/hooks/useBlockStructure'; import { removeBlockDraft } from '@/lib/indexed-db'; @@ -201,7 +201,11 @@ export default function WorkspacePage() { workId={workId} activeChapterId={activeChapterId} onSelectChapter={setActiveChapter} - expectedWorkRevision={workRevision(work)} + // 传作品真实 revision(未加载时为 undefined,不可兜底 1):workRevision 的 ?? 1 兜底用于 work delete 路径, + // 但建章乐观锁不能拿假版本——work 详情(含 revision)异步加载,而 "正在创作:" 标题静态恒显、 + // 用户/测试可在 work 未到位时就回车建章。若兜底 1,真实 work revision 已是 N(>1)→ 后端乐观锁冲突 1041000002(偶发红)。 + // 传 undefined 则交给 ChapterPanel 既有 `expectedWorkRevision == null` 早返门禁,挡住 work 未加载时的过早提交。 + expectedWorkRevision={(work as { revision?: number } | undefined)?.revision} /> {/* 右侧编辑器动态装配区 */}