48 KiB
OpenCloud API 前端开发文档
读者:前端开发者。本文档覆盖 API 的全部端点:请求参数、响应结构、可能出现的错误与已知坑点。
事实来源:业务接口以
src/schema/*.ts与src/router/*.ts为准;认证以src/auth.ts中的 Better Auth 配置、src/index.ts中的挂载路径及src/middleware/auth.ts中的业务鉴权规则为准。若文档与代码不一致,以代码为准。重要免责:错误消息文案可能调整。前端逻辑应依赖 HTTP 状态码及需要时的机器可读错误码,严禁匹配错误消息字符串。文档中列出消息原文仅供调试对照。
目录
- 通用约定
- 错误处理与已知不一致
- 认证
- 端点 · 认证
/auth - 端点 · 云图
/cloud - 端点 · 个人资料
/profile - 端点 · 信息
/info - 端点 · 管理
/admin - 端点 · 系统
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 信封:
{
"status_code": 404,
"error": "未找到图片"
}
② 参数校验失败(400)——zod 原始结构,不是上面的信封:
{
"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 ⚠️ 已知不一致清单
以下是文档写作时核实的现状描述,未来可能修复。前端如需为之写防御代码,请做好移除准备。
- 两种错误形状并存:400 校验错误不是业务错误信封(见 2.1)。
- 部分 404 是纯文本而非 JSON,来源有三:(a) 未实现的占位端点(各章已标注);(b)
GET /profile/me与GET /admin/users的数据库异常会从空catch落到纯文本 404;(c) 未匹配任何路由的路径。Better Auth 认证端点使用自己的错误格式。 - 认证端点使用 Better Auth 格式:
/auth/*的字段、状态码与错误响应由 Better Auth 定义,不能用业务错误信封解析;见第 4 章。 - 管理员创建用户需验证邮箱:
POST /admin/user会通过 Better Auth 创建用户并尝试发送验证邮件;验证前不能登录。 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。前端解析该字段时需兼容两种格式。- 用户名/邮箱数据库长度限制:用户名 ≤ 16 字符、邮箱 ≤ 64 字符;前端应限制输入长度。
GET /profile/:user_id向任意登录用户暴露对方邮箱:任何登录用户都能查到任意用户的email。前端展示他人资料时不应展示邮箱,也不应假设该接口未来仍返回邮箱。- 用户名大小写:数据库唯一约束区分大小写,注册和改名均受该约束。
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 客户端的自定义路径配置一致。
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 客户端时,也要单独配置:
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 集成文档。
3.3 会话与业务权限
登录成功后,使用 authClient.getSession() 或对应框架的 useSession() 获取会话:
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 会话管理。
头像使用 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 邮箱密码文档。
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 插件说明。
4. 端点 · 认证 /auth
以下为本项目已启用的 Better Auth 邮箱密码功能,密码至少 8 字符。请求 JSON 使用原生 camelCase,响应不带业务 status_code 信封。前端推荐调用第 3.2 节的客户端;直接请求时使用表中方法和路径。Better Auth 还会暴露其他内置或 Admin 插件端点,其输入、权限和响应以 Better Auth 官方文档及当前安装版本为准。
| 操作 | 方法与路径 | 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 }) |
需登录;可选择撤销其他会话 |
验证邮箱页面的请求示例:
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)
[
{
"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
{
"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
{
"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
{
"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 布尔值) |
{
"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>" } 对象 |
{
"cloud_ids": [
{ "cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c" },
{ "cloud_id": "b4c5d6e7-f8a9-4b5c-9d0e-1f2a3b4c5d6e" }
]
}
成功响应:200
{
"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(上传原图) |
两种图片变体采用相同权限规则。示例:
<img src="https://api.example.com/image/3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c/preview" alt="云图" />
成功响应:
GET返回 200 和图片字节,Content-Type仅可能为image/jpeg、image/png或image/webpHEAD返回与 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)
{
"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) | 新用户名 |
{
"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 升序:
[
{
"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 |
目标角色 |
{
"user_role": "admin"
}
成功响应:200
{
"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 字符 | 新密码;管理员直接覆盖,无需旧密码 |
{
"new_password": "new-s3cret-password"
}
成功响应:200
{
"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 字符 | 描述,可为空字符串 |
{
"name": "积云",
"genus": "Cumulus",
"icon_id": null,
"rarity": 1,
"description": "底部平坦、顶部蓬松的白色云块"
}
成功响应:201
{
"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
{
"status_code": 200,
"message": "云类型修改成功"
}
错误:参数或空请求体无效返回 400;云类型不存在返回 404 云类型不存在;数据库异常返回 500。
8.8 DELETE /admin/cloudtypes/:cloud_type_id — 删除云类型
- 认证:admin
路径参数:cloud_type_id(int)。
成功响应:200
{
"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 |
{
"cloud_ids": [
{ "cloud_id": "3f8a2c1e-7b9d-4e5f-a1c2-9d8e7f6a5b4c" }
],
"status": "approved"
}
成功响应:200
{
"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
{
"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)。