# 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 `;这不是旧 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": "" }` 对象 | ```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 云图 ``` **成功响应**: - `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": "" }` 对象 | | `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`)。