实现游戏共创 M3:创作族谱树与衍生列表

- 新增 module-game-distribution/src/lineage.rs:锚点可读性、可见性过滤、稳定排序、上限截断的纯函数,附 15 个单测(复用 M1 的软删除判定,不重写第二套规则)
- 新增公开只读 procedure get_game_distribution_lineage / list_game_distribution_derived_games;锚点必须公开可读(未公开/已软删除/不存在一律按不存在处理),树内只出现未软删除且已公开的节点,不补 null 占位节点
- api-server 新增 GET /games/{id}/lineage 与 /games/{id}/derived(匿名可读、no-store),响应走结构化 DTO,节点键集合由测试钉死,杜绝下发对象键/素材键
- 作者自有游戏聚合追加 forkCount(与公开详情 forkCount 同口径),供「被改编 N」入口使用
- shared-contracts 与 TS 契约新增 LineageNode / LineageResponse / DerivedResponse 三型并登记 DTO 一致性检查
- 前端新增 /games/lineage 族谱页,登记 stage 类型、路由表、页面标题与壳层渲染分支;详情页共创卡新增「查看创作族谱」入口(仅已公开作品)
- 我的作品页新增「被改编 N」行内展开,列出直接子代的标题/作者/代际/状态,含空态与失败重试(仅已公开作品)
- 补前端组件/路由/标题/客户端用例与 api-server 路由、负载映射用例;同步里程碑验收标准与技术方案 §3.4 的锚点可读性口径
This commit is contained in:
2026-10-05 01:02:03 +08:00
parent 3d51d1309a
commit 39669763fe
36 changed files with 2780 additions and 27 deletions
@@ -185,6 +185,46 @@ export type GameDistributionLineage = {
parentAuthorName?: string | null;
};
/**
* 族谱树上的单个作品节点。
*
* 只有公开且未软删除的作品会成为节点(锚点不可读时接口直接 404),因此 `title` 必然存在;
* `authorName` 为 null 表示作者信息不可得。`parentGameId` 指向的作品可能不在树里
* (父作品已删/未公开),展示层据此把该节点标注为「原作品不可用」,不猜测父作品内容。
* 节点只带作品级公开信息,不含对象键、素材键或 owner user id 之外的私有字段。
*/
export type GameDistributionLineageNode = {
gameId: string;
title: string;
authorName?: string | null;
/** 代际;母版为 0。 */
generation: number;
/** 父作品 ID;母版为 null。 */
parentGameId?: string | null;
playCount: number;
status: GameDistributionGameVisibility;
};
/**
* 族谱读接口响应:`nodes` 按代际升序、同代按创建时间升序稳定排序,含根(若根可见);
* 超过节点上限时截断尾部并置 `truncated`,不静默丢弃。
*/
export type GameDistributionLineageResponse = {
/** 请求作品所属的根作品 ID;根不可用时仍返回,展示层据此标注树顶不可用。 */
rootGameId: string;
/** 根节点在可见集合中的投影;根已删或未公开时为 null。 */
root?: GameDistributionLineageNode | null;
nodes: GameDistributionLineageNode[];
truncated: boolean;
};
/** 直接衍生作品(「被改编」)列表响应;只含对外可见的直接子代。 */
export type GameDistributionDerivedResponse = {
gameId: string;
nodes: GameDistributionLineageNode[];
truncated: boolean;
};
export type GameDistributionGame = {
id: string;
title: string;