diff --git a/docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md b/docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md index db06a7eba..7a0c6fec6 100644 --- a/docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md +++ b/docs/project-memory/plans/【里程碑】共创主题与作品树-2026-10-06.md @@ -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=`,与 `/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 条)。 ## 已知留白 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 5f485efac..410ac1cbc 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -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` diff --git a/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md b/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md index 98f18afff..de5675d8e 100644 --- a/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md +++ b/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md @@ -308,9 +308,11 @@ pub(crate) project_bundle_sha256: Option, | 方法 / 路径 | 说明 | | --- | --- | -| `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, | 方法 / 路径 | 说明 | | --- | --- | | `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`——未带 / 失效会话 → **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. **「永久链上溯源」表述**:实现为平台持久化的不可变父子链,不上链。需确认该措辞是否可以调整。