refactor: organize features and standardize frontend tooling

Colocate feature views, components, stores, and upload logic; centralize shared map utilities without changing API behavior.

Add ESLint, Prettier, EditorConfig, and VS Code settings; standardize four-space indentation and document the project structure and checks.

Validation: npm run check, npm run build, and git diff --check.
This commit is contained in:
2026-09-29 18:05:56 +08:00
parent c3e49bd9b4
commit ea47eec47c
104 changed files with 14116 additions and 10576 deletions
+279 -282
View File
@@ -24,10 +24,10 @@
### 1.1 Base URL
| 环境 | 地址 |
|---|---|
| 环境 | 地址 |
| -------- | ---------------------------------------- |
| 本地开发 | `http://localhost:3000`(`vc dev` 启动) |
| 生产 | 以部署地址为准 |
| 生产 | 以部署地址为准 |
业务接口除 `/image` 成功响应直接返回图片字节外,响应通常为 `application/json`(个别异常情形返回纯文本,见 2.2)。Better Auth 的 `/auth/*` 有独立响应格式,部分操作也可能重定向,见第 4 章。响应携带两个调试响应头:
@@ -44,19 +44,19 @@
业务成功响应**没有统一包装**,分两种形态;`/auth/*` 不使用下表中的业务信封:
| 场景 | 形态 | 示例 |
|---|---|---|
| 场景 | 形态 | 示例 |
| -------------------------------------------- | -------- | ----------------------------------------------- |
| 写操作成功(POST/PATCH/DELETE 的动作类端点) | 信封对象 | `{ "status_code": 200, "message": "删除成功" }` |
| 读操作成功(业务 GET) | 裸数据 | 单资源为对象、列表为**数组** |
| 读操作成功(业务 GET) | 裸数据 | 单资源为对象、列表为**数组** |
### 1.4 分页
所有分页端点使用相同的 query 参数:
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `page` | int | 否 | ≥ 1,默认 `1` | 页码 |
| `page_size` | int | 否 | 1–100,默认 `50` | 每页数量 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ---- | ---- | ---------------- | -------- |
| `page` | int | 否 | ≥ 1,默认 `1` | 页码 |
| `page_size` | int | 否 | 1–100,默认 `50` | 每页数量 |
⚠️ **响应不返回总数**,也没有 `has_more` 字段。判断「是否还有下一页」只能依靠「返回数组长度 < `page_size`」。超出数据范围的页返回 `200` + 空数组 `[]`,不是 404。
@@ -68,49 +68,49 @@
**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 | 是否隐藏(上传者控制,与审核状态独立) |
| 字段 | 类型 | 说明 |
| --------------------- | ------------ | --------------------------------------------------------------------------------------------------------------- |
| `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) |
| 字段 | 类型 | 说明 |
| --------------------- | ------------ | ---------------------------------------------------- |
| `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 条 |
| 字段 | 类型 | 说明 |
| ------------- | ------------ | ------------------------------------------------------- |
| `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`)
@@ -154,20 +154,20 @@
### 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` |
| 状态码 | 含义 | 前端通用动作 |
| ------ | ----------------------------------------------------------- | ---------------------------------------------- |
| 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。
@@ -192,16 +192,16 @@
后端现在由 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` |
| 旧接口 | 当前接口 / 前端动作 | 主要变化 |
| -------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `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 字段仍按后续章节使用。
@@ -210,21 +210,21 @@
前端安装与后端当前版本一致的 `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";
import { createAuthClient } from 'better-auth/client'
const API_ORIGIN = "http://localhost:3000"; // 生产环境改成实际 API origin
const API_ORIGIN = 'http://localhost:3000' // 生产环境改成实际 API origin
export const authClient = createAuthClient({
baseURL: `${API_ORIGIN}/auth`,
fetchOptions: { credentials: "include" },
});
fetchOptions: { credentials: 'include' },
})
```
Better Auth 浏览器客户端默认会携带凭证;上面显式写出该选项,便于与业务请求保持一致。业务接口使用原生 `fetch` 或其他 HTTP 客户端时,也要单独配置:
```ts
const response = await fetch(`${API_ORIGIN}/profile/me`, {
credentials: "include",
});
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)。
@@ -234,10 +234,10 @@ const response = await fetch(`${API_ORIGIN}/profile/me`, {
登录成功后,使用 `authClient.getSession()` 或对应框架的 `useSession()` 获取会话:
```ts
const { error } = await authClient.signIn.email({ email, password });
if (error) throw error;
const { error } = await authClient.signIn.email({ email, password })
if (error) throw error
const { data: session } = await authClient.getSession();
const { data: session } = await authClient.getSession()
// session 为 { user, session } 或 null;退出时调用 await authClient.signOut()。
```
@@ -267,29 +267,28 @@ Better Auth 端点的错误是其原生结构,通常包含 `code`、`message`
以下为本项目已启用的 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 })` | 需登录;可选择撤销其他会话 |
| 操作 | 方法与路径 | 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 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("邮箱验证失败");
const response = await fetch(`${API_ORIGIN}/auth/verify-email?token=${encodeURIComponent(token)}`, {
credentials: 'include',
})
if (!response.ok) throw new Error('邮箱验证失败')
// 验证成功后引导用户登录;当前配置不会自动创建会话。
```
@@ -307,12 +306,12 @@ if (!response.ok) throw new Error("邮箱验证失败");
**Query 参数**(在分页参数 1.4 基础上):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `page` | int | 否 | 默认 `1` | 见 1.4 |
| `page_size` | int | 否 | 默认 `50`,最大 `100` | 见 1.4 |
| `filter` | enum | 否 | `type` / `owner`,默认 `type` | 搜索维度 |
| `value` | string | 否 | 默认 `""`(空 = 不过滤) | 搜索词 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ------ | ---- | ----------------------------- | -------- |
| `page` | int | 否 | 默认 `1` | 见 1.4 |
| `page_size` | int | 否 | 默认 `50`,最大 `100` | 见 1.4 |
| `filter` | enum | 否 | `type` / `owner`,默认 `type` | 搜索维度 |
| `value` | string | 否 | 默认 `""`(空 = 不过滤) | 搜索词 |
搜索逻辑:
@@ -354,11 +353,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | 参数校验失败(如 `page_size=0`) | zod 形状(2.1) | 检查参数 |
| 404 | 按用户名搜索但该用户不存在 | `该用户不存在` | 提示「没有找到这个用户」,不要显示为空列表 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | -------------------------------- | -------------------- | ------------------------------------------ |
| 400 | 参数校验失败(如 `page_size=0`) | zod 形状(2.1) | 检查参数 |
| 404 | 按用户名搜索但该用户不存在 | `该用户不存在` | 提示「没有找到这个用户」,不要显示为空列表 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:按云类型搜索时类型名不存在**不报错**,返回空数组(与用户名搜索的 404 行为不同)。
@@ -368,22 +367,22 @@ if (!response.ok) throw new Error("邮箱验证失败");
**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` | 返回上限(不分页) |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ------------ | ----------- | ---- | ------------------------------------------------- | ----------------------------------- |
| `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 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------ | ------------------------------------------------ | ------------ |
| 400 | `start` 晚于 `end` | zod 形状,issues 中含 `开始时间不能晚于结束时间` | 表单前置校验 |
| 400 | 其他参数校验失败 | zod 形状(2.1) | 检查参数 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:`time_field=captured_at`(默认)时,`captured_at` 为 `null` 的云图**不会出现在结果中**;需要全量数据时请用 `uploaded_at`。
@@ -394,19 +393,19 @@ if (!response.ok) throw new Error("邮箱验证失败");
**路径参数**:
| 参数 | 类型 | 说明 |
|---|---|---|
| 参数 | 类型 | 说明 |
| ---------- | ---- | ------- |
| `cloud_id` | uuid | 云图 ID |
**成功响应**:`200`,单个 CloudInfo 对象(字段见 1.5),结构同 5.1 的数组元素。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | `cloud_id` 不是合法 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 云图不存在,**或**对当前用户不可见(待审核/已驳回/已隐藏) | `未找到图片` | 统一提示「云图不存在或不可见」——服务端故意不区分,防止泄露私有云图的存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
| 400 | `cloud_id` 不是合法 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 云图不存在,**或**对当前用户不可见(待审核/已驳回/已隐藏) | `未找到图片` | 统一提示「云图不存在或不可见」——服务端故意不区分,防止泄露私有云图的存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 5.4 `GET /cloud/type/:cloud_type_id` — 按云类型列出云图
@@ -414,9 +413,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**路径参数**:
| 参数 | 类型 | 说明 |
|---|---|---|
| `cloud_type_id` | int | 云类型 ID(非数字返回 400) |
| 参数 | 类型 | 说明 |
| --------------- | ---- | --------------------------- |
| `cloud_type_id` | int | 云类型 ID(非数字返回 400) |
**Query 参数**:分页参数,见 1.4。
@@ -424,11 +423,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | 参数校验失败 | zod 形状(2.1) | 检查参数 |
| 404 | 云类型不存在 | `云类型不存在` | 提示类型不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------ | -------------------- | -------------- |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查参数 |
| 404 | 云类型不存在 | `云类型不存在` | 提示类型不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 5.5 `PUT /cloud/:cloud_id/like` — 点赞
@@ -446,11 +445,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在,或不是公开可见状态 | `图片不存在` | 提示云图不可点赞;与「不存在」共用是设计意图 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------------------ | -------------------- | -------------------------------------------- |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在,或不是公开可见状态 | `图片不存在` | 提示云图不可点赞;与「不存在」共用是设计意图 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**幂等**——重复点赞返回相同的 200,点赞数不会增加。旧 `POST /cloud/:cloud_id/like` 已移除;前端改用 `PUT`。前端无需在点击前查询是否已赞,但仍建议本地置灰防止连点。
@@ -470,10 +469,10 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------- | -------------------- | ------------ |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**完全幂等**——从未点赞、云图不存在,同样返回 200。没有 404 分支。
@@ -484,15 +483,15 @@ if (!response.ok) throw new Error("邮箱验证失败");
**表单字段**(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 |
| 字段 | 类型 | 必填 | 约束 | 说明 |
| ------------- | ----------- | ------ | ------------------------------------------- | ---------------------------------------- |
| `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`
@@ -506,18 +505,18 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 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 | 服务端异常(含存储/数据库失败) | `服务器发生内部错误` | 稍后重试;服务端已做失败补偿清理,可安全重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | --------------------------------------------------- | ------------------------------- | -------------------------------------------- |
| 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 | 服务端异常(含存储/数据库失败) | `服务器发生内部错误` | 稍后重试;服务端已做失败补偿清理,可安全重试 |
**注意事项**:
@@ -532,14 +531,14 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(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 布尔值) |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ------------- | ----------- | ---- | ---------- | ------------------------------- |
| `latitude` | number | 否 | -90 ~ 90 | 纬度 |
| `longitude` | number | 否 | -180 ~ 180 | 经度 |
| `type_id` | int | 否 | 正整数 | ⚠️ 不校验存在性,非法值导致 500 |
| `description` | string | 否 | ≤ 128 字符 | 描述 |
| `captured_at` | date string | 否 | 合法日期 | 拍摄时间 |
| `is_hidden` | boolean | 否 | — | 是否隐藏(JSON 布尔值) |
```json
{
@@ -552,13 +551,13 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | 未提供任何修改字段 | zod 形状,issues 中含 `至少需要提供一个修改字段` | 表单前置校验 |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常(含 `type_id` 不存在) | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | --------------------------------- | ------------------------------------------------ | -------------- |
| 400 | 未提供任何修改字段 | zod 形状,issues 中含 `至少需要提供一个修改字段` | 表单前置校验 |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常(含 `type_id` 不存在) | `服务器发生内部错误` | 稍后重试 |
### 5.9 `DELETE /cloud/:cloud_id` — 删除云图
@@ -568,11 +567,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | -------------------------- | -------------------- | -------------- |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 404 | 云图不存在或不属于当前用户 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 5.10 `DELETE /cloud` — 批量删除云图
@@ -580,9 +579,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `cloud_ids` | array | 是 | 1–100 个元素,不允许重复 | 元素为 `{ "cloud_id": "<uuid>" }` 对象 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ----- | ---- | ------------------------ | -------------------------------------- |
| `cloud_ids` | array | 是 | 1–100 个元素,不允许重复 | 元素为 `{ "cloud_id": "<uuid>" }` 对象 |
```json
{
@@ -604,11 +603,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | ID 重复 / 数量超出 1–100 / 格式非法 | zod 形状,ID 重复时 issues 含 `图片 ID 不能重复` | 前端去重后提交 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ----------------------------------- | ------------------------------------------------ | -------------- |
| 400 | ID 重复 / 数量超出 1–100 / 格式非法 | zod 形状,ID 重复时 issues 含 `图片 ID 不能重复` | 前端去重后提交 |
| 401 | 未登录 / token 失效 | 见 3.3 | 引导登录 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**部分成功语义**——只删除属于当前用户的云图,他人或不存在的 ID 被静默跳过,消息中的 `N` 是实际删除数(可能小于请求数,甚至为 0)。前端应以 `N` 为准刷新列表,而不是假设全部删除成功。
@@ -620,10 +619,10 @@ if (!response.ok) throw new Error("邮箱验证失败");
**路径参数**:
| 参数 | 类型 | 说明 |
|---|---|---|
| `cloud_id` | uuid | Cloud ID,由上传响应、列表或详情接口取得 |
| `variant` | enum | `preview`(最大 640×640 WebP)或 `original`(上传原图) |
| 参数 | 类型 | 说明 |
| ---------- | ---- | ------------------------------------------------------- |
| `cloud_id` | uuid | Cloud ID,由上传响应、列表或详情接口取得 |
| `variant` | enum | `preview`(最大 640×640 WebP)或 `original`(上传原图) |
两种图片变体采用相同权限规则。示例:
@@ -640,23 +639,23 @@ if (!response.ok) throw new Error("邮箱验证失败");
**缓存**:
| 调用方式 | `Cache-Control` |
|---|---|
| 匿名读取具备 PublicVisibility 的 Cloud | `public, no-cache` |
| 任何有效 Cookie/Bearer 请求 | `private, no-store` |
| 调用方式 | `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 异常 | 稍后重试 |
| 状态码 | 触发条件 | 前端建议动作 |
| ------ | ------------------------------------------------------- | ------------------------------ |
| 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`。
@@ -689,9 +688,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 404 | ⚠️ 数据库异常(空 `catch`,2.3 第 2 条) | 纯文本 `404 Not Found` | 视为服务异常,稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------- | ---------------------- | ---------------------- |
| 404 | ⚠️ 数据库异常(空 `catch`,2.3 第 2 条) | 纯文本 `404 Not Found` | 视为服务异常,稍后重试 |
### 6.2 `PATCH /profile/me` — 修改用户名
@@ -699,9 +698,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `user_name` | string | 是 | ⚠️ 数据库限 ≤ 16 字符,API 层不校验(超长返回 500) | 新用户名 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ------ | ---- | --------------------------------------------------- | -------- |
| `user_name` | string | 是 | ⚠️ 数据库限 ≤ 16 字符,API 层不校验(超长返回 500) | 新用户名 |
```json
{
@@ -713,11 +712,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 |
| 409 | 用户名已被占用(数据库唯一约束,大小写敏感) | `用户名已被占用` | 提示换用户名 |
| 500 | 服务端异常(含用户名超长) | `服务器发生内部错误` | 前端应限制 ≤ 16 字符避免误报 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | -------------------------------------------- | -------------------- | ---------------------------- |
| 400 | 参数校验失败 | zod 形状(2.1) | 检查字段 |
| 409 | 用户名已被占用(数据库唯一约束,大小写敏感) | `用户名已被占用` | 提示换用户名 |
| 500 | 服务端异常(含用户名超长) | `服务器发生内部错误` | 前端应限制 ≤ 16 字符避免误报 |
### 6.3 `GET /profile/me/likes` — 我点赞过的云图
@@ -748,11 +747,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | `user_id` 不是合法 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 用户不存在 | `该用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ----------------------- | -------------------- | -------------- |
| 400 | `user_id` 不是合法 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 用户不存在 | `该用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:⚠️ 响应包含对方 `email`(2.3 第 7 条)。展示他人资料页时不应展示邮箱字段。
@@ -766,10 +765,10 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 403 | 查看他人且非 admin | `无权查看该用户的图片` | 同上,前置判断 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------ | ---------------------- | -------------- |
| 403 | 查看他人且非 admin | `无权查看该用户的图片` | 同上,前置判断 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:想查看某个用户的**公开**云图,应使用 `GET /cloud?filter=owner&value=<用户名>`(5.1)而非本端点——本端点是管理/个人视角,返回含非公开状态的完整列表。
@@ -811,11 +810,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | `cloud_type_id` 非数字 | zod 形状(2.1) | 检查链接 |
| 404 | 云类型不存在 | `未找到` | 提示类型不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------- | -------------------- | -------------- |
| 400 | `cloud_type_id` 非数字 | zod 形状(2.1) | 检查链接 |
| 404 | 云类型不存在 | `未找到` | 提示类型不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
---
@@ -835,9 +834,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 404 | ⚠️ 数据库异常(空 `catch`,2.3 第 2 条) | 纯文本 `404 Not Found` | 视为服务异常,稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------- | ---------------------- | ---------------------- |
| 404 | ⚠️ 数据库异常(空 `catch`,2.3 第 2 条) | 纯文本 `404 Not Found` | 视为服务异常,稍后重试 |
**注意事项**:**无分页参数**,一次返回全部用户。用户量大时注意性能。
@@ -849,9 +848,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `user_role` | enum | 是 | `user` / `admin` | 目标角色 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ---- | ---- | ---------------- | -------- |
| `user_role` | enum | 是 | `user` / `admin` | 目标角色 |
```json
{
@@ -870,12 +869,12 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | 参数校验失败(`user_role` 非法 / `user_id` 非 uuid) | zod 形状(2.1) | 检查参数 |
| 400 | 管理员尝试撤销自己的 admin 角色 | `不能撤销自己的管理员角色` | 保持当前管理员角色 |
| 404 | 用户不存在 | `用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------------------------- | -------------------------- | ------------------ |
| 400 | 参数校验失败(`user_role` 非法 / `user_id` 非 uuid) | zod 形状(2.1) | 检查参数 |
| 400 | 管理员尝试撤销自己的 admin 角色 | `不能撤销自己的管理员角色` | 保持当前管理员角色 |
| 404 | 用户不存在 | `用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:修改角色会撤销目标用户的全部 Better Auth 会话,必须重新登录。前端改完角色后可提示目标用户重新登录。
@@ -887,9 +886,9 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `new_password` | string | 是 | 至少 8 字符 | 新密码;管理员直接覆盖,无需旧密码 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| -------------- | ------ | ---- | ----------- | ---------------------------------- |
| `new_password` | string | 是 | 至少 8 字符 | 新密码;管理员直接覆盖,无需旧密码 |
```json
{
@@ -908,11 +907,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | 参数校验失败(`user_id` 非 uuid 或密码少于 8 字符) | zod 形状(2.1) | 检查参数 |
| 404 | 用户不存在(含凭据记录缺失) | `用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | --------------------------------------------------- | -------------------- | -------------- |
| 400 | 参数校验失败(`user_id` 非 uuid 或密码少于 8 字符) | zod 形状(2.1) | 检查参数 |
| 404 | 用户不存在(含凭据记录缺失) | `用户不存在` | 提示用户不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:成功后会撤销目标用户的全部 Better Auth 会话,必须重新登录。
@@ -926,13 +925,13 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `name` | string | 是 | 去除首尾空白后非空 | 云类型名称 |
| `genus` | string\|null | 否 | 默认 `null` | 属;传 `null` 表示未设置 |
| `icon_id` | uuid\|null | 否 | 默认 `null` | 图标 ID;传 `null` 表示未设置 |
| `rarity` | int | 是 | — | 稀有度 |
| `description` | string | 是 | 最长 128 字符 | 描述,可为空字符串 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ------------- | ------------ | ---- | ------------------ | ----------------------------- |
| `name` | string | 是 | 去除首尾空白后非空 | 云类型名称 |
| `genus` | string\|null | 否 | 默认 `null` | 属;传 `null` 表示未设置 |
| `icon_id` | uuid\|null | 否 | 默认 `null` | 图标 ID;传 `null` 表示未设置 |
| `rarity` | int | 是 | — | 稀有度 |
| `description` | string | 是 | 最长 128 字符 | 描述,可为空字符串 |
```json
{
@@ -996,18 +995,18 @@ if (!response.ok) throw new Error("邮箱验证失败");
**Query 参数**(在分页参数 1.4 基础上):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `status` | enum | 否 | `pending` / `approved` / `rejected` | 按审核状态过滤;不传返回全部状态 |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| -------- | ---- | ---- | ----------------------------------- | -------------------------------- |
| `status` | enum | 否 | `pending` / `approved` / `rejected` | 按审核状态过滤;不传返回全部状态 |
**成功响应**:`200`,CloudInfo 数组(字段见 1.5),按上传时间倒序。
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | `status` 非法等参数错误 | zod 形状(2.1) | 检查参数 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ----------------------- | -------------------- | ------------ |
| 400 | `status` 非法等参数错误 | zod 形状(2.1) | 检查参数 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
### 8.10 `POST /admin/clouds/review` — 批量审核云图
@@ -1015,16 +1014,14 @@ if (!response.ok) throw new Error("邮箱验证失败");
**请求体**(JSON):
| 参数 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `cloud_ids` | array | 是 | 1–100 个元素,不允许重复 | 元素为 `{ "cloud_id": "<uuid>" }` 对象 |
| `status` | enum | 是 | `approved` / `rejected` | 目标审核状态;**不支持打回 `pending`** |
| 参数 | 类型 | 必填 | 约束 | 说明 |
| ----------- | ----- | ---- | ------------------------ | -------------------------------------- |
| `cloud_ids` | array | 是 | 1–100 个元素,不允许重复 | 元素为 `{ "cloud_id": "<uuid>" }` 对象 |
| `status` | enum | 是 | `approved` / `rejected` | 目标审核状态;**不支持打回 `pending`** |
```json
{
"cloud_ids": [
{ "cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c" }
],
"cloud_ids": [{ "cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c" }],
"status": "approved"
}
```
@@ -1040,10 +1037,10 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | ID 重复 / `status` 非法 / 数量超限 | zod 形状(2.1) | 检查请求 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ---------------------------------- | -------------------- | ------------ |
| 400 | ID 重复 / `status` 非法 / 数量超限 | zod 形状(2.1) | 检查请求 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:**部分成功语义**——消息中的 `N` 只统计状态**实际发生变化**的云图;已是目标状态的与不存在的一律静默跳过。审核只改变审核状态,不影响 `is_hidden` 与点赞数据。`pending ↔ approved/rejected`、`approved ↔ rejected` 均可。
@@ -1064,11 +1061,11 @@ if (!response.ok) throw new Error("邮箱验证失败");
**错误**:
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
|---|---|---|---|
| 400 | `cloud_id` 非 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 云图不存在 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
| 状态码 | 触发条件 | 消息原文 | 前端建议动作 |
| ------ | ------------------ | -------------------- | -------------- |
| 400 | `cloud_id` 非 uuid | zod 形状(2.1) | 检查链接 |
| 404 | 云图不存在 | `图片不存在` | 提示云图不存在 |
| 500 | 服务端异常 | `服务器发生内部错误` | 稍后重试 |
**注意事项**:管理员删除不受审核状态或隐藏状态限制。删除会级联删除该云图的点赞记录,并尽力清理 MinIO 中的图片对象;清理失败不报错,可能残留孤儿对象(可接受)。删除后前端应立即刷新相关列表。