Files
opencloud/API.md

48 KiB
Raw Permalink Blame History

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 信封:

{
    "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 ⚠️ 已知不一致清单

以下是文档写作时核实的现状描述,未来可能修复。前端如需为之写防御代码,请做好移除准备。

  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 客户端的自定义路径配置一致。

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/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)

{
    "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)。