176 lines
6.3 KiB
Markdown
176 lines
6.3 KiB
Markdown
|
|
---
|
|||
|
|
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/<slug>#post-<id>` 锚点滚动。
|
|||
|
|
|
|||
|
|
### 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`。
|