Skip to content
Clone
7a0eb3a
389 lines 17.5 KB Markdown

API 用户关联 UUID 改造计划

1. 背景

当前项目的多个 API 响应 DTO 直接暴露用户关联 UUID,例如 author_id、user_id、creator_id、owner_id、actor_id、publisher_id 等。不同领域还存在多套重复且字段不一致的用户投影:

  • profile 领域的 UserPublic;
  • auth 领域的 UserPublic;
  • repo 领域的 UserMini 与多个 UserSummary;
  • issues 领域的 IssueUserView;
  • post、project、space 等领域直接返回裸 UUID。

这会导致以下问题:

  • 公开 API 泄露裸用户标识,调用方无法直接获得展示所需的基础信息;
  • 用户投影的字段命名不一致,例如 uid/id、avatar/avatar_url;
  • 部分投影可能携带 email 等不应默认公开的字段;
  • DTO 直接从数据库模型转换,批量关联用户时容易产生 N+1 查询;
  • OpenAPI schema、handler、usecase 和测试之间的响应契约不统一。

2. 核心目标

  1. 所有公开 API 响应中,用户关系不再以裸用户 UID 或用户关联 UUID 返回。

  2. 用户关系统一返回类似以下结构的公开基础信息:

    UserBasicInfo {
        uid,
        username,
        display_name,
        avatar_url,
        ...
    }
    
  3. 用户已确认采用直接迁移:删除旧的响应字段,不同时保留 *_id 和嵌套用户对象。

  4. 通过共享批量加载逻辑避免逐条查询用户,防止引入 N+1 查询。

  5. 统一更新 DTO、mapper、usecase、handler、OpenAPI schema 和测试。

3. 范围边界

3.1 领域覆盖清单

本次改造是跨领域的 API 响应契约迁移,不是只修改 user 模块。以下领域均属于一等范围:

领域重点用户关系响应
user / auth / profile / social公开用户、登录用户、本人信息、关注/屏蔽关系、通知 actor
projectcreator、lead、activity actor、project member
issues / issueissue author、member、watcher、event actor、mention/user 关系
spaceowner、member、invited_by、invite user、ban user/banned_by
repo / pull requestPR author、comment/review/event user、collaborator、publisher、assignee、reviewer、commit author/committer、fork owner
postpost/reply author、last reply、reaction/mention/watch user

每个领域都必须从 response DTO、mapper、usecase、handler、OpenAPI schema 和测试链路完整迁移;不能只新增 UserBasicInfo 而遗漏某个领域的裸用户关系。

3.2 需要迁移

  • 已确认属于 HTTP 公开响应的用户关系字段;
  • 各领域 response view 中的用户作者、所有者、创建者、操作者、发布者、成员、审核者等关系;
  • 负责构造这些响应的 usecase、mapper 和 handler;
  • 对应的 serde/OpenAPI schema、fixtures 和行为测试。

3.3 明确保留不变

  • request body DTO 中的用户 UUID;
  • path、query、分页过滤器中的用户 UUID;
  • 领域实体 ID,例如 issue_id、project_id、space_id、repo_id、channel_id、message_id、comment_id、review_id、role_id 等;
  • JWT、session、数据库模型、存储映射和其他未确认处于 HTTP 序列化边界的内部结构;
  • AppResponseData<T> 信封及其通用 IntoResponse 序列化逻辑。

3.4 需要单独评估

  • 本人/鉴权响应中的特权字段;
  • 管理员、审计、举报和安全相关响应;
  • 有隐私规则的 profile-detail 响应;
  • chat 等看似 API view、但需要确认 handler/OpenAPI 暴露边界的结构。

4. 已确认的代码边界

项目是大型 Rust monorepo。主要响应序列化边界如下:

  • lib/export/data.rs:AppResponseData<T>,负责统一响应 envelope 和 Axum IntoResponse;
  • lib/webapi/openapi.rs:组合 webapi、auth、issues、post、project、repo、space、user 的 OpenAPI 文档;
  • lib/export/openapi.rs:处理 API path 前缀;
  • 各领域 handler/openapi 模块:注册具体 response DTO 和 schema;
  • 各领域 feature usecase:从 DB model 或查询结果构造 response view。

现有 profile social usecase 已有可复用的批量加载模式:收集 UID,使用 uid.is_in(...) 一次查询用户,构造 HashMap<Uuid, user_model::Model>,然后映射嵌套用户对象。新实现应抽取或复用这一模式,而不是在每个领域重复实现。

5. 目标公共用户投影

5.1 新增 canonical 类型

