游戏发行入口改为服务端按模板派生

- 后台审核页删除「发行入口」输入框与前端 HTTPS 校验,审核请求只提交结论与公开修订号
- api-server 新增 GENARRATIVE_GAME_DISTRIBUTION_RELEASE_ENTRY_TEMPLATE 模板配置,审核通过时读版本取 gameId 并派生该游戏独立来源地址
- 模板缺 {gameId}、生产未配置模板、gameId 非主机安全字符或派生结果非法时审核通过失败关闭,非生产未配置时回落本地回环发行网关
- 同步更新发行来源 nginx 模板与说明、两份 api-server 环境变量样例、平台与运维主规范、发行里程碑实施计划和决策记录
- 真实栈 e2e 脚本不再传 entryUrl,并断言审核结果里的入口为服务端派生值
This commit is contained in:
2026-09-23 21:33:42 +08:00
parent e0c324c0ac
commit cf19241544
15 changed files with 172 additions and 102 deletions
@@ -501,7 +501,6 @@ test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键'
{
decision: 'approve',
expectedPublicationRevision: 3,
entryUrl: 'https://games.example.test/releases/game_1/index.html',
},
);
@@ -521,7 +520,6 @@ test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键'
body: JSON.stringify({
decision: 'approve',
expectedPublicationRevision: 3,
entryUrl: 'https://games.example.test/releases/game_1/index.html',
}),
}),
);
-1
View File
@@ -1110,7 +1110,6 @@ export interface AdminGameDistributionReviewRequest {
decision: 'approve' | 'reject';
expectedPublicationRevision: number;
reviewReason?: string;
entryUrl?: string;
}
export interface AdminGameDistributionReviewResponse {
@@ -9,10 +9,7 @@ import {
suspendAdminGameDistributionGame,
} from '../api/adminApiClient';
import type { AdminGameDistributionReviewEntry } from '../api/adminApiTypes';
import {
AdminGameDistributionReviewPage,
resolveGameReleaseEntryUrlError,
} from './AdminGameDistributionReviewPage';
import { AdminGameDistributionReviewPage } from './AdminGameDistributionReviewPage';
vi.mock('../api/adminApiClient', () => ({
isAdminApiError: vi.fn(
@@ -53,23 +50,7 @@ beforeEach(() => {
});
});
test('发行入口必须是带完整来源的 HTTPS 地址', () => {
expect(resolveGameReleaseEntryUrlError('')).toBe('请填写发行入口');
expect(
resolveGameReleaseEntryUrlError('http://games.test/a/index.html'),
).toBe('发行入口必须以 https:// 开头');
expect(
resolveGameReleaseEntryUrlError('https://games.test/a/index.html?token=1'),
).toBe('发行入口不能包含 query 或 fragment');
expect(
resolveGameReleaseEntryUrlError('https://u:p@games.test/a/index.html'),
).toBe('发行入口不能包含凭据');
expect(
resolveGameReleaseEntryUrlError('https://games.test/a/index.html'),
).toBe('');
});
test('通过审核时提交当前 publicationRevision 与发行入口并刷新列表', async () => {
test('通过审核只提交当前 publicationRevision 并刷新列表', async () => {
vi.mocked(reviewAdminGameDistributionVersion).mockResolvedValue({
version: { ...entry, status: 'published' },
replayed: false,
@@ -83,9 +64,8 @@ test('通过审核时提交当前 publicationRevision 与发行入口并刷新
);
await screen.findByText('game_1');
fireEvent.change(screen.getByLabelText('发行入口'), {
target: { value: 'https://games.test/releases/game_1/index.html' },
});
expect(screen.queryByLabelText('发行入口')).toBeNull();
expect(screen.getByText('通过后由系统分配发行地址')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '通过' }));
await waitFor(() =>
@@ -99,7 +79,6 @@ test('通过审核时提交当前 publicationRevision 与发行入口并刷新
expect(payload).toEqual({
decision: 'approve',
expectedPublicationRevision: 4,
entryUrl: 'https://games.test/releases/game_1/index.html',
});
await waitFor(() =>
expect(vi.mocked(listAdminGameDistributionReviews)).toHaveBeenCalledTimes(
@@ -47,26 +47,6 @@ function createReviewIdempotencyKey(versionId: string) {
return `game-review-${versionId}-${random}`.slice(0, 128);
}
export function resolveGameReleaseEntryUrlError(value: string) {
const normalized = value.trim();
if (!normalized) return '请填写发行入口';
if (!normalized.startsWith('https://')) {
return '发行入口必须以 https:// 开头';
}
if (normalized.includes('?') || normalized.includes('#')) {
return '发行入口不能包含 query 或 fragment';
}
try {
const parsed = new URL(normalized);
if (parsed.username || parsed.password) {
return '发行入口不能包含凭据';
}
} catch {
return '发行入口不是合法 URL';
}
return '';
}
export function AdminGameDistributionReviewPage({
token,
onUnauthorized,
@@ -78,9 +58,6 @@ export function AdminGameDistributionReviewPage({
const [busyVersionId, setBusyVersionId] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [statusMessage, setStatusMessage] = useState('');
const [entryUrlByVersion, setEntryUrlByVersion] = useState<
Record<string, string>
>({});
const [reasonByVersion, setReasonByVersion] = useState<
Record<string, string>
>({});
@@ -111,15 +88,8 @@ export function AdminGameDistributionReviewPage({
entry: AdminGameDistributionReviewEntry,
decision: 'approve' | 'reject',
) {
const entryUrl = (entryUrlByVersion[entry.versionId] ?? '').trim();
const reason = (reasonByVersion[entry.versionId] ?? '').trim();
if (decision === 'approve') {
const invalid = resolveGameReleaseEntryUrlError(entryUrl);
if (invalid) {
setErrorMessage(invalid);
return;
}
} else if (!reason) {
if (decision === 'reject' && !reason) {
setErrorMessage('拒绝审核必须填写理由');
return;
}
@@ -135,7 +105,6 @@ export function AdminGameDistributionReviewPage({
? {
decision,
expectedPublicationRevision: entry.publicationRevision,
entryUrl,
}
: {
decision,
@@ -269,23 +238,9 @@ export function AdminGameDistributionReviewPage({
<td>
<div className="admin-action-row">
<div className="admin-field">
<label
htmlFor={`game-release-url-${entry.versionId}`}
>
发行入口
</label>
<input
id={`game-release-url-${entry.versionId}`}
value={entryUrlByVersion[entry.versionId] ?? ''}
placeholder="https://"
onChange={(event) =>
setEntryUrlByVersion((current) => ({
...current,
[entry.versionId]: event.target.value,
}))
}
disabled={busy}
/>
<span className="admin-muted-text">
通过后由系统分配发行地址
</span>
</div>
<button
type="button"