修复付费作品作者与管理员在详情页被误判为未购买,并去掉后台审核价格的客户端回退
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

- GameDetailPage:动作判定改为「服务端是否授予可玩入口」优先(canPlay = currentVersion.entryUrl 存在或 purchased 为 true),作者本人与管理员虽 purchased=false 但入口已下发时显示「立即玩」而不是「N 泥点购买」

- GameDistributionPages.test:新增「付费未购买但服务端已下发公开入口(作者/管理员)时显示立即玩」用例,保留既有付费未购买显示购买、付费已购买显示立即玩用例

- AdminGameDistributionReviewPage:删除 applyFrozenGameMetadata 中 priceMudPoints 的客户端回退,价格单一口径沿用服务端 version_detail_payload 写好的 game.priceMudPoints

- AdminGameDistributionReviewPage.test:详情价格用例改为断言后端下发的版本冻结价(不再本地叠加冻结资料价格),历史版本无价格按免费的用例保持不变
This commit is contained in:
2026-10-05 17:47:32 +08:00
parent 532c847620
commit 054709db65
4 changed files with 33 additions and 12 deletions
@@ -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 },
@@ -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`,客户端直接沿用该字段。
};
}
@@ -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);
/**
* 公开视角按「后端是否下发入口」判定可否开玩:付费未购买时入口为空。
@@ -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(
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={onPlay} />,
);
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,