在合适的公共 export/user 模块中新增并导出 UserBasicInfo,建议候选位置为:

  • lib/export/user/basic.rs;
  • 或项目已有的公共 user/export 模块,避免与 auth/profile feature 形成反向依赖。

最终位置应遵循 workspace crate 依赖方向,并保证各领域 DTO 可以稳定引用。

5.2 字段策略

默认只包含公开、低敏感、适合列表和关联展示的字段:

  • uid:公开用户标识,类型和序列化形式需统一;
  • username;
  • display_name;
  • avatar_url,或经确认后统一使用现有 avatar 命名;
  • 其他确有公开需求且经过隐私评估的基础字段。

默认不包含:

  • email;
  • active、admin、restricted 等状态和权限字段;
  • last login;
  • 安全、会话、设备和内部审计字段;
  • profile-detail 中受隐私规则保护的字段。

需要明确 uid 是允许作为嵌套 UserBasicInfo 的公开字段返回的用户标识,而不是继续作为 response 顶层裸关联字段返回。

5.3 兼容策略

用户已确认直接迁移为 breaking change:

  • 删除公开 response 中的 author_id、user_id、creator_id、owner_id 等用户关系字段;
  • 不保留旧字段作为兼容字段;
  • 使用语义化嵌套字段,例如 author、user、creator、owner、actor、publisher、invited_by;
  • 更新 API 文档和测试中的预期 JSON。

6. 缺失用户策略(已确认)

已确认:当关联用户已删除、不可见或无法加载时,统一返回 null,而不是返回裸 UUID 或伪造用户对象。该规则同时适用于 required 和 optional 用户关系,不因单个关联缺失而让整个响应失败。

活动、事件、通知等历史响应中的用户关系也必须迁移;历史数据无法解析用户时同样返回 null。如未来需要保留历史展示名称,应使用独立的非 UUID 快照字段,不能恢复裸用户关联 UUID。

7. 实施步骤

阶段一:建立公共类型和加载器

  1. 确定 UserBasicInfo 的 crate/module 位置和字段 allowlist。
  2. 实现 DB user model 到 UserBasicInfo 的显式转换。
  3. 抽取可复用的批量 user loader:
    • 输入去重后的 Uuid 集合;
    • 空集合不发起数据库查询;
    • 单次查询并返回 HashMap<Uuid, UserBasicInfo> 或带缺失状态的映射;
    • 明确删除、不可见和不存在用户的处理;
    • 支持 required/optional 关系的不同映射方式;
    • 不把 email、权限和安全字段带入公共投影。
  4. 为 loader 增加查询次数、去重和缺失用户测试。

阶段二:迁移现有公共用户投影

  1. 评估 profile lib/user/features/profile/types.rs::UserPublic 是否直接改为 canonical 类型,或保留薄 wrapper/re-export。
  2. 评估 auth lib/auth/features/login/types.rs::UserPublic 的登录响应是否引用 canonical 类型。
  3. 使 social follower/following/blocked 响应使用 canonical 投影。
  4. 处理 ProfileView 的 user_id:遵循 profile 隐私规则,改为 user: Option<UserBasicInfo>,移除裸 user_id。
  5. 保留 UserMe 的本人/特权字段,但不要把它当作公开 UserBasicInfo 使用。

阶段三:迁移 post 和 issues

涉及主要文件:

  • lib/export/post/views.rs;
  • lib/export/issues/issue.rs;
  • lib/export/issues/issue_member.rs;
  • lib/export/issues/issue_watch.rs;
  • lib/export/issues/issue_event.rs;
  • lib/export/issues/issue_user.rs。

需要处理的公开关系包括:

  • PostView.author_id、last_reply_by;
  • PostReplyView.author_id、deleted_by;
  • post reaction/mention/watch 的 user_id;
  • IssueView.author_id;
  • issue member/watch/event/user view 的 user_id;
  • 删除者、最后回复者等可选关系。

需要保留的请求字段包括 ListPostQuery.author_id、CreateMentionOption.user_id、创建/更新 issue/member 等 command 中的用户 UUID。

各 handler/usecase 应先收集页面内所有用户关系,再批量加载,最后构造 page/list response,避免在 From<Model> 中逐条查询。

阶段四:迁移 repo 和 pull request

涉及主要文件:

  • lib/export/repo/pull_request.rs;
  • lib/export/repo/collab.rs;
  • lib/export/repo/release.rs;
  • lib/export/repo/extras.rs;
  • 相关 repo extras、collab、release、pull request usecase/handler。

需要处理的公开关系包括:

  • PullRequestView.author_id;
  • PR comment/review/event 的 user_id;
  • CollaboratorView.user_id;
  • release publisher;
  • UserMini、多个 UserSummary;
  • assignee、reviewer、commit author/committer、fork owner 等关联。

