Files
opencloud/API.md
T

1089 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenCloud API 前端开发文档
> **读者**:前端开发者。本文档覆盖 API 的全部端点:请求参数、响应结构、可能出现的错误与已知坑点。
>
> **事实来源**:业务接口以 `src/schema/*.ts` 与 `src/router/*.ts` 为准;认证以 `src/auth.ts` 中的 Better Auth 配置、`src/index.ts` 中的挂载路径及 `src/middleware/auth.ts` 中的业务鉴权规则为准。若文档与代码不一致,以代码为准。
>
> **重要免责**:错误消息文案可能调整。**前端逻辑应依赖 HTTP 状态码及需要时的机器可读错误码,严禁匹配错误消息字符串**。文档中列出消息原文仅供调试对照。
## 目录
1. 通用约定
2. 错误处理与已知不一致
3. 认证
4. 端点 · 认证 `/auth`
5. 端点 · 云图 `/cloud`
6. 端点 · 个人资料 `/profile`
7. 端点 · 信息 `/info`
8. 端点 · 管理 `/admin`
9. 端点 · 系统
---
## 1. 通用约定
### 1.1 Base URL
| 环境 | 地址 |
| -------- | ---------------------------------------- |
| 本地开发 | `http://localhost:3000`(`vc dev` 启动) |
| 生产 | 以部署地址为准 |
业务接口除 `/image` 成功响应直接返回图片字节外,响应通常为 `application/json`(个别异常情形返回纯文本,见 2.2)。Better Auth 的 `/auth/*` 有独立响应格式,部分操作也可能重定向,见第 4 章。响应携带两个调试响应头:
- `X-Request-Id`:请求唯一 ID,反馈问题时请附上
- `X-Response-Time`:服务端处理耗时,如 `12ms`
### 1.2 命名与格式
- 业务接口使用 **snake_case**(如 `cloud_id`、`page_size`);Better Auth 的 `/auth/*` 接口使用其原生 **camelCase** 字段
- 日期时间一律为 **ISO 8601 字符串**(如 `2026-08-15T08:30:00.000Z`),可空的日期字段值为 `null`。唯一的例外见 2.3 第 5 条
- UUID 字段为标准 UUID 字符串
### 1.3 响应形态
业务成功响应**没有统一包装**,分两种形态;`/auth/*` 不使用下表中的业务信封:
| 场景 | 形态 | 示例 |
| -------------------------------------------- | -------- | ----------------------------------------------- |
| 写操作成功(POST/PATCH/DELETE 的动作类端点) | 信封对象 | `{ "status_code": 200, "message": "删除成功" }` |
| 读操作成功(业务 GET) | 裸数据 | 单资源为对象、列表为**数组** |
### 1.4 分页
所有分页端点使用相同的 query 参数:
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ---- | ---- | ---------------- | -------- |
| `page` | int | 否 | ≥ 1,默认 `1` | 页码 |
| `page_size` | int | 否 | 1–100,默认 `50` | 每页数量 |
⚠️ **响应不返回总数**,也没有 `has_more` 字段。判断「是否还有下一页」只能依靠「返回数组长度 < `page_size`」。超出数据范围的页返回 `200` + 空数组 `[]`,不是 404。
云图列表排序为 `uploaded_at` 倒序(同刻按 `id` 倒序);点赞列表按点赞时间倒序(同刻按云图 ID 倒序)。
### 1.5 共享数据模型
以下模型在多个端点复用,端点章节中直接引用名称。
**CloudInfo(云图)**
| 字段 | 类型 | 说明 |
| --------------------- | ------------ | --------------------------------------------------------------------------------------------------------------- |
| `id` | uuid | 云图 ID |
| `owner` | object | 上传者:`{ id: uuid, name: string }` |
| `type` | object | 云类型:`{ id: int, name: string, genus: string\|null, icon_id: uuid\|null, rarity: int, description: string }` |
| `latitude` | number | 纬度,-90 ~ 90 |
| `longitude` | number | 经度,-180 ~ 180 |
| `description` | string | 描述,可能为空字符串 |
| `captured_at` | string\|null | 拍摄时间(ISO 8601) |
| `uploaded_at` | string | 上传时间(ISO 8601) |
| `updated_at` | string | 最后更新时间(ISO 8601) |
| `received_like_count` | int | 收到点赞数 |
| `status` | enum | 审核状态:`pending`(待审核)/ `approved`(已通过)/ `rejected`(已驳回) |
| `is_hidden` | boolean | 是否隐藏(上传者控制,与审核状态独立) |
**公开可见规则**:未登录访客与无关用户只能看到 `status === "approved"` 且 `is_hidden === false` 的云图;上传者本人与 admin 不受限。
**UserProfile(用户)**
| 字段 | 类型 | 说明 |
| --------------------- | ------------ | ---------------------------------------------------- |
| `id` | uuid | 用户 ID |
| `name` | string | 用户名 |
| `email` | string | 邮箱(注意 2.3 第 7 条的暴露范围问题) |
| `image` | string\|null | 用户头像 URL,由 Better Auth 管理;未设置时为 `null` |
| `cloud_count` | int | 云图数(该用户上传的云图总数) |
| `received_like_count` | int | 收到点赞数(名下云图获赞总和) |
| `last_online` | string\|null | 最后在线时间(ISO 8601) |
| `role` | enum | `user` / `admin` |
| `is_disabled` | boolean | 是否被禁用 |
| `created_at` | string | 注册时间(ISO 8601) |
**CloudTypeInfo(云类型)**
| 字段 | 类型 | 说明 |
| ------------- | ------------ | ------------------------------------------------------- |
| `id` | int | 云类型 ID |
| `name` | string | 名称,如「积云」 |
| `genus` | string\|null | 属(拉丁名),如 `Cumulus` |
| `icon_id` | uuid\|null | 图标 ID |
| `rarity` | int | 稀有度 |
| `description` | string | 描述 |
| `created_at` | string | 创建时间。⚠️ 列表与详情端点的格式不一致,见 2.3 第 5 条 |
### 1.6 上传约束(`POST /cloud`)
- 请求体总大小上限 **21 MiB**,超出返回 413
- 图片文件上限 **20 MB**;仅支持 **JPEG / PNG / WebP**;**不支持动图**(含动态 WebP、GIF)
- 图片分辨率过高会被拒绝(像素上限)
- multipart 表单字段均为字符串,注意 `is_hidden` 的编码规则(见 5.7)
---
## 2. 错误处理与已知不一致
### 2.1 两种错误响应形状
业务接口存在**两种**错误响应形状,前端必须分别处理。`/auth/*` 使用 Better Auth 自己的错误格式,见 3.5。
**① 业务错误**(绝大多数错误)——ErrorMessage 信封:
```json
{
"status_code": 404,
"error": "未找到图片"
}
```
**② 参数校验失败(400)**——zod 原始结构,**不是**上面的信封:
```json
{
"success": false,
"error": {
"name": "ZodError",
"message": "[{\"code\":\"invalid_format\",\"path\":[\"email\"],\"message\":\"Invalid email address\"}]"
}
}
```
注意 `error.message` 是一个 **JSON 字符串**(issues 数组的序列化结果),需要二次 `JSON.parse` 才能拿到字段级错误明细;每个 issue 含 `path`(出错字段路径)、`code`、`message`。不需要逐字段提示时,直接弹「参数错误」即可。
**判别方式**:响应体含 `success: false` → 形状②;含 `status_code` → 形状①。
### 2.2 状态码总表
| 状态码 | 含义 | 前端通用动作 |
| ------ | ----------------------------------------------------------- | ---------------------------------------------- |
| 200 | 成功 | — |
| 201 | 业务创建成功(如上传云图、管理员创建用户) | — |
| 304 | 图片 ETag 未变化,无响应 body | 继续使用本地缓存 |
| 400 | 参数校验失败(形状②),或业务规则拒绝(形状①) | 检查参数;形状②可解析 `error.message` 定位字段 |
| 401 | 业务接口没有有效 Better Auth 会话,或邮箱未验证、帐号被禁用 | 重新获取会话,必要时引导登录或验证邮箱 |
| 403 | 业务接口已认证但权限不足,如角色不足或越权访问他人资源 | 按场景提示 |
| 404 | 资源不存在(形状①);**也可能是纯文本**(见下方警告) | 区分 content-type 后处理 |
| 405 | HTTP 方法不受端点支持 | 读取 `Allow` 响应头 |
| 409 | 部分业务接口发生唯一性冲突,如管理员创建用户或修改用户名 | 提示用户更换;普通注册见 3.5 |
| 413 | 上传请求体超过 21 MiB | 提示压缩图片后重试 |
| 500 | 服务器内部错误 | 提示稍后重试,反馈时附 `X-Request-Id` |
| 503 | 图片存储服务暂时不可用 | 稍后重试,反馈时附 `X-Request-Id` |
⚠️ **404 有两种 body**:业务 404(资源不存在)是 JSON 信封;但「未实现的占位端点」和「个别端点的数据库异常」返回的是 **纯文本 `404 Not Found`**(`text/plain`),见 2.3 第 2 条。前端解析 404 响应前必须先判断 content-type。
### 2.3 ⚠️ 已知不一致清单
以下是文档写作时核实的现状描述,**未来可能修复**。前端如需为之写防御代码,请做好移除准备。
1. **两种错误形状并存**:400 校验错误不是业务错误信封(见 2.1)。
2. **部分 404 是纯文本而非 JSON**,来源有三:(a) 未实现的占位端点(各章已标注);(b) `GET /profile/me` 与 `GET /admin/users` 的数据库异常会从空 `catch` 落到纯文本 404;(c) 未匹配任何路由的路径。Better Auth 认证端点使用自己的错误格式。
3. **认证端点使用 Better Auth 格式**:`/auth/*` 的字段、状态码与错误响应由 Better Auth 定义,不能用业务错误信封解析;见第 4 章。
4. **管理员创建用户需验证邮箱**:`POST /admin/user` 会通过 Better Auth 创建用户并尝试发送验证邮件;验证前不能登录。
5. **`GET /info/cloudtype`(列表)的 `created_at` 格式与其他端点不同**:是 PostgreSQL 原生字符串(如 `2026-08-15 08:30:00.123456+00`),而非 ISO 8601(`2026-08-15T08:30:00.000Z`)。详情端点 `/info/cloudtype/:id` 及其他所有端点均为 ISO 8601。前端解析该字段时需兼容两种格式。
6. **用户名/邮箱数据库长度限制**:用户名 ≤ 16 字符、邮箱 ≤ 64 字符;前端应限制输入长度。
7. **`GET /profile/:user_id` 向任意登录用户暴露对方邮箱**:任何登录用户都能查到任意用户的 `email`。前端展示他人资料时不应展示邮箱,也不应假设该接口未来仍返回邮箱。
8. **用户名大小写**:数据库唯一约束区分大小写,注册和改名均受该约束。
---
## 3. 认证
### 3.1 从旧认证接口迁移
后端现在由 Better Auth 处理注册、登录、邮箱验证、会话和密码。旧认证路由已移除;请替换所有旧请求与响应解析:
| 旧接口 | 当前接口 / 前端动作 | 主要变化 |
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `POST /auth/register` | `POST /auth/sign-up/email` | 请求仍为 `name`、`email`、`password`;成功为 Better Auth 的 `200`,不会自动登录 |
| `POST /auth/login` | `POST /auth/sign-in/email` | 使用 Better Auth 会话 Cookie,不再保存旧 JWT |
| `POST /auth/logout` | `POST /auth/sign-out` | 撤销当前会话;不要只清除前端状态 |
| `POST /auth/resend-confirmation` | `POST /auth/send-verification-email` | 请求 `{ "email": "..." }` |
| `POST /auth/confirm-email` | `GET /auth/verify-email?token=...` | token 从前端验证页的 URL 读取,放入查询参数 |
| `POST /auth/forgot-password` | `POST /auth/request-password-reset` | 请求 `{ "email": "..." }` |
| `POST /auth/reset-password` | `POST /auth/reset-password` | 路径相同,请求改为 `{ "token": "...", "newPassword": "..." }`;不再提交 `email` / `new_password` |
| `PATCH /auth/password` | `POST /auth/change-password` | 请求改为 `currentPassword`、`newPassword`,可选 `revokeOtherSessions` |
旧路由的 `status_code` / `message` 信封、JWT 及认证相关的 snake_case 请求字段不适用于新端点。普通注册不要提交 `role`:角色由服务端设置为 `user`;创建管理员用户走第 8 章的管理接口。业务接口的路径与 snake_case 字段仍按后续章节使用。
### 3.2 浏览器接入
前端安装与后端当前版本一致的 `better-auth@1.7.6`,创建客户端时将 **API 地址连同 `/auth` 路径**传入 `baseURL`。服务端挂载路径是 `/auth`,不是 Better Auth 默认的 `/api/auth`;不要把业务 API 根地址直接用作认证客户端的完整 `baseURL`。下面是框架无关的示例;React 可从 `better-auth/react` 导入 `createAuthClient`。这与 [Better Auth 客户端的自定义路径配置](https://better-auth.com/docs/concepts/client)一致。
```ts
import { createAuthClient } from 'better-auth/client'
const API_ORIGIN = 'http://localhost:3000' // 生产环境改成实际 API origin
export const authClient = createAuthClient({
baseURL: `${API_ORIGIN}/auth`,
fetchOptions: { credentials: 'include' },
})
```
Better Auth 浏览器客户端默认会携带凭证;上面显式写出该选项,便于与业务请求保持一致。业务接口使用原生 `fetch` 或其他 HTTP 客户端时,也要单独配置:
```ts
const response = await fetch(`${API_ORIGIN}/profile/me`, {
credentials: 'include',
})
```
浏览器会自动保存并发送 HttpOnly 会话 Cookie。不要尝试在 JavaScript 中读取 Cookie,也不要将登录响应中的 token 当作旧 JWT 存储。服务端 `CORS_ORIGIN` 必须与前端 origin(协议、域名、端口)精确匹配;生产环境 Cookie 为 `SameSite=None; Secure`,本地开发为 `SameSite=Lax`。`BETTER_AUTH_URL` 是后端公开地址;前端客户端仍需使用上面含 `/auth` 的 URL。跨源接入和凭证设置也见 [Better Auth 的 Hono 集成文档](https://better-auth.com/docs/integrations/hono)。
### 3.3 会话与业务权限
登录成功后,使用 `authClient.getSession()` 或对应框架的 `useSession()` 获取会话:
```ts
const { error } = await authClient.signIn.email({ email, password })
if (error) throw error
const { data: session } = await authClient.getSession()
// session 为 { user, session } 或 null;退出时调用 await authClient.signOut()。
```
需要业务资料、统计及角色时,再请求 `GET /profile/me`,不要将 Better Auth 的 `user` 对象当作第 1.5 节的 UserProfile。完整客户端方法见 [Better Auth 会话管理](https://better-auth.com/docs/concepts/session-management)。
头像使用 Better Auth 的 `user.image` 字段。前端可调用 `authClient.updateUser({ image: "https://example.com/avatar.png" })` 设置头像 URL;业务资料接口和管理员用户列表均返回同一 `image` 值。原 `avatar_id` 字段已移除。
所有受保护的业务接口都再次检查会话、邮箱验证状态、禁用状态及角色。**业务接口**在无会话、邮箱未验证或帐号被禁用时返回 `401`;会话有效但角色或资源权限不足时返回 `403`。管理员修改角色或密码后会撤销目标用户的全部会话,前端应刷新登录状态。
两种可选认证端点行为不同:`GET /cloud/:cloud_id` 对无效/过期凭证按匿名处理;`GET|HEAD /image/:cloud_id/:variant` 在显式提供无效凭证时返回 `401`。同时发送 `Authorization` 和 Cookie 时,业务接口优先使用 Bearer 凭证。
### 3.4 邮件与密码页面
注册后会发送验证邮件,用户需先验证邮箱再登录;验证成功后也不会自动登录。当前邮箱验证 token 有效期为 24 小时,密码重置 token 默认有效期为 1 小时。后端发出的邮件仍指向前端 `/verify-email?token=...` 与 `/reset-password?email=...&token=...` 页面;它们是**前端页面**,不是 API。验证页应将 token 交给 `GET /auth/verify-email?token=...`,重置页应以 token 和新密码调用 `POST /auth/reset-password`。邮件中的 `email` 可用于页面展示,重置请求不需要它。密码重置成功会撤销该用户的全部会话。
重发验证邮件使用 `authClient.sendVerificationEmail({ email })`;申请密码重置使用 `authClient.requestPasswordReset({ email })`。用户不存在时,密码重置申请仍返回通用成功结果,不要据此判断邮箱是否已注册。验证及重置流程见 [Better Auth 邮箱密码文档](https://better-auth.com/docs/authentication/email-password)。
### 3.5 认证错误与非浏览器客户端
Better Auth 端点的错误是其原生结构,通常包含 `code`、`message`;客户端调用返回 `{ data, error }`。按 HTTP 状态、客户端的 `error` 和必要时的 `error.code` 处理,不要匹配英文错误文案,也不要按业务接口的 `status_code` 或 Zod 形状解析。未验证邮箱登录会被 Better Auth 拒绝(当前版本的错误码为 `EMAIL_NOT_VERIFIED`),此时可显示验证页并提供重发邮件操作。
当前配置要求邮箱验证,且注册后不自动登录。Better Auth 对重复邮箱注册可能返回与新邮箱相同的 **200** 响应和临时生成的用户对象;因此注册成功仅表示「请查收验证邮件」,**不能仅凭响应中的 `user.id` 判断账户已创建,也不能将其用作已登录用户 ID**。应在验证后重新登录。该行为来自当前安装的 Better Auth `1.7.6`。
非浏览器客户端可使用已启用的 Bearer 插件。登录成功响应头 `set-auth-token` 暴露会话 token,后续请求使用 `Authorization: Bearer <token>`;这不是旧 JWT,也不是密码重置或邮箱验证 token。前端浏览器仍使用 Cookie。[Better Auth Bearer 插件说明](https://better-auth.com/docs/plugins/bearer)。
## 4. 端点 · 认证 `/auth`
以下为本项目已启用的 Better Auth 邮箱密码功能,密码至少 8 字符。请求 JSON 使用原生 **camelCase**,响应不带业务 `status_code` 信封。前端推荐调用第 3.2 节的客户端;直接请求时使用表中方法和路径。Better Auth 还会暴露其他内置或 Admin 插件端点,其输入、权限和响应以 [Better Auth 官方文档](https://better-auth.com/docs)及当前安装版本为准。
| 操作 | 方法与路径 | Better Auth 客户端方法 | 请求与结果要点 |
| ------------ | ------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| 注册 | `POST /auth/sign-up/email` | `authClient.signUp.email({ name, email, password })` | 成功 `200`,`{ token: null, user }`;发送验证邮件,不建立会话 |
| 登录 | `POST /auth/sign-in/email` | `authClient.signIn.email({ email, password })` | 验证邮箱后才可登录;成功 `200` 并设置会话 Cookie |
| 当前会话 | `GET /auth/get-session` | `authClient.getSession()` | 有效会话返回 `{ user, session }`;无会话返回 `null` |
| 登出当前会话 | `POST /auth/sign-out` | `authClient.signOut()` | 需携带当前 Cookie 或 Bearer;撤销当前会话 |
| 重发验证邮件 | `POST /auth/send-verification-email` | `authClient.sendVerificationEmail({ email })` | `{ email }`;匿名请求对不存在或已验证邮箱也可能返回 `{ status: true }` |
| 确认邮箱 | `GET /auth/verify-email?token=...` | 可用 `fetch` 发 GET | 查询参数 `token`;无 `callbackURL` 时成功返回包含 `status: true` 的 JSON |
| 申请密码重置 | `POST /auth/request-password-reset` | `authClient.requestPasswordReset({ email })` | `{ email }`;返回通用结果,不泄露账户是否存在 |
| 重置密码 | `POST /auth/reset-password` | `authClient.resetPassword({ token, newPassword })` | `{ token, newPassword }`;成功 `{ status: true }`,撤销该用户全部会话 |
| 修改当前密码 | `POST /auth/change-password` | `authClient.changePassword({ currentPassword, newPassword, revokeOtherSessions })` | 需登录;可选择撤销其他会话 |
验证邮箱页面的请求示例:
```ts
const token = new URLSearchParams(location.search).get('token')
if (!token) throw new Error('缺少验证 token')
const response = await fetch(`${API_ORIGIN}/auth/verify-email?token=${encodeURIComponent(token)}`, {
credentials: 'include',
})
if (!response.ok) throw new Error('邮箱验证失败')
// 验证成功后引导用户登录;当前配置不会自动创建会话。
```
上面的 `API_ORIGIN` 与第 3.2 节相同。直接调用 `GET /auth/verify-email` 时不需要旧接口的 JSON 请求体;若传入 `callbackURL`,Better Auth 可能改为重定向,前端应按重定向处理。
管理员的业务管理操作继续使用第 8 章 `/admin/*` 路由。它们保持原业务响应格式,并在修改角色或密码后撤销目标会话;直接调用 Better Auth Admin 插件端点不会自动执行这些业务路由的附加操作。
---
## 5. 端点 · 云图 `/cloud`
### 5.1 `GET /cloud` — 云图列表(公开搜索)
- **认证**:无(公开);仅返回公开可见(`approved` 且未隐藏)的云图
**Query 参数**(在分页参数 1.4 基础上):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ------ | ---- | ----------------------------- | -------- |
| `page` | int | 否 | 默认 `1` | 见 1.4 |
| `page_size` | int | 否 | 默认 `50`,最大 `100` | 见 1.4 |
| `filter` | enum | 否 | `type` / `owner`,默认 `type` | 搜索维度 |
| `value` | string | 否 | 默认 `""`(空 = 不过滤) | 搜索词 |
搜索逻辑:
- `value` 为空 → 不过滤,返回全部公开云图
- `filter=owner`,**或** `value` 以 `@` 开头 → 按**用户名精确匹配**搜索该用户的公开云图(`@` 前缀会被去掉)
- 其他情况 → 按**云类型名称精确匹配**搜索
- 均为精确匹配,不支持模糊搜索
**成功响应**:`200`,CloudInfo 数组(字段见 1.5)
```json
[
{
"id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c",
"owner": {
"id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"name": "cloudwatcher"
},
"type": {
"id": 3,
"name": "积云",
"genus": "Cumulus",
"icon_id": null,
"rarity": 2,
"description": "底部平坦、顶部蓬松的白色云块"
},
"latitude": 39.9042,
"longitude": 116.4074,
"description": "午后拍到的淡积云",
"captured_at": "2026-08-14T07:20:00.000Z",
"uploaded_at": "2026-08-15T08:30:00.000Z",
"updated_at": "2026-08-15T08:30:00.000Z",
"received_like_count": 12,
"status": "approved",
"is_hidden": false
}
]
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | -------------------------------- | -------------------- | ------------------------------------------ |
| 400 | 参数校验失败(如 `page_size=0`) | zod 形状(2.1) | 检查参数 |
| 404 | 按用户名搜索但该用户不存在 | `该用户不存在` | 提示「没有找到这个用户」,不要显示为空列表 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:按云类型搜索时类型名不存在**不报错**,返回空数组(与用户名搜索的 404 行为不同)。
### 5.2 `GET /cloud/map` — 时间范围地图数据
- **认证**:无(公开);仅返回公开可见的云图
**Query 参数**:
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ------------ | ----------- | ---- | ------------------------------------------------- | ----------------------------------- |
| `start` | date string | 是 | 须 ≤ `end` | 范围起点,如 `2026-08-01T00:00:00Z` |
| `end` | date string | 是 | — | 范围终点 |
| `time_field` | enum | 否 | `captured_at` / `uploaded_at`,默认 `captured_at` | 按拍摄时间还是上传时间过滤 |
| `limit` | int | 否 | 1–1000,默认 `1000` | 返回上限(不分页) |
**成功响应**:`200`,CloudInfo 数组,按 `time_field` 倒序(同刻按 `id` 倒序),结构同 5.1。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------ | ------------------------------------------------ | ------------ |
| 400 | `start` 晚于 `end` | zod 形状,issues 中含 `开始时间不能晚于结束时间` | 表单前置校验 |
| 400 | 其他参数校验失败 | zod 形状(2.1) | 检查参数 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:`time_field=captured_at`(默认)时,`captured_at` 为 `null` 的云图**不会出现在结果中**;需要全量数据时请用 `uploaded_at`。
### 5.3 `GET /cloud/:cloud_id` — 云图详情
- **认证**:可选。匿名访客按公开可见规则过滤;上传者本人可见自己的全部云图;admin 可见全部
- 无效/过期会话凭证按匿名处理,**不会**返回 401
**路径参数**:
| 参数 | 类型 | 说明 |
| ---------- | ---- | ------- |
| `cloud_id` | uuid | 云图 ID |
**成功响应**:`200`,单个 CloudInfo 对象(字段见 1.5),结构同 5.1 的数组元素。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
| 400 | `cloud_id` 不是合法 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 云图不存在,**或**对当前用户不可见(待审核/已驳回/已隐藏) | `未找到图片` | 统一提示「云图不存在或不可见」——服务端故意不区分,防止泄露私有云图的存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 5.4 `GET /cloud/type/:cloud_type_id` — 按云类型列出云图
- **认证**:无(公开);仅返回公开可见的云图
**路径参数**:
| 参数 | 类型 | 说明 |
| --------------- | ---- | --------------------------- |
| `cloud_type_id` | int | 云类型 ID(非数字返回 400) |
**Query 参数**:分页参数,见 1.4。
**成功响应**:`200`,CloudInfo 数组,结构同 5.1。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------ | -------------------- | -------------- |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查参数 |
| 404 | 云类型不存在 | `云类型不存在` | 提示类型不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 5.5 `PUT /cloud/:cloud_id/like` — 点赞
- **认证**:需登录(任意角色)
- **请求体**:无
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "点赞成功"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------------------ | -------------------- | -------------------------------------------- |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在,或不是公开可见状态 | `图片不存在` | 提示云图不可点赞;与「不存在」共用是设计意图 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**幂等**——重复点赞返回相同的 200,点赞数不会增加。旧 `POST /cloud/:cloud_id/like` 已移除;前端改用 `PUT`。前端无需在点击前查询是否已赞,但仍建议本地置灰防止连点。
### 5.6 `DELETE /cloud/:cloud_id/like` — 取消点赞
- **认证**:需登录(任意角色)
- **请求体**:无
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "取消点赞成功"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------- | -------------------- | ------------ |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**完全幂等**——从未点赞、云图不存在,同样返回 200。没有 404 分支。
### 5.7 `POST /cloud` — 上传云图
- **认证**:需登录(任意角色)
- **Content-Type**:`multipart/form-data`;请求体总上限 21 MiB(图片约束见 1.6)
**表单字段**(multipart 字段均为字符串):
| 字段 | 类型 | 必填 | 约束 | 说明 |
| ------------- | ----------- | ------ | ------------------------------------------- | ---------------------------------------- |
| `image` | file | 是 | 1 B – 20 MB,JPEG/PNG/WebP,非动图 | 图片文件 |
| `type_id` | int | 是 | 正整数 | 云类型 ID(从 `/info/cloudtype` 获取) |
| `latitude` | number | 是 | -90 ~ 90 | 纬度 |
| `longitude` | number | 是 | -180 ~ 180 | 经度 |
| `description` | string | 否 | ≤ 128 字符 | 描述 |
| `captured_at` | date string | 否 | 合法日期 | 拍摄时间 |
| `is_hidden` | string | **是** | 仅接受 `"true"` / `"false"` / `"1"` / `"0"` | 是否隐藏;其他写法(如 `"yes"`)返回 400 |
**成功响应**:`201`
```json
{
"status_code": 201,
"message": "上传成功",
"cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | --------------------------------------------------- | ------------------------------- | -------------------------------------------- |
| 400 | 图片为空文件 | `图片为空` | 提示重新选择 |
| 400 | 图片超过 20 MB | `图片大小超过 20MB 限制` | 提示压缩 |
| 400 | 格式不支持 | `仅支持 JPEG、PNG、WebP 格式` | 提示转换格式 |
| 400 | 动图 | `不支持动图` | 提示使用静态图 |
| 400 | 分辨率超上限 | `图片分辨率超过限制` | 提示缩小尺寸 |
| 400 | 其他图片处理失败 | `压缩参数无效` / `图片处理失败` | 提示换图重试 |
| 400 | 表单字段校验失败(如 `is_hidden` 编码非法、缺字段) | zod 形状(2.1) | 检查表单 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 413 | 请求体超过 21 MiB | `上传文件过大` | 提示压缩图片 |
| 500 | 服务端异常(含存储/数据库失败) | `服务器发生内部错误` | 稍后重试;服务端已做失败补偿清理,可安全重试 |
**注意事项**:
- 上传成功后云图为 `pending`(待审核),**不会出现在公开列表**,本人可在「我的云图」(6.4)中看到
- `type_id` 指向不存在的云类型会导致 500(外键约束),前端应确保类型 ID 来自 `/info/cloudtype`
### 5.8 `PATCH /cloud/:cloud_id` — 更新云图
- **认证**:需登录,且为上传者本人。**非本人的云图返回 404 而非 403**(防止泄露存在性)
**路径参数**:`cloud_id`(uuid)。
**请求体**(JSON,至少提供一个字段,否则 400):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ------------- | ----------- | ---- | ---------- | ------------------------------- |
| `latitude` | number | 否 | -90 ~ 90 | 纬度 |
| `longitude` | number | 否 | -180 ~ 180 | 经度 |
| `type_id` | int | 否 | 正整数 | ⚠️ 不校验存在性,非法值导致 500 |
| `description` | string | 否 | ≤ 128 字符 | 描述 |
| `captured_at` | date string | 否 | 合法日期 | 拍摄时间 |
| `is_hidden` | boolean | 否 | — | 是否隐藏(JSON 布尔值) |
```json
{
"description": "补充:拍摄于景山公园",
"is_hidden": false
}
```
**成功响应**:`200`,`{ "status_code": 200, "message": "更新成功" }`
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | --------------------------------- | ------------------------------------------------ | -------------- |
| 400 | 未提供任何修改字段 | zod 形状,issues 中含 `至少需要提供一个修改字段` | 表单前置校验 |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常(含 `type_id` 不存在) | `服务器发生内部错误` | 稍后重试 |
### 5.9 `DELETE /cloud/:cloud_id` — 删除云图
- **认证**:需登录,且为上传者本人(非本人返回 404,同 5.8)
**成功响应**:`200`,`{ "status_code": 200, "message": "删除成功" }`
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | -------------------------- | -------------------- | -------------- |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 5.10 `DELETE /cloud` — 批量删除云图
- **认证**:需登录;仅删除属于当前用户的云图
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ----- | ---- | ------------------------ | -------------------------------------- |
| `cloud_ids` | array | 是 | 1–100 个元素,不允许重复 | 元素为 `{ "cloud_id": "<uuid>" }` 对象 |
```json
{
"cloud_ids": [
{ "cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c" },
{ "cloud_id": "b4c5d6e7-f8a9-4b5c-9d0e-1f2a3b4c5d6e" }
]
}
```
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "成功删除 2 张图片"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ----------------------------------- | ------------------------------------------------ | -------------- |
| 400 | ID 重复 / 数量超出 1–100 / 格式非法 | zod 形状,ID 重复时 issues 含 `图片 ID 不能重复` | 前端去重后提交 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**部分成功语义**——只删除属于当前用户的云图,他人或不存在的 ID 被静默跳过,消息中的 `N` 是实际删除数(可能小于请求数,甚至为 0)。前端应以 `N` 为准刷新列表,而不是假设全部删除成功。
### 5.11 `GET|HEAD /image/:cloud_id/:variant` — 读取图片变体
- **认证**:可选。匿名访客与无关用户只能读取具备 PublicVisibility 的 Cloud;上传者可读取自己的全部 Cloud;admin 可读取全部 Cloud
- **严格凭证**:完全不携带凭证时按匿名处理;一旦携带无效/过期 Bearer 或 Cookie,返回 401,不会降级为匿名。Bearer 与 Cookie 同时存在时 Bearer 优先
- **响应**:应用先查询 Cloud 权限,再从 MinIO 流式代理图片;客户端不能提交 bucket 或 object key
**路径参数**:
| 参数 | 类型 | 说明 |
| ---------- | ---- | ------------------------------------------------------- |
| `cloud_id` | uuid | Cloud ID,由上传响应、列表或详情接口取得 |
| `variant` | enum | `preview`(最大 640×640 WebP)或 `original`(上传原图) |
两种图片变体采用相同权限规则。示例:
```html
<img src="https://api.example.com/image/3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c/preview" alt="云图" />
```
**成功响应**:
- `GET` 返回 200 和图片字节,`Content-Type` 仅可能为 `image/jpeg`、`image/png` 或 `image/webp`
- `HEAD` 返回与 GET 相同的授权结果和响应头,但没有 body,也不会读取 MinIO 对象 body
- 返回 `Content-Length`、`Content-Disposition: inline`、`ETag`、`Last-Modified` 与 `X-Content-Type-Options: nosniff`
- 携带匹配的 `If-None-Match` 时返回 304;认证和 PublicVisibility 检查仍会执行
**缓存**:
| 调用方式 | `Cache-Control` |
| -------------------------------------- | ------------------- |
| 匿名读取具备 PublicVisibility 的 Cloud | `public, no-cache` |
| 任何有效 Cookie/Bearer 请求 | `private, no-store` |
响应同时包含 `Vary: Authorization, Cookie`。公开缓存每次复用前都必须回源验证,因此 Cloud 被隐藏或改判后立即失效。
**错误**:
| 状态码 | 触发条件 | 前端建议动作 |
| ------ | ------------------------------------------------------- | ------------------------------ |
| 400 | Cloud ID 或 ImageVariant 非法 | 检查 URL |
| 401 | 显式提供的 Better Auth 会话无效或过期 | 清除本地登录态 |
| 404 | Cloud 不存在、当前用户不可见,或对应 MinIO 对象不存在 | 统一显示占位图,不推断具体原因 |
| 405 | 使用 GET/HEAD 之外的业务方法;响应含 `Allow: GET, HEAD` | 修正请求方法 |
| 500 | 数据库或应用内部错误 | 稍后重试 |
| 503 | MinIO 暂时不可用或对象 metadata 异常 | 稍后重试 |
具备 PublicVisibility 的 Cloud 允许被其他网站嵌入;流量限制由部署网关/CDN/WAF 负责。端点不支持 Range 请求。CORS 预检 `OPTIONS` 是协议级例外,由全局中间件返回 204,并只声明 `GET, HEAD`。
---
## 6. 端点 · 个人资料 `/profile`
本章所有端点**都需要登录**(任意角色)。未登录统一返回 401(见 3.3),下文错误表不再重复列出。
### 6.1 `GET /profile/me` — 我的资料
- **认证**:需登录
**成功响应**:`200`,UserProfile 对象(字段见 1.5)
```json
{
"id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"name": "cloudwatcher",
"email": "watcher@example.com",
"image": "https://example.com/avatar.png",
"cloud_count": 7,
"received_like_count": 42,
"last_online": "2026-08-15T08:30:00.000Z",
"role": "user",
"is_disabled": false,
"created_at": "2026-07-01T10:00:00.000Z"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------- | ---------------------- | ---------------------- |
| 404 | ⚠️ 数据库异常(空 `catch`,2.3 第 2 条) | 纯文本 `404 Not Found` | 视为服务异常,稍后重试 |
### 6.2 `PATCH /profile/me` — 修改用户名
- **认证**:需登录
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ------ | ---- | --------------------------------------------------- | -------- |
| `user_name` | string | 是 | ⚠️ 数据库限 ≤ 16 字符,API 层不校验(超长返回 500) | 新用户名 |
```json
{
"user_name": "newname"
}
```
**成功响应**:`200`,更新后的 UserProfile 对象(结构同 6.1)。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | -------------------------------------------- | -------------------- | ---------------------------- |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 |
| 409 | 用户名已被占用(数据库唯一约束,大小写敏感) | `用户名已被占用` | 提示换用户名 |
| 500 | 服务端异常(含用户名超长) | `服务器发生内部错误` | 前端应限制 ≤ 16 字符避免误报 |
### 6.3 `GET /profile/me/likes` — 我点赞过的云图
- **认证**:需登录
- **Query 参数**:分页参数 `page`、`page_size`,见 1.4
**成功响应**:`200`,CloudInfo 数组(字段见 1.5),按点赞时间倒序、同刻按云图 ID 倒序;默认返回第 1 页的 50 条,最多每页 100 条。超出范围的页返回 `[]`。
**注意事项**:包含**所有状态**的云图(含待审核/已驳回/已隐藏的),因为这些是你自己点过赞的——展示时如需隐藏非公开项请自行过滤 `status` 与 `is_hidden`。
**错误**:分页参数非法时返回 400(见 2.1);服务端异常返回 500 `服务器发生内部错误`。
### 6.4 `GET /profile/me/clouds` — 我上传的云图
- **认证**:需登录
**成功响应**:`200`,CloudInfo 数组(字段见 1.5),按上传时间倒序,包含所有状态(`pending`/`approved`/`rejected`)与隐藏的云图。这是上传后查看审核进度的入口。
**错误**:500 `服务器发生内部错误`。
### 6.5 `GET /profile/:user_id` — 查看用户资料
- **认证**:需登录(任意登录用户可查看任意用户)
**路径参数**:`user_id`(uuid)。
**成功响应**:`200`,UserProfile 对象(结构同 6.1)。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ----------------------- | -------------------- | -------------- |
| 400 | `user_id` 不是合法 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 用户不存在 | `该用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:⚠️ 响应包含对方 `email`(2.3 第 7 条)。展示他人资料页时不应展示邮箱字段。
### 6.6 `GET /profile/:user_id/clouds` — 查看用户上传的云图
- **认证**:需登录,且为**本人或 admin**
**路径参数**:`user_id`(uuid)。
**成功响应**:`200`,CloudInfo 数组,按上传时间倒序,结构同 5.1。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------ | ---------------------- | -------------- |
| 403 | 查看他人且非 admin | `无权查看该用户的图片` | 同上,前置判断 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:想查看某个用户的**公开**云图,应使用 `GET /cloud?filter=owner&value=<用户名>`(5.1)而非本端点——本端点是管理/个人视角,返回含非公开状态的完整列表。
---
## 7. 端点 · 信息 `/info`
### 7.1 `GET /info/cloudtype` — 云类型列表
- **认证**:无(公开)
**成功响应**:`200`,CloudTypeInfo 数组(字段见 1.5),按 `id` 升序:
```json
[
{
"id": 1,
"name": "积云",
"genus": "Cumulus",
"icon_id": null,
"rarity": 1,
"description": "底部平坦、顶部蓬松的白色云块",
"created_at": "2026-01-01 00:00:00+00"
}
]
```
**注意事项**:⚠️ 本端点的 `created_at` 是 PostgreSQL 原生格式(上例),**不是** ISO 8601(2.3 第 5 条),解析时自行兼容。
**错误**:500 `服务器发生内部错误`。
### 7.2 `GET /info/cloudtype/:cloud_type_id` — 云类型详情
- **认证**:无(公开)
**路径参数**:`cloud_type_id`(int)。
**成功响应**:`200`,单个 CloudTypeInfo 对象;本端点的 `created_at` 为 ISO 8601 格式。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------- | -------------------- | -------------- |
| 400 | `cloud_type_id` 非数字 | zod 形状(2.1) | 检查链接 |
| 404 | 云类型不存在 | `未找到` | 提示类型不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
---
## 8. 端点 · 管理 `/admin`
本章所有端点**仅 admin 角色可用**。未登录返回 401;已登录但非 admin 统一返回 403 `权限不足`,下文错误表不再重复列出。
### 8.1 `GET /admin/stats` — 统计数据
> ⚠️ **未实现**(占位端点,无 handler)。admin 请求返回纯文本 `404 Not Found`;未登录/非 admin 仍按本章规则先返回 401/403。请勿接入。
### 8.2 `GET /admin/users` — 用户列表
- **认证**:admin
**成功响应**:`200`,UserProfile 数组(字段见 1.5)。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------- | ---------------------- | ---------------------- |
| 404 | ⚠️ 数据库异常(空 `catch`,2.3 第 2 条) | 纯文本 `404 Not Found` | 视为服务异常,稍后重试 |
**注意事项**:**无分页参数**,一次返回全部用户。用户量大时注意性能。
### 8.3 `PATCH /admin/users/:user_id` — 修改用户角色
- **认证**:admin
**路径参数**:`user_id`(uuid)。
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ---- | ---- | ---------------- | -------- |
| `user_role` | enum | 是 | `user` / `admin` | 目标角色 |
```json
{
"user_role": "admin"
}
```
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "角色修改成功"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------------------- | -------------------------- | ------------------ |
| 400 | 参数校验失败(`user_role` 非法 / `user_id` 非 uuid) | zod 形状(2.1) | 检查参数 |
| 400 | 管理员尝试撤销自己的 admin 角色 | `不能撤销自己的管理员角色` | 保持当前管理员角色 |
| 404 | 用户不存在 | `用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:修改角色会撤销目标用户的全部 Better Auth 会话,必须重新登录。前端改完角色后可提示目标用户重新登录。
### 8.4 `PATCH /admin/users/:user_id/password` — 设置用户密码
- **认证**:admin
**路径参数**:`user_id`(uuid)。
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| -------------- | ------ | ---- | ----------- | ---------------------------------- |
| `new_password` | string | 是 | 至少 8 字符 | 新密码;管理员直接覆盖,无需旧密码 |
```json
{
"new_password": "new-s3cret-password"
}
```
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "密码修改成功"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | --------------------------------------------------- | -------------------- | -------------- |
| 400 | 参数校验失败(`user_id` 非 uuid 或密码少于 8 字符) | zod 形状(2.1) | 检查参数 |
| 404 | 用户不存在(含凭据记录缺失) | `用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:成功后会撤销目标用户的全部 Better Auth 会话,必须重新登录。
### 8.5 `POST /admin/user` — 创建用户
管理员认证后提交 `{ "name": "Alice", "email": "alice@example.com", "password": "password123", "role": "user" }`。通过 Better Auth Admin 插件创建用户和密码凭据,再尝试发送邮箱验证邮件。成功返回 `201`,包含 `status_code`、`message`、`user_id`、`email_sent`;用户名或邮箱冲突返回 `409`。密码至少 8 字符。
### 8.6 `POST /admin/cloudtypes` — 添加云类型
- **认证**:admin
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ------------- | ------------ | ---- | ------------------ | ----------------------------- |
| `name` | string | 是 | 去除首尾空白后非空 | 云类型名称 |
| `genus` | string\|null | 否 | 默认 `null` | 属;传 `null` 表示未设置 |
| `icon_id` | uuid\|null | 否 | 默认 `null` | 图标 ID;传 `null` 表示未设置 |
| `rarity` | int | 是 | — | 稀有度 |
| `description` | string | 是 | 最长 128 字符 | 描述,可为空字符串 |
```json
{
"name": "积云",
"genus": "Cumulus",
"icon_id": null,
"rarity": 1,
"description": "底部平坦、顶部蓬松的白色云块"
}
```
**成功响应**:`201`
```json
{
"status_code": 201,
"message": "云类型添加成功",
"cloud_type_id": 12
}
```
云类型 ID 由服务端生成;并发添加不会分配重复 ID。参数校验失败返回 400,数据库异常返回 500。
### 8.7 `PATCH /admin/cloudtypes/:cloud_type_id` — 修改云类型
- **认证**:admin
**路径参数**:`cloud_type_id`(int)。请求体字段与 8.6 相同,但均为可选;至少须提供一个字段。仅提交的字段会被修改,`genus` 和 `icon_id` 可显式传 `null` 清空。
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "云类型修改成功"
}
```
**错误**:参数或空请求体无效返回 400;云类型不存在返回 404 `云类型不存在`;数据库异常返回 500。
### 8.8 `DELETE /admin/cloudtypes/:cloud_type_id` — 删除云类型
- **认证**:admin
**路径参数**:`cloud_type_id`(int)。
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "云类型删除成功"
}
```
**错误**:参数无效返回 400;云类型不存在返回 404 `云类型不存在`;仍有云图使用该类型时返回 409 `云类型仍被云图使用,无法删除`;其他数据库异常返回 500。
### 8.9 `GET /admin/clouds` — 全状态云图列表
- **认证**:admin;可见任意状态与隐藏的云图
**Query 参数**(在分页参数 1.4 基础上):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| -------- | ---- | ---- | ----------------------------------- | -------------------------------- |
| `status` | enum | 否 | `pending` / `approved` / `rejected` | 按审核状态过滤;不传返回全部状态 |
**成功响应**:`200`,CloudInfo 数组(字段见 1.5),按上传时间倒序。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ----------------------- | -------------------- | ------------ |
| 400 | `status` 非法等参数错误 | zod 形状(2.1) | 检查参数 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 8.10 `POST /admin/clouds/review` — 批量审核云图
- **认证**:admin
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ----- | ---- | ------------------------ | -------------------------------------- |
| `cloud_ids` | array | 是 | 1–100 个元素,不允许重复 | 元素为 `{ "cloud_id": "<uuid>" }` 对象 |
| `status` | enum | 是 | `approved` / `rejected` | 目标审核状态;**不支持打回 `pending`** |
```json
{
"cloud_ids": [{ "cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c" }],
"status": "approved"
}
```
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "成功审核 1 张"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------- | -------------------- | ------------ |
| 400 | ID 重复 / `status` 非法 / 数量超限 | zod 形状(2.1) | 检查请求 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**部分成功语义**——消息中的 `N` 只统计状态**实际发生变化**的云图;已是目标状态的与不存在的一律静默跳过。审核只改变审核状态,不影响 `is_hidden` 与点赞数据。`pending ↔ approved/rejected`、`approved ↔ rejected` 均可。
### 8.11 `DELETE /admin/clouds/:cloud_id` — 删除云图
- **认证**:admin
**路径参数**:`cloud_id`(uuid)。
**成功响应**:`200`
```json
{
"status_code": 200,
"message": "删除成功"
}
```
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------ | -------------------- | -------------- |
| 400 | `cloud_id` 非 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 云图不存在 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:管理员删除不受审核状态或隐藏状态限制。删除会级联删除该云图的点赞记录,并尽力清理 MinIO 中的图片对象;清理失败不报错,可能残留孤儿对象(可接受)。删除后前端应立即刷新相关列表。
### 8.12 `GET /admin/users/:user_id/likes` — 查看用户点赞记录
- **认证**:admin
- **路径参数**:`user_id`(uuid)
- **Query 参数**:分页参数 `page`、`page_size`,见 1.4
**成功响应**:`200`,与 6.3 相同的 CloudInfo 数组和排序,包含该用户已点赞但后来被隐藏或改判的云图;没有点赞或页码超出范围时返回 `[]`。
**错误**:`user_id` 或分页参数非法时返回 400(见 2.1);用户不存在时返回 404 `{ "status_code": 404, "error": "用户不存在" }`;服务端异常返回 500 `服务器发生内部错误`。非管理员统一返回 403(本章规则)。旧 `GET /profile/:user_id/likes` 已移除;本人使用 6.3,管理员使用本端点。
---
## 9. 端点 · 系统
### 9.1 `ALL /health` · 9.2 `ALL /status`
> ⚠️ **均未实现**(占位端点,任意 HTTP 方法)。当前返回纯文本 `404 Not Found`。健康检查请直接探测任意已实现端点(如 `GET /info/cloudtype`)。