|
| 1 | +# 轻量发文(Posts 模块) |
| 2 | + |
| 3 | +用户可在 `/editor` 写 Markdown 文章,直接 `POST /api/posts` 落后端数据库, |
| 4 | +无需走 Git PR 流程。文章发布后可在 `/feed` 原创 Tab、个人主页 `/u/{username}/posts` |
| 5 | +和详情页 `/u/{username}/posts/{slug}` 查看,支持一键发起转正 PR 收录进 `/docs`。 |
| 6 | + |
| 7 | +## 数据流 |
| 8 | + |
| 9 | +``` |
| 10 | +浏览器(/editor) |
| 11 | + → POST /api/posts (next.config.mjs rewrite 透传到后端) |
| 12 | + → Spring Boot /api/posts (sa-token 鉴权,返回 PostView) |
| 13 | + → 跳转 /u/{username}/posts/{slug} |
| 14 | +``` |
| 15 | + |
| 16 | +图片上传走独立路径: |
| 17 | + |
| 18 | +``` |
| 19 | +浏览器粘贴图片 |
| 20 | + → POST /api/upload (Next API Route,非 rewrite) |
| 21 | + → 后端预签名 R2 URL |
| 22 | + → 浏览器 PUT 直传 R2 |
| 23 | +``` |
| 24 | + |
| 25 | +## 关键约束 |
| 26 | + |
| 27 | +**Auth Header**:所有 `/api/posts*` 的 fetch 用 `satoken: token`(rewrite 透传, |
| 28 | +后端 `sa-token.token-name=satoken` 直接读这个 header 名)。 |
| 29 | +`/api/upload` 是 Next API Route,客户端用 `x-satoken`,由 route handler 内部翻译。 |
| 30 | + |
| 31 | +**BACKEND_URL**:`fetchPosts()`、`fetchPost()` 在 Server Component 里直接调后端, |
| 32 | +必须配置 `BACKEND_URL` 环境变量。本地 dev 用 `http://localhost:8081`, |
| 33 | +生产用 `https://api.involutionhell.com`。next.config.mjs 的 rewrite fallback 是 |
| 34 | +`:8080`(生产镜像端口),但 posts 模块只在新后端部署,不依赖 fallback。 |
| 35 | + |
| 36 | +## 文件结构 |
| 37 | + |
| 38 | +| 文件 | 职责 | |
| 39 | +| -------------------------------------------------- | ----------------------------------------------------- | |
| 40 | +| `app/types/post.ts` | PostView / PostSummaryView / PostRequest 类型定义 | |
| 41 | +| `app/components/PostContent.tsx` | UGC Markdown 渲染(react-markdown + rehype-sanitize) | |
| 42 | +| `app/components/PromoteToDocsButton.tsx` | 三态转正按钮(idle / pending / promoted) | |
| 43 | +| `app/[locale]/editor/EditorPageClient.tsx` | 编辑器直发逻辑,`buildFrontmatter` 导出给转正按钮复用 | |
| 44 | +| `app/[locale]/feed/components/FeedTabSwitcher.tsx` | 原创文章 / 分享链接 Tab 切换(client 组件) | |
| 45 | +| `app/[locale]/feed/components/PostCard.tsx` | 文章卡片,`showAuthor` prop 控制作者显示 | |
| 46 | +| `app/[locale]/feed/page.tsx` | /feed 页,默认 Tab=posts,`fetchPosts()` 三次退避 | |
| 47 | +| `app/[locale]/u/[username]/posts/` | 个人文章列表页(client)+ 详情页(SSR) | |
| 48 | +| `app/[locale]/u/[username]/PostsLinkOnProfile.tsx` | 个人主页文章入口计数 | |
| 49 | + |
| 50 | +## /feed Tab 行为 |
| 51 | + |
| 52 | +- 无 `?tab` 或 `?tab=posts` → 原创文章(默认) |
| 53 | +- `?tab=links` → 分享链接(原有逻辑不变) |
| 54 | +- 切换 Tab 时:posts → links 保留 `?category`;links → posts 清空 `?category` |
| 55 | + |
| 56 | +## 转正路径(PromoteToDocsButton) |
| 57 | + |
| 58 | +1. idle → selecting:弹出 DocsDestinationForm 选目标目录 |
| 59 | +2. selecting → pending:`window.open` 打开 GitHub 新建文件页(预填 frontmatter + |
| 60 | + 正文),同时 fire-and-forget `POST /api/posts/{id}/promote` |
| 61 | +3. pending → promoted:后端写 `promotedAt`,用户刷新详情页后由 `initialPromoted=true` |
| 62 | + 初始化进入 promoted 态 |
| 63 | + |
| 64 | +pending 态物理锁死(无 border/hover),不用 `disabled` 属性。 |
| 65 | + |
| 66 | +## 路由分类 |
| 67 | + |
| 68 | +| 路由 | 类型 | 原因 | |
| 69 | +| ------------------------------------- | --------- | -------------------------------------------- | |
| 70 | +| `/[locale]/u/[username]/posts` | ƒ Dynamic | 客户端组件,读 localStorage token 判定 owner | |
| 71 | +| `/[locale]/u/[username]/posts/[slug]` | ƒ Dynamic | SSR,`cache: "no-store"`,内容随时更新 | |
| 72 | + |
| 73 | +两条新路由都是预期的 ƒ Dynamic,不影响已有路由分类。 |
| 74 | + |
| 75 | +## 上线检查清单 |
| 76 | + |
| 77 | +1. 后端 `feat/posts-module` 和前端 `feat/posts-lightweight-publish` 同步上线 |
| 78 | +2. 后端 SaTokenConfigure 公开读白名单包含 `GET /api/posts/feed`、 |
| 79 | + `GET /api/posts/*/*`(否则匿名用户访问 /feed 和详情页会 401) |
| 80 | +3. 生产环境变量 `BACKEND_URL=https://api.involutionhell.com`(Vercel 配置) |
| 81 | +4. posts 表随 `SPRING_SQL_INIT_MODE=always` 首次启动自动建表 |
0 commit comments