# 04 · HTTP API 合约

> **读者**：机器客户端 / 集成方；浏览器 UI **不**使用本文件作为主路径  
> **前置**：[03-data-model.md](03-data-model.md)、[08-gitea-ssr-architecture.md](08-gitea-ssr-architecture.md)  
> **源码**：[`routers/setup.go`](../../routers/setup.go)、[`modules/auth/auth.go`](../../modules/auth/auth.go)

本分支（`rebuild/gitea-ssr`）浏览器走 **`routers/web` 模板 + 表单**。  
下列 JSON 合约保留作历史对照与未来机器 API；**当前进程仅注册** health / OIDC / robots / sitemap / media 等机器相关路由，论坛 CRUD 的 `/api/*` 已从路由表移除（handler 源码可删可留，不以 SPA 兼容为目的）。

`main` 分支 SPA 仍完整实现下表；对照请 checkout `main`。

---

## 1. 本分支已注册的机器入口

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/health` | 探活 |
| GET | `/robots.txt` | 抓取规则 |
| GET | `/sitemap.xml` | 站点地图 |
| GET/POST | `/oauth/*`、`/.well-known/openid-configuration` | OIDC Provider |
| GET | `/media/thumb/*`、`/uploads/*` | 媒体 |

---

## 2. 历史 JSON 合约（main / 对照，本分支默认不挂载）

以下章节描述原 SPA 使用的 `/api` 形状，便于迁移业务语义；**实现 UI 时请用 web 表单，勿恢复双轨。**

### 通用约定（历史）

| 项 | 约定 |
|----|------|
| Base | 同源；`credentials: 'same-origin'` |
| 成功 | HTTP 2xx + JSON body |
| 失败 | 非 2xx + `{ "error": "..." }` |
| 鉴权 | Cookie `jiang13_session`（opaque）；机器 OIDC 用 Bearer |

### 分页形态差异

| 场景 | 典型字段 |
|------|----------|
| 前台帖列表 | `posts`, `total`, `page`, `size`, `has_more` |
| 后台多数列表 | `total`, `page`, `total_pages` + 实体数组 |
| 私信会话消息 | `before` 游标式 |

---

## 3. 基础设施 / SEO / 静态（节选，仍有效）

| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| GET | `/health` | 无 | `{ "status": "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`](../../services/oidc.go) / [`handler/oidc.go`](../../routers/api/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/JSON: username, password, nickname, email, captcha_id, captcha, 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, before；peerId=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` | **后置**（501） |
| POST | `/settings/gitea/sync` | **后置**（501） |
| 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`](（仅 main）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`](../../modules/auth/auth.go)。
