# 设计:管理员用户列表 — 逐用户统计与公开页链接 **日期**:2026-04-18 **状态**:已定稿(与产品对话一致) ## 1. 背景与目标 管理员(代码中与 **`role === 'admin'`** 一致,沿用 `requireAdmin`;不引入新角色)在「用户管理」页需要: 1. **逐用户统计**(表格列):文章数、时间线条目数、RSS 订阅源数。 2. **跳转与复制**:当用户存在 **`publicSlug`** 时,可打开站内公开主页 `/@{publicSlug}`,并可将 **完整 URL**(`origin + '/@' + publicSlug`)复制到剪贴板。 **明确不包含(第一期)**:RSS 条目总数(`rss_items`)、全局汇总卡片、代用户登录、仅按可见性过滤后的计数。 ## 2. 统计语义 对每位用户,三个计数均为对应表中 **`user_id` 等于该用户 id 的全表行数**,**不按** `visibility` 或其它展示规则过滤。理由:该页面向运营/审计,需反映库内真实数据量;若未来要「仅公开内容」计数,应单独开需求并改字段命名以免歧义。 ## 3. 架构与方案选择 在已选方案下:**扩展现有 `GET /api/admin/users`**,在同一分页响应中为每个用户返回三个非负整数字段,避免二次请求与 id 合并问题。 未采用:独立 stats 接口、用户表冗余计数列。 ## 4. API 契约 - **路径**:`GET /api/admin/users`(不变)。 - **鉴权**:现有全局 auth + `requireAdmin`。 - **分页**:沿用现有 `limit`(默认 50,最大 100)、`offset`。 - **响应**:`users` 数组元素在现有字段(`id`, `username`, `email`, `role`, `status`, `publicSlug`, `createdAt`)基础上增加: | 字段 | 类型 | 说明 | |------|------|------| | `postCount` | 整数 ≥ 0 | `posts` 表中该 `user_id` 行数 | | `timelineEventCount` | 整数 ≥ 0 | `timeline_events` 表中该 `user_id` 行数 | | `rssFeedCount` | 整数 ≥ 0 | `rss_feeds` 表中该 `user_id` 行数 | 实现细节:使用 Drizzle + SQLite 可维护的聚合方式(例如相关子查询或等价 join),以保证与分页 `ORDER BY` 一致且性能可接受;`user_id` 上应可利用现有外键/索引。 ## 5. 前端(用户管理页) **文件**:`app/pages/me/admin/users/index.vue`(为主变更点)。 - 表格新增三列,展示上述三个计数。 - **`publicSlug` 非空**: - **打开**:链到路由 `/@{publicSlug}`(与站内其它公开页一致)。 - **复制**:点击后写入剪贴板的 URL 应与浏览器打开 `/@{publicSlug}` 时一致;推荐使用 `URL(\`/@${publicSlug}\`, window.location.origin).href` 生成,避免手写拼接与编码不一致。 - **`publicSlug` 为空**:不渲染可点击外链;复制按钮禁用或隐藏,并显示「—」等占位。 - **复制失败**:向用户展示简短错误提示(与项目现有 toast/通知模式一致)。 ## 6. 错误与边界 - 计数为 `0` 时正常显示。 - 网络或权限错误沿用现有 `request`/`unwrap` 行为。 ## 7. 测试 第一期以手动验证为主。若仓库已有 admin 列表或 Drizzle 集成测试惯例,可补充最小用例(例如种子数据下某用户的三个计数);非强制阻塞项,在实现计划中标注。 ## 8. 非目标 - 不修改 `users` 表结构。 - 不增加 `rss_items` 计数列。 - 不实现「超级管理员」与 `admin` 的区分。