应统一现有 UserMini/UserSummary 的字段和命名,移除默认 email 暴露。review_id、repo_id 等 PR/领域 ID 不是用户关系,不能误改。创建、更新、merge、review submit 和 list query 中的请求 ID 保持不变。

阶段五:迁移 project 和 space

涉及主要文件:

  • lib/export/project/lifecycle.rs;
  • lib/export/project/activities.rs;
  • project member usecase/handler;
  • lib/export/space/spaces.rs;
  • lib/export/space/members.rs;
  • lib/export/space/invites.rs;
  • lib/export/space/bans.rs;
  • 必要时扩展 roles、channels、overwrites 等响应 DTO。

需要处理的公开关系包括:

  • ProjectView.creator_id、lead_id;
  • ActivityView.actor_id;
  • project member user;
  • SpaceView.owner_id;
  • SpaceMemberView.user_id、invited_by;
  • SpaceInviteView.user_id;
  • SpaceBanView.user_id、banned_by。

ActivityView.target_id 必须根据具体 verb/target 类型判断,不能仅因字段名相近就当作用户关系。space_id、parent_dir、role/channel/message 等领域 ID,以及 create/add/list command 中的请求用户 UUID 保持不变。

阶段六:迁移 notifications、auth 和管理员响应

  1. NotificationView.actor_uid 改为可选嵌套 actor: Option<UserBasicInfo>。
  2. auth login/me 继续保留鉴权语义和 UserMe 特权字段,但公开登录用户信息应复用 canonical 基础投影,避免同名 DTO 漂移。
  3. ReportView 的 reporter_uid、reported_uid、resolver_uid 改为嵌套 Option<UserBasicInfo>,并继续执行现有管理员授权检查;不在公开响应中保留裸 UUID。
  4. deletion、suspension、agreement、security 等 account/admin DTO 逐一确认是否为管理员/本人响应;确认属于 HTTP response 的用户关系均改为嵌套公开信息,不能通过全局字段重命名直接迁移。
  5. chat/storagelayer/mapping.rs、JWT/session、DB/storage model 只有在确认处于 HTTP response 序列化边界后才纳入迁移。

阶段七:更新 OpenAPI、示例和测试

  1. 为 UserBasicInfo 添加 Serialize、Deserialize、ToSchema 以及必要的文档说明。
  2. 更新各领域 response DTO 的 schema 和嵌套关系。
  3. 检查 lib/webapi/openapi.rs 的聚合结果和各 handler/openapi 模块注册,确保没有残留旧字段。
  4. 更新 API fixtures、响应 JSON 示例和客户端契约文档。
  5. 不修改 AppResponseData<T> 的 envelope 结构。

8. 重点文件清单

公共类型和基础边界

  • lib/export/data.rs
  • lib/export/lib.rs
  • lib/export/user/basic.rs(建议新增)
  • lib/webapi/openapi.rs
  • lib/export/openapi.rs

用户、鉴权和社交

  • lib/user/features/profile/types.rs
  • lib/user/features/profile/usecase.rs
  • lib/user/features/social/types.rs
  • lib/user/features/social/usecase.rs
  • lib/auth/features/login/types.rs
  • lib/auth/features/login/usecase.rs
  • lib/auth/handler/me.rs
  • lib/export/user/notifications.rs

Post 和 issues

  • lib/export/post/views.rs
  • lib/export/issues/issue.rs
  • lib/export/issues/issue_member.rs
  • lib/export/issues/issue_watch.rs
  • lib/export/issues/issue_event.rs
  • lib/export/issues/issue_user.rs
  • 对应 lib/post、lib/issues 的 handler/usecase/impls/openapi。

Repo 和 pull request

  • lib/export/repo/pull_request.rs
  • lib/export/repo/collab.rs
  • lib/export/repo/release.rs
  • lib/export/repo/extras.rs
  • 对应 lib/repo 的 usecase/handler/openapi。

Project 和 space

  • lib/export/project/lifecycle.rs
  • lib/export/project/activities.rs
  • lib/export/space/spaces.rs
  • lib/export/space/members.rs
  • lib/export/space/invites.rs
  • lib/export/space/bans.rs
  • 对应 lib/project、lib/space 的 usecase/handler/openapi。

CLI/OpenAPI 输出

  • app/apihost/src/cmd_openapi.rs

以上清单是首批重点范围,不代表可以跳过其他公开 response DTO。最终范围必须以序列化边界和 AST 审计结果为准。

9. 测试计划

