补齐游戏分发发布恢复链路并修掉双击提交建两份游戏
Project CI / AI game creator shell Rust crates (push) Successful in 1m37s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m7s
Project CI / Backend tests (push) Successful in 4m12s
Project CI / Frontend tests (push) Successful in 2m9s
Project CI / Native shell tests (push) Successful in 6m20s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m28s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m31s
Project CI / AI game creator shell web tests (push) Successful in 1m40s
Project CI / Repository checks (push) Successful in 2m25s

- 新增 scripts/check-game-distribution-web-publish-recovery-e2e.mjs:上传中断留草稿、关窗/登录失效后重登续传沿用原 versionId、双击只落一份、换账号不显示他人草稿,共 10 项 PASS
- GamePublishPage 加同步 submitInFlightRef 在途守卫与按次稳定的 Idempotency-Key(失败重试复用、成功后清空),双击不再创建第二份游戏
- 游戏分发里程碑阶段 C 第 5 条改为已勾选
- pitfalls 记录「点一次会建资源的入口需要同步守卫或稳定幂等键」
This commit is contained in:
kdletters
2026-09-28 20:37:48 +08:00
parent bbbd91db88
commit bbe416ca60
5 changed files with 401 additions and 7 deletions
@@ -196,6 +196,19 @@ export function GamePublishPage({
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [publishedGameId, setPublishedGameId] = useState('');
/**
* 同步的在途提交标记与稳定的幂等键。
*
* `isSubmitting` 是 React 状态,双击发生在同一次渲染窗口里时还没生效;再叠上
* `prepareGamePackage` 的压缩耗时,两次点击会各建一份游戏。这里用 ref 同步拦截,
* 并让同一次发布(含失败后重试)复用同一组 Idempotency-Key,服务端按幂等重放。
*/
const submitInFlightRef = useRef(false);
const publishKeyRef = useRef<string | null>(null);
const resolvePublishKey = () => {
if (!publishKeyRef.current) publishKeyRef.current = createPublishKey();
return publishKeyRef.current;
};
const [draft, setDraft] = useState<GamePublishDraft | null>(null);
const [draftDetail, setDraftDetail] =
useState<GameDistributionVersionDetail | null>(null);
@@ -420,10 +433,12 @@ export function GamePublishPage({
setError('请选择上次那个发行包 ZIP 继续上传');
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setIsSubmitting(true);
setError('');
try {
const resumeKey = createPublishKey();
const resumeKey = resolvePublishKey();
if (draftDetail.version.recoveryAction !== 'submit') {
const prepared = await prepareGamePackage(packageFile as File);
await uploadGamePackage(
@@ -444,11 +459,13 @@ export function GamePublishPage({
setDraftDetail(null);
setIsResumingDraft(false);
setPublishedGameId(draftDetail.game.id);
publishKeyRef.current = null;
} catch (resumeError: unknown) {
setError(
resumeError instanceof Error ? resumeError.message : '继续发布失败',
);
} finally {
submitInFlightRef.current = false;
setIsSubmitting(false);
}
}
@@ -482,11 +499,13 @@ export function GamePublishPage({
setError('请选择发行包 ZIP');
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setIsSubmitting(true);
setError('');
try {
const prepared = await prepareGamePackage(packageFile);
const updateKey = createPublishKey();
const updateKey = resolvePublishKey();
const inputModes: GameDistributionInputMode[] = ['keyboard', 'mouse'];
if (deviceSupport.touch) inputModes.push('touch');
const tags = tagsInput
@@ -535,11 +554,13 @@ export function GamePublishPage({
? { ...current, versionNumber: version.versionNumber }
: current,
);
publishKeyRef.current = null;
} catch (updateError: unknown) {
setError(
updateError instanceof Error ? updateError.message : '发布新版本失败',
);
} finally {
submitInFlightRef.current = false;
setIsSubmitting(false);
}
}
@@ -573,6 +594,8 @@ export function GamePublishPage({
setError('请选择发行包 ZIP');
return;
}
if (submitInFlightRef.current) return;
submitInFlightRef.current = true;
setIsSubmitting(true);
setError('');
try {
@@ -596,7 +619,7 @@ export function GamePublishPage({
inputModes,
orientation,
};
const publishKey = createPublishKey();
const publishKey = resolvePublishKey();
const game = await createGame(gameMetadata, `${publishKey}:game`);
const version = await createGameVersion(
game.id,
@@ -631,11 +654,13 @@ export function GamePublishPage({
);
clearPublishDraft();
setPublishedGameId(game.id);
publishKeyRef.current = null;
} catch (submitError: unknown) {
setError(
submitError instanceof Error ? submitError.message : '发布游戏失败',
);
} finally {
submitInFlightRef.current = false;
setIsSubmitting(false);
}
}