docs+feat: 重构规格与 Gitea 式 SSR 骨架(首页)

在 rebuild/gitea-ssr 落地产品规格、Cursor 规则,以及 Go 模板 SSR 首页/板块列表;未迁移路径仍回落 SPA,便于对照 main。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 03:11:25 +08:00
parent 4dc3bbb13f
commit 1414c71dec
28 changed files with 2891 additions and 24 deletions

376
docs/rebuild-spec/04-api.md Normal file
View File

@@ -0,0 +1,376 @@
# 04 · HTTP API 合约
> **读者**:实现后端 / BFF / 前端数据层的 AI
> **前置**[03-data-model.md](03-data-model.md)
> **源码**[`router/router.go`](../../router/router.go)、[`frontend/src/api/client.ts`](../../frontend/src/api/client.ts)、[`frontend/src/api/types.ts`](../../frontend/src/api/types.ts)、[`middleware/auth.go`](../../middleware/auth.go)
不要求 OpenAPI YAML以下表格 + JSON 形状即为合约。新站可加 `/v1` 前缀,但**字段名建议保持**以便对照迁移。
---
## 1. 通用约定
| 项 | 约定 |
|----|------|
| Base | 同源;前端 `credentials: 'same-origin'` |
| 成功 | HTTP 2xx + JSON body |
| 失败 | 非 2xx + `{ "error": "人类可读中文或英文消息" }` |
| 鉴权 | Cookie `jiang13_token`HttpOnly部分也接受 Authorization Bearer以实现为准 |
| 内容类型 | JSON 默认;部分写接口用 `multipart/form-data`FormData |
| OptionalAuth | 有 cookie 则解析用户,无则游客继续 |
| RequireAuth | 必须登录且未禁言 |
| RequireAdmin | 必须 `role=admin` |
### 分页形态差异
| 场景 | 典型字段 |
|------|----------|
| 前台帖列表 | `posts`, `total`, `page`, `size`, `has_more` |
| 后台多数列表 | `total`, `page`, `total_pages` + 实体数组 |
| 私信会话消息 | `before` 游标式 |
---
## 2. 基础设施 / SEO / 静态
| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| GET | `/health` | 无 | `{ "status": "ok" }`DB ping 失败则非 ok以实现为准 |
| GET | `/robots.txt` | 无 | 文本 |
| GET | `/sitemap.xml` | 无 | XML |
| GET | `/media/thumb/*filepath` | 无 | 缩略图 / WebP 等 |
| GET | `/uploads/*` | 无 | 静态上传文件 |
---
## 3. OIDC Provider
| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| GET | `/.well-known/openid-configuration` | 无 | Discovery |
| GET | `/oauth/jwks` | 无 | JWKS |
| GET | `/oauth/authorize` | OptionalAuth | 授权码流程 |
| POST | `/oauth/token` | 无(客户端凭证) | 换 token |
| GET/POST | `/oauth/userinfo` | Bearer | 用户信息 |
| GET/POST | `/oauth/logout` | 视实现 | 登出 |
细节以 [`service/oidc.go`](../../service/oidc.go) / [`handler/oidc.go`](../../handler/oidc.go) 为准。
---
## 4. 公开 API`/api` + OptionalAuth
### 4.1 会话与站点
| 方法 | 路径 | 响应要点 |
|------|------|----------|
| GET | `/api/me` | `{ user: UserSelf \| null }` |
| GET | `/api/stats` | `{ users, posts, boards, comments }` |
| GET | `/api/forum-limits` | `ForumLimitsPublic`(无限流内部字段) |
| GET | `/api/site-branding` | `SiteBranding`(可含 `site_url` |
| GET | `/api/captcha` | `{ id, image }` image 为 data URL 或 base64 |
| GET | `/api/register/config` | 见下 |
**RegisterConfig**
```json
{
"is_first_user": true,
"mail_ready": false,
"require_email_code": false,
"register_open": true,
"email_code_len": 6
}
```
### 4.2 认证(限流)
| 方法 | 路径 | Body | 响应 |
|------|------|------|------|
| POST | `/api/register` | Form: username, password, nickname, email, email_code? | 成功后通常种 cookie |
| POST | `/api/login` | Form: username, password | 种 cookie |
| POST | `/api/register/email-code` | JSON `{ email }` | `{ message }` |
| POST | `/api/password-reset/email-code` | JSON `{ email }` | `{ message }` |
| POST | `/api/password-reset` | JSON `{ email, email_code, new_password }` | `{ message }` |
### 4.3 内容只读
| 方法 | 路径 | Query / 说明 |
|------|------|----------------|
| GET | `/api/boards` | `{ boards: Board[] }` |
| GET | `/api/posts` | 见下表 |
| GET | `/api/posts/hot` | 热门列表 |
| GET | `/api/posts/:id` | `skip_view=1` 可选;返回 `PostDetailResponse` |
| GET | `/api/posts/:id/comments` | `my_ids` 可选(逗号分隔,便于标自己的楼) |
| GET | `/api/tags` | `limit` 默认 40 → `{ tags: [{name,count}] }` |
| GET | `/api/comments/recent` | `{ comments: RecentComment[] }` |
| GET | `/api/users/search` | `q`, `limit` |
| GET | `/api/users/recent` | `{ users: RecentUser[] }` |
| GET | `/api/users/:id` | `{ user: UserPublic, stats }` |
| GET | `/api/pages` | 已发布摘要列表 |
| GET | `/api/pages/:slug` | 单页详情 |
| GET | `/api/projects` | `page`, `limit`, `q` |
**GET `/api/posts` Query**
| 参数 | 说明 |
|------|------|
| page | 默认 1 |
| size | 默认 page_size_default上限 100 |
| board_id | 0 或不传=全部 |
| user_id | 某用户的帖 |
| keyword | 搜索词 |
| tag | 标签 |
| author | 用户名优先,否则昵称精确匹配 |
| title_only | `1`/`true` 仅搜标题 |
| sort | `latest` \| `reply` \| `hot` |
**响应示例**
```json
{
"posts": [ /* PostItem */ ],
"total": 100,
"page": 1,
"size": 30,
"has_more": true
}
```
**PostDetailResponse 要点**
```json
{
"post": { /* PostItem + content */ },
"comment_count": 0,
"liked": false,
"favorited": false,
"has_replied": false,
"can_edit": true,
"edit_block_reason": "",
"is_edited": false,
"post_edit_window_hours": 24,
"poll": { /* PollView */ },
"lottery": { /* PostLotteryView */ },
"bounty_can_refund": false,
"bounty_refund_block_reason": "",
"bounty_eligible_reply_count": 0
}
```
### 4.4 游客可写评论
| 方法 | 路径 | 限流 | Body |
|------|------|------|------|
| POST | `/api/posts/:id/comments` | comment | Form: content, reply_to?, is_private?, 以及游客字段(以实现为准) |
登录用户发评也走此路径RequireAuth 组外公开组已注册该路由)。
---
## 5. 需登录 API`/api` + RequireAuth
### 5.1 会话与资料
| 方法 | 路径 | Body | 响应 |
|------|------|------|------|
| POST | `/api/logout` | | 清 cookie |
| GET | `/api/favorites` | | `{ favorites, total }` |
| GET | `/api/profile/stats` | | `{ stats: UserActivityStats }` |
| POST | `/api/profile/nickname` | Form nickname | |
| POST | `/api/profile/signature` | Form signature | `{ message, user }` |
| POST | `/api/profile/password` | Form old_password, new_password | |
| POST | `/api/profile/avatar` | Form avatar=file | `{ avatar }` |
| POST | `/api/uploads/image` | Form image=file | `{ url }` |
### 5.2 帖子写操作
| 方法 | 路径 | Body | 响应 |
|------|------|------|------|
| POST | `/api/posts` | Form: board_id, title, content, tags?, post_type?, poll_options?, bounty_points?, lottery_winner_count? | `{ message, post_id, status }` |
| PUT | `/api/posts/:id` | Form: title, content, tags?, board_id?, post_type? | `{ message }` |
| DELETE | `/api/posts/:id` | | 软删 |
| GET | `/api/posts/:id/revisions` | | `{ revisions }` |
| GET | `/api/posts/:id/revisions/:revId` | | `{ revision }` |
| POST | `/api/posts/:id/like` | | `{ liked, like_count }` |
| POST | `/api/posts/:id/favorite` | | `{ favorited }` |
| POST | `/api/posts/:id/resolve` | Form resolved=`1`\|`0` | `{ question_resolved }` |
| POST | `/api/posts/:id/poll/vote` | JSON `{ option_ids: number[] }` | `{ poll }` |
| POST | `/api/posts/:id/poll/close` | | `{ poll }` |
| POST | `/api/posts/:id/bounty/award` | Form comment_id | |
| POST | `/api/posts/:id/bounty/refund` | | |
| POST | `/api/posts/:id/lottery/draw` | | `{ lottery }` |
| POST | `/api/posts/:id/report` | JSON `{ reason, detail? }` | `{ report }` |
| POST | `/api/posts/:id/unlock` | JSON `{ block_key }` | 见下 |
**poll_options JSON 示例**Form 字段字符串)
```json
{
"multi": false,
"max_choices": 1,
"ends_at": "2026-09-01T12:00:00Z",
"options": [{ "text": "选项A" }, { "text": "选项B" }]
}
```
**unlock 响应**
```json
{
"message": "...",
"unlock": {
"block_key": "abcdef0123456789",
"cost": 10,
"points_balance": 90,
"inner_html": "<p>...</p>"
}
}
```
### 5.3 评论写操作
| 方法 | 路径 | Body |
|------|------|------|
| POST | `/api/comments/:id/like` | → `{ liked, like_count }` |
| POST | `/api/comments/:id/report` | JSON `{ reason, detail? }` |
| PUT | `/api/comments/:id` | Form content |
| DELETE | `/api/comments/:id` | |
### 5.4 私信
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/messages/unread-count` | `{ count, dm_count?, notify_count? }` |
| GET | `/api/messages/notifications` | page, size, kind |
| POST | `/api/messages/notifications/read` | |
| GET | `/api/messages/conversations` | page, size |
| GET | `/api/messages/conversations/:peerId` | size, beforepeerId=0 为系统 |
| POST | `/api/messages/conversations/:peerId/read` | |
| POST | `/api/messages` | JSON `{ to_user_id, subject?, content }` |
| POST | `/api/messages/read-all` | |
### 5.5 经济
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/me/points` | page含 ledger、check_in、lottery |
| GET/POST | `/api/me/check-in` | 状态 / 执行签到 |
| GET/POST | `/api/me/lottery` | 状态 / 抽奖 |
### 5.6 友链申请
| 方法 | 路径 | Body |
|------|------|------|
| POST | `/api/friend-links/apply` | JSON name, url, logo, link_on_homepage, reciprocal_page_url? |
| POST | `/api/friend-links/logo` | Form logo=file → `{ url }` |
| GET | `/api/friend-links/my-applies` | |
| PUT | `/api/friend-links/applies/:id` | 同申请字段 |
| DELETE | `/api/friend-links/applies/:id` | 取消 |
---
## 6. 管理 API`/api/admin` + Auth + Admin
### 6.1 仪表盘与设置
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/dashboard` | AdminDashboard |
| GET | `/settings` | AdminSettings 聚合 |
| PUT | `/settings/forum` | ForumLimits |
| PUT | `/settings/mail` | MailConfig |
| POST | `/settings/mail/test` | `{ to }` |
| PUT | `/settings/oidc` | OIDCConfig |
| PUT | `/settings/gitea` | GiteaSyncConfig |
| POST | `/settings/gitea/sync` | 手动同步 |
| PUT | `/settings/storage` | StorageConfig |
| PUT | `/settings/branding` | SiteBranding |
| POST | `/settings/branding/upload` | Form kind=`logo`\|`favicon`\|`og_image`, file |
| POST | `/settings/branding/clear` | JSON `{ kind }` |
| GET/PUT | `/settings/filter-words` | GET 读PUT `{ content }` |
(上表路径均相对于 `/api/admin`。)
### 6.2 OAuth 客户端
| 方法 | 路径 |
|------|------|
| GET/POST | `/oauth/clients` |
| PUT/DELETE | `/oauth/clients/:id` |
创建/更新 body`name`, `redirect_uris`, `client_id?`, `enabled?`, `client_secret?`, `rotate_secret?`
### 6.3 板块 / 单页 / 友链
| 方法 | 路径 |
|------|------|
| POST/PUT/DELETE | `/boards`, `/boards/:id` |
| GET/POST | `/pages` |
| GET/PUT/DELETE | `/pages/:id` |
| PUT | `/pages/:id/published``{ published }` |
| GET | `/friend-link-applies` |
| PUT | `/friend-link-settings` |
| POST | `/friend-link-applies/:id/approve` \| `reject` \| `recheck` |
### 6.4 帖子审核与运营
| 方法 | 路径 | Body |
|------|------|------|
| GET | `/posts` | page, keyword, status |
| GET | `/posts/trash` | |
| POST | `/posts/:id/pin` | `{ pinned }` |
| POST | `/posts/:id/board-pin` | `{ board_pinned }` |
| POST | `/posts/:id/feature` | `{ featured }` |
| POST | `/posts/:id/lock` | `{ locked }` → edit_locked |
| POST | `/posts/:id/comments-lock` | `{ locked }` |
| POST | `/posts/:id/approve` | |
| POST | `/posts/:id/reject` | `{ reason }` |
| POST | `/posts/:id/restore` | |
| DELETE | `/posts/:id/purge` | 硬删 |
| DELETE | `/posts/:id` | 软删 |
### 6.5 评论 / 举报 / 用户 / 徽章 / 媒体 / 备份
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/comments`, `/comments/trash` | |
| GET | `/comments/:id/revisions` | |
| POST | `/comments/:id/approve` \| `reject` \| `restore` | reject 可带 reason |
| DELETE | `/comments/:id`, `/comments/:id/purge` | |
| GET | `/reports` | page, status |
| POST | `/reports/:id/handle` | `{ action, handle_note?, reject_reason? }`action=`dismiss`\|`resolve`\|`reject_post`\|`reject_comment` |
| GET | `/users` | page, keyword, filter |
| POST | `/users/:id/ban` | `{ banned }` |
| POST | `/users/:id/verify` | `{ verified }` |
| POST | `/users/:id/level` | `{ level }` |
| POST | `/users/:id/points` | `{ delta, note? }` |
| POST | `/users/:id/badges` | `{ badge_id, revoke? }` |
| GET/POST | `/badges` | 列表 / upsert |
| GET | `/media` | category, page, size, q |
| POST | `/media/delete` | `{ urls: string[] }` |
| POST | `/backup` | `{ filename, download }` |
| GET | `/backup/download/:name` | 文件下载 |
---
## 7. 核心类型速查(与前端对齐)
详见 [`frontend/src/api/types.ts`](../../frontend/src/api/types.ts)。实现时至少对齐:
- `User` / `UserPublic` / `UserActivityStats`
- `Board` / `PostItem` / `PostDetailResponse` / `Comment`
- `ForumLimits` / `ForumLimitsPublic` / `SiteBranding`
- `PollView` / `PostLotteryView`
- `PrivateMessage` / `MessageConversation`
- `PostReport` / `FriendLinkApply` / `BadgeDef` / `PointLedger`
- `CheckInStatus` / `LotteryStatus`
- `AdminDashboard` / `AdminSettings` / `StorageConfig` / `MailConfig` / `OIDCConfig`
---
## 8. 鉴权错误语义(现网)
中间件对未登录 / 过期 / 禁言返回 JSON error并可能清 cookie。前端统一 `throw new Error(data.error)`。新站应保持可区分的错误文案或错误码,避免前端无法提示。
源:[`middleware/auth.go`](../../middleware/auth.go)。