Browse Source

docs: refine multitenant hub spec and add implementation plan

Made-with: Cursor
main
npmrun 3 days ago
parent
commit
42b2b7ac7a
  1. 600
      docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md
  2. 17
      docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md

600
docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md

@ -0,0 +1,600 @@
# Person Panel 多用户数据中心与 RSS 实现计划
> **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:** 在现有 Nuxt 4 + Nitro + SQLite + Drizzle + `@nuxt/ui` 项目上,实现多用户隔离的个人资料/文章/时光机/RSS 收件箱、条目级可见性与分享链接、实例管理员开号与禁用、以及进程内 RSS 定时同步(符合 `docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md`)。
**Architecture:** 单体 Nitro API 分 `public` / `me` / `admin` 三层;内容域按 `userId` 强隔离;可见性三态统一;RSS 抓取与调度放在 `server/service/rss` + `server/plugins` 进程内定时器;前端用 Nuxt UI 统一空状态与表单,公开路由与 API 白名单同步放开。
**Tech Stack:** Nuxt 4.4、Nitro、`@nuxt/ui` 4.x、Drizzle ORM、SQLite、better-sqlite3、bcryptjs、Zod、(新增)适用于 RSS 解析的轻量 XML 解析库、原生 `fetch`
---
## File Structure Map(新建 / 修改)
| 路径 | 职责 |
|------|------|
| `packages/drizzle-pkg/database/sqlite/schema/auth.ts` | 扩展 `users`:`role`、`status`、`publicSlug`、`bioMarkdown`、`bioVisibility`、`socialLinksJson`、`avatarVisibility` 等 |
| `packages/drizzle-pkg/database/sqlite/schema/content.ts`(新建) | `posts`、`timeline_events` 表定义 |
| `packages/drizzle-pkg/database/sqlite/schema/rss.ts`(新建) | `rss_feeds`、`rss_items` |
| `packages/drizzle-pkg/lib/schema/*` | 重新导出新建表 |
| `packages/drizzle-pkg/seed.ts` | Bootstrap 管理员创建(读 env,仅在无 admin 时插入) |
| `server/constants/visibility.ts`(新建) | `Visibility` 枚举与 Zod schema |
| `server/utils/share-token.ts`(新建) | 生成/校验 `shareToken`(crypto 随机 24+ bytes URL-safe) |
| `server/utils/rss-url.ts`(新建) | SSRF 前置校验:scheme、主机、禁止私网 IP(解析后)、重定向上限策略的入口 |
| `server/utils/admin-guard.ts`(新建) | `requireAdmin(event)` |
| `server/middleware/10.auth-guard.ts` | 配合扩展 allowlist;`getCurrent` 已登录但用户 disabled → 401 + 清 cookie(见 Task 3) |
| `server/utils/auth-api-routes.ts` | 增加 `/api/public` 前缀规则、`/api/admin` 是否走会话(admin 需登录但不 allowlist 全开放) |
| `server/service/auth/index.ts` | `getCurrentUser` join `users.status`,`active` 以外返回 `null`;`MinimalUser` 扩展 `role` |
| `server/service/auth/context.ts` | `MinimalUser` 类型扩展 |
| `server/api/auth/me.get.ts` | 返回 `role`、`publicSlug` 等安全字段 |
| `server/api/admin/users.post.ts` 等 | 管理员创建/列表/禁用 |
| `server/api/me/posts.*.ts` | 文章 CRUD |
| `server/api/me/timeline.*.ts` | 时间线 CRUD |
| `server/api/me/profile.put.ts`、`me/profile.get.ts` | 资料读写 |
| `server/api/me/rss/feeds.*.ts`、`me/rss/items.*.ts`、`me/rss/sync.post.ts` | 订阅与同步触发 |
| `server/api/public/profile/[publicSlug].get.ts` | 聚合公开字段与 `public` 内容列表 |
| `server/api/public/unlisted.get.ts``[publicSlug]/[token].get.ts` | 按 slug+token 解析条目类型并返回 |
| `server/service/rss/fetch.ts`(新建) | fetch feed、解析、入库、去重 |
| `server/service/rss/scheduler.ts`(新建) | 到期扫描、并发池、写回 `lastError` |
| `server/plugins/04.rss-scheduler.ts`(新建) | `nitroApp.hooks.hook("ready", ...)` 注册 `setInterval` + 启动补跑 |
| `app/utils/auth-routes.ts` | `PUBLIC_ROUTE_PREFIXES`: `/@`, `/p/`;移除或条件化 `/register` |
| `app/pages/@/[publicSlug]/index.vue`(新建) | 公开主页 |
| `app/pages/p/[publicSlug]/t/[shareToken].vue`(新建) | 仅链接阅读页 |
| `app/pages/me/**``app/pages/dashboard/**` | 后台列表与编辑(按你偏好的前缀统一) |
| `app/components/AppShell.vue` | 侧栏导航、空状态入口、admin 菜单 |
| `server/service/auth/index.test.ts` 或新测试文件 | 扩展测试 |
---
### Task 1: Drizzle schema — 用户扩展与内容/RSS 表
**Files:**
- Modify: `packages/drizzle-pkg/database/sqlite/schema/auth.ts`
- Create: `packages/drizzle-pkg/database/sqlite/schema/content.ts`
- Create: `packages/drizzle-pkg/database/sqlite/schema/rss.ts`
- Modify: `packages/drizzle-pkg/lib/schema/auth.ts`(若需聚合导出)
- Create: `packages/drizzle-pkg/lib/schema/content.ts`、`rss.ts` 导出
- [ ] **Step 1: 扩展 `users` 列**
`auth.ts``users` 表增加(类型与默认值与迁移一致):
- `role`:`text`,默认 `'user'`,非空
- `status`:`text`,默认 `'active'`,非空
- `publicSlug`:`text`,唯一索引(nullable 允许首登后填写)
- `bioMarkdown`:`text`,可选
- `bioVisibility`:`text`,默认 `'private'`
- `socialLinksJson`:`text`,默认 `'[]'`(存 JSON 字符串)
- `avatarVisibility`:`text`,默认 `'private'`
- [ ] **Step 2: 定义 `posts`**
字段建议:`id`、`userId`、`title`、`slug`、`bodyMarkdown`、`excerpt`、`coverUrl`、`tagsJson`、`publishedAt`、`visibility`、`shareToken`、`createdAt`、`updatedAt`;唯一索引 `(userId, slug)`
- [ ] **Step 3: 定义 `timeline_events`**
字段:`id`、`userId`、`occurredOn`(timestamp)、`title`、`bodyMarkdown`、`linkUrl`、`visibility`、`shareToken`、`createdAt`、`updatedAt`。
- [ ] **Step 4: 定义 `rss_feeds` / `rss_items`**
`rss_feeds`:`id`、`userId`、`feedUrl`(唯一 per user)、`title`、`siteUrl`、`lastFetchedAt`、`lastError`、`pollIntervalMinutes` 默认 null(用全局配置)、`createdAt`。
`rss_items`:`id`、`userId`、`feedId`、`guid`、`canonicalUrl`、`title`、`summary`、`contentSnippet`、`author`、`publishedAt`、`visibility` 默认 `private`、`shareToken`、`createdAt`;索引 `(feedId, guid)`、`(userId, canonicalUrl)` 辅助去重。
- [ ] **Step 5: 生成并应用迁移**
Run:
```bash
cd /home/dash/projects/person-panel && bun run db:generate -- add-multitenant-content-rss
bun run db:migrate
```
Expected: 生成新 SQL 迁移且无报错。
- [ ] **Step 6: Commit**
```bash
git add packages/drizzle-pkg
git commit -m "feat(db): add multitenant profile, posts, timeline, and rss tables"
```
---
### Task 2: Bootstrap 首个管理员(env + seed)
**Files:**
- Modify: `packages/drizzle-pkg/seed.ts`
- Modify: `packages/drizzle-pkg/env.ts`(若集中定义 env)
- [ ] **Step 1: 在 `seed.ts` 增加幂等 bootstrap**
逻辑:
1. 查询是否存在 `users.role = 'admin'`
2. 若不存在,读取 `process.env.BOOTSTRAP_ADMIN_USERNAME``BOOTSTRAP_ADMIN_PASSWORD`;缺一则 **跳过并 log warn**(避免误创建)。
3. 使用与线上一致的 `bcrypt` hash 流程插入管理员:`role='admin'`、`status='active'`、`publicSlug` 可由 username 派生(小写+校验)或留空。
- [ ] **Step 2: 文档化运维步骤**
`docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md` 已描述;在 README 或 `.env.example` 增加变量名(若仓库允许)。
- [ ] **Step 3: 手动验证**
Run:
```bash
BOOTSTRAP_ADMIN_USERNAME=admin BOOTSTRAP_ADMIN_PASSWORD=adminadmin bun run db:seed
```
Expected: 第二次执行不应再创建第二个 admin(幂等)。
- [ ] **Step 4: Commit**
```bash
git add packages/drizzle-pkg
git commit -m "feat(db): bootstrap first admin from environment"
```
---
### Task 3: 会话用户加载 role/status,禁用用户拒绝访问
**Files:**
- Modify: `server/service/auth/index.ts`
- Modify: `server/service/auth/context.ts`
- Modify: `server/api/auth/me.get.ts`
- [ ] **Step 1: 扩展 `getCurrentUser` 查询列**
`select` 增加 `users.role`、`users.status`,`where` 保持 session 有效条件,并加 `eq(users.status, 'active')`(或在内存判断)。
`status !== 'active'`:**删除该 session** 或保留 session 但返回 null(推荐删除 session + null,避免僵尸会话)。
- [ ] **Step 2: 扩展 `MinimalUser`**
`{ id, username, role }`
- [ ] **Step 3: `/api/auth/me` 返回**
`user: { id, username, role, publicSlug }`(`publicSlug` 来自 DB)。
- [ ] **Step 4: 运行现有测试**
Run:
```bash
cd /home/dash/projects/person-panel && bun test server/service/auth/index.test.ts
```
Expected: PASS;如失败则更新测试夹具。
- [ ] **Step 5: Commit**
```bash
git add server/service/auth server/api/auth/me.get.ts
git commit -m "feat(auth): expose role and enforce disabled users"
```
---
### Task 4: API 白名单与 `public` 前缀
**Files:**
- Modify: `server/utils/auth-api-routes.ts`
- Modify: `server/middleware/10.auth-guard.ts`(若需 trace)
- [ ] **Step 1: 实现前缀 allowlist**
增加函数:`isPublicApiPath(path: string): boolean`,当 `path.startsWith('/api/public/')` 时为 true(含 GET/POST 中只读方法,禁止开放写)。
`isAllowlistedApiPath` 改为:`rule match OR isPublicApiPath(path)`。
保留 `/api/auth/login`、`/api/auth/register`(后续 Task 会收敛 register)。
- [ ] **Step 2: 手动探测**
启动 dev 后 `curl -i http://localhost:3000/api/public/health`(若实现)应 200 无 cookie。
- [ ] **Step 3: Commit**
```bash
git add server/utils/auth-api-routes.ts server/middleware/10.auth-guard.ts
git commit -m "feat(api): allow unauthenticated access to /api/public"
```
---
### Task 5: 管理员守卫 + 简易限流
**Files:**
- Create: `server/utils/admin-guard.ts`
- Create: `server/utils/simple-rate-limit.ts`(可选内联于 admin 首行)
- [ ] **Step 1: `requireAdmin`**
```ts
import type { H3Event } from "h3";
export async function requireAdmin(event: H3Event) {
const user = await event.context.auth.requireUser();
if (user.role !== "admin") {
throw createError({ statusCode: 403, statusMessage: "Forbidden" });
}
return user;
}
```
- [ ] **Step 2: 内存限流(每 IP 每分钟 60 次 admin 写操作)**
使用 `event.node.req.socket.remoteAddress` 为 key,`Map` 存计数与时间窗;超限 `429`
- [ ] **Step 3: Commit**
```bash
git add server/utils/admin-guard.ts server/utils/simple-rate-limit.ts
git commit -m "feat(admin): require admin role and basic rate limiting"
```
---
### Task 6: Admin 用户 API
**Files:**
- Create: `server/api/admin/users.post.ts`
- Create: `server/api/admin/users.get.ts`
- Create: `server/api/admin/users/[id].patch.ts`
- [ ] **Step 1: POST 创建用户**
Body:`{ username, password, email? }`;Zod 校验;`role` 固定 `user`;`status` `active`;密码 bcrypt;冲突返回 409。
- [ ] **Step 2: GET 列表**
分页:`limit/offset`;仅返回 `id, username, email, role, status, publicSlug, createdAt`
- [ ] **Step 3: PATCH 禁用/启用**
`{ status: 'active' | 'disabled' }`;若禁用,删除该用户所有 `sessions`(SQLite `delete from sessions where user_id = ?`)。
- [ ] **Step 4: 保护路由**
这些路径 **不要** allowlist;依赖全局 auth guard + `requireAdmin`
- [ ] **Step 5: Commit**
```bash
git add server/api/admin
git commit -m "feat(admin): user provisioning and status endpoints"
```
---
### Task 7: 可见性与 shareToken 工具 + 单元测试
**Files:**
- Create: `server/constants/visibility.ts`
- Create: `server/utils/share-token.ts`
- Create: `server/utils/visibility.test.ts`
- [ ] **Step 1: 定义 `VISIBILITY` 与 Zod**
`private | unlisted | public` 三值。
- [ ] **Step 2: `ensureShareToken(visibility, existing?: string | null)`**
`unlisted` 且无 existing → 生成;`public/private` → 返回 `null`(或保留策略:清 token)。
- [ ] **Step 3: 测试**
Run:
```bash
bun test server/utils/visibility.test.ts
```
- [ ] **Step 4: Commit**
```bash
git add server/constants/visibility.ts server/utils/share-token.ts server/utils/visibility.test.ts
git commit -m "test: visibility and share token helpers"
```
---
### Task 8: RSS URL 校验 + 测试
**Files:**
- Create: `server/utils/rss-url.ts`
- Create: `server/utils/rss-url.test.ts`
- [ ] **Step 1: 实现 `assertSafeHttpUrl(url: string)`**
- 仅 `http:`/`https:`
- 使用 `new URL(url)`;hostname 非空
- `dns.lookup` 或先 `fetch` 前解析 IP(若环境限制,可先用 `isHostnameIpLiteral` 分支处理 IP 字面量禁止私网段)
- 拒绝 obvious 内网 host(`localhost`、`.local` 可选)
- [ ] **Step 2: 测试用例**
允许 `https://example.com/feed`,拒绝 `http://127.0.0.1/`、`http://192.168.1.1/`。
- [ ] **Step 3: Commit**
```bash
git add server/utils/rss-url.ts server/utils/rss-url.test.ts
git commit -m "feat(rss): block obvious SSRF targets for feed URLs"
```
---
### Task 9: Posts 服务与 `me`/`public` API
**Files:**
- Create: `server/service/posts/index.ts`
- Create: `server/api/me/posts.get.ts`、`me/posts.post.ts`、`me/posts/[id].get.ts`、`me/posts/[id].put.ts`、`me/posts/[id].delete.ts`
- Create: `server/api/public/posts/[publicSlug].get.ts`(或聚合在 profile API)
- [ ] **Step 1: 服务层函数**
`listForUser`、`create`、`update`、`delete`,均在 SQL 层带 `userId`
- [ ] **Step 2: `public` 列表**
`visibility='public'` 且用户 `status=active`;按 `publishedAt` 排序。
- [ ] **Step 3: unlisted 不进入列表**
由 Task 10 的专用路由处理。
- [ ] **Step 4: Commit**
```bash
git add server/service/posts server/api/me/posts server/api/public
git commit -m "feat(posts): CRUD and public listing"
```
---
### Task 10: Timeline 与 APIs
**Files:**
- Create: `server/service/timeline/index.ts`
- Create: `server/api/me/timeline.*.ts`
- 扩展: `server/api/public/profile/[publicSlug].get.ts` 合并时间线
- [ ] **Step 1: CRUD 与可见性字段同步**
更新 `visibility` 时调用 `ensureShareToken`
- [ ] **Step 2: Commit**
```bash
git add server/service/timeline server/api/me/timeline
git commit -m "feat(timeline): CRUD with visibility"
```
---
### Task 11: Profile `me` + 公开聚合 API
**Files:**
- Create: `server/service/profile/index.ts`
- Create: `server/api/me/profile.get.ts`、`me/profile.put.ts`
- Create: `server/api/public/profile/[publicSlug].get.ts`
- [ ] **Step 1: 公开聚合返回结构**
```json
{
"user": { "publicSlug", "nickname", "avatar" },
"bio": { "markdown", "visibility" },
"links": [{ "label", "url", "visibility" }],
"posts": [ ...public only ],
"timeline": [ ...public only ],
"rssItems": [ ...public only ]
}
```
仅包含 `visibility === 'public'` 的块;`bio`/`avatar` 遵循各自 visibility。
- [ ] **Step 2: disabled 用户**
查询用户时若 `status !== 'active'` → 404。
- [ ] **Step 3: Commit**
```bash
git add server/service/profile server/api/me/profile server/api/public/profile
git commit -m "feat(profile): editable profile and public aggregate endpoint"
```
---
### Task 12: RSS 订阅、抓取、去重、`me` API
**Files:**
- Modify: `package.json`(添加 XML 解析依赖,例如 `fast-xml-parser`
- Create: `server/service/rss/fetch.ts`
- Create: `server/service/rss/parser.ts`
- Create: `server/api/me/rss/feeds.*.ts`、`me/rss/items.*.ts`、`me/rss/sync.post.ts`
- [ ] **Step 1: 安装依赖**
Run:
```bash
cd /home/dash/projects/person-panel && bun add fast-xml-parser
```
- [ ] **Step 2: 抓取流程**
对每个 feed:`assertSafeHttpUrl` → `fetch`(timeout + max bytes 读取截断)→ 解析 item → `insert` 忽略唯一冲突;新行 `visibility: private`
- [ ] **Step 3: `sync.post.ts`**
触发当前用户所有到期源或指定 `feedId`
- [ ] **Step 4: Commit**
```bash
git add package.json server/service/rss server/api/me/rss
git commit -m "feat(rss): feeds, items, fetch, and manual sync"
```
---
### Task 13: 进程内调度器
**Files:**
- Create: `server/service/rss/scheduler.ts`
- Create: `server/plugins/04.rss-scheduler.ts`
- [ ] **Step 1: `ready` 钩子**
`setInterval``RSS_SYNC_INTERVAL_MINUTES` 扫描;`setTimeout` 启动 5s 后补跑。
- [ ] **Step 2: 并发池**
例如 `p-limit` 风格手写 semaphore:`RSS_MAX_CONCURRENT_FEEDS`。
- [ ] **Step 3: Commit**
```bash
git add server/service/rss/scheduler.ts server/plugins/04.rss-scheduler.ts
git commit -m "feat(rss): in-process polling scheduler"
```
---
### Task 14: Unlisted 公开详情 API
**Files:**
- Create: `server/api/public/unlisted/[publicSlug]/[shareToken].get.ts`(路径按 Nitro 约定调整)
- [ ] **Step 1: 查询顺序**
依次尝试 `posts`、`timeline_events`、`rss_items`(可加 `type` 查询参数减少歧义;若 token 全局唯一概率足够可单次 union)。
推荐:**token 全局唯一**(随机 32 byte)→ 单表查询 `where shareToken = ? AND user.publicSlug = ? join users`
- [ ] **Step 2: 返回 404**
用户 disabled 或 `visibility !== 'unlisted'`
- [ ] **Step 3: Commit**
```bash
git add server/api/public/unlisted
git commit -m "feat(public): resolve unlisted share links"
```
---
### Task 15: 前端路由与 auth 规则
**Files:**
- Modify: `app/utils/auth-routes.ts`
- Modify: `app/middleware/auth.global.ts`(若需把 `/me` 纳入保护)
- Modify: `app/pages/login/index.vue`(文案)
- [ ] **Step 1: `PUBLIC_ROUTE_PREFIXES`**
加入 `"/@"`、`"/p/"`。
- [ ] **Step 2: 登录后落地页**
指向 `/me``/dashboard`(与侧栏一致)。
- [ ] **Step 3: 注册页策略**
若关闭自助注册:`allowRegister` 配置为 false(沿用 `useGlobalConfig`),`register` 路由 middleware 重定向登录或 404。
- [ ] **Step 4: Commit**
```bash
git add app/utils/auth-routes.ts app/middleware/auth.global.ts app/pages/login/index.vue
git commit -m "feat(app): public routes for profile and unlisted links"
```
---
### Task 16: Nuxt UI — 公开页与后台壳
**Files:**
- Create: `app/pages/@/[publicSlug]/index.vue`
- Create: `app/pages/p/[publicSlug]/t/[shareToken].vue`
- Create: `app/layouts/public.vue`
- Modify: `app/components/AppShell.vue`
- Create: `app/pages/me/index.vue`、`app/pages/me/posts/**` 等(按最终 IA)
- [ ] **Step 1: 公开主页**
`useFetch('/api/public/profile/' + slug)`;`UPage`、`UCard` 列表;空状态 `UEmpty`
- [ ] **Step 2: 分享页**
`useFetch` unlisted API;403/404 用 `UAlert`
- [ ] **Step 3: `AppShell` 侧栏**
使用 `UNavigationMenu`;admin 显示「用户管理」。
- [ ] **Step 4: Commit**
```bash
git add app/pages app/layouts/public.vue app/components/AppShell.vue
git commit -m "feat(ui): public and authenticated shells with Nuxt UI"
```
---
### Task 17: 集成验证与文档收尾
- [ ] **Step 1: 手测脚本(记录在 PR 描述)**
1. 创建 admin + 普通用户;普通用户登录创建 `publicSlug`
2. 写文章 `public`,匿名访问 `/@slug` 可见。
3. 写 `unlisted`,匿名访问 `/p/slug/t/token` 可见且不在列表。
4. 添加 RSS,手动 sync,条目默认 `private`,改 `public` 后出现在公开页。
5. 禁用用户,公开页 404。
- [ ] **Step 2: 文档**
如有实现偏差,更新 `docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md` 对应段落;**全部验收通过后再**将 spec 顶部状态改为「已实现」。
- [ ] **Step 3: Commit(若有文档变更)**
```bash
git add docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md
git commit -m "docs: sync spec with implementation"
```
---
## Plan Self-Review(对照 spec)
| Spec 章节 | 覆盖任务 |
|-----------|----------|
| §1–2 目标/模块 | Task 1–17 |
| §3 数据模型/可见性/token | Task 1,7,9–12,14 |
| §4 API 分层 | Task 4–6,9–14 |
| §5 管理员/bootstrap/注册 | Task 2,6,15 |
| §6 UI/§6.1 体验 | Task 15–16 |
| §7 定时任务 | Task 13 |
| §8 安全 | Task 5,8,12 |
| §9–11 错误/测试/配置 | Task 7–8 测试 + Task 17 |
**占位符扫描:** 无 TBD;具体页面路径在 Task 16 允许按实现微调但需保持单一路由风格。
**类型一致性:** `MinimalUser` 在 Task 3 增加 `role`;`visibility` 三态在 Task 7 单列常量。
---
**Plan complete and saved to `docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md`. Two execution options:**
1. **Subagent-Driven (recommended)** — 每个 Task 派生子代理并在任务间复核,迭代快
2. **Inline Execution** — 本会话用 executing-plans 批量执行并设检查点
**你更倾向哪一种?**

17
docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md

@ -62,11 +62,11 @@
| 域 | 表 | 要点 | | 域 | 表 | 要点 |
|----|-----|------| |----|-----|------|
| 账号 | `users`(扩展) | `role`: `admin \| user`;`status`: `active \| disabled`;`publicSlug` 唯一(公开主页) | | 账号 | `users`(扩展) | `role`: `admin \| user`;`status`: `active \| disabled`;`publicSlug` 唯一(公开主页);**资料**:`bioMarkdown` + **`bioVisibility`**;**社交链接**存 `socialLinksJson`(JSON 数组,元素含 `label, url, visibility`,实现条目级可见);头像沿用 `avatar` 字段并单独 **`avatarVisibility`**(或默认与 `bio` 同步,实现时二选一并在本表注明) |
| 文章 | `posts` | Markdown 正文、元数据、`visibility`、`userId`;`slug` 用户内唯一 | | 文章 | `posts` | Markdown 正文、元数据、`visibility`、`userId`;`slug` 用户内唯一;**`shareToken`**(`unlisted` 时必填且随机不可猜) |
| 时光机 | `timeline_events` | 日期/时间段、标题、正文、`visibility`、`userId` | | 时光机 | `timeline_events` | 日期/时间段、标题、正文、`visibility`、`userId`;**`shareToken`** |
| RSS | `rss_feeds` | `feedUrl`、`userId`、拉取状态(`lastFetchedAt`、`lastError` 等)、可选 `pollIntervalMinutes` | | RSS | `rss_feeds` | `feedUrl`、`userId`、拉取状态(`lastFetchedAt`、`lastError` 等)、可选 `pollIntervalMinutes` |
| RSS | `rss_items` | `feedId`、`userId`(冗余便于查询)、`guid`、`canonicalUrl`、标题、摘要、正文片段、`publishedAt`;默认 `visibility=private` | | RSS | `rss_items` | `feedId`、`userId`(冗余便于查询)、`guid`、`canonicalUrl`、标题、摘要、正文片段、`publishedAt`;默认 `visibility=private`;**`shareToken`** |
**去重**:优先 `guid`,否则规范化后的 `canonicalUrl` **去重**:优先 `guid`,否则规范化后的 `canonicalUrl`
@ -127,6 +127,14 @@
- **RSS**:左侧订阅源(本人)、右侧条目流;支持 **手动触发同步**;条目上切换可见性。 - **RSS**:左侧订阅源(本人)、右侧条目流;支持 **手动触发同步**;条目上切换可见性。
- **公开站**:独立简洁布局,以阅读为主。 - **公开站**:独立简洁布局,以阅读为主。
### 6.1 体验与展示闭环(第一期必须满足)
- **空状态**:文章 / RSS / 时间线 / 资料未完成时,使用 Nuxt UI 空状态文案 + 单一主按钮(去创建、去添加订阅源);避免空白屏。
- **导航**:登录内与公开站 **顶部显式返回**「我的主页(公开)」链接(路径形如 `/@:publicSlug`)与「后台」切换;公开站仅展示与该用户 `publicSlug` 相关区块,不暴露他人入口。
- **可见性控件**:每条条目使用同一套三态控件(`private` / `unlisted` / `public`);切到 `unlisted` 时展示 **可复制的分享链接**(U1);切离时可使 `shareToken` 失效(重新生成或清空,实现计划二选一,须防缓存旧链)。
- **登录页文案**:关闭自助注册后,提示「请联系管理员开通账号」,避免用户困惑。
- **路由与白名单**:前端 `/@:publicSlug`、`/p/:publicSlug/t/:shareToken` 必须加入 **页面级 `PUBLIC_ROUTE_PREFIXES`**;后端 **`/api/public/**`** 全方法加入 API allowlist(或按路径前缀匹配规则扩展现有 `isAllowlistedApiPath`),避免被 `10.auth-guard` 误杀。
--- ---
## 7. 定时任务(进程内) ## 7. 定时任务(进程内)
@ -180,6 +188,7 @@
- **一致性**:多用户隔离、条目级可见性、RSS 默认私密、单实例进程内任务彼此一致。 - **一致性**:多用户隔离、条目级可见性、RSS 默认私密、单实例进程内任务彼此一致。
- **范围**:第一期不引入队列/多实例;协作与 OPML 等未承诺。 - **范围**:第一期不引入队列/多实例;协作与 OPML 等未承诺。
- **歧义**:公开路径已定 `/@:publicSlug`;禁用用户公开 404;unlisted 为 U1。 - **歧义**:公开路径已定 `/@:publicSlug`;禁用用户公开 404;unlisted 为 U1。
- **闭环补充(2026-04-18)**:资料域字段与 `shareToken` 列已写入 §3.2;§6.1 补齐空状态、分享链、路由白名单与登录文案,覆盖「直观 / 展示完整」缺口。
--- ---

Loading…
Cancel
Save