# 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. 用户关系统一返回类似以下结构的公开基础信息: ```rust 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 | | `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` 信封及其通用 `IntoResponse` 序列化逻辑。 ### 3.4 需要单独评估 - 本人/鉴权响应中的特权字段; - 管理员、审计、举报和安全相关响应; - 有隐私规则的 profile-detail 响应; - chat 等看似 API view、但需要确认 handler/OpenAPI 暴露边界的结构。 ## 4. 已确认的代码边界 项目是大型 Rust monorepo。主要响应序列化边界如下: - `lib/export/data.rs`:`AppResponseData`,负责统一响应 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`,然后映射嵌套用户对象。新实现应抽取或复用这一模式,而不是在每个领域重复实现。 ## 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` 或带缺失状态的映射; - 明确删除、不可见和不存在用户的处理; - 支持 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`,移除裸 `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` 中逐条查询。 ### 阶段四:迁移 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`。 2. auth login/me 继续保留鉴权语义和 `UserMe` 特权字段,但公开登录用户信息应复用 canonical 基础投影,避免同名 DTO 漂移。 3. `ReportView` 的 `reporter_uid`、`reported_uid`、`resolver_uid` 改为嵌套 `Option`,并继续执行现有管理员授权检查;不在公开响应中保留裸 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` 的 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` 直接返回 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`,删除裸用户关系字段与 `From`,改用公开 `from_model(m, ...)` 构造方法。 - 请求 DTO、path/query 参数、领域/内部 ID 全部保留;`AppResponseData` 信封未改。 - 验证:`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)。 - 未提交,改动保留在工作树中。