From f391d578fc918694db81fbc6e2cab5117137c145 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 18 Apr 2026 14:49:47 +0800 Subject: [PATCH] docs(plan): admin user stats and public profile links Made-with: Cursor --- ...6-04-18-admin-user-stats-implementation-plan.md | 228 +++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-18-admin-user-stats-implementation-plan.md diff --git a/docs/superpowers/plans/2026-04-18-admin-user-stats-implementation-plan.md b/docs/superpowers/plans/2026-04-18-admin-user-stats-implementation-plan.md new file mode 100644 index 0000000..8102b2e --- /dev/null +++ b/docs/superpowers/plans/2026-04-18-admin-user-stats-implementation-plan.md @@ -0,0 +1,228 @@ +# Admin 用户列表统计与公开页链接 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在管理员用户列表接口与页面中,为每位用户展示文章 / 时间线 / RSS 源数量,并在存在 `publicSlug` 时提供站内打开与复制完整公开页 URL。 + +**Architecture:** 在单一 `GET /api/admin/users` 响应中通过 Drizzle + SQLite 为每行附加三个计数(相关子查询 `COUNT(*)`,不按 `visibility` 过滤);前端在用户管理表格增加列与操作,复制 URL 使用 `URL` 构造函数与可单测的小工具函数生成,失败时用 Nuxt UI `useToast` 提示。 + +**Tech Stack:** Nuxt 4 / Nitro、Vue 3、Drizzle ORM 0.45、SQLite(`drizzle-pkg`)、Bun test、`@nuxt/ui` 4.6。 + +**Spec:** `docs/superpowers/specs/2026-04-18-admin-user-stats-design.md` + +--- + +## 文件结构(将创建 / 修改) + +| 路径 | 职责 | +|------|------| +| `server/api/admin/users.get.ts` | 在现有分页查询的 `select` 中增加三个计数字段并保持 `requireAdmin` | +| `app/utils/public-profile-url.ts` | 根据 `origin` 与 `publicSlug` 生成与站内 `/@slug` 一致的绝对 URL(供复制与单测) | +| `app/utils/public-profile-url.test.ts` | 对 URL 构造函数的回归测试 | +| `app/pages/me/admin/users/index.vue` | 表格列、打开链接、`useToast` + 剪贴板复制 | + +--- + +### Task 1: 公开页绝对 URL 小工具(TDD) + +**Files:** + +- Create: `app/utils/public-profile-url.ts` +- Create: `app/utils/public-profile-url.test.ts` + +- [ ] **Step 1: 编写失败测试** + +`app/utils/public-profile-url.test.ts`: + +```typescript +import { describe, expect, test } from "bun:test"; +import { buildPublicProfileAbsoluteUrl } from "./public-profile-url"; + +describe("buildPublicProfileAbsoluteUrl", () => { + test("joins origin with /@slug path", () => { + expect(buildPublicProfileAbsoluteUrl("https://example.com", "my-slug")).toBe( + "https://example.com/@my-slug", + ); + }); + + test("normalizes origin without trailing slash", () => { + expect(buildPublicProfileAbsoluteUrl("https://example.com/", "ab")).toBe("https://example.com/@ab"); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: + +```bash +cd /home/dash/projects/person-panel && bun test app/utils/public-profile-url.test.ts +``` + +Expected: FAIL(模块或函数不存在)。 + +- [ ] **Step 3: 最小实现** + +`app/utils/public-profile-url.ts`: + +```typescript +/** 与站内路由 `/@${publicSlug}` 对应的绝对 URL(用于复制到剪贴板)。 */ +export function buildPublicProfileAbsoluteUrl(origin: string, publicSlug: string): string { + const base = origin.replace(/\/+$/, ""); + const path = `/@${publicSlug}`; + return new URL(path, `${base}/`).href; +} +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: + +```bash +cd /home/dash/projects/person-panel && bun test app/utils/public-profile-url.test.ts +``` + +Expected: PASS。 + +- [ ] **Step 5: Commit** + +```bash +cd /home/dash/projects/person-panel +git add app/utils/public-profile-url.ts app/utils/public-profile-url.test.ts +git commit -m "feat(admin-ui): add public profile absolute URL helper" +``` + +--- + +### Task 2: 扩展 `GET /api/admin/users` 返回三个计数 + +**Files:** + +- Modify: `server/api/admin/users.get.ts` + +- [ ] **Step 1: 在 `select` 中加入相关子查询计数** + +在文件顶部增加导入: + +```typescript +import { sql } from "drizzle-orm"; +import { posts, timelineEvents } from "drizzle-pkg/lib/schema/content"; +import { rssFeeds } from "drizzle-pkg/lib/schema/rss"; +``` + +将 `dbGlobal.select({ ... })` 的对象扩展为在现有字段之外增加(字段名与 spec 一致): + +```typescript +postCount: sql`(select count(*) from ${posts} where ${posts.userId} = ${users.id})`.mapWith( + Number, +), +timelineEventCount: sql`(select count(*) from ${timelineEvents} where ${timelineEvents.userId} = ${users.id})`.mapWith( + Number, +), +rssFeedCount: sql`(select count(*) from ${rssFeeds} where ${rssFeeds.userId} = ${users.id})`.mapWith( + Number, +), +``` + +保持 `from(users)`、`orderBy(desc(users.id))`、`limit`/`offset` 不变。若本地运行时报错(与 Drizzle 生成 SQL 有关),可改为「三个 `groupBy(userId)` 子查询 `leftJoin`」的等价写法,但须保持响应字段名不变。 + +- [ ] **Step 2: 手工验证 API** + +1. 以管理员账号登录(`role` 为 `admin`)。 +2. 在浏览器开发者工具中请求 `GET /api/admin/users`(或 `curl` 带上有效会话 Cookie)。 +3. 确认 JSON 中每个 `users[]` 元素含 `postCount`、`timelineEventCount`、`rssFeedCount`,且为非负整数;与库内实际行数一致(可任选一名用户用 DB 客户端 `select count(*)` 对照)。 + +- [ ] **Step 3: 回归测试** + +Run: + +```bash +cd /home/dash/projects/person-panel && bun test +``` + +Expected: 全部 PASS(含 Task 1 新增测试)。 + +- [ ] **Step 4: Commit** + +```bash +cd /home/dash/projects/person-panel +git add server/api/admin/users.get.ts +git commit -m "feat(admin): include per-user content counts in users list API" +``` + +--- + +### Task 3: 用户管理页表格与复制链接 + +**Files:** + +- Modify: `app/pages/me/admin/users/index.vue` + +- [ ] **Step 1: 扩展行类型与导入** + +在 `