Files
Arkie-Library-Frontend/.unipi/docs/specs/2026-05-25-posts-api-contract.md

177 lines
8.1 KiB
Markdown
Raw Normal View History

---
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-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms"
text?: string; // 可选,纯文本/图说;前端做 https → 链接自动识别
attachments: Attachment[]; // 0~Ntext-only post 时为 []
isRecommended: boolean;
publishedAt: string; // ISO 8601用于排序 + 日期分组
updatedAt: string;
};
type PostListResponse = {
items: Post[];
nextCursor?: string; // 不透明 cursorundefined = 没有下一页
};
```
### 关键约定
- **图片当文档**(在前端显示为「文件下载卡」):`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-CN` / `en` / `ja` / `ko` / `vi` / `id` / `ms` |
| `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`