9.1 类型和序列化测试

  • UserBasicInfo 的字段 allowlist 测试;
  • 确认不存在 email、权限、session、安全字段;
  • nested user JSON 使用统一字段名;
  • 所有用户关系(包括 required 和 optional)在用户缺失、删除或不可见时序列化为 null;
  • response JSON 中不再出现旧的用户关系 *_id 字段。

9.2 loader/usecase 测试

  • 输入 UID 去重;
  • 空 UID 集合不查询;
  • 页面内多个关系只触发一次用户查询;
  • 缺失、删除、不可见用户的处理统一返回 null,且不会回退为裸 UUID;
  • required/optional 关系分别覆盖成功和失败分支;
  • 映射结果保持原有列表顺序。

9.3 handler/行为测试

至少覆盖:

  • post author/reply/reaction/mention/watch;
  • issue author/member/watch/event/user;
  • pull request author/comment/review/event;
  • repo collaborator/release/extras;
  • project creator/lead/activity/member;
  • space owner/member/invite/ban;
  • notification actor;
  • profile public、social、auth login/me;
  • admin/audit response 的权限和脱敏行为。

现有测试按行为命名,不能只搜索 *test*.rs。重点测试目录包括:

  • lib/issues/tests/;
  • lib/post/tests/;
  • lib/repo/tests/pull_request/;
  • 相关 lib/project、lib/space、lib/user、lib/auth 测试。

9.4 OpenAPI 测试

  • 更新已有 post、pull request OpenAPI 测试;
  • 为受影响领域补充 schema 断言;
  • 确认生成 schema 使用 UserBasicInfo,且旧裸 UUID 用户关系字段已删除;
  • 检查响应中的领域 ID 仍然存在且类型未被误改。

10. 验证和审计

实现后按受影响 workspace crate 执行:

  1. cargo fmt;
  2. 对 export、user、auth、issues、post、repo、project、space、webapi、apihost 进行 cargo check;
  3. 运行相关领域测试和 OpenAPI 测试;
  4. 必要时运行完整 workspace test;
  5. 生成并检查 OpenAPI 文档;
  6. 使用 grep/AST 审计所有公开 response DTO:
    • 找出用户关系字段仍为裸 Uuid 的响应结构;
    • 排除 request/path/query、领域 ID、内部结构和经确认的管理员审计例外;
    • 审查所有 From<Model> 直接返回 response 的路径;
  7. 检查查询日志或 mock query 次数,确认没有新增 N+1。

11. 验收标准

  • 公开 response 中的用户关系均为统一的嵌套 UserBasicInfo 或明确的受控审计投影;
  • 旧的公开 author_id、user_id、creator_id、owner_id 等用户关系字段已按直接迁移决策删除;
  • request、path/query、领域和内部 ID 未被误改;
  • UserBasicInfo 不包含 email、权限、session、安全或未授权 profile 字段;
  • optional 关联用户的缺失行为符合最终确认的策略;
  • 批量关系加载无 N+1,空集合和缺失用户行为有测试;
  • OpenAPI schema、示例和实际 JSON 一致;
  • 受影响领域测试、OpenAPI 测试、格式化和编译验证通过;
  • 最终 AST/序列化边界审计有明确的 intentional exceptions 清单。

12. 待审查问题

  1. UserBasicInfo.uid 的公开字符串格式是否统一沿用现有 UUID 序列化方式?该细节不影响字段结构和迁移范围。

本文件只记录改动计划,待审查确认后再实施业务代码变更。


13. 实施状态(2026-08-12 已执行完毕)

  • 共享基础设施:新增 lib/db/user/users_loader.rs(load_users/load_user 批量/单点加载,防 N+1)与 lib/export/user/basic.rs(canonical UserBasicInfo { uid, username, display_name, avatar_url })。
  • 全部域迁移完成:user(profile/social)、post、issues、repo(PR/collab/release/extras/subscriber)、project(lifecycle/activities/members/documents/views/favorites)、space(spaces/members/invites/bans/roles/channels/overwrites)、user(notifications/admin reports/suspensions/deletion)。所有响应 DTO 改为嵌套 Option<UserBasicInfo>,删除裸用户关系字段与 From<Model>,改用公开 from_model(m, ...) 构造方法。
  • 请求 DTO、path/query 参数、领域/内部 ID 全部保留;AppResponseData<T> 信封未改。
  • 验证:cargo check --workspace 通过;cargo fmt 通过;裸 UUID 审计通过(残留 17 处均为请求 DTO/查询参数/领域 ID);cargo test -p export -p user -p issues -p post -p project -p repo -p space 全部通过(含 space 4 个 rename 测试,测试辅助已补 space_members 表 DDL)。
  • 未提交,改动保留在工作树中。