docs(游戏共创): 共创主题文档回填(§3.4 接口表 / §3.10 实现状态 / 里程碑 Status 与验收)
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m57s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / Backend tests (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 / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m57s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / Backend tests (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 / Native shell tests (pull_request) Has been cancelled
服务端三块(`2fa201e0d` 模型与纯函数、`742723a58` 公开读、`033e3aa79` 后台写)已落地,本次把文档从「只落设计」补齐到「服务端已实现」,并写死一条已知限制。
- **技术方案 `§3.4`**:新增 5 条接口行(真实路由/鉴权/错误码/字段名均以 `git show` 核对代码为准,非猜测)——公开 `GET /api/game-distribution/themes`(匿名 + `no-store`;只含 `published`;limit 缺省 20 / 上限 50 / 超界截断;游标 `"{createdAtMicros}:{themeId}"`;非法游标 400;末页 `nextCursor: null`;`memberCount` = 公开可见成员数)、`GET /api/game-distribution/themes/{themeId}`(顶层扁平 `{themeId,name,summary,badge,memberCount,roots}`;`roots` 复用公开目录同一份投影;不存在/draft/archived → 404 不返回空壳;已发布空成员 → 200 + 空 `roots`),以及 `GET /games/{gameId}` 的 `themes` 增量(匿名与登录都发、空数组恒发;先取根再按根反查 ⇒ 第 N 代作品也能跳主题页);后台 5 条(`require_admin_auth` + `AuthenticatedAdmin`,未登录/失效 401 `UNAUTHORIZED`、非 admin 403;创建与编辑要求 `Idempotency-Key`,同键重放 `replayed: true`、同键换请求 409 `THEME_IDEMPOTENCY_CONFLICT`;成员增删**不要求**幂等键(确定性主键天然幂等,删不存在也 200);非根成员 409 `THEME_MEMBER_NOT_ROOT`;后台列表含 draft/archived、limit 缺省与上限 200、无游标;`theme_id` 服务端生成 `theme-{uuid}`)。
- **`§3.10`**:各小节「本轮实现 / 留白」标注更新为现状(服务端已实现;公开前端与后台 UI 仍留白);`§3.10.6`/`§3.10.9` 明确写下**已知限制**——主题详情 `roots` 沿用页上限 **50** 且 `memberCount == roots.len()`,可见成员 > 50 时会**静默截断且没有「还有更多」标志**,三个备选(去上限 / 给成员加游标 / `memberCount` 报真实可见数)待拍板,本轮不改契约。
- **`§7`**:第 3 条(共创主题)改为「设计已采纳 + 服务端已实现」,仍待拍板收敛为两条:① 主题级排序口径(公开列表当前按 `created_at` 倒序 + `themeId` 兜底;若要按运营 `sort_order` 展示需改游标格式并同步前端);② 上述 `roots` 50 上限行为。
- **里程碑文件**:`Status` 由 `proposed` → 「服务端已实现(附四个提交号);公开前端与后台管理 UI 待做」,Version 1.0 → 1.1,新增「已落地提交」行;实施清单 A–F 标注已落地提交、G 标未做(待前端)、H 分「已落地层 / 未做层」、I 补执行状态;**验收标准勾 17/19**(第 17 条公开前端、第 18 条 SPA 路由门禁留空并注「等前端」);文末写明「dev 栈端到端整链未跑(`scripts/check-game-distribution-theme-e2e.mjs` 未创建),依赖真实库存的运行时分支目前只到单元/结构/映射层」。
- **数据契约文档**:两张主题表小节的「写入路径属后续里程碑」改为现状(读/写 procedure 与事务名),表结构描述未动。
- **门禁**:`check:doc-index` 0(252 份 Markdown,current=121 / historical=15 / review=1);`check:encoding` 0(5414 files);`git diff --check` 0。
- **环境限制(标注,非绿)**:`check:generated-bindings` 本机 `EXIT=1`,原因是 `apps/ai-game-creator-shell/src-tauri/build.rs:167` 校验随包资源时缺 `bin/win-x64/agc_godot_editor.dll`(需先跑随包资源准备步骤)——纯环境前置,与本次只改 `docs/**` 无关;该检查的 AGC 段本地从来跑不了(CI 绿)。
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
| 字段 | 值 |
|
||||
| ----------- | --------------------------------------------------------------------- |
|
||||
| Version | 1.0 |
|
||||
| Status | proposed(设计已定稿于技术方案 §3.10;**本轮只落设计,未写业务代码**) |
|
||||
| Version | 1.1 |
|
||||
| Status | 服务端已实现(`2fa201e0d` 模型/纯函数/契约、`742723a58` 公开读、`033e3aa79` 后台写);**公开前端(共创 Tab / 主题页 / 详情入口)与后台管理 UI 待做** |
|
||||
| Date | 2026-10-06 |
|
||||
| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md`(§3.10 共创主题) |
|
||||
| 已落地提交 | `063c04a1b`(设计定稿:技术方案 §3.10 + 本文件)、`2fa201e0d`(数据模型 / 领域纯函数 / 契约 / 数据契约表 94→96)、`742723a58`(公开读路径:3 个 procedure + client + 2 条公开路由 + 作品详情 `themes` 增量)、`033e3aa79`(后台写路径:5 条 admin 路由 + 事务 + 幂等 + `THEME_*` 错误码) |
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -40,12 +41,16 @@
|
||||
- M3(族谱与衍生列表)已落地:主题页的树**直接复用** `/games/{id}/lineage`,本里程碑不重建任何树查询或树整形。
|
||||
- 公开目录同一份 `public_game_payload` 投影已存在于 api-server(主题详情与成员卡片都复用它)。
|
||||
- 游标分页惯例已由 `/my-collections`(2026-10-06)落地:默认 20 / 上限 50 / 游标 `"{micros}:{id}"` / 非法游标 400 / 末页 `nextCursor: null`。
|
||||
- 设计已由技术方案 §3.10 定稿(含四条留白)。**待产品确认的只有「主题级排序口径」**(见文末「待确认项」)。
|
||||
- 设计已由技术方案 §3.10 定稿(含四条留白)。**待产品确认的有两条**:①「主题级排序口径」;② 主题详情 `roots` 的 **50 上限**行为(见文末「待确认项」)。
|
||||
|
||||
## 实施清单(下一轮照此执行)
|
||||
## 实施清单(执行结果)
|
||||
|
||||
A–F 已落地(服务端);G 前端与后台 UI 未做。
|
||||
|
||||
### A. 数据模型与迁移
|
||||
|
||||
**已落地(`2fa201e0d`)。**
|
||||
|
||||
1. 在 `server-rs/crates/spacetime-module/src/game_distribution.rs` 追加两张表(列、索引、注释按技术方案 §3.10.1 的表声明逐字落地):
|
||||
- `game_distribution_theme`:`theme_id`(PK,`theme-*`)、`name`、`summary`、`badge`、`sort_order: i64`、`status: String`、`created_by_user_id`、`created_at`、`updated_at`;具名索引 `by_game_distribution_theme_created_at`(btree `created_at`)。
|
||||
- `game_distribution_theme_member`:`member_id`(PK,`"{theme_id}:{root_game_id}"`)、`theme_id`、`root_game_id`、`sort_order: i64`、`created_at`;**两条具名 btree 索引** `by_game_distribution_theme_member_theme_id`、`by_game_distribution_theme_member_root_game_id`。
|
||||
@@ -55,6 +60,8 @@
|
||||
|
||||
### B. 领域纯函数(`server-rs/crates/module-game-distribution/src/theme.rs`,新文件,不碰 `ReducerContext`)
|
||||
|
||||
**已落地(`2fa201e0d`;`033e3aa79` 补充后台 `status` 过滤白名单与文本上限校验)。**
|
||||
|
||||
| 函数 / 常量 | 职责 | 关键约束 |
|
||||
| --- | --- | --- |
|
||||
| `GAME_DISTRIBUTION_THEME_STATUSES: [&str; 3]` | `["draft", "published", "archived"]` | 三处共用同一个白名单(可见性、后台列表过滤、DTO 校验),不复制字面量 |
|
||||
@@ -72,6 +79,8 @@
|
||||
|
||||
### C. 事务与 procedure(`server-rs/crates/spacetime-module/src/game_distribution.rs`)
|
||||
|
||||
**已落地(读:`742723a58`;写:`033e3aa79`)。**
|
||||
|
||||
命名沿用既有:**写** = `*_and_return`,**读** = `list_*` / `get_*`。
|
||||
|
||||
| procedure | 事务职责 |
|
||||
@@ -90,6 +99,8 @@
|
||||
|
||||
### D. api-server 路由(`server-rs/crates/api-server/src/modules/game_distribution.rs`)
|
||||
|
||||
**已落地(公开族:`742723a58`;后台族:`033e3aa79`)。**
|
||||
|
||||
公开族(挂 `public_games` 那一支,带 `add_no_store_response_headers`):
|
||||
|
||||
| 方法 / 路径 | handler | 响应 | 错误 |
|
||||
@@ -110,6 +121,8 @@
|
||||
|
||||
### E. 错误码映射(api-server 集中映射,沿用 `FORK_*` 那套「前缀字符串 → 状态码」写法)
|
||||
|
||||
**已落地(6 个 `THEME_*` 码:`742723a58`;领域错误类型:`033e3aa79`)。** 未登记的码兜底 **400 `THEME_ERROR`**(不落 axum 默认 422 纯文本)。
|
||||
|
||||
| 领域码 | HTTP | 触发 |
|
||||
| --- | --- | --- |
|
||||
| `THEME_NOT_FOUND` | 404 | 后台按 `theme_id` 找不到主题(公开侧「不存在 / 未发布」一律 404 且不带该码) |
|
||||
@@ -122,13 +135,17 @@
|
||||
|
||||
### F. 契约与 DTO 同步
|
||||
|
||||
**已落地(类型与数据契约表:`2fa201e0d`;5 条响应构建器登记:`742723a58`)。** DTO parity 现为 **58 组类型 / 10 个手拼响应构建器**。
|
||||
|
||||
- Rust DTO:`server-rs/crates/shared-contracts/src/game_distribution.rs`(主题列表 / 详情 / 后台写请求的私有类型;**不下发**任何对象键或内部计数)。
|
||||
- TS DTO:`packages/shared/src/contracts/gameDistribution.ts`。
|
||||
- `scripts/check-game-distribution-dto-parity.mjs`:登记新响应构建器(`public_themes_payload` / `public_theme_detail_payload` / 详情 `themes` 增量构建器),证明这些路径确实会发出新键。
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`:**随表落地时**补 `game_distribution_theme` / `game_distribution_theme_member` 小节(本轮不动该文档;表落地后 `check:spacetime-schema` 会要求)。
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`:**已随表落地补齐**(`2fa201e0d`,两张表小节;`check:spacetime-schema` 现为 **96** tables)。
|
||||
|
||||
### G. 前端(公开侧;后台 UI 不在本轮)
|
||||
|
||||
**未做(待前端里程碑)。** 下表是待执行的改动清单:
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `src/routing/activeAppPageRoutes.ts` | 新增 `['game-themes', '/games/themes']`、`['game-theme', '/games/theme']`(详情用 `?id=<themeId>`,与 `/games/lineage?id=` 同款);同步 `SelectionStage` 类型 |
|
||||
@@ -143,6 +160,8 @@
|
||||
|
||||
### H. 测试清单
|
||||
|
||||
**已落地**:纯函数(`module-game-distribution`,含 `033e3aa79` 补的 status 过滤 / 文本校验)、事务结构断言(`spacetime-module`)、api-server 路由与错误码定向测试、契约 parity。**未做**:前端 vitest、真实 dev 栈 e2e(`scripts/check-game-distribution-theme-e2e.mjs` 尚未创建)。
|
||||
|
||||
| 层 | 用例 |
|
||||
| --- | --- |
|
||||
| 纯函数(`module-game-distribution`) | 状态白名单(合法三态 / 未知值);主题公开可见性(`draft` / `archived` false);成员可见性四组合 + 「未删已公开但无当前公开版本」必须 false;`member_id` 确定性(同输入同输出、不同主题不同行);成员只允许根(`has_lineage_row` true → 拒绝);游标编解码往返 + 非法游标(缺冒号 / 非数字 / 空 ID)+ 解析只切第一个冒号;页大小归一化(缺省 20 / 0 取默认 / 超界截断到 50);**过滤后切页不重不漏**(可见性过滤位置在排序之前);排序兜底(`sort_order` 相同按 `member_id` 升序;`created_at` 相同按 `theme_id` 升序) |
|
||||
@@ -183,30 +202,36 @@ E2E_ADMIN_USER=<管理员> E2E_ADMIN_PASSWORD=<密码> node scripts/check-game-d
|
||||
|
||||
新增 npm script `check:game-distribution-theme-e2e` → `node scripts/check-game-distribution-theme-e2e.mjs`(与 `check:game-distribution-collection-e2e` 同族命名)。
|
||||
|
||||
**执行状态**:已跑门禁 —— `check:spacetime-schema`(**96** tables)、`check:game-distribution-dto-parity`(**58 组 / 10 构建器**)、`check:encoding`、`check:doc-index`、`git diff --check`、`cargo test`(见下「验收标准」的勾选与「证据要求」的现状)。**未跑**:前端 vitest / typecheck / lint、`check:nginx-spa-routes`、`check:pingora-route-parity`(前端未动,且未新增 SPA 路由)、以及 `check-game-distribution-theme-e2e.mjs`(脚本尚未创建)。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 两张表与两条成员索引落地;schema 检查通过;生成绑定只含与本次 schema 相关的文件。
|
||||
- [ ] `theme_id` 由服务端生成、形如 `theme-*`;`member_id` 为 `"{theme_id}:{root_game_id}"`,重复添加同一 (主题, 根) 不产生第二行。
|
||||
- [ ] 成员只允许根:目标作品有血缘行(非根)时写入被拒绝且不写库;无血缘行的作品可入成员。
|
||||
- [ ] 同一根可同时属于多个主题,各主题各有独立成员行;作品详情的 `themes` 返回**多值**。
|
||||
- [ ] 公开侧只出现 `status == published` 的主题;`draft` / `archived` 主题的详情与列表均不可见。
|
||||
- [ ] 成员只出现「公开未删且存在当前公开版本」的根;成员作品下架 / 软删除后从投影中跳过但**行不删除**,重新公开后自动回到主题页。
|
||||
- [ ] 主题不存在或未发布时详情返回 404,不返回空壳;已发布但可见成员为空时返回 200 + 空成员列表(前端按空态而非错误渲染)。
|
||||
- [ ] 公开主题列表分页沿用既有游标惯例:默认 20、上限 50、超界截断;非法游标 400;`nextCursor` 为真实值且末页为 `null`;排序为 `created_at` 倒序 + `theme_id` 升序兜底,翻页不重不漏。
|
||||
- [ ] 主题详情的 `roots` 按 `sort_order` 升序 + `member_id` 升序稳定排序,逐条为既有公开作品投影(不泄露对象键、不泄露未公开作品信息)。
|
||||
- [ ] 作品详情新增 `themes`:**第 N 代作品**(非根)也能看到并跳到其所属公开主题(按根反查),且只含公开主题。
|
||||
- [ ] 主题列表与详情的 `memberCount` 等于同响应里可见成员数(`roots` 长度),不含草稿 / 已下架成员。
|
||||
- [ ] 两条公开主题路径匿名可读且带 `Cache-Control: no-store`。
|
||||
- [ ] 后台五条路由未带 admin 会话一律 401;鉴权复用既有 `require_admin_auth` + `AuthenticatedAdmin`,不自造。
|
||||
- [ ] 后台写接口错误码可区分:空名 / 非法状态 400、未知主题 404、作品不存在 404、非根 409、同键不同请求 409;同键重放如实回报幂等重放。
|
||||
- [ ] 后台 `DELETE` 成员重复调用结果相同且成功(幂等),成员不存在不是错误。
|
||||
- [ ] 后台列表含 `draft` / `archived` 主题;非法 `status` 过滤值返回 400。
|
||||
- [ ] 公开前端:共创 Tab 能列出主题并进入主题页;主题页按成员根渲染**多棵树**(复用 `/games/{id}/lineage`);已发布空主题显示空态;详情页 `themes` 能跳到主题页。
|
||||
- [ ] 新增路由在桌面与窄屏可用,且 `check:nginx-spa-routes` / `check:pingora-route-parity` 通过。
|
||||
- [ ] 契约同步:DTO parity 通过;数据契约表文档随表落地补齐。
|
||||
- [x] 两张表与两条成员索引落地;schema 检查通过(96 tables);生成绑定随 `2fa201e0d` 提交。
|
||||
- [x] `theme_id` 由服务端生成、形如 `theme-*`;`member_id` 为 `"{theme_id}:{root_game_id}"`,重复添加同一 (主题, 根) 不产生第二行。
|
||||
- [x] 成员只允许根:目标作品有血缘行(非根)时写入被拒绝且不写库(纯函数 `game_distribution_theme_root_acceptable` 单测 + 事务结构断言);无血缘行的作品可入成员。
|
||||
- [x] 同一根可同时属于多个主题,各主题各有独立成员行;作品详情的 `themes` 返回**多值**(DTO 为数组 + parity 钉住)。
|
||||
- [x] 公开侧只出现 `status == published` 的主题;`draft` / `archived` 主题的详情与列表均不可见(纯函数单测 + 事务分别断言列表与详情两条路径)。
|
||||
- [x] 成员只出现「公开未删且存在当前公开版本」的根;成员作品下架 / 软删除后从投影中跳过但**行不删除**,重新公开后自动回到主题页(可见性委托收藏口径,逐格单测 + `member_row_survives_unpublish_and_returns_after_republish` + 事务「不删行」结构断言)。
|
||||
- [x] 主题不存在或未发布时详情返回 404,不返回空壳;已发布但可见成员为空时返回 200 + 空成员列表。
|
||||
- [x] 公开主题列表分页沿用既有游标惯例:默认 20、上限 50、超界截断;非法游标 400;`nextCursor` 为真实值且末页为 `null`;排序为 `created_at` 倒序 + `theme_id` 升序兜底,翻页不重不漏。
|
||||
- [x] 主题详情的 `roots` 按 `sort_order` 升序 + `member_id` 升序稳定排序,逐条为既有公开作品投影(不泄露对象键、不泄露未公开作品信息)。
|
||||
- [x] 作品详情新增 `themes`:**第 N 代作品**(非根)也能看到并跳到其所属公开主题(按根反查),且只含公开主题。
|
||||
- [x] 主题列表与详情的 `memberCount` 等于同响应里可见成员数(`roots` 长度),不含草稿 / 已下架成员(**注意 50 上限:> 50 时该等式仍成立,因为两者都被截断,见「待确认项」**)。
|
||||
- [x] 两条公开主题路径匿名可读且带 `Cache-Control: no-store`。
|
||||
- [x] 后台五条路由未带 admin 会话一律 401;鉴权复用既有 `require_admin_auth` + `AuthenticatedAdmin`,不自造。
|
||||
- [x] 后台写接口错误码可区分:空名 / 非法状态 400、未知主题 404、作品不存在 404、非根 409、同键不同请求 409;同键重放如实回报幂等重放(错误码映射单测 + 领域错误类型单测;**经真实库存的 404 / 409 等 dev 栈 e2e**)。
|
||||
- [x] 后台 `DELETE` 成员重复调用结果相同且成功(幂等),成员不存在不是错误(事务结构断言:响应不含「之前存不存在」)。
|
||||
- [x] 后台列表含 `draft` / `archived` 主题;非法 `status` 过滤值返回 400。
|
||||
- [ ] 公开前端:共创 Tab 能列出主题并进入主题页;主题页按成员根渲染**多棵树**(复用 `/games/{id}/lineage`);已发布空主题显示空态;详情页 `themes` 能跳到主题页。**等前端**
|
||||
- [ ] 新增路由在桌面与窄屏可用,且 `check:nginx-spa-routes` / `check:pingora-route-parity` 通过。**等前端**
|
||||
- [x] 契约同步:DTO parity 通过(58 组 / 10 构建器);数据契约表文档随表落地补齐(`2fa201e0d`)。
|
||||
|
||||
**勾选口径**:勾选项由已落地的纯函数单测 / 事务结构断言 / api-server 定向测试 / DTO parity / schema 门禁证实;**未跑真实 dev 栈的端到端整链**(`scripts/check-game-distribution-theme-e2e.mjs` 未创建),所以凡依赖真实库存的运行时分支(未知主题 404、作品不存在 404、同键换请求 409、下架→重公开自动回归等)目前只到单元 / 结构 / 映射层。第 17 / 18 条(前端)**等前端**。
|
||||
|
||||
## 证据要求
|
||||
|
||||
**现状**:自动化已覆盖纯函数 / 事务结构 / api-server 定向 / 契约 / schema;**运行时**(真实本地栈整链、浏览器桌面与窄屏)**未跑**,`scripts/check-game-distribution-theme-e2e.mjs` **未创建**——「证据要求」的运行时与脚本两项仍缺。
|
||||
|
||||
- 自动化:`module-game-distribution` 纯函数单测(可见性 / 排序 / 游标 / 根约束)、`spacetime-module` 事务断言、api-server 路由与错误码定向测试、DTO parity、schema 与生成绑定检查、前端 vitest、`check:encoding` / `check:doc-index` / `git diff --check`。
|
||||
- 运行时:真实本地栈上完成「建主题(draft)→ 加两个根成员 → 发布 → 匿名读列表与详情 → 作品详情看到主题 → 把一个成员下架(从投影消失、行仍在)→ 重新公开(自动回来)→ 归档主题(公开侧不可见、行仍在)→ 后台列表仍可见」整链;浏览器在桌面与窄屏走一遍共创 Tab 与主题页。
|
||||
- 边界:已发布但零可见成员、非根作品入成员、跨主题同根、非法游标、末页游标、未知主题 404、未带 admin 会话 401、重复成员写入、`DELETE` 不存在成员。
|
||||
@@ -214,7 +239,8 @@ E2E_ADMIN_USER=<管理员> E2E_ADMIN_PASSWORD=<密码> node scripts/check-game-d
|
||||
|
||||
## 待确认项(需用户 / 产品拍板)
|
||||
|
||||
- **主题级排序口径**:本方案按「沿用既有游标惯例」把公开列表排在 `created_at`(倒序)+ `theme_id`(升序兜底)上,`sort_order` 只用于**主题内成员排序**与后台列表。若产品要求共创 Tab 按运营 `sort_order` 展示,需要把游标改成 `(sort_order, theme_id)` 双键并同步前端——**这会在实现前改一次接口契约**。
|
||||
- **主题级排序口径**:本方案按「沿用既有游标惯例」把公开列表排在 `created_at`(倒序)+ `theme_id`(升序兜底)上,`sort_order` 只用于**主题内成员排序**与后台列表。若产品要求共创 Tab 按运营 `sort_order` 展示,需要把游标改成 `(sort_order, theme_id)` 双键并同步前端——**这会改一次接口契约**(已按现状实现,改需另开一轮)。
|
||||
- **主题详情 `roots` 的 50 上限行为**:`get_game_distribution_theme_detail_tx` 用 `GAME_DISTRIBUTION_THEME_PAGE_LIMIT_MAX`(= 50)截断可见成员,且 `memberCount == roots.len()`,因此**可见成员 > 50 时静默截断且没有「还有更多」标志**(无 `truncated`、无成员游标)。三个备选:① 去上限;② 给成员加**独立游标**;③ 保持截断但 `memberCount` 报**真实可见数**。**本轮不改契约**(同步写在技术方案 §3.10.6 / §3.10.9 / §7 第 3 条)。
|
||||
|
||||
## 已知留白
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 状态口径:`status` 三态白名单 `draft` / `published` / `archived`(`module_game_distribution::GAME_DISTRIBUTION_THEME_STATUSES`);只有 `published` 进入公开投影(`game_distribution_theme_public_visible`),`draft` 与 `archived` 在公开列表 / 公开详情 / 作品详情 `themes` 里都不可见,后台仍可见全量。归档**不是删除**:行与成员行都保留,语义可逆。
|
||||
- 索引:`by_game_distribution_theme_created_at`。公开列表按创建时间倒序 + `theme_id` 升序兜底翻页(游标 `"{created_at_micros}:{theme_id}"`,默认 20 / 上限 50 / 超界截断 / 非法游标 400);`sort_order` 只用于主题内成员排序与后台列表展示,不参与公开列表排序。
|
||||
- 迁移:随迁移导出/导入(运营业务事实);归档只改变公开投影,不删除行。`migration_tables!` 已登记该表。
|
||||
- 写入路径:后台写接口(`create_game_distribution_theme_and_return` / `update_game_distribution_theme_and_return`)属后续里程碑;本轮只落表、生成绑定、迁移登记与契约。
|
||||
- 读写路径(均已落地,2026-10-06):公开读列表 `list_game_distribution_themes_and_return`(事务 `list_game_distribution_themes_tx`)、公开详情 `get_game_distribution_theme_detail_and_return`(`get_game_distribution_theme_detail_tx`);后台写 `create_game_distribution_theme_and_return` / `update_game_distribution_theme_and_return`(`create_game_distribution_theme_tx` / `update_game_distribution_theme_tx`)与后台列表 `list_admin_game_distribution_themes_and_return`(`list_admin_game_distribution_themes_tx`)。私有表,只经 api-server 的受信服务身份调用,不经连接订阅 cache 或前端本地状态伪造。
|
||||
|
||||
### `game_distribution_theme_member`
|
||||
|
||||
@@ -536,7 +536,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 可见性:成员只出现在「作品公开未删且存在当前公开版本」时——判定**委托**收藏的同一条口径 `module_game_distribution::game_distribution_theme_member_visible`(内部即 `game_distribution_collection_visible`),不新写第二个同义判定。作品下架 / 软删除时该成员在**投影里跳过但不删除行**,作品重新公开后自动回到主题页;主题详情的 `roots` 与列表的 `memberCount` 用同一判定,因此两个数永远一致。
|
||||
- 排序:主题内成员按 `sort_order` 升序 + `member_id` 升序兜底(`sort_order` 允许重复,兜底键保证全序、翻页/渲染不重不漏);可见性过滤必须发生在排序切页之前。
|
||||
- 迁移:随迁移导出/导入(运营业务事实);归档主题与下架作品都不删除成员行。`migration_tables!` 已登记该表。
|
||||
- 写入路径:后台成员写接口(`upsert_game_distribution_theme_member_and_return` / `remove_game_distribution_theme_member_and_return`)属后续里程碑;本轮只落表、生成绑定、迁移登记与契约。
|
||||
- 读写路径(均已落地,2026-10-06):公开详情 `get_game_distribution_theme_detail_and_return`(成员投影在 `get_game_distribution_theme_detail_tx` 内按 `by_game_distribution_theme_member_theme_id` 取行)、作品详情 `themes` 增量 `list_game_distribution_theme_refs_for_root_and_return`(`list_game_distribution_theme_refs_for_root_tx`,按 `by_game_distribution_theme_member_root_game_id` 反查);后台写 `upsert_game_distribution_theme_member_and_return` / `remove_game_distribution_theme_member_and_return`(`upsert_game_distribution_theme_member_tx` / `remove_game_distribution_theme_member_tx`)。私有表,只经 api-server 的受信服务身份调用。
|
||||
|
||||
### `game_distribution_review`
|
||||
|
||||
|
||||
@@ -308,9 +308,11 @@ pub(crate) project_bundle_sha256: Option<String>,
|
||||
|
||||
| 方法 / 路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `GET /games/{gameId}`(**既有,响应增量**) | 追加 `forkAuthorization`、`forkCount`、`lineage`(可选)。**不追加 `forkSourceAvailable`**:网页端「改造这个作品」入口的显隐只依据 `forkAuthorization`(已公开 + 非禁止即可引导去取件),真实可复刻形态由 `/games/{gameId}/fork-source` 的 `source` 字段回答,不需要在公开详情里提前判断;若 M2b 需要在详情页区分「可源码级改造 / 只能参考」,再在 M2b 里加该字段。**另追加 `collected`(仅登录用户,2026-10-06)**:登录已认证时返回 `true` / `false`(真实投影,值来自 `is_game_distribution_collected_and_return`);**匿名请求不返回该字段**(也不发 `false`——`false` 会把「未登录」说成「没收藏」)。该字段随请求者变化,所以这条路径必须 `no-store`、不得有任何共享缓存 |
|
||||
| `GET /games/{gameId}`(**既有,响应增量**) | 追加 `forkAuthorization`、`forkCount`、`lineage`(可选)。**不追加 `forkSourceAvailable`**:网页端「改造这个作品」入口的显隐只依据 `forkAuthorization`(已公开 + 非禁止即可引导去取件),真实可复刻形态由 `/games/{gameId}/fork-source` 的 `source` 字段回答,不需要在公开详情里提前判断;若 M2b 需要在详情页区分「可源码级改造 / 只能参考」,再在 M2b 里加该字段。**另追加 `collected`(仅登录用户,2026-10-06)**:登录已认证时返回 `true` / `false`(真实投影,值来自 `is_game_distribution_collected_and_return`);**匿名请求不返回该字段**(也不发 `false`——`false` 会把「未登录」说成「没收藏」)。该字段随请求者变化,所以这条路径必须 `no-store`、不得有任何共享缓存。**再追加 `themes: [{ themeId, name, badge }]`(2026-10-06 增量,共创主题)**:该作品所属的**公开**主题(只含 `status == published`);**匿名与登录都发、无主题时恒发空数组**(与「仅登录才发」的 `collected` 不同——没有主题与没登录因此不会被混成同一种缺键)。口径是「先取该作品的**根**、再按根反查公开主题」,所以**第 N 代作品也能看到并跳到主题页**;单条只够渲染跳转入口(简介与成员数去主题页取) |
|
||||
| `GET /games/{gameId}/lineage`(新) | 以该 game 的根为顶返回树:`{ rootGameId, root: LineageNode \| null, nodes: [LineageNode], truncated }`,`LineageNode = { gameId, title, authorName, generation, parentGameId, playCount, status }`,按代际升序 / 同代创建时间升序稳定排序;节点上限 200,超出返回 `truncated: true`。**锚点必须公开可读**:未公开、已软删除或不存在的作品返回 404(与公开详情同口径),不用空标题占位或空树代替 404。树内只出现未软删除且已公开的作品;父/祖辈被排除时孩子照常出现并保留 `generation` 与 `parentGameId`,由展示层标注「原作品已不可用」,不补 null 占位节点 |
|
||||
| `GET /games/{gameId}/derived`(新,可选分页) | 直接子代列表:`{ gameId, nodes: [LineageNode], truncated }`,只含未软删除且已公开的直接子代(与公开详情 `forkCount` 同口径,因此条数与「被改编 N」一致);锚点同样必须公开可读,否则 404 |
|
||||
| `GET /api/game-distribution/themes?limit=&cursor=`(新,2026-10-06) | 公开共创主题列表。**匿名可读 + `no-store`**,只含 `status == published` 的主题。响应 `{ themes: [{ themeId, name, summary, badge, memberCount }], nextCursor }`;`memberCount` = 该主题**当前公开可见成员数**(与详情 `roots` 长度同一判定,不是成员行总数)。分页沿用 `/my-collections` 那套游标惯例:`limit` 缺省 **20**、上限 **50**、超界**截断**(客户端拿到一个完整页,而不是需要重试的错误);`cursor` 形如 `"{createdAtMicros}:{themeId}"`(解析只切第一个冒号);**非法游标 → 400 `THEME_INVALID_CURSOR`**(模块侧报错透传,不吞成 200 空页);`nextCursor` 为真实值,**末页为 `null`**。排序 `created_at` 倒序 + `themeId` 升序兜底(全序,翻页不重不漏),顺序定义为「**先按可见性过滤、再排序切页**」 |
|
||||
| `GET /api/game-distribution/themes/{themeId}`(新,2026-10-06) | 公开主题详情。**匿名可读 + `no-store`**;顶层**扁平** `{ themeId, name, summary, badge, memberCount, roots }`(与 `GET /games/{gameId}` 同形,不引入第二套包装)。`roots` 逐条是公开目录**同一份** `public_game_payload` 投影,按 `sort_order` 升序 + `member_id` 升序稳定排序(不泄露对象键、不泄露未公开作品)。主题**不存在 / `draft` / `archived` 一律 404**(模块侧回同一句「主题不存在」,由既有映射落 404;**不返回空壳**、公开侧不发 `THEME_*` 码);**已发布但可见成员为空 = 200 + 空 `roots`**(空态而不是错误)。**已知限制(待拍板)**:`roots` 受页上限 **50** 约束且 `memberCount == roots.len()`,可见成员 > 50 时**静默截断且没有「还有更多」标志**(三个备选见 §3.10.6 与 §3.10.9) |
|
||||
|
||||
#### 作者(Bearer + 发布灰度)
|
||||
|
||||
@@ -345,6 +347,11 @@ pub(crate) project_bundle_sha256: Option<String>,
|
||||
| 方法 / 路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `GET /admin/api/game-distribution/games`(**既有,响应增量**) | 追加 `forkAuthorization` / `generation` / `forkedFromGameId` / `derivedCount`(只读展示) |
|
||||
| `POST /admin/api/game-distribution/themes`(新,2026-10-06) | 创建主题。**鉴权复用既有 admin 体系**:`route_layer(require_admin_auth)` + handler 取 `Extension<AuthenticatedAdmin>`——未带 / 失效会话 → **401 `UNAUTHORIZED`**(与同组 games/reviews 路由同码同形),非 admin role → 403;模块侧第二层再要求受信服务身份(只有 api-server 能调 procedure)。`theme_id` 由**服务端**生成 `theme-{uuid}`(请求体里没有该字段,外部无法指定),`created_by_user_id` 取 admin 会话主体。要求 `Idempotency-Key`(缺 / 超 128 字符 → 400 `BAD_REQUEST`,沿用既有写接口口径,不为主题另开一套);**同键同请求摘要重放 → `replayed: true` 且回库里那一行(不写库)**,同键不同请求 → **409 `THEME_IDEMPOTENCY_CONFLICT`**。响应 `{ theme, replayed }`。空 `name` / 文本超长(名称 40 / 简介 200 / 角标 16)/ 未知 `status` → 400 `THEME_BAD_REQUEST`(不是 axum 默认 422 纯文本) |
|
||||
| `PUT /admin/api/game-distribution/themes/{themeId}`(新,2026-10-06) | 整体覆盖 `name` / `summary` / `badge` / `sortOrder` / `status`。鉴权与幂等口径同上(要求 `Idempotency-Key`;生效即刷新 `updated_at`,**重放不写库、不刷新**)。`themeId` 不存在 → **404 `THEME_NOT_FOUND`**;字段非法 → 400 `THEME_BAD_REQUEST`。响应 `{ theme, replayed }` |
|
||||
| `GET /admin/api/game-distribution/themes?limit=&status=`(新,2026-10-06) | 后台列表,**含 `draft` / `archived`**。`limit` 缺省与上限同为 **200**(超界截断),**无游标**(主题是运营维护的小集合)。`status` 白名单 `all` / `draft` / `published` / `archived`(缺省 = `all`),非法过滤值 → 400 `THEME_BAD_REQUEST`(失败关闭,不退化成全量)。响应 `{ themes: [{ themeId, name, summary, badge, sortOrder, status, memberCount, createdAt, updatedAt }] }`;此处 `memberCount` 是**成员行总数**(含当前对外不可见的成员),与公开侧同名键的「可见成员数」口径不同 |
|
||||
| `PUT /admin/api/game-distribution/themes/{themeId}/members/{rootGameId}`(新,2026-10-06) | 增 / 改成员(幂等 upsert,body 可带 `sortOrder`,缺省 0)。**不要求 `Idempotency-Key`**:成员身份完全由路径给出,确定性主键 `"{themeId}:{rootGameId}"` 天然幂等,重复调用只更新 `sortOrder`(`createdAt` 不变)。只允许**根作品**:非根(有血缘行)→ **409 `THEME_MEMBER_NOT_ROOT`**;作品不存在 → 404 `THEME_MEMBER_GAME_NOT_FOUND`;主题不存在 → 404 `THEME_NOT_FOUND`。**不做「作品必须已公开」的前置校验**(可先挂草稿根,作品公开后自动进入公开投影)。响应 `{ themeId, rootGameId, sortOrder, createdAt }` |
|
||||
| `DELETE /admin/api/game-distribution/themes/{themeId}/members/{rootGameId}`(新,2026-10-06) | 移除成员。**不要求 `Idempotency-Key`**:按确定性主键删除,**成员不存在也算成功(200)**;响应刻意不含「之前存不存在」,重复调用逐字节相同。主题不存在仍是 404 `THEME_NOT_FOUND`(那是路径里的主题 ID 错了)。响应 `{ themeId, rootGameId }` |
|
||||
|
||||
#### 契约同步(强制)
|
||||
|
||||
@@ -484,9 +491,9 @@ A 路线里有一个必须提前知道的互斥点:`create_npm_scaffold` 的
|
||||
|
||||
> **形态**:共创 Tab 里有多个「共创主题」,点进去是一棵(或一组)作品树;**主题由平台 / 运营命名,不由根作品决定**;作品在游戏 Tab 里仍作为独立作品展示,详情页有 Fork 入口,并能看到 / 跳到对应主题页。
|
||||
>
|
||||
> 本节把 §7 第 3 条(「是否引入共创主题实体」)从**待拍板**收敛为**已拍板**:采纳「平台命名主题 → 作品树」。归属关系的**曝光口径不变**——作品之间不互相挂靠,主题是运营侧的**额外归组维度**,不是作品的可读锚点。本轮只落设计(本节 + 里程碑《共创主题与作品树-2026-10-06》),**实现是下一轮**;后台 UI 亦不在本轮。
|
||||
> 本节把 §7 第 3 条(「是否引入共创主题实体」)从**待拍板**收敛为**已拍板**:采纳「平台命名主题 → 作品树」。归属关系的**曝光口径不变**——作品之间不互相挂靠,主题是运营侧的**额外归组维度**,不是作品的可读锚点。**设计定稿于 2026-10-06(commit `063c04a1b`);服务端三块随后落地**:数据模型与领域纯函数 `2fa201e0d`、公开读路径 `742723a58`、后台写路径 `033e3aa79`。**前台(共创 Tab / 主题页 / 详情页入口)与后台管理 UI 仍未做**。
|
||||
>
|
||||
> 每小节末尾显式标注 **本轮实现**(下一轮按里程碑清单实施)或 **留白**(本轮明确不做,需另立需求)。
|
||||
> 每小节末尾显式标注 **已实现**(服务端已落地,附提交号)或 **留白**(明确不做,需另立需求);**已实现**只表示服务端接口 / 事务 / 纯函数已落地,不代表前台 UI 或 dev 栈端到端已验收。
|
||||
|
||||
#### 3.10.1 实体:`game_distribution_theme` + `game_distribution_theme_member`
|
||||
|
||||
@@ -551,7 +558,7 @@ pub struct GameDistributionThemeMember {
|
||||
| `status` 三态:`draft` / `published` / `archived` | 三态白名单 | 运营需要「未发布」「已发布」「已归档下架」三种处置;归档不是删除(成员行保留,见 §3.10.5),语义比 `deleted_at` 更弱也更可逆 | 每次新增一态都要在三处(可见性纯函数、后台列表过滤白名单、DTO)同步,靠测试与 DTO parity 钉住 |
|
||||
| `created_by_user_id` = 运营账号 | 审计字段 | 后台写接口是运营动作,出问题时必须能答「谁建的 / 谁改的」;与仓库现有 admin 操作留痕同口径 | 仅记录创建者,不记录后续每次改动者(本轮不建单独的主题修改日志) |
|
||||
|
||||
**本轮实现**:两表、两条具名索引、`migration.rs` 的 `migration_tables!` 白名单登记、生成绑定与 schema 门禁。
|
||||
**已实现(`2fa201e0d`)**:两表、两条具名索引、`migration.rs` 的 `migration_tables!` 白名单登记、生成绑定与 schema 门禁。
|
||||
|
||||
#### 3.10.2 成员只允许「根作品」
|
||||
|
||||
@@ -569,7 +576,7 @@ pub struct GameDistributionThemeMember {
|
||||
| --- | --- | --- | --- |
|
||||
| 同一根能否同时属于多个主题 | **可以**,这是预期行为 | 主题是**运营叙事**而不是分类学唯一归属:「平台精选」「双人合作」完全可能同时收录同一个根。`member_id` 的确定性主键只保证「**同一主题内**同一根不重复」,跨主题重复是设计的一部分 | ① 运营改作品时要意识到它可能出现在多个主题里(本轮不做「该作品属于 N 个主题」的运营侧提示);② 作品详情必须返回**多值** `themes`(§3.10.7),不是一个可选单值 |
|
||||
|
||||
**本轮实现**:多归属由 `(theme_id, root_game_id)` 的复合语义自然承载,不需要额外结构。
|
||||
**已实现(`2fa201e0d`)**:多归属由 `(theme_id, root_game_id)` 的复合语义自然承载,不需要额外结构。
|
||||
|
||||
#### 3.10.4 与血缘树的关系:直接复用 `/games/{id}/lineage`
|
||||
|
||||
@@ -578,7 +585,7 @@ pub struct GameDistributionThemeMember {
|
||||
| 主题页怎么拿树 | 主题页呈现**多棵树**(一个主题可含多个根),树数据**直接复用既有 `/games/{id}/lineage`** | 该端点已落地且已收敛全部口径(锚点 404、只出现公开未删节点、稳定排序、上限截断,见 §3.4 与 `lineage.rs`)。**不在主题侧重建任何树查询或树整形**,就没有第二套可见性/排序规则可漂移 | 前端要按成员根清单**逐个**请求树(N 次),存在 N+1 |
|
||||
| 主题详情返回什么 | **只返回成员根清单**(按 `sort_order` 稳定排序 + 每个根的既有公开摘要) | 服务端职责边界清晰:主题管「有哪些根、什么顺序」,树是 lineage 的职责。两个端点各自可独立演进(例如将来 lineage 加上限/分页,不需要动主题) | 响应里没有整棵树,客户端组装;**留白**:将来如确有必要,再提供一个**批量树端点**(§3.10.9) |
|
||||
|
||||
**本轮实现**:主题详情返回 `roots`(逐条为既有 `public_game_payload` 投影)。**留白**:批量树端点。
|
||||
**已实现(`742723a58`)**:主题详情返回 `roots`(逐条为既有 `public_game_payload` 投影)。**留白**:批量树端点。
|
||||
|
||||
#### 3.10.5 可见性口径(与现有一致)
|
||||
|
||||
@@ -596,7 +603,7 @@ pub struct GameDistributionThemeMember {
|
||||
| 主题已发布、但可见成员为空 | **200 + 空成员列表** | 主题是**运营实体**,不是「不可读锚点」:它的存在本身由运营发布行为对外确认,且不指向任何具体作品,空树不构成泄露。用 404 反而会让「刚建好还没挂作品」的正常运营状态被误判为故障 | 前端要显式写空态(不能把「空」当「错」) |
|
||||
| 成员作品下架 / 软删除 | 该成员在**投影里跳过**,**行不删除** | 与收藏同口径:重新公开后**自动回来**,不需要运营重新挂 | 主题页的成员数会随作品状态变化(这是真实投影,不是计数漂移);行与投影的差异必须靠测试钉住 |
|
||||
|
||||
**本轮实现**:`theme` 可见性纯函数、`member` 可见性纯函数、以及「投影跳过但不删行」的事务级断言。**留白**:主题级联下架 / 归档时的成员清理(不做——归档本身就是「不再公开」,成员行按同一口径保留)。
|
||||
**已实现(`2fa201e0d` 纯函数 + `742723a58` 事务投影)**:`theme` 可见性纯函数、`member` 可见性纯函数、以及「投影跳过但不删行」的事务级断言。**留白**:主题级联下架 / 归档时的成员清理(不做——归档本身就是「不再公开」,成员行按同一口径保留)。
|
||||
|
||||
#### 3.10.6 公开接口(匿名可读、`Cache-Control: no-store`)
|
||||
|
||||
@@ -610,8 +617,9 @@ pub struct GameDistributionThemeMember {
|
||||
- **`memberCount` = 当前公开可见成员数**(与 `roots` 长度一致,同一可见性纯函数)。理由:若回报「全部成员行数」,运营就能通过计数变化探测「存在草稿或被下架成员」,且它会与 `roots` 长度**对不上**,客户端无法解释两个数为什么不一致。代价:计数是逐主题现算的(不新增物化计数字段,与仓库「实时算、不加物化计数」的既有取舍一致)。
|
||||
- 排序与切页顺序定义为「**先按可见性过滤、再排序切页**」,游标位置落在已过滤序列上——否则每翻一页都会漏掉自己的若干条(与 `/my-collections` 同一条纪律)。
|
||||
- 全路径 `no-store`,挂在既有 `add_no_store_response_headers` 上;不新增第二套响应头中间件。
|
||||
- **已知限制(待拍板):主题详情的 `roots` 沿用页上限 50,且 `memberCount == roots.len()`。** 事务用 `GAME_DISTRIBUTION_THEME_PAGE_LIMIT_MAX`(= 50,与公开列表同一常量)截断可见成员,并把 `member_count` 直接取成截断后的 `roots.len()`——因此**可见成员 > 50 时静默截断且没有「还有更多」标志**(`truncated` / `nextCursor` 都没有,客户端无法知道自己少看了)。三个备选**待拍板**:① 去掉详情 `roots` 的上限(一次全发,风险是超大主题响应体积);② 给详情成员加**独立游标**(`roots` 分页,但契约要从「一次拿全」改成翻页);③ 保持截断但让 `memberCount` 报**真实可见数**(不再等于 `roots.len()`,客户端据此能发现「还有更多」但仍然拿不到)。**本轮不改契约**,先如实记录(同一条限制在 §3.10.9 与里程碑「待确认项」)。
|
||||
|
||||
**本轮实现**:上述两条公开路由 + 可见性过滤 + 游标分页。
|
||||
**已实现(`742723a58`)**:上述两条公开路由 + 可见性过滤 + 游标分页;DTO parity 登记 `public_themes_payload` / `public_theme_detail_payload` / `public_theme_summary_payload` 三个构建器。
|
||||
|
||||
#### 3.10.7 作品详情增量:`themes: [{ themeId, name, badge }]`
|
||||
|
||||
@@ -628,7 +636,7 @@ pub struct GameDistributionThemeMember {
|
||||
| 多值数组,不是可选单值 | 跨主题多归属(§3.10.3) | DTO 是数组,前端要按数组渲染 |
|
||||
| 排序用与公开列表**同一份**排序纯函数(创建时间倒序 + `theme_id` 升序兜底) | 同一份比较器复用,避免「列表一种顺序、详情另一种顺序」的第二套语义 | 数组顺序不承载运营意图(运营侧顺序是成员的 `sort_order`,不是主题之间的顺序) |
|
||||
|
||||
**本轮实现**:详情 payload 追加 `themes`;DTO parity 登记该构建器(证明这条路径确实会发出该键)。**留白**:主题入口在详情页的 UI 呈现(属前端里程碑)。
|
||||
**已实现(`742723a58`)**:详情 payload 追加 `themes`;DTO parity 登记该构建器(证明这条路径确实会发出该键)。**留白**:主题入口在详情页的 UI 呈现(属前端里程碑)。
|
||||
|
||||
#### 3.10.8 后台接口与鉴权(后台 UI 不在本轮)
|
||||
|
||||
@@ -649,9 +657,9 @@ pub struct GameDistributionThemeMember {
|
||||
- **`sort_order` 的定位**:本轮的**公开主题列表排序由游标惯例决定(`created_at` 倒序 + `theme_id` 升序兜底)**,`sort_order` 用于**主题内成员排序**与后台列表展示顺序。理由:游标格式的主键是 micros,若公开列表改按 `sort_order` 排,就需要 `(sort_order, theme_id)` 双键游标,与「沿用刚落地的那套游标惯例」冲突。若产品要求共创 Tab 按运营序展示,需要显式改游标格式(见 §7 与本轮里程碑的待确认项)。**留白**:主题级排序的运营拖拽 UI。
|
||||
- **错误码命名**:沿用 `FORK_*` 那套「`模块前缀_原因` 字符串 → 状态码」的映射写法(`theme_*` 纯函数产出,api-server 集中映射),不落到 axum 默认的 422 纯文本。
|
||||
|
||||
**本轮实现**:五条后台路由 + 权限校验 + 幂等 + 错误码映射 + 后台列表(含 `draft` / `archived`)。**留白**:后台管理页面。
|
||||
**已实现(`033e3aa79`)**:五条后台路由 + 权限校验 + 幂等 + 错误码映射 + 后台列表(含 `draft` / `archived`)。**留白**:后台管理页面(UI 不在本轮,本条只落接口与事务)。
|
||||
|
||||
#### 3.10.9 本轮留白(显式列出,不静默省略)
|
||||
#### 3.10.9 留白与已知限制(显式列出,不静默省略)
|
||||
|
||||
| 留白项 | 为什么不做 | 将来怎么接 |
|
||||
| --- | --- | --- |
|
||||
@@ -661,6 +669,12 @@ pub struct GameDistributionThemeMember {
|
||||
| 主题内「**跳到某一代节点**」高亮 | 属 UI 层能力,族谱页已有 `from` 参数(§3.10.2),后端不需要新字段;在没有主题页 UI 之前做它没有消费方 | 主题页 UI 落地时直接复用 `from` 参数 |
|
||||
| **批量树端点** | 本轮接受前端 N 次 `/games/{id}/lineage`(N = 成员数);主题成员量级小,且「不在主题侧重建树查询」的价值高于省这几次请求 | 若主题成员规模上升或出现首屏超时,再提供一个批量端点(服务端内部仍是同一份 `build_lineage_tree`,不新造规则) |
|
||||
|
||||
**已知限制(已落地行为,等拍板;不是「留白」)**:
|
||||
|
||||
| 限制 | 现状 | 备选(待拍板) |
|
||||
| --- | --- | --- |
|
||||
| 主题详情 `roots` 的 **50 上限** | `get_game_distribution_theme_detail_tx` 用 `GAME_DISTRIBUTION_THEME_PAGE_LIMIT_MAX`(= 50)截断可见成员,并把 `memberCount` 取成截断后的 `roots.len()`。因此**可见成员 > 50 时静默截断,且既没有 `truncated` 也没有成员 `nextCursor`**——客户端拿不到「还有更多」的任何信号(详见 §3.10.6) | ① 去上限(一次全发,承担响应体积);② 给成员加**独立游标**(详情节改成翻页);③ 保持截断但 `memberCount` 报**真实可见数**(客户端能发现不一致)。**本轮不改契约** |
|
||||
|
||||
#### 3.10.10 迁移、契约与门禁
|
||||
|
||||
| 项 | 口径 |
|
||||
@@ -668,7 +682,7 @@ pub struct GameDistributionThemeMember {
|
||||
| 迁移 | 两张**新表**,初始为空,无回填;`migration.rs` 的 `migration_tables!` 白名单登记新表,并在注释中写明「主题与成员是运营业务事实,随迁移导出/导入」 |
|
||||
| 生成绑定 | 必须重跑 `npm run spacetime:generate`,并跑 `npm run check:generated-bindings`、`npm run check:spacetime-schema` |
|
||||
| DTO | Rust 侧 `server-rs/crates/shared-contracts/src/game_distribution.rs`、TS 侧 `packages/shared/src/contracts/gameDistribution.ts`;新响应构建器登记进 `check:game-distribution-dto-parity` |
|
||||
| 数据契约表 | `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` **随表落地时**再补(`check:spacetime-schema` 会在表存在后要求) |
|
||||
| 数据契约表 | `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` **已随表落地补齐**(`2fa201e0d`,两张表小节;`check:spacetime-schema` 现为 **96** tables) |
|
||||
| 不改的东西 | 不动 `/api/external/v1`(不新增 External OpenAPI 条目);不新增数据库访问通道;不新增第二套作品系统;游戏 Tab / 广场的公开目录**不变**(主题是额外维度,不改变作品的独立展示) |
|
||||
|
||||
---
|
||||
@@ -772,7 +786,7 @@ pub struct GameDistributionThemeMember {
|
||||
|
||||
1. **成品包路径的事实边界已改写**(原条目「成品包路径默认要做……建议:做」的前提已被否证):成品包路径能试玩、能提供素材,但**不能发布**(§3.5.1)。因此要拍板的不再是「要不要顺带做工程源包」,而是「是否接受对外口径从『一键复刻完整工程』改成『参考改编 / 素材复用』,并把工程源包作为唯一源码级路径」。建议:接受并改口径(§3.5.4 路线 A 先行、B 排后续)。
|
||||
2. **授权默认值**:本文档按需求描述取「默认禁止」;飞书文档评论中包仲航建议改为「默认允许 + 发布前合同勾选」。两者会改变默认曝光面与合规口径,需产品拍板(默认允许对生态更友好,但要处理存量作品的合法性回溯)。(M1 已按「默认禁止」实现,改动需连带迁移口径。)
|
||||
3. **「共创主题」实体已拍板采纳(2026-10-06,原为待拍板)**:采纳包仲航提出的「平台命名主题 → 作品树」,且明确作品之间不存在曝光挂靠。设计见 §3.10,实施清单见 `docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md`(M4)。本轮只落设计(不写业务代码);**仍待产品确认的是主题级排序口径**——§3.10.8 按「沿用既有 micros 游标」把公开列表排在 `created_at` 上,`sort_order` 只用于主题内成员排序;若要求共创 Tab 按运营序展示,需要改游标格式(`(sort_order, theme_id)`)并同步前端。
|
||||
3. **「共创主题」已拍板采纳且服务端已实现(2026-10-06,原为待拍板)**:采纳包仲航提出的「平台命名主题 → 作品树」,且明确作品之间不存在曝光挂靠。设计见 §3.10,实施清单见 `docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md`(M4)。**已落地**:设计定稿 `063c04a1b`、数据模型与领域纯函数 `2fa201e0d`、公开读路径 `742723a58`、后台写路径 `033e3aa79`(前台共创 Tab / 主题页与后台管理 UI **仍未做**)。**仍待产品拍板的是两点**:① **主题级排序口径**——公开列表当前按「沿用既有 micros 游标」排在 `created_at` 倒序 + `themeId` 升序兜底上,`sort_order` 只用于主题内成员排序;若要求共创 Tab 按运营序展示,需要把游标改成 `(sort_order, theme_id)` 双键并同步前端;② **主题详情 `roots` 的 50 上限**——可见成员 > 50 时静默截断且无「还有更多」标志(`memberCount == roots.len()`),三个备选(去上限 / 给成员加独立游标 / `memberCount` 报真实可见数)见 §3.10.6 与 §3.10.9,**本轮不改契约**。
|
||||
4. **收益分成**:需求文档要求「每一代均享有权益(署名 / 流量回馈 / 版权分成)」。署名本期做,流量回馈与分成本期不做(无账本、无算力成本口径,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算)。
|
||||
5. **相似度反洗稿校验**:需求文档要求「低改动度复刻判定」。本期不做;本方案只保证来源声明真实、不可伪造。若要做,只能基于工程源包做结构化比对,属于独立议题。
|
||||
6. **「永久链上溯源」表述**:实现为平台持久化的不可变父子链,不上链。需确认该措辞是否可以调整。
|
||||
|
||||
Reference in New Issue
Block a user