diff --git a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx index dda1e4022..33f240423 100644 --- a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx +++ b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx @@ -367,10 +367,12 @@ test('详情展示发布者和冻结资料,并试玩当前待审版本', async ); }); -test('详情优先展示待审版本冻结价格,而不是游戏行当前价格', async () => { +test('详情价格以后端下发的版本冻结价为准,客户端不再本地叠加', async () => { vi.mocked(getAdminGameDistributionVersion).mockResolvedValue({ - // 游戏行当前是免费,待审版本冻结为 30 泥点:审核员要看到将要生效的价格。 - game: { ...detailResponse.game, priceMudPoints: 0 }, + // 后端 version_detail_payload 已按「待审版本冻结价优先、缺字段按 0」算好 + // game.priceMudPoints(此处待审版本冻结为 30 泥点,游戏行现价为 0); + // 审核页只渲染该字段,不再从 frozenMetadata 里二次取价。 + game: { ...detailResponse.game, priceMudPoints: 30 }, version: { ...detailResponse.version, frozenMetadata: { title: '测试游戏', priceMudPoints: 30 }, diff --git a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx index 46fb377cb..e9ad5184e 100644 --- a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx +++ b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx @@ -98,11 +98,8 @@ function applyFrozenGameMetadata( typeof metadata.orientation === 'string' ? metadata.orientation : game.orientation, - // 价格随版本冻结:审核页要展示待审版本将要生效的价格,而不是游戏行当前已公开价格。 - priceMudPoints: - typeof metadata.priceMudPoints === 'number' - ? metadata.priceMudPoints - : game.priceMudPoints, + // 价格不在这里二次叠加:服务端 `version_detail_payload` 已按「版本冻结价优先、 + // 缺字段按 0」把结果写进 `game.priceMudPoints`,客户端直接沿用该字段。 }; } diff --git a/src/components/game-distribution/GameDetailPage.tsx b/src/components/game-distribution/GameDetailPage.tsx index 3704408b3..eca60fa1c 100644 --- a/src/components/game-distribution/GameDetailPage.tsx +++ b/src/components/game-distribution/GameDetailPage.tsx @@ -497,12 +497,15 @@ function GameDetailContent({ const latestReviewReason = latestVersion?.reviewReason?.trim() ?? ''; const mobilePlaybackBlocked = isMobileViewport && !game.deviceSupport.mobile; /** - * 买断价与购买态:缺字段按免费/未购买兜底,付费且未购买时后端不下发可播放入口, - * 前端也不再展示「立即玩」,改由购买动作接手。 + * 买断价与购买态:缺字段按免费/未购买兜底。 + * 购买态只是服务端视角的一部分:作者本人与管理员并不会被记为已购买 + * (`purchased=false`),但服务端仍会下发公开入口。动作判定必须以 + * 「服务端是否授予可玩入口」为准,详情页不自行推断 owner/admin 身份。 */ const priceMudPoints = game.priceMudPoints ?? 0; - const isPaidLocked = - !ownerView && priceMudPoints > 0 && game.purchased !== true; + const canPlay = + Boolean(game.currentVersion?.entryUrl) || game.purchased === true; + const isPaidLocked = !ownerView && priceMudPoints > 0 && !canPlay; const hasPlayableEntry = Boolean(version?.entryUrl); /** * 公开视角按「后端是否下发入口」判定可否开玩:付费未购买时入口为空。 diff --git a/src/components/game-distribution/GameDistributionPages.test.tsx b/src/components/game-distribution/GameDistributionPages.test.tsx index 46466cfd3..1bf1fb301 100644 --- a/src/components/game-distribution/GameDistributionPages.test.tsx +++ b/src/components/game-distribution/GameDistributionPages.test.tsx @@ -288,6 +288,25 @@ describe('GameDetailPage', () => { expect(onPlay).toHaveBeenCalledWith('game-1'); }); + it('付费未购买但服务端已下发公开入口(作者/管理员)时显示立即玩而不是购买', async () => { + const onPlay = vi.fn(); + // 作者本人与管理员不会被记为已购买(purchased=false),但后端仍授予可玩入口。 + getGameMock.mockResolvedValueOnce( + createGame({ priceMudPoints: 120, purchased: false }), + ); + render( + , + ); + + const playButton = await screen.findByRole('button', { name: /立即玩/u }); + expect((playButton as HTMLButtonElement).disabled).toBe(false); + expect(screen.queryByRole('button', { name: /泥点购买/u })).toBeNull(); + expect(screen.getByText('120 泥点')).toBeTruthy(); + + fireEvent.click(playButton); + expect(onPlay).toHaveBeenCalledWith('game-1'); + }); + function createLockedPaidGame(): GameDistributionGame { return createGame({ priceMudPoints: 120,