fix(studio): 修新建章节双提交 + work 乐观锁版本兜底两处 bug(studio e2e 真连揪出)
studio e2e chapter-create-delete 偶发 code=1041000002,根因两个独立前端 bug:①ChapterPanel 双提交:input onKeyDown(Enter→submitCreate)+onBlur=submitCreate,Enter 提交成功后 setIsCreating(false)卸载 input→onBlur 二次触发,isCreatingPending(异步)+disabled 滞后挡不住→重复 POST(第 2 个同 reqRev→500)。修:submitCreate 加同步 useRef 守卫(提交中早返+置位+onSettled 复位),只挡同次重入、不挡首次/失焦补提交。②WorkspacePage expectedWorkRevision 兜底:原 workRevision(work)??1,work 详情异步未加载时兜成 1、但真实 revision 已 >1→乐观锁冲突。改传真实 revision(未加载 undefined→ChapterPanel 既有 ==null 早返挡过早提交)。 实证:单次新建 POST 修前 2(第 1 成功+第 2 1041000002)→修后 6 轮恒 1;chapter-create-delete 连跑 8 次全绿。相邻写路 spec 不回归、tsc 0 错、editor 单测 33/33。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
80106c2d40
commit
443a1f3414
@ -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;
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
{/* 右侧编辑器动态装配区 */}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user