--- title: "Posts API Contract (for backend)" type: api-contract date: 2026-05-25 audience: backend status: draft --- # Posts API Contract > 这份文档是从 `2026-05-25-telegram-style-resource-stream-design.md` §1–§2 抽出,供后端实现使用。前端已用 mock data 完成视觉;上线时把 `VITE_USE_MOCK_POSTS=false` 即可切真接口。 ## 1. 数据模型 ```ts type AttachmentKind = "image" | "video" | "document"; type Attachment = { id: string; // 唯一 id kind: AttachmentKind; // 三大类,前端按此分支渲染 url: string; // 原始文件地址 mime: string; // image/jpeg, application/pdf, video/mp4, ... filename: string; // 显示用文件名,含扩展名 sizeBytes: number; // 字节数;前端格式化为 "3.5 MB" width?: number; // image/video 用于占位比例(CLS 优化) height?: number; durationSec?: number; // video 专用 posterUrl?: string; // video 海报缩略图 thumbnailUrl?: string; // image 缩略,列表用减少流量 }; type Post = { id: string; categoryId: number; categorySlug: string; language: string; // "zh-TW" | "zh-CN" | "en" text?: string; // 可选,纯文本/图说;前端做 https → 链接自动识别 attachments: Attachment[]; // 0~N;text-only post 时为 [] isRecommended: boolean; publishedAt: string; // ISO 8601;用于排序 + 日期分组 updatedAt: string; }; type PostListResponse = { items: Post[]; nextCursor?: string; // 不透明 cursor;undefined = 没有下一页 }; ``` ### 关键约定 - **图片当文档**(在前端显示为「文件下载卡」):`kind === "document"` 且 `mime.startsWith("image/")`。Admin 上传时通过开关决定走 image 还是 document 通道。 - **图片当图片**(前端显示为图片预览):`kind === "image"`。 - **多图相册**:一个 Post 带多个 `kind === "image"` 的 attachments。前端会在 2-4 grid 中渲染,attachments.length > 4 时第 4 格模糊 + `+N`。 - **图片 + 文字**:Post 同时有 `text` 与 attachments。 - **纯文本 / 链接**:Post 仅有 `text`,`attachments: []`。 - **视频**:`kind === "video"` 单 attachment。`posterUrl` 用于预览,`durationSec` 用于角标。 - Attachment 内不携带任何「上传者头像 / 管理员标签」等社交字段(前端已下线)。 ## 2. Endpoints ### 2.1 列表(核心) ``` GET /api/posts ``` Query 参数: | 参数 | 必填 | 说明 | |---|---|---| | `lang` | 是 | UI 语言;后端可据此选择不同语言版本的 `text` | | `category` | 否 | category slug;不传 = 全部分类 | | `type` | 否 | `all` / `image` / `video` / `pdf` / `ppt` / `text` / `link` / `archive`;语义见 §3 | | `language` | 否 | 资源语言:`zh-TW` / `zh-CN` / `en` | | `cursor` | 否 | 上一次返回的 `nextCursor`;不传 = 第一页 | | `limit` | 否 | 默认 20,最大 50 | 返回:`PostListResponse` 排序:`publishedAt DESC`。 ### 2.2 Home 用聚合接口(可选,沿用现状) ``` GET /api/posts/recommended?lang=&limit= GET /api/posts/latest?lang=&limit= ``` 返回:`{ items: Post[] }`(不分页) ### 2.3 单条(用于老链接 301 落地) ``` GET /api/posts/:id ``` 返回:`Post`(或 404) 前端 `/resource/:id` 现在是轻量重定向:拿到 `categorySlug` → `/category/#post-` 锚点滚动。 ### 2.4 分类(不变) ``` GET /api/categories?lang= ``` 返回:现有 `Category[]`。 ### 2.5 Admin CRUD ``` POST /api/admin/posts PUT /api/admin/posts/:id DELETE /api/admin/posts/:id GET /api/admin/posts?... (含未发布草稿) ``` 需求: - 支持多附件上传(一次 multipart 或先 `POST /api/admin/upload` 拿到 url 再创建 Post)。 - Admin UI 需要一个开关:「图片以图片形式呈现 / 以文档形式呈现」,对应 attachment.kind 的 image vs document。 - 支持发布/隐藏、置顶/官方推荐。 > Admin UI 改造单独建 spec / plan,本契约仅说明后端必须支持这些字段。 ## 3. `type` 参数语义 一个 Post 命中某个 `type`,规则: | type | 命中条件 | |---|---| | `all` | 全部 | | `image` | `attachments` 中至少一个 `kind === "image"` 或 `mime.startsWith("image/")` | | `video` | 至少一个 `kind === "video"` 或 `mime.startsWith("video/")` | | `pdf` | 至少一个 `mime === "application/pdf"` 或扩展名为 `pdf` | | `ppt` | 至少一个扩展名为 `ppt` / `pptx` / `key` 或 mime 含 `presentation` | | `archive` | 至少一个扩展名为 `zip` / `rar` / `7z` / `tar` / `gz` | | `text` | `text` 非空 | | `link` | `text` 非空且匹配 `https?://` | 前端 mock 已按此规则过滤,便于切真接口时口径一致。 ## 4. 删除 / 废弃 | 项 | 处理 | |---|---| | `POST /api/resources/:id/favorite` | 删除 | | `GET /api/favorites` / 收藏列表 | 删除(前端 `/favorites` 路由已移除) | | `/r/:id` 老前端路由 | 已合并到 `/resource/:id` 重定向 | | 老 `/api/resources*` 系列 | 后端可保留过渡期。建议提供数据迁移脚本:每个老 Resource → 一个 Post(带 1 个 attachment 或 text-only)。`isRecommended` / `language` / `categorySlug` 字段迁移;`favorite count` 字段丢弃。 | | Resource.coverImage 与 Resource.fileUrl 二选一 | 转为 attachments[0](kind 由后端判断 image vs document)| ## 5. Search `GET /api/resources?q=...` 当前仍被 SearchPage 使用(在新 schema 上线前过渡)。后端可视情况: - 短期:保留旧接口 - 长期:新增 `GET /api/posts/search?q=...` 返回 `PostListResponse`,前端再切 ## 6. 错误格式 沿用现状(HTTP 状态码 + 文本 body)。前端 `getJSON` 会把非 2xx 当作 `Error(text)` 抛出,`MessageStream` 显示红色横幅 + 重试按钮。 ## 7. 兼容性 / 灰度 后端 ready 时步骤: 1. 把示例数据导入到 Posts 表 2. `/api/posts` 在 staging 通过 3. 前端 staging 部署设 `VITE_USE_MOCK_POSTS=false`,跑通后再 prod 4. 前端代码层面:删除 `src/mocks/mockPosts.ts` 与 `usePostStream.ts` 中的 mock 分支,或保留 mock 用于本地离线开发 ## 8. 联系 前端:Terry。Spec 主文档:`.unipi/docs/specs/2026-05-25-telegram-style-resource-stream-design.md`。