Merge remote-tracking branch 'origin/master' into feat/tribo3d-integeration
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m38s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m4s
Project CI / Backend tests (pull_request) Successful in 5m5s
Project CI / Native shell tests (pull_request) Successful in 6m21s
Project CI / Frontend tests (pull_request) Successful in 2m20s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m11s
Project CI / Repository checks (pull_request) Failing after 1m42s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m13s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m38s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m4s
Project CI / Backend tests (pull_request) Successful in 5m5s
Project CI / Native shell tests (pull_request) Successful in 6m21s
Project CI / Frontend tests (pull_request) Successful in 2m20s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m11s
Project CI / Repository checks (pull_request) Failing after 1m42s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m13s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m29s
# Conflicts: # docs/project-memory/shared-memory/decision-log.md # docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
@@ -8,10 +8,12 @@ import {
|
||||
getAdminUserDetail,
|
||||
importAdminAgcTemplates,
|
||||
listAdminAgcTrackingEvents,
|
||||
listAdminGameDistributionGames,
|
||||
listAdminGameDistributionReviews,
|
||||
listAdminRechargeOrders,
|
||||
reconcileAdminUserConsumption,
|
||||
resolveAdminRechargeRefundManualReview,
|
||||
restoreAdminGameDistributionGame,
|
||||
reviewAdminGameDistributionVersion,
|
||||
suspendAdminGameDistributionGame,
|
||||
updateAdminAccount,
|
||||
@@ -501,7 +503,6 @@ test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键'
|
||||
{
|
||||
decision: 'approve',
|
||||
expectedPublicationRevision: 3,
|
||||
entryUrl: 'https://games.example.test/releases/game_1/index.html',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -521,12 +522,90 @@ test('游戏审核列表与审核动作使用约定的 URL、方法和幂等键'
|
||||
body: JSON.stringify({
|
||||
decision: 'approve',
|
||||
expectedPublicationRevision: 3,
|
||||
entryUrl: 'https://games.example.test/releases/game_1/index.html',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('游戏管理列表与恢复动作使用约定的 URL、方法和幂等键', async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ ok: true, data: { games: [] } }), {
|
||||
status: 200,
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
data: {
|
||||
game: {
|
||||
id: 'game/1',
|
||||
title: '测试游戏',
|
||||
status: 'published',
|
||||
publicationRevision: 10,
|
||||
},
|
||||
replayed: false,
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const controller = new AbortController();
|
||||
|
||||
await listAdminGameDistributionGames(
|
||||
'admin-token',
|
||||
{ limit: 80 },
|
||||
controller.signal,
|
||||
);
|
||||
await restoreAdminGameDistributionGame(
|
||||
'admin-token',
|
||||
' game/1 ',
|
||||
' game-restore-key-1 ',
|
||||
{ expectedPublicationRevision: 9 },
|
||||
);
|
||||
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
||||
'/admin/api/game-distribution/games?limit=50',
|
||||
);
|
||||
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer admin-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
||||
'/admin/api/game-distribution/games/game%2F1/restore',
|
||||
);
|
||||
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer admin-token',
|
||||
'Idempotency-Key': 'game-restore-key-1',
|
||||
}),
|
||||
body: JSON.stringify({ expectedPublicationRevision: 9 }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
restoreAdminGameDistributionGame('admin-token', ' ', 'key', {
|
||||
expectedPublicationRevision: 9,
|
||||
}),
|
||||
).toThrow('缺少游戏 ID');
|
||||
expect(() =>
|
||||
restoreAdminGameDistributionGame('admin-token', 'game-1', ' ', {
|
||||
expectedPublicationRevision: 9,
|
||||
}),
|
||||
).toThrow('恢复幂等键必须是 1 到 128 个字符');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('安全下架请求携带公开修订号、原因与幂等键', async () => {
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
|
||||
@@ -32,6 +32,9 @@ import type {
|
||||
AdminExternalApiKeyListQuery,
|
||||
AdminExternalApiKeyListResponse,
|
||||
AdminFeatureGateConfigResponse,
|
||||
AdminGameDistributionGameListResponse,
|
||||
AdminGameDistributionRestoreRequest,
|
||||
AdminGameDistributionRestoreResponse,
|
||||
AdminGameDistributionReviewListResponse,
|
||||
AdminGameDistributionReviewRequest,
|
||||
AdminGameDistributionReviewResponse,
|
||||
@@ -1241,6 +1244,46 @@ export function listAdminGameDistributionReviews(token: string, limit = 48) {
|
||||
);
|
||||
}
|
||||
|
||||
export function listAdminGameDistributionGames(
|
||||
token: string,
|
||||
options: { limit?: number } = {},
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const requestedLimit = options.limit ?? 50;
|
||||
const normalizedLimit = Number.isFinite(requestedLimit)
|
||||
? Math.min(Math.max(Math.trunc(requestedLimit), 1), 50)
|
||||
: 50;
|
||||
return request<AdminGameDistributionGameListResponse>(
|
||||
`/admin/api/game-distribution/games?limit=${normalizedLimit}`,
|
||||
{ token, signal },
|
||||
);
|
||||
}
|
||||
|
||||
export function restoreAdminGameDistributionGame(
|
||||
token: string,
|
||||
gameId: string,
|
||||
idempotencyKey: string,
|
||||
payload: AdminGameDistributionRestoreRequest,
|
||||
) {
|
||||
const normalizedGameId = gameId.trim();
|
||||
const normalizedKey = idempotencyKey.trim();
|
||||
if (!normalizedGameId) {
|
||||
throw new Error('缺少游戏 ID');
|
||||
}
|
||||
if (!normalizedKey || normalizedKey.length > 128) {
|
||||
throw new Error('恢复幂等键必须是 1 到 128 个字符');
|
||||
}
|
||||
return request<AdminGameDistributionRestoreResponse>(
|
||||
`/admin/api/game-distribution/games/${encodeURIComponent(normalizedGameId)}/restore`,
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
headers: { 'Idempotency-Key': normalizedKey },
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核游戏发行版本。幂等键由调用方生成并在同一次提交内复用,避免重复点击产生
|
||||
* 两条审核结论。
|
||||
|
||||
@@ -193,7 +193,10 @@ export interface AdminDashboardRangePayload {
|
||||
|
||||
export interface AdminDashboardMetricsPayload {
|
||||
generatedAssets: number;
|
||||
/** 已对冲退还(生成失败 / 精选审核返还 / LLM Router 冲正)的净消耗泥点。 */
|
||||
consumedMudPoints: number;
|
||||
/** 同期退还泥点,用于核对「消耗 + 退还」的毛消耗口径。 */
|
||||
refundedMudPoints: number;
|
||||
totalRegisteredUsers: number;
|
||||
newRegisteredUsers: number;
|
||||
newUserPaymentConversion: AdminDashboardPaymentConversionPayload;
|
||||
@@ -989,6 +992,8 @@ export interface AdminRechargeOrderEntryPayload {
|
||||
productTitle: string;
|
||||
productKind: string;
|
||||
amountCents: number;
|
||||
/** 真实支付金额(分):未支付 / 已关闭 / 已过期订单固定为 0,不能拿订单金额当实付。 */
|
||||
paidAmountCents: number;
|
||||
status: string;
|
||||
paymentChannel: string;
|
||||
paidAtMicros?: number | null;
|
||||
@@ -1028,6 +1033,8 @@ export interface AdminUserDetailResponse {
|
||||
phoneBound: boolean;
|
||||
wechatBound: boolean;
|
||||
historicalConsumedPoints: number;
|
||||
/** 累计充值金额(分):读取失败或命中读取上限时为 null,前端按未知展示。 */
|
||||
cumulativeRechargedCents?: number | null;
|
||||
canReconcileConsumption: boolean;
|
||||
wallet: AdminProfileWalletPayload;
|
||||
rechargeOrders: AdminRechargeOrderEntryPayload[];
|
||||
@@ -1137,7 +1144,6 @@ export interface AdminGameDistributionReviewRequest {
|
||||
decision: 'approve' | 'reject';
|
||||
expectedPublicationRevision: number;
|
||||
reviewReason?: string;
|
||||
entryUrl?: string;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionReviewResponse {
|
||||
@@ -1160,6 +1166,57 @@ export interface AdminGameDistributionSuspendResponse {
|
||||
replayed: boolean;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionGameVersionEntry {
|
||||
versionId: string;
|
||||
gameId: string;
|
||||
versionNumber: number;
|
||||
status: string;
|
||||
reviewReason: string | null;
|
||||
packageBytes: number;
|
||||
packageSha256: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
reviewedAt: string | null;
|
||||
publishedAt: string | null;
|
||||
entryUrl: string | null;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionGameEntry {
|
||||
gameId: string;
|
||||
title: string;
|
||||
author: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
status: string;
|
||||
versionCount: number;
|
||||
playCount: number;
|
||||
activeVersionId: string | null;
|
||||
publicationRevision: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
versions: AdminGameDistributionGameVersionEntry[];
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionGameListResponse {
|
||||
games: AdminGameDistributionGameEntry[];
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionRestoreRequest {
|
||||
expectedPublicationRevision: number;
|
||||
}
|
||||
|
||||
export interface AdminGameDistributionRestoreResponse {
|
||||
game: {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
publicationRevision: number;
|
||||
};
|
||||
replayed: boolean;
|
||||
}
|
||||
|
||||
export interface AdminAgcTemplatePayload {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user