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. 核心目标
-
所有公开 API 响应中,用户关系不再以裸用户 UID 或用户关联 UUID 返回。
-
用户关系统一返回类似以下结构的公开基础信息:
UserBasicInfo { uid, username, display_name, avatar_url, ... } -
用户已确认采用直接迁移:删除旧的响应字段,不同时保留
*_id和嵌套用户对象。 -
通过共享批量加载逻辑避免逐条查询用户,防止引入 N+1 查询。
-
统一更新 DTO、mapper、usecase、handler、OpenAPI schema 和测试。
3. 范围边界
3.1 领域覆盖清单
本次改造是跨领域的 API 响应契约迁移,不是只修改 user 模块。以下领域均属于一等范围:
| 领域 | 重点用户关系响应 |
|---|---|
user / auth / profile / social | 公开用户、登录用户、本人信息、关注/屏蔽关系、通知 actor |
project | creator、lead、activity actor、project member |
issues / issue | issue author、member、watcher、event actor、mention/user 关系 |
space | owner、member、invited_by、invite user、ban user/banned_by |
repo / pull request | PR author、comment/review/event user、collaborator、publisher、assignee、reviewer、commit author/committer、fork owner |
post | post/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 和 AxumIntoResponse;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. 实施步骤
阶段一:建立公共类型和加载器
- 确定
UserBasicInfo的 crate/module 位置和字段 allowlist。 - 实现 DB user model 到
UserBasicInfo的显式转换。 - 抽取可复用的批量 user loader:
- 输入去重后的
Uuid集合; - 空集合不发起数据库查询;
- 单次查询并返回
HashMap<Uuid, UserBasicInfo>或带缺失状态的映射; - 明确删除、不可见和不存在用户的处理;
- 支持 required/optional 关系的不同映射方式;
- 不把 email、权限和安全字段带入公共投影。
- 输入去重后的
- 为 loader 增加查询次数、去重和缺失用户测试。
阶段二:迁移现有公共用户投影
- 评估 profile
lib/user/features/profile/types.rs::UserPublic是否直接改为 canonical 类型,或保留薄 wrapper/re-export。 - 评估 auth
lib/auth/features/login/types.rs::UserPublic的登录响应是否引用 canonical 类型。 - 使 social follower/following/blocked 响应使用 canonical 投影。
- 处理
ProfileView的user_id:遵循 profile 隐私规则,改为user: Option<UserBasicInfo>,移除裸user_id。 - 保留
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 和管理员响应
NotificationView.actor_uid改为可选嵌套actor: Option<UserBasicInfo>。- auth login/me 继续保留鉴权语义和
UserMe特权字段,但公开登录用户信息应复用 canonical 基础投影,避免同名 DTO 漂移。 ReportView的reporter_uid、reported_uid、resolver_uid改为嵌套Option<UserBasicInfo>,并继续执行现有管理员授权检查;不在公开响应中保留裸 UUID。- deletion、suspension、agreement、security 等 account/admin DTO 逐一确认是否为管理员/本人响应;确认属于 HTTP response 的用户关系均改为嵌套公开信息,不能通过全局字段重命名直接迁移。
chat/storagelayer/mapping.rs、JWT/session、DB/storage model 只有在确认处于 HTTP response 序列化边界后才纳入迁移。
阶段七:更新 OpenAPI、示例和测试
- 为
UserBasicInfo添加Serialize、Deserialize、ToSchema以及必要的文档说明。 - 更新各领域 response DTO 的 schema 和嵌套关系。
- 检查
lib/webapi/openapi.rs的聚合结果和各 handler/openapi 模块注册,确保没有残留旧字段。 - 更新 API fixtures、响应 JSON 示例和客户端契约文档。
- 不修改
AppResponseData<T>的 envelope 结构。
8. 重点文件清单
公共类型和基础边界
lib/export/data.rslib/export/lib.rslib/export/user/basic.rs(建议新增)lib/webapi/openapi.rslib/export/openapi.rs
用户、鉴权和社交
lib/user/features/profile/types.rslib/user/features/profile/usecase.rslib/user/features/social/types.rslib/user/features/social/usecase.rslib/auth/features/login/types.rslib/auth/features/login/usecase.rslib/auth/handler/me.rslib/export/user/notifications.rs
Post 和 issues
lib/export/post/views.rslib/export/issues/issue.rslib/export/issues/issue_member.rslib/export/issues/issue_watch.rslib/export/issues/issue_event.rslib/export/issues/issue_user.rs- 对应
lib/post、lib/issues的 handler/usecase/impls/openapi。
Repo 和 pull request
lib/export/repo/pull_request.rslib/export/repo/collab.rslib/export/repo/release.rslib/export/repo/extras.rs- 对应
lib/repo的 usecase/handler/openapi。
Project 和 space
lib/export/project/lifecycle.rslib/export/project/activities.rslib/export/space/spaces.rslib/export/space/members.rslib/export/space/invites.rslib/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 执行:
cargo fmt;- 对
export、user、auth、issues、post、repo、project、space、webapi、apihost进行cargo check; - 运行相关领域测试和 OpenAPI 测试;
- 必要时运行完整 workspace test;
- 生成并检查 OpenAPI 文档;
- 使用 grep/AST 审计所有公开 response DTO:
- 找出用户关系字段仍为裸
Uuid的响应结构; - 排除 request/path/query、领域 ID、内部结构和经确认的管理员审计例外;
- 审查所有
From<Model>直接返回 response 的路径;
- 找出用户关系字段仍为裸
- 检查查询日志或 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. 待审查问题
UserBasicInfo.uid的公开字符串格式是否统一沿用现有 UUID 序列化方式?该细节不影响字段结构和迁移范围。
本文件只记录改动计划,待审查确认后再实施业务代码变更。
13. 实施状态(2026-08-12 已执行完毕)
- 共享基础设施:新增
lib/db/user/users_loader.rs(load_users/load_user批量/单点加载,防 N+1)与lib/export/user/basic.rs(canonicalUserBasicInfo { 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)。 - 未提交,改动保留在工作树中。