Files
opencloud/API.md
T

49 KiB
Raw Blame History

OpenCloud API 前端开发文档

读者:前端开发者。本文档覆盖 API 的全部端点:请求参数、响应结构、可能出现的错误与已知坑点。

事实来源:本文档的字段名、约束与错误消息以 src/schema/*.ts(zod 校验定义)与 src/router/*.ts(路由实现)为最终依据整理。若文档与代码不一致,以代码为准。

重要免责:错误消息文案(error 字段的中文内容)可能随时调整。前端逻辑只能依赖 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)。所有响应携带两个调试响应头:

  • X-Request-Id:请求唯一 ID,反馈问题时请附上
  • X-Response-Time:服务端处理耗时,如 12ms

1.2 命名与格式

  • 请求与响应字段一律 snake_case(如 cloud_id、page_size、received_like_count)
  • 日期时间一律为 ISO 8601 字符串(如 2026-08-15T08:30:00.000Z),可空的日期字段值为 null。唯一的例外见 2.3 第 5 条
  • UUID 字段为标准 UUID 字符串

1.3 响应形态

响应没有统一包装,分两种形态:

场景 形态 示例
写操作成功(POST/PATCH/DELETE 的动作类端点) 信封对象 { "status_code": 200, "message": "删除成功" }
读操作成功(GET 及登录) 裸数据 单资源为对象、列表为数组、登录为 { "access_token" }

1.4 分页

所有分页端点使用相同的 query 参数:

参数 类型 必填 约束 说明
page int 否 ≥ 1,默认 1 页码
page_size int 否 1–100,默认 50 每页数量

⚠️ 响应不返回总数,也没有 has_more 字段。判断「是否还有下一页」只能依靠「返回数组长度 < page_size」。超出数据范围的页返回 200 + 空数组 [],不是 404。

云图列表排序为 uploaded_at 倒序(同刻按 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 条的暴露范围问题)
avatar_id uuid|null 头像 ID(当前恒为 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 两种错误响应形状

本 API 存在两种错误响应形状,前端必须分别处理:

① 业务错误(绝大多数错误)——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 未认证:未携带 token,或 token 无效/过期/已失效 清除本地登录态,跳转登录页
403 已认证但无权限:角色不足、邮箱未确认、帐号被禁用、越权访问他人资源 按消息场景提示
404 资源不存在(形状①);也可能是纯文本(见下方警告) 区分 content-type 后处理
405 HTTP 方法不受端点支持 读取 Allow 响应头
409 唯一性冲突:邮箱/用户名已被占用 提示用户更换
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) 未匹配任何路由的路径。注册与邮箱确认失败始终返回 JSON 信封。
  3. 静默续签只写 Cookie:token 到期前的自动续签仅通过 Set-Cookie 下发新 token(见 3.4)。使用 Bearer header 模式的客户端拿不到新 token,30 分钟后必然 401,需重新登录。
  4. POST /admin/user 是假端点:它原样回显请求体(含明文密码),并不创建用户。
  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. 用户名/邮箱长度不在 API 层校验:数据库限制用户名 ≤ 16 字符、邮箱 ≤ 64 字符,但 zod 校验不拦截。超长注册与超长改名(PATCH /profile/me)会返回 JSON 500。前端应自行限制输入长度。
  7. GET /profile/:user_id 向任意登录用户暴露对方邮箱:任何登录用户都能查到任意用户的 email。前端展示他人资料时不应展示邮箱,也不应假设该接口未来仍返回邮箱。
  8. 用户名唯一性的大小写规则不一致:注册时的查重是大小写不敏感的(Alice/alice 视为重复),但数据库唯一约束与 PATCH /profile/me 改名是大小写敏感的(可改成仅大小写不同的名字)。

3. 认证

3.1 机制概览

  • JWT(HS256),有效期 30 分钟
  • 获取方式:POST /auth/login 成功后在响应体返回 access_token,同时通过 Set-Cookie 写入 HttpOnly Cookie
  • 受保护端点接受两种携带方式(服务端优先读 header):
    1. 请求头 Authorization: Bearer <access_token>
    2. Cookie token=<jwt>(HttpOnly; SameSite=Strict; Path=/; Max-Age=1800;非开发/测试环境附加 Secure)
模式 适用 注意事项
Cookie 浏览器前端(推荐) 登录后浏览器自动携带;fetch 需设 credentials: "include";HttpOnly 使 JS 无法读取 token,天然防 XSS 窃取;可享受静默续签
Bearer 非浏览器客户端(App、脚本) 自行存储 access_token 并注入 header;无法静默续签(2.3 第 3 条),30 分钟后需重新登录

3.3 CORS

  • 服务端仅放行单个 origin(环境变量 CORS_ORIGIN,本地默认应为 http://localhost:5173),且 credentials: true
  • 前端本地开发的服务端口/origin 必须与后端配置完全一致(协议、主机、端口),否则 Cookie 模式的请求会被浏览器拦截
  • 放行的请求头:Content-Type、Authorization
  • 私有图片通过 <img src> 直接加载时依赖 Cookie;生产环境前端与 API 必须位于同一 site(例如 app.example.com 与 api.example.com),否则 SameSite=Strict Cookie 不会随图片请求发送

3.4 有效期、续签与失效

  • token 有效期 30 分钟(Cookie 的 Max-Age 同为 1800 秒)
  • 静默续签:携带剩余有效期 ≤ 300 秒的 token 访问受保护端点时,服务端会在响应中追加 Set-Cookie 写入新 token。Cookie 模式下浏览器自动替换,前端无感知;Bearer 模式收不到新 token
  • 续签不适用的路径:POST /auth/logout、PATCH /auth/password
  • /image 请求只验证身份,不触发静默续签,避免一个页面的并发图片请求重复写 Cookie
  • 认证中间件每次请求都会重新检查用户仍已确认邮箱且未被管理员禁用;任一条件不满足时,已有 JWT 也会被拒绝
  • 立即失效的情况:调用 /auth/logout、调用 PATCH /auth/password 改密、通过 /auth/reset-password 重置密码、用户被禁用,以及管理员修改了该用户的角色或密码(PATCH /admin/users/:user_id 见 8.3、PATCH /admin/users/:user_id/password 见 8.4)。除「用户被禁用」由中间件每次请求重新校验外,其余情况都会递增 tokenVersion,使该用户所有已签发的 token 全部作废(包括其他设备上的);用户自己改密与密码重置成功时还会写入过期 Cookie

3.5 认证相关错误消息对照

状态码 消息原文 场景
401 未登录 受保护端点未携带任何 token
401 Token 无效或已过期 token 签名无效、过期、已被吊销(登出、改密或密码重置后),或用户不再同时满足「邮箱已确认且未禁用」
403 权限不足 已登录但角色不满足(如非 admin 访问 /admin)

注意「帐号被禁用」在登录时报 403「帐号已被禁用」,但已登录后被禁用,后续请求报的是 401「Token 无效或已过期」。


4. 端点 · 认证 /auth

4.1 POST /auth/register — 注册

  • 认证:无(公开)

请求体(JSON):

参数 类型 必填 约束 说明
name string 是 ⚠️ 数据库限 ≤ 16 字符,API 层不校验(2.3 第 6 条) 用户名,大小写不敏感查重
email string 是 合法邮箱格式;⚠️ 数据库限 ≤ 64 字符 服务端去除首尾空白并转为小写后存储
password string 是 无强度校验 密码
{
    "name": "cloudwatcher",
    "email": "watcher@example.com",
    "password": "s3cret-password"
}

成功响应:201

{
    "status_code": 201,
    "message": "注册成功,请查收邮件并确认邮箱",
    "email_sent": true
}

数据库提交成功但邮件暂时未发送时仍返回 201,email_sent 为 false,消息会提示稍后使用重发验证流程。用户、密码凭据和 24 小时有效的确认记录已经保留,不应重试注册。

错误:

状态码 触发条件 消息原文 前端建议动作
400 参数校验失败 zod 形状(2.1) 检查字段
409 已确认邮箱重复注册(大小写不敏感) 该邮箱已被注册 提示直接登录
409 未确认邮箱重复注册 该邮箱已被注册,请使用重发验证流程 引导至重发验证流程,不会覆盖原用户名或密码
409 用户名已使用(大小写不敏感) 该用户名已被使用 提示换用户名
500 数据库、密码散列或服务端配置异常 注册失败 稍后重试;若此前已收到 201,不要重复注册

注意事项:

  • 新用户的 is_disabled 为 false,email_verified_at 为空;两个状态相互独立。注册后必须先确认邮箱,再调用 /auth/login
  • 注册响应不返回 token、不写 Cookie
  • 确认邮件由 Opencloud <opencloud@catpl.top> 发出,同时包含 https://cloud.catpl.top/verify-email?token=... 链接和原始 token 备用文本。数据库仅保存使用 JWT_SECRET 生成的 HMAC 摘要
  • ⚠️ 并发注册(同一邮箱/用户名几乎同时提交)命中数据库唯一约束时,冲突消息文案与上方表格不同(邮箱冲突会返回 该邮箱已被注册;若尚未验证,请使用重发验证流程)。按本文档免责声明,前端仍只依据状态码 409 处理

4.2 POST /auth/resend-confirmation — 重发确认邮件

  • 认证:无(公开)

请求体(JSON):

参数 类型 必填 说明
email string 是 与注册、登录相同:服务端去除首尾空白并转为小写;不接受旧字段 user_email
{
    "email": "watcher@example.com"
}

成功响应:200

{
    "status_code": 200,
    "message": "如果该邮箱符合条件,我们将发送验证邮件"
}

未知邮箱、已确认邮箱、被管理员禁用、处于 60 秒冷却期、符合发送条件以及邮件供应商失败,都会收到完全相同的状态码和响应体。前端不能根据该响应判断邮件是否实际发送,也不应显示「邮箱存在」之类的提示。

只有尚未确认、未被管理员禁用且不在冷却期内的用户会触发邮件发送。成功签发新 token 后,同一用户此前未消费的邮箱确认 token 全部失效;新 token 仍有 24 小时有效期,邮件内容与注册邮件相同。已消费记录会保留作为审计信息。

重叠请求会按用户串行处理,至多签发一个可用 token。数据库签发或 Resend 失败不会改变上述公共响应。

错误:请求体校验失败返回 400 zod 形状(2.1)。

4.3 POST /auth/confirm-email — 确认邮箱

  • 认证:无(公开)

前端从确认链接读取 token,再提交:

{
    "token": "0123456789abcdef0123456789abcdef"
}

成功响应:200

{
    "status_code": 200,
    "message": "邮箱确认成功,请重新登录"
}

确认会在一个数据库事务中消费未过期、未消费且用途为 email_confirmation 的记录,并写入用户的邮箱确认时间。它不会签发 JWT、不会写认证 Cookie,也绝不修改管理员禁用状态。

状态码 触发条件 消息原文 前端建议动作
400 token 未知、过期、已消费,或来自其他用途 确认链接无效或已过期 统一提示链接无效,并引导重发
500 数据库或服务端配置异常 确认邮箱失败 稍后重试

4.4 POST /auth/login — 登录

  • 认证:无(公开)

请求体(JSON):

参数 类型 必填 说明
email string 是 服务端去除首尾空白、转为小写后匹配
password string 是 密码
{
    "email": "watcher@example.com",
    "password": "s3cret-password"
}

成功响应:200,同时写入认证 Cookie(见 3.1)

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

错误:

状态码 触发条件 消息原文 前端建议动作
400 参数校验失败 zod 形状(2.1) 检查字段
401 邮箱不存在或密码错误(两者共用,防枚举) 邮箱或密码错误 统一提示「邮箱或密码错误」
403 帐号被管理员禁用 帐号已被禁用 提示联系管理员
403 密码正确但邮箱尚未确认 邮箱尚未验证 引导至重发验证流程
500 服务端异常 登录失败 稍后重试

4.5 POST /auth/logout — 登出

  • 认证:需登录(任意角色)
  • 请求体:无

成功响应:200,同时写入过期 Cookie

{
    "status_code": 200,
    "message": "已登出"
}

错误:

状态码 触发条件 消息原文 前端建议动作
401 未登录 / token 失效 见 3.5 本地直接清理登录态即可
500 数据库异常 连接数据库发生错误,登出失败 提示重试;⚠️ 此时服务端 token 可能仍有效

注意事项:登出会使该用户所有设备的 token 立即失效(tokenVersion 递增),不只是当前会话。

4.6 POST /auth/forgot-password — 申请重置密码

  • 认证:无(公开)

请求体(JSON):

参数 类型 必填 说明
email string 是 服务端去除首尾空白并转为小写;不接受旧字段 user_email
{
    "email": "watcher@example.com"
}

成功响应:200

{
    "status_code": 200,
    "message": "如果该邮箱符合条件,我们将发送密码重置邮件"
}

未知邮箱、邮箱未确认、被管理员禁用、处于 60 秒冷却期、符合发送条件以及邮件供应商失败,都会收到完全相同的状态码和响应体。只有邮箱已确认、未被禁用且不在冷却期内的用户会触发邮件发送。

新签发的 password_reset token 有效期为 30 分钟,并使此前未消费的密码重置 token 失效;它与邮箱确认 token 用 purpose 隔离。邮件由 Opencloud <opencloud@catpl.top> 发出,链接到 https://cloud.catpl.top/reset-password?email=...&token=...。数据库和 Resend 失败不改变公共响应。

错误:请求体校验失败返回 400 zod 形状(2.1)。没有 /auth/forget-password 兼容路径。

4.7 POST /auth/reset-password — 重置密码

  • 认证:无(公开)

请求体(JSON):

参数 类型 必填 说明
email string 是 邮件链接中的邮箱;服务端再次规范化
token string 是 邮件链接中的原始 token
new_password string 是 新密码;不能与当前密码相同,不额外扩大密码策略
{
    "email": "watcher@example.com",
    "token": "0123456789abcdef0123456789abcdef",
    "new_password": "new-s3cret-password"
}

成功响应:200,同时写入过期认证 Cookie

{
    "status_code": 200,
    "message": "密码重置成功,请使用新密码重新登录"
}

成功时,服务端在同一数据库事务中消费 token、更新密码哈希和凭据更新时间,并递增 tokenVersion。此前在所有设备签发的 JWT 随即失效;响应不会签发新 JWT,用户必须重新登录。

token 必须同时匹配规范化邮箱、HMAC 摘要、password_reset purpose、有效期、未消费状态和目标用户;用户在消费时仍须邮箱已确认且未被管理员禁用。重置不会修改邮箱确认时间或管理员禁用状态。

状态码 触发条件 消息原文 前端建议动作
400 token 未知、过期、已消费、已被替换、用途错误、邮箱不匹配,或用户不再符合状态要求 重置链接无效或已过期 统一提示链接无效,重新申请
400 新密码与当前密码相同 新密码不能与当前密码相同 要求输入不同密码
500 数据库或服务端配置异常 重置密码失败 稍后重试

4.8 PATCH /auth/password — 修改密码

  • 认证:需登录(任意角色)

请求体(JSON):

参数 类型 必填 说明
current_password string 是 当前密码
new_password string 是 新密码,无强度校验
{
    "current_password": "s3cret-password",
    "new_password": "new-s3cret-password"
}

成功响应:200,同时写入过期 Cookie

{
    "status_code": 200,
    "message": "密码修改成功,请重新登录"
}

错误:

状态码 触发条件 消息原文 前端建议动作
400 新旧密码相同 新密码不能与当前密码相同 表单前置校验可避免
400 参数校验失败 zod 形状(2.1) 检查字段
401 当前密码错误 当前密码错误 提示重新输入
401 未登录 / token 失效 见 3.5 跳登录
404 用户无凭据记录(数据异常) 未找到用户凭据 反馈后端,附 X-Request-Id
500 服务端异常 修改密码时发生错误 稍后重试

注意事项:改密成功后所有已签发 token 立即失效(含当前使用的这个),前端必须清理登录态并跳转登录页。


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 可见全部
  • 无效/过期 token 按匿名处理,不会返回 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 POST /cloud/:cloud_id/like — 点赞

  • 认证:需登录(任意角色)
  • 请求体:无

成功响应:200

{
    "status_code": 200,
    "message": "点赞成功"
}

错误:

状态码 触发条件 消息原文 前端建议动作
401 未登录 / token 失效 见 3.5 引导登录
404 云图不存在,或不是公开可见状态 图片不存在 提示云图不可点赞;与「不存在」共用是设计意图
500 服务端异常 服务器发生内部错误 稍后重试

注意事项:幂等——重复点赞返回相同的 200,点赞数不会增加。前端无需在点击前查询是否已赞,但仍建议本地置灰防止连点。

5.6 DELETE /cloud/:cloud_id/like — 取消点赞

  • 认证:需登录(任意角色)
  • 请求体:无

成功响应:200

{
    "status_code": 200,
    "message": "取消点赞成功"
}

错误:

状态码 触发条件 消息原文 前端建议动作
401 未登录 / token 失效 见 3.5 引导登录
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.5 引导登录
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.5 引导登录
404 云图不存在或不属于当前用户 图片不存在 提示云图不存在
500 服务端异常(含 type_id 不存在) 服务器发生内部错误 稍后重试

5.9 DELETE /cloud/:cloud_id — 删除云图

  • 认证:需登录,且为上传者本人(非本人返回 404,同 5.8)

成功响应:200,{ "status_code": 200, "message": "删除成功" }

错误:

状态码 触发条件 消息原文 前端建议动作
401 未登录 / token 失效 见 3.5 引导登录
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.5 引导登录
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 显式提供的 JWT 无效、过期或已失效 清除本地登录态
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.5),下文错误表不再重复列出。

6.1 GET /profile/me — 我的资料

  • 认证:需登录

成功响应:200,UserProfile 对象(字段见 1.5)

{
    "id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
    "name": "cloudwatcher",
    "email": "watcher@example.com",
    "avatar_id": null,
    "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 — 我点赞过的云图

  • 认证:需登录

成功响应:200,CloudInfo 数组(字段见 1.5),按点赞时间倒序。

注意事项:包含所有状态的云图(含待审核/已驳回/已隐藏的),因为这些是你自己点过赞的——展示时如需隐藏非公开项请自行过滤 status 与 is_hidden。

错误: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/likes — 查看用户点赞记录

  • 认证:需登录,且为本人或 admin

路径参数:user_id(uuid)。

成功响应:200,CloudInfo 数组,按点赞时间倒序,结构同 5.1。

错误:

状态码 触发条件 消息原文 前端建议动作
403 查看他人且非 admin 无权查看该用户的点赞记录 前端应在进入页面前判断身份,避免触发
500 服务端异常 服务器发生内部错误 稍后重试

6.7 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) 检查参数
404 用户不存在 用户不存在 提示用户不存在
500 服务端异常 服务器发生内部错误 稍后重试

注意事项:修改角色会同时递增 tokenVersion,该用户所有已签发 token 立即失效,必须重新登录(见 3.4)。前端改完角色后可提示目标用户重新登录。

8.4 PATCH /admin/users/:user_id/password — 设置用户密码

  • 认证:admin

路径参数:user_id(uuid)。

请求体(JSON):

参数 类型 必填 约束 说明
new_password string 是 无强度校验 新密码;管理员直接覆盖,无需旧密码
{
    "new_password": "new-s3cret-password"
}

成功响应:200

{
    "status_code": 200,
    "message": "密码修改成功"
}

错误:

状态码 触发条件 消息原文 前端建议动作
400 参数校验失败(user_id 非 uuid) zod 形状(2.1) 检查参数
404 用户不存在(含凭据记录缺失) 用户不存在 提示用户不存在
500 服务端异常 服务器发生内部错误 稍后重试

注意事项:成功后同样递增 tokenVersion,该用户所有已签发 token 立即失效,必须重新登录(见 3.4)。

8.5 POST /admin/user — 创建用户

⚠️ 假端点(2.3 第 4 条):通过 zod 校验后原样回显请求体(含明文密码),不创建任何用户。响应为 200 { name, email, password, role }。请勿接入,更不要在任何持久化日志中记录其响应。

8.6 GET /admin/clouds — 全状态云图列表

  • 认证:admin;可见任意状态与隐藏的云图

Query 参数(在分页参数 1.4 基础上):

参数 类型 必填 约束 说明
status enum 否 pending / approved / rejected 按审核状态过滤;不传返回全部状态

成功响应:200,CloudInfo 数组(字段见 1.5),按上传时间倒序。

错误:

状态码 触发条件 消息原文 前端建议动作
400 status 非法等参数错误 zod 形状(2.1) 检查参数
500 服务端异常 服务器发生内部错误 稍后重试

8.7 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.8 DELETE /admin/clouds/:cloud_id — 删除云图

  • 认证:admin

路径参数:cloud_id(uuid)。

成功响应:200

{
    "status_code": 200,
    "message": "删除成功"
}

错误:

状态码 触发条件 消息原文 前端建议动作
400 cloud_id 非 uuid zod 形状(2.1) 检查链接
404 云图不存在 图片不存在 提示云图不存在
500 服务端异常 服务器发生内部错误 稍后重试

注意事项:管理员删除不受审核状态或隐藏状态限制。删除会级联删除该云图的点赞记录,并尽力清理 MinIO 中的图片对象;清理失败不报错,可能残留孤儿对象(可接受)。删除后前端应立即刷新相关列表。


9. 端点 · 系统

9.1 ALL /health · 9.2 ALL /status

⚠️ 均未实现(占位端点,任意 HTTP 方法)。当前返回纯文本 404 Not Found。健康检查请直接探测任意已实现端点(如 GET /info/cloudtype)。