From e0e3463550e788ebfd8944d0552e49cd2c9fb729 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 6 Oct 2026 18:14:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B1=E5=88=9B=E5=8F=91=E5=B8=83:=20?= =?UTF-8?q?=E8=A1=8D=E7=94=9F=E4=BD=9C=E5=93=81=E5=BF=85=E5=A1=AB=E3=80=8C?= =?UTF-8?q?=E6=9C=AC=E6=AC=A1=E6=A0=B8=E5=BF=83=E6=94=B9=E5=8A=A8=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E3=80=8D(=E5=89=8D=E7=AB=AF=E5=90=8C=E7=AD=89?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=20+=20=E6=8F=90=E4=BA=A4=20changeSummary)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GamePublishPage.test.tsx | 182 ++++++++++++++++++ .../game-distribution/GamePublishPage.tsx | 67 +++++++ 2 files changed, 249 insertions(+) diff --git a/src/components/game-distribution/GamePublishPage.test.tsx b/src/components/game-distribution/GamePublishPage.test.tsx index 6bfe3d94c..d92b03eb0 100644 --- a/src/components/game-distribution/GamePublishPage.test.tsx +++ b/src/components/game-distribution/GamePublishPage.test.tsx @@ -789,6 +789,188 @@ test('线上封面没有冻结素材时要求重新选择封面', async () => { expect(createGameVersion).not.toHaveBeenCalled(); }); +test('衍生作品发布新版本必须填「本次核心改动说明」', async () => { + vi.mocked(listMyGames).mockResolvedValue([ + { + id: 'game-1', + title: '星轨防线', + summary: '守住轨道城', + description: '旧版资料', + category: '动作', + tags: ['塔防'], + publicationRevision: 4, + // 有血缘 → 衍生作品:核心改动说明必填。 + lineage: { + generation: 1, + rootGameId: 'game-root', + rootTitle: '根原创', + parentGameId: 'game-root', + parentTitle: '根原创', + parentAuthorName: '原作者', + }, + latestVersion: { + versionId: 'gamever-2', + gameId: 'game-1', + versionNumber: 2, + packageSha256: 'd'.repeat(64), + packageBytes: 1024, + status: 'published', + publicationRevision: 4, + reviewReason: null, + createdAt: '2026-09-20T08:00:00Z', + updatedAt: '2026-09-20T09:00:00Z', + }, + versions: [], + } as never, + ]); + vi.mocked(getGameVersion).mockResolvedValue({ + game: { + id: 'game-1', + title: '星轨防线', + summary: '守住轨道城', + description: '旧版资料', + category: '动作', + tags: ['塔防'], + publicationRevision: 4, + }, + version: { + versionId: 'gamever-2', + versionNumber: 2, + status: 'published', + publicationRevision: 4, + recoveryAction: 'none', + priceMudPoints: 0, + frozenMetadata: { + coverAssetId: 'asset-cover-old', + coverObjectKey: 'game-distribution/cover/old.png', + screenshots: [ + { + assetId: 'asset-shot-old', + objectKey: 'game-distribution/screenshot/old.png', + }, + ], + }, + }, + } as never); + vi.mocked(createGameVersion).mockResolvedValue({ + gameId: 'game-1', + versionId: 'gamever-3', + versionNumber: 3, + status: 'awaiting_upload', + }); + + renderPublishPage(createAuthValue(), 'game-1'); + await screen.findByLabelText('游戏名称'); + + const field = await screen.findByLabelText('本次核心改动说明'); + expect(field).toBeTruthy(); + + const file = await buildZipFile(); + fireEvent.change(screen.getByLabelText(/发行包 ZIP/u), { + target: { files: [file] }, + }); + // 空着提交:前端同等校验拦截(服务端同样会以 FORK_CHANGE_SUMMARY_REQUIRED 失败关闭)。 + fireEvent.click(screen.getByRole('button', { name: '提交新版本审核' })); + expect( + await screen.findByText(/衍生作品必须填写「本次核心改动说明」/u), + ).toBeTruthy(); + expect(createGameVersion).not.toHaveBeenCalled(); + + const summaryText = + '把跳台改成三段,并重画全部背景与音效,同时调整了整体节奏'; + fireEvent.change(field, { target: { value: summaryText } }); + fireEvent.click(screen.getByRole('button', { name: '提交新版本审核' })); + + await waitFor(() => expect(createGameVersion).toHaveBeenCalledTimes(1)); + expect(vi.mocked(createGameVersion).mock.calls[0]?.[1]).toEqual( + expect.objectContaining({ changeSummary: summaryText }), + ); +}); + +test('母版发布新版本不渲染改动说明字段,请求体也不带它', async () => { + vi.mocked(listMyGames).mockResolvedValue([ + { + id: 'game-1', + title: '星轨防线', + summary: '守住轨道城', + description: '旧版资料', + category: '动作', + tags: ['塔防'], + publicationRevision: 4, + latestVersion: { + versionId: 'gamever-2', + gameId: 'game-1', + versionNumber: 2, + packageSha256: 'd'.repeat(64), + packageBytes: 1024, + status: 'published', + publicationRevision: 4, + reviewReason: null, + createdAt: '2026-09-20T08:00:00Z', + updatedAt: '2026-09-20T09:00:00Z', + }, + versions: [], + } as never, + ]); + vi.mocked(getGameVersion).mockResolvedValue({ + game: { + id: 'game-1', + title: '星轨防线', + summary: '守住轨道城', + description: '旧版资料', + category: '动作', + tags: ['塔防'], + publicationRevision: 4, + }, + version: { + versionId: 'gamever-2', + versionNumber: 2, + status: 'published', + publicationRevision: 4, + recoveryAction: 'none', + priceMudPoints: 0, + frozenMetadata: { + coverAssetId: 'asset-cover-old', + coverObjectKey: 'game-distribution/cover/old.png', + screenshots: [ + { + assetId: 'asset-shot-old', + objectKey: 'game-distribution/screenshot/old.png', + }, + ], + }, + }, + } as never); + vi.mocked(createGameVersion).mockResolvedValue({ + gameId: 'game-1', + versionId: 'gamever-3', + versionNumber: 3, + status: 'awaiting_upload', + }); + vi.mocked(uploadGamePackage).mockResolvedValue({ + versionId: 'gamever-3', + status: 'uploaded', + }); + vi.mocked(submitGameVersion).mockResolvedValue({ + version: { status: 'pending_review' }, + }); + + renderPublishPage(createAuthValue(), 'game-1'); + await screen.findByLabelText('游戏名称'); + expect(screen.queryByLabelText('本次核心改动说明')).toBeNull(); + + const file = await buildZipFile(); + fireEvent.change(screen.getByLabelText(/发行包 ZIP/u), { + target: { files: [file] }, + }); + fireEvent.click(screen.getByRole('button', { name: '提交新版本审核' })); + + await waitFor(() => expect(createGameVersion).toHaveBeenCalledTimes(1)); + expect(vi.mocked(createGameVersion).mock.calls[0]?.[1]).not.toHaveProperty( + 'changeSummary', + ); +}); + test('更新模式读不到归属游戏时失败关闭且不提交', async () => { vi.mocked(listMyGames).mockResolvedValue([]); diff --git a/src/components/game-distribution/GamePublishPage.tsx b/src/components/game-distribution/GamePublishPage.tsx index 9dc2cc97d..50060c4f2 100644 --- a/src/components/game-distribution/GamePublishPage.tsx +++ b/src/components/game-distribution/GamePublishPage.tsx @@ -58,8 +58,19 @@ type GameUpdateContext = { publicationRevision: number; versionNumber: number; title: string; + /** 衍生作品(有父作品血缘):发布新版本时必须填「本次核心改动说明」。 */ + isDerivative: boolean; }; +/** + * 「本次核心改动说明」的字数口径,与 Rust 侧 + * `module-game-distribution/src/domain.rs` 的 `GAME_DISTRIBUTION_CHANGE_SUMMARY_MIN_CHARS = 20` / + * `GAME_DISTRIBUTION_CHANGE_SUMMARY_MAX_CHARS = 500` 一致(按字符计,CJK 友好);服务端的失败码是 + * `FORK_CHANGE_SUMMARY_REQUIRED` / `FORK_CHANGE_SUMMARY_INVALID`。 + */ +const CHANGE_SUMMARY_MIN_CHARS = 20; +const CHANGE_SUMMARY_MAX_CHARS = 500; + let publishKeySeed = 0; function createPublishKey() { publishKeySeed += 1; @@ -82,6 +93,7 @@ export function GamePublishPage({ const [description, setDescription] = useState(''); const [category, setCategory] = useState('休闲'); const [tagsInput, setTagsInput] = useState(''); + const [changeSummary, setChangeSummary] = useState(''); const [deviceSupport, setDeviceSupport] = useState({ desktop: true, @@ -178,6 +190,8 @@ export function GamePublishPage({ publicationRevision: detail.version.publicationRevision, versionNumber: detail.version.versionNumber, title: detail.game.title, + // 血缘行存在(generation > 0)就是衍生作品;母版没有父作品。 + isDerivative: (game.lineage?.generation ?? 0) > 0, }); void applyVersionMediaDefaults(detail, { setCover: setCoverAsset, @@ -379,6 +393,27 @@ export function GamePublishPage({ setError(metadataError); return; } + // 衍生作品必须填「本次核心改动说明」:前端做同等校验并给可读提示(服务端同样失败关闭, + // 码为 FORK_CHANGE_SUMMARY_REQUIRED / FORK_CHANGE_SUMMARY_INVALID)。 + const normalizedChangeSummary = changeSummary.trim(); + if (updateContext.isDerivative) { + const charCount = [...normalizedChangeSummary].length; + if (charCount === 0) { + setError( + `衍生作品必须填写「本次核心改动说明」(不少于 ${CHANGE_SUMMARY_MIN_CHARS} 个字)。`, + ); + return; + } + if ( + charCount < CHANGE_SUMMARY_MIN_CHARS || + charCount > CHANGE_SUMMARY_MAX_CHARS + ) { + setError( + `「本次核心改动说明」需要 ${CHANGE_SUMMARY_MIN_CHARS}~${CHANGE_SUMMARY_MAX_CHARS} 个字(当前 ${charCount} 个字)。`, + ); + return; + } + } if (!packageFile) { setError('请选择发行包 ZIP'); return; @@ -413,6 +448,10 @@ export function GamePublishPage({ packageFileCount: prepared.fileCount, packageEntryPath: 'index.html', gameMetadata, + // 母版不传该字段(服务端可空/忽略),衍生作品传去重后的文本。 + ...(updateContext.isDerivative + ? { changeSummary: normalizedChangeSummary } + : {}), }, `${updateKey}:version`, ); @@ -782,6 +821,34 @@ export function GamePublishPage({ onChange={(e) => setTagsInput(e.target.value)} /> + {/* + 「本次核心改动说明」:**衍生作品必填**(服务端 `FORK_CHANGE_SUMMARY_REQUIRED`), + 母版可空(服务端忽略)。校验口径与 Rust 侧 + `module-game-distribution/src/domain.rs` 的 MIN/MAX 常量一致(按字符计,CJK 友好)。 + */} + {updateContext?.isDerivative ? ( +