恢复游戏分发完整实现(特性分支)

- 主站:游戏广场、详情、在线游玩、网页发布与作者中心,以及共享契约与客户端服务
- 后端:module-game-distribution 领域层、SpacetimeDB 表/迁移/绑定、spacetime-client facade、api-server 路由与发行网关
- 后台:游戏审核页(待审列表、通过/拒绝、安全下架)
- AGC:发布面板、本地导出包读取命令与发布服务,含默认跳过的真实链路测试
- 运维:发行来源 nginx 模板与门禁、game-distribution:publish 灰度发布开关、OSS PutObject 受控重试
- 文档:主规范、里程碑与实施计划、决策日志与踩坑记录
This commit is contained in:
2026-09-20 20:49:42 +08:00
parent cde34428f9
commit 328ac31844
113 changed files with 17590 additions and 22 deletions
@@ -43,6 +43,21 @@ const profileCenterMock = vi.hoisted(() => ({
rechargeCenter: null as { walletBalance: number } | null,
}));
const gameDistributionMock = vi.hoisted(() => ({
cancelGameVersion: vi.fn(),
createGame: vi.fn(),
createGameVersion: vi.fn(),
getGame: vi.fn(),
getGameVersion: vi.fn(),
listGames: vi.fn(),
listMyGames: vi.fn(),
submitGameVersion: vi.fn(),
unpublishGame: vi.fn(),
uploadGamePackage: vi.fn(),
}));
vi.mock('../../services/gameDistributionClient', () => gameDistributionMock);
vi.mock('../auth/AuthUiContext', () => ({
useAuthUi: () => authUiMock.value,
}));
@@ -187,6 +202,10 @@ describe('PlatformEntryActiveFlowShell', () => {
profileCenterMock.rechargeCenter = null;
authUiMock.value.user = null;
authUiMock.value.canAccessProtectedData = false;
window.history.replaceState(null, '', '/creation');
for (const mock of Object.values(gameDistributionMock)) {
mock.mockReset();
}
authUiMock.value.openLoginModal.mockReset();
responsiveMock.isDesktopLayout = true;
});
@@ -417,7 +436,7 @@ describe('PlatformEntryActiveFlowShell', () => {
within(navigation)
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['创作', '项目', '我的']);
).toEqual(['创作', '项目', '游戏', '我的']);
expect(
within(navigation)
.getByRole('button', { name: '创作' })
@@ -466,7 +485,7 @@ describe('PlatformEntryActiveFlowShell', () => {
).toBe('page');
});
it('keeps only profile reachable from the mobile dock', async () => {
it('keeps only games and profile reachable from the mobile dock', async () => {
responsiveMock.isDesktopLayout = false;
const setSelectionStage = vi.fn();
render(
@@ -483,7 +502,7 @@ describe('PlatformEntryActiveFlowShell', () => {
within(navigation)
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['我的']);
).toEqual(['游戏', '我的']);
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
expect(screen.getByRole('button', { name: '下载客户端' })).toBeTruthy();
expect(
@@ -568,7 +587,7 @@ describe('PlatformEntryActiveFlowShell', () => {
within(navigation)
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['我的']);
).toEqual(['游戏', '我的']);
expect(
within(navigation)
.getByRole('button', { name: '我的' })
@@ -653,6 +672,146 @@ describe('PlatformEntryActiveFlowShell', () => {
});
});
it('在我的游戏里进入发布新版本时沿用同一个 gameId', async () => {
authUiMock.value.user = {
id: 'user-1',
publicUserCode: '100001',
displayName: '测试用户',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
};
authUiMock.value.canAccessProtectedData = true;
gameDistributionMock.listMyGames.mockResolvedValue([
{
id: 'game-1',
title: '星轨防线',
summary: '守住轨道城',
description: '旧版资料',
category: '动作',
tags: ['塔防'],
coverColor: '#d77a51',
icon: '✦',
author: { id: 'user-1', name: '测试用户' },
deviceSupport: { desktop: true, mobile: false, touch: false },
status: 'published',
publicationRevision: 3,
currentVersion: null,
playCount: 1,
createdAt: '2026-09-20T08:00:00Z',
latestVersion: {
versionId: 'gamever-2',
gameId: 'game-1',
versionNumber: 2,
packageSha256: 'e'.repeat(64),
packageBytes: 1024,
status: 'published',
publicationRevision: 3,
reviewReason: null,
createdAt: '2026-09-20T08:00:00Z',
updatedAt: '2026-09-20T09:00:00Z',
},
versions: [],
},
]);
const setSelectionStage = vi.fn();
render(
<PlatformEntryFlowShellImpl
selectionStage="game-mine"
setSelectionStage={setSelectionStage}
/>,
);
fireEvent.click(await screen.findByRole('button', { name: '发布新版本' }));
expect(setSelectionStage).toHaveBeenCalledWith('game-publish', {
path: '/games/publish?game=game-1',
});
});
it('发布页带 game 参数时进入更新模式并预填既有资料', async () => {
authUiMock.value.user = {
id: 'user-1',
publicUserCode: '100001',
displayName: '测试用户',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
};
authUiMock.value.canAccessProtectedData = true;
gameDistributionMock.listMyGames.mockResolvedValue([
{
id: 'game-9',
title: '星轨防线',
summary: '守住轨道城',
description: '旧版资料',
category: '动作',
tags: ['塔防'],
coverColor: '#d77a51',
icon: '✦',
author: { id: 'user-1', name: '测试用户' },
deviceSupport: { desktop: true, mobile: false, touch: false },
status: 'published',
publicationRevision: 5,
currentVersion: null,
playCount: 1,
createdAt: '2026-09-20T08:00:00Z',
latestVersion: {
versionId: 'gamever-9',
gameId: 'game-9',
versionNumber: 4,
packageSha256: 'f'.repeat(64),
packageBytes: 2048,
status: 'published',
publicationRevision: 5,
reviewReason: null,
createdAt: '2026-09-20T08:00:00Z',
updatedAt: '2026-09-20T09:00:00Z',
},
versions: [],
},
]);
gameDistributionMock.getGameVersion.mockResolvedValue({
game: {
id: 'game-9',
title: '星轨防线',
summary: '守住轨道城',
description: '旧版资料',
category: '动作',
tags: ['塔防'],
publicationRevision: 5,
},
version: {
versionId: 'gamever-9',
versionNumber: 4,
status: 'published',
publicationRevision: 5,
recoveryAction: 'none',
},
});
window.history.replaceState(null, '', '/games/publish?game=game-9');
render(
<PlatformEntryFlowShellImpl
selectionStage="game-publish"
setSelectionStage={vi.fn()}
/>,
);
expect(
await screen.findByText(/正在为《星轨防线》发布新版本 v5/u),
).toBeTruthy();
expect(screen.getByLabelText('游戏名称')).toHaveProperty(
'value',
'星轨防线',
);
});
it('keeps guide and tool intent in editor navigation URLs', async () => {
const setSelectionStage = vi.fn();
const { rerender } = render(