fix(游戏共创): 非法游标回归稳定码 THEME_INVALID_CURSOR(契约码此前在 HTTP 面不可达)
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (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
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 / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (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
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
端到端实测(54 项里唯一红的那条):`GET /api/game-distribution/themes?cursor=abc` 回 400,但
`error.code = "BAD_REQUEST"`——技术方案 §3.4 与主题 e2e 脚本承诺的稳定码 `THEME_INVALID_CURSOR`
在 HTTP 面上拿不到,客户端只能去匹配中文文案。
根因:`map_spacetime_error` 的 `THEME_` 分支前置条件是「消息含 `THEME_`」,而模块侧游标解析只回
中文串「主题列表游标格式无效」⇒ 落兜底分支成 400 + 通用 `BAD_REQUEST`;契约常量与那条映射分支
因此是死代码。且 api-server 原有单测**明确要求**文案不含 `THEME_`(口径留了两套)。
口径二选一,选**让码可达**(模块产出前缀),删掉「要求不含 `THEME_`」的断言:
- `module-game-distribution/src/errors.rs`:新增 6 个主题码常量
`GAME_DISTRIBUTION_THEME_{NOT_FOUND,BAD_REQUEST,INVALID_CURSOR,IDEMPOTENCY_CONFLICT,MEMBER_NOT_ROOT,MEMBER_GAME_NOT_FOUND}_CODE`,
并把 `Display` 从字面量改为**复用同一批常量**(消灭「常量改了、`Display` 没改」这类只有真栈才发现的漂移)。
- `theme.rs`:`parse_game_distribution_theme_cursor` 产出 `THEME_INVALID_CURSOR: 主题列表游标格式无效`;
文档注释更新为「只保留 `FORK_` 这个排在 `THEME_` 分支之前的子串禁忌」(其余子串已抢不到)。
- `lib.rs`:导出新增的 6 个码常量。
- 测试:
· `domain.rs::theme_errors_are_prefixed_with_their_machine_code` 改为**枚举全部 6 个主题码**(含唯一
不走领域变体的游标路径——正是当初漏掉它的原因),并加「任何新增码都必须进这张表」的覆盖完整性断言;
· `theme.rs::invalid_cursor_message_stays_in_the_bad_request_bucket` 增加「必须以稳定码开头」;
· api-server 新增 `theme_error_codes_are_reachable_from_module_messages`:用**模块真实产出的消息**
逐码断言 `(状态码, 稳定码)` 可达,并比对模块常量与 `shared-contracts` 常量是同一组字符串;
· api-server `theme_list_invalid_cursor_maps_to_bad_request` 改为断言映射出 `THEME_INVALID_CURSOR`,
只把 `FORK_` 留在禁忌列表里(并写明理由)。
门禁:`cargo test -p module-game-distribution` **100 passed**;`cargo test -p api-server game_distribution`
**86 passed**;`cargo check --all-targets` 0;DTO parity 58 组 / 10 构建器;`check:encoding` 0;
`cargo fmt --all -- --check` 0;`git diff --check` 0。
This commit is contained in:
@@ -98,12 +98,29 @@ import type {
|
||||
ProfileTaskConfigAdminResponse,
|
||||
ProfileWalletConfigAdminResponse,
|
||||
} from './adminApiTypes';
|
||||
import type {
|
||||
AdminCreateThemeRequest,
|
||||
AdminGameDistributionThemeListResponse,
|
||||
AdminGameDistributionThemeMemberResponse,
|
||||
AdminGameDistributionThemeMutationResponse,
|
||||
AdminPublicThemeDetail,
|
||||
AdminThemeStatusFilter,
|
||||
AdminUpdateThemeRequest,
|
||||
AdminUpsertThemeMemberRequest,
|
||||
} from './adminGameThemeTypes';
|
||||
|
||||
const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope';
|
||||
const ADMIN_API_BASE_URL = normalizeBaseUrl(
|
||||
import.meta.env.VITE_ADMIN_API_BASE_URL ?? '',
|
||||
);
|
||||
|
||||
/**
|
||||
* 后台主题列表的 `limit` 上界:与服务端 `MAX_ADMIN_THEME_LIST_LIMIT` 同值。
|
||||
*
|
||||
* 服务端按上限截断(不报错),客户端也按同一口径夹一次,避免发出一条注定被截断的请求。
|
||||
*/
|
||||
const ADMIN_THEME_LIST_LIMIT_MAX = 200;
|
||||
|
||||
interface AdminRequestOptions {
|
||||
method?: string;
|
||||
token?: string;
|
||||
@@ -1488,3 +1505,135 @@ export function importAdminAgcTemplates(token: string, formData: FormData) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台共创主题列表:`GET /admin/api/game-distribution/themes?limit=&status=`。
|
||||
*
|
||||
* 含 draft / archived;`status=all`(或省略)不过滤。**没有游标**:`limit` 上限 200,服务端按
|
||||
* 上限截断,这里也按同一口径夹一次,避免把一条必然被截断的请求发出去。
|
||||
*/
|
||||
export function listAdminGameDistributionThemes(
|
||||
token: string,
|
||||
options: {
|
||||
limit?: number;
|
||||
status?: AdminThemeStatusFilter;
|
||||
} = {},
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const requestedLimit = options.limit ?? ADMIN_THEME_LIST_LIMIT_MAX;
|
||||
const normalizedLimit = Number.isFinite(requestedLimit)
|
||||
? Math.min(Math.max(Math.trunc(requestedLimit), 1), ADMIN_THEME_LIST_LIMIT_MAX)
|
||||
: ADMIN_THEME_LIST_LIMIT_MAX;
|
||||
const params = new URLSearchParams({ limit: String(normalizedLimit) });
|
||||
const status = options.status?.trim();
|
||||
if (status && status !== 'all') params.set('status', status);
|
||||
return request<AdminGameDistributionThemeListResponse>(
|
||||
`/admin/api/game-distribution/themes?${params.toString()}`,
|
||||
{ token, signal },
|
||||
);
|
||||
}
|
||||
|
||||
/** 后台创建主题:`theme_id` 由服务端生成,客户端只给字段;写操作必须带幂等键。 */
|
||||
export function createAdminGameDistributionTheme(
|
||||
token: string,
|
||||
idempotencyKey: string,
|
||||
payload: AdminCreateThemeRequest,
|
||||
) {
|
||||
return request<AdminGameDistributionThemeMutationResponse>(
|
||||
'/admin/api/game-distribution/themes',
|
||||
{
|
||||
token,
|
||||
method: 'POST',
|
||||
headers: { 'Idempotency-Key': normalizeThemeIdempotencyKey(idempotencyKey) },
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 后台整体覆盖主题字段:`name/summary/badge/sortOrder/status` 都是必填(不是部分更新)。 */
|
||||
export function updateAdminGameDistributionTheme(
|
||||
token: string,
|
||||
themeId: string,
|
||||
idempotencyKey: string,
|
||||
payload: AdminUpdateThemeRequest,
|
||||
) {
|
||||
return request<AdminGameDistributionThemeMutationResponse>(
|
||||
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}`,
|
||||
{
|
||||
token,
|
||||
method: 'PUT',
|
||||
headers: { 'Idempotency-Key': normalizeThemeIdempotencyKey(idempotencyKey) },
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增 / 改主题成员:**不要求幂等键**(成员身份完全由路径给出,重复调用只改 `sortOrder`)。
|
||||
*
|
||||
* 只允许根作品:非根作品服务端回 409 `THEME_MEMBER_NOT_ROOT`。
|
||||
*/
|
||||
export function upsertAdminGameDistributionThemeMember(
|
||||
token: string,
|
||||
themeId: string,
|
||||
rootGameId: string,
|
||||
payload: AdminUpsertThemeMemberRequest,
|
||||
) {
|
||||
return request<AdminGameDistributionThemeMemberResponse>(
|
||||
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}/members/${encodeURIComponent(normalizeRootGameId(rootGameId))}`,
|
||||
{ token, method: 'PUT', body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
/** 移除主题成员:不要求幂等键;成员不存在也算成功(200)。 */
|
||||
export function removeAdminGameDistributionThemeMember(
|
||||
token: string,
|
||||
themeId: string,
|
||||
rootGameId: string,
|
||||
) {
|
||||
return request<AdminGameDistributionThemeMemberResponse>(
|
||||
`/admin/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}/members/${encodeURIComponent(normalizeRootGameId(rootGameId))}`,
|
||||
{ token, method: 'DELETE' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开主题详情(匿名单读)。
|
||||
*
|
||||
* 后台用它列出**已发布**主题的当前可见成员:服务端目前没有「后台读成员」的接口(只有按根作品 ID
|
||||
* 的 PUT/DELETE),可见成员能且只能从这条公开路径取得。未发布 / 已归档主题这里会 404,调用方按
|
||||
* 「拿不到成员列表」处理,不要把它渲染成空列表。
|
||||
*/
|
||||
export function getPublicGameDistributionTheme(
|
||||
themeId: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return request<AdminPublicThemeDetail>(
|
||||
`/api/game-distribution/themes/${encodeURIComponent(normalizeThemeId(themeId))}`,
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeThemeId(themeId: string) {
|
||||
const normalized = themeId.trim();
|
||||
if (!normalized) {
|
||||
throw new Error('缺少主题 ID');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeRootGameId(rootGameId: string) {
|
||||
const normalized = rootGameId.trim();
|
||||
if (!normalized) {
|
||||
throw new Error('缺少根作品 ID');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeThemeIdempotencyKey(idempotencyKey: string) {
|
||||
const normalized = idempotencyKey.trim();
|
||||
if (!normalized || normalized.length > 128) {
|
||||
throw new Error('主题写操作幂等键必须是 1 到 128 个字符');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user