Mplan e75833ee4b 重构表结构:合并 profiles→users,展平 API 响应,新增计数器字段
- models: 删除 Profile 模型,字段合并到 User;User 新增云朵/公开/收藏计数器;Cloud 新增 favorite_count
- schemas/serializers: 展平 UserOut/AuthOut/MeOut/AdminUserOut,移除嵌套 profile
- deps: user.profile.role→user.role,移除 selectinload profile
- auth/register: 注册时直接设置 user 字段,不再创建 Profile
- clouds: 创建/删除云朵时同步 user.cloud_count 计数器,状态变化时同步 public_cloud_count
- favorites: 点赞/取消时同步 cloud.favorite_count 计数器
- admin: 审批/隐藏/批量删时同步 public_cloud_count 计数器
- profiles/stats: 用计数器替代实时 COUNT 查询
- alembic: 新增 migration 合并 profiles 到 users,初始化 counters
2026-07-28 23:48:57 +08:00
2026-07-18 20:32:51 +08:00
2026-07-18 20:32:51 +08:00
2026-07-18 20:32:51 +08:00
2026-07-18 20:32:51 +08:00
2026-07-18 20:32:51 +08:00
2026-07-18 20:32:51 +08:00
2026-07-18 20:32:51 +08:00

OpenCloud Backend

OpenCloud 的 FastAPI 后端,替代原项目中的 Supabase Database、Auth 和 Storage。后端提供邮箱密码认证、可撤销刷新会话、云图上传与缩略图、地图/画廊查询、图鉴解锁、个人主页和管理员审核。

技术栈

  • Python 3.13、FastAPI、Pydantic
  • PostgreSQL 17、SQLAlchemy 2(异步)、Alembic
  • Argon2 密码哈希、JWT Access Token、数据库 Refresh Session
  • 本地持久化图片目录、Pillow 图片验证和缩略图生成
  • SMTP 邮箱确认与密码重置
  • 依赖管理:uvpyproject.toml + uv.lock

项目结构

app/
  main.py              # FastAPI 入口、CORS、/media 静态挂载
  config.py            # 环境变量与生产校验
  database.py          # 异步引擎与会话
  models.py            # SQLAlchemy 模型
  schemas.py           # 请求/响应 Pydantic 模型
  security.py          # 密码哈希、JWT、Token 工具
  deps.py              # 依赖注入(当前用户、管理员)
  serializers.py       # ORM → 响应 DTO
  seed.py              # 云类型与初始管理员
  routers/
    auth.py            # 认证
    clouds.py          # 云图
    cloud_types.py     # 云类型
    collections.py     # 图鉴
    favorites.py       # 照片点赞收藏
    profiles.py        # 用户资料
    admin.py           # 管理后台
    health.py          # 健康检查
  services/
    email.py           # 邮件发送
    storage.py         # 图片校验、存储与删除
alembic/               # 数据库迁移
tests/                 # 按模块划分的 pytest 用例
data/uploads/          # 运行时图片目录(gitignore

本地启动

复制配置并生成开发密钥:

cp .env.example .env
openssl rand -hex 32

把生成值写入 .envSECRET_KEY,然后安装依赖:

uv sync

准备一个可访问的 PostgreSQL 数据库,并在 .env 中填写实际连接信息:

DATABASE_URL=postgresql+asyncpg://用户名:密码@主机:5432/数据库名

应用和 Alembic 都只从 .env 读取数据库连接,不需要在其他配置文件中重复填写。

创建表并写入十种云类型、可选初始管理员:

uv run alembic upgrade head
uv run python -m app.seed

启动开发服务器:

uv run uvicorn app.main:app --reload
  • APIhttp://localhost:8000/api/v1
  • Swaggerhttp://localhost:8000/docs
  • ReDochttp://localhost:8000/redoc
  • 健康检查:http://localhost:8000/api/v1/health
  • 媒体文件:http://localhost:8000/media/...

认证约定

登录成功后响应包含有效期较短的 access_token(默认 15 分钟)。前端在请求头中发送:

Authorization: Bearer <access_token>

Refresh Token 只保存在 HttpOnly Cookie 中(默认名 opencloud_refresh,有效期 30 天)。前端调用 /api/v1/auth/refresh/api/v1/auth/logout 时必须允许 Cookie

fetch(url, { credentials: 'include' })

开发环境默认 EMAIL_DELIVERY_MODE=console,邮箱确认和密码重置链接会输出到后端日志。如需真实发信,请在 .env 中配置 SMTP 参数并将 EMAIL_DELIVERY_MODE 改为 smtp

密码最短 8 位;注册后需完成邮箱确认才能登录。管理员接口需要 profile.role = admin 的 Access Token。

主要接口

精确的请求/响应字段以 Swagger/OpenAPI 为准。以下为当前路由清单与常用约定。

系统

  • GET /api/v1/health:健康检查

认证

方法 路径 说明
POST /api/v1/auth/register 注册
POST /api/v1/auth/resend-confirmation 重发确认邮件
POST /api/v1/auth/confirm-email 确认邮箱
POST /api/v1/auth/login 登录,返回 access token 并设置 refresh cookie
POST /api/v1/auth/refresh 用 refresh cookie 换取新 access token
POST /api/v1/auth/logout 注销当前会话
GET /api/v1/auth/me 当前用户与资料
POST /api/v1/auth/forgot-password 发起密码重置
POST /api/v1/auth/reset-password 凭 token 重置密码
PATCH /api/v1/auth/password 已登录用户修改密码

云图

方法 路径 说明
GET /api/v1/clouds 公开画廊分页;支持 pagepage_sizetype_idsearch
GET /api/v1/clouds/map 地图点位;必填 start/end,可选 time_fieldlimit
GET /api/v1/clouds/{id} 云图详情(未审核/隐藏仅本人或管理员可见)
POST /api/v1/clouds 上传云图(需登录)
PATCH /api/v1/clouds/{id} 更新元数据(本人)
DELETE /api/v1/clouds/{id} 删除(本人)
POST /api/v1/clouds/batch-delete 批量删除(本人)

上传使用 multipart/form-data,文件字段为 image;其他表单字段包括:

  • cloud_type_idcustom_cloud_type 二选一
  • latitude / longitude(成对出现,服务端保留两位小数)
  • location_namedescriptioncaptured_atis_hidden

服务端会验证格式(JPEG/PNG/WebP)、限制大小与像素、重编码原图并生成 JPEG 缩略图。新图片状态始终为 pendinguser_id 从 Access Token 获取。审核通过后,若使用标准云类型会自动解锁图鉴。

画廊 search 约定:

  • 普通关键词:匹配中英文云类型名、自定义类型名
  • @ 开头:按用户名模糊搜索

云类型与图鉴

方法 路径 说明
GET /api/v1/cloud-types 全部云类型
GET /api/v1/cloud-types/{id} 单个云类型
GET /api/v1/cloud-types/{id}/clouds 该类型下的公开云图分页
GET /api/v1/collections/me 当前用户已解锁图鉴(需登录)

seed 会写入 10 种标准云类型(积云、层云、卷云等),含中英文名、云族与稀有度。

点赞收藏

对照片的点赞/收藏(与云类型「图鉴」不同)。数据表 cloud_favorites(user_id, cloud_id) 唯一。

方法 路径 说明
GET /api/v1/clouds/{id}/favorite 查询喜欢状态(需登录)
PUT /api/v1/clouds/{id}/favorite 设置或取消收藏(需登录)
GET /api/v1/favorites/me 当前用户收藏列表分页(按收藏时间倒序)

查询喜欢状态(默认当前用户;管理员可传 user_id 查他人):

GET /api/v1/clouds/{id}/favorite
GET /api/v1/clouds/{id}/favorite?user_id=<uuid>

设置/取消请求体:

{ "favorited": true }

响应:

{
  "cloud_id": "…",
  "user_id": "…",
  "favorited": true,
  "favorite_count": 3
}

可见性与查看云图一致:公开已审核照片,或本人/管理员可见的待审、隐藏照片。重复收藏为幂等;删除用户或云图时收藏记录级联删除。普通用户只能查自己的喜欢状态。

用户资料

方法 路径 说明
GET /api/v1/profiles/me 当前用户的公开资料(无需知道 UUID)
GET /api/v1/profiles/{user_id} 任意用户的公开资料
PATCH /api/v1/profiles/me 修改自己的昵称(需登录)
POST /api/v1/profiles/me/avatar 上传/更换头像(需登录,multipart,自动缩放到 512px
DELETE /api/v1/profiles/me/avatar 删除头像(需登录)
GET /api/v1/profiles/me/clouds 当前用户云图分页(含 pending/rejected/hidden
GET /api/v1/profiles/{user_id}/clouds 用户云图分页;本人/管理员可见全部,他人仅公开已审核
GET /api/v1/profiles/me/stats 当前用户统计(上传数、公开上传数、收藏数)
GET /api/v1/profiles/{user_id}/stats 用户统计(本人或管理员可查)

ProfileStatsOut 字段:

  • user_idusernameemailcreated_at
  • cloud_count:该用户全部云图数
  • public_cloud_count:已审核且未隐藏的公开云图数
  • collection_count:已解锁图鉴数

管理后台

均需管理员 Access Token。

方法 路径 说明
GET /api/v1/admin/stats 全站统计(用户数、今日上传、审核状态分布等;“今日”按 Asia/Shanghai
GET /api/v1/admin/users 用户列表分页
POST /api/v1/admin/users 管理员创建用户(邮箱自动确认,可指定 role)
PATCH /api/v1/admin/users/{user_id} 修改角色或禁用状态(不能禁用/降权自己)
GET /api/v1/admin/clouds 全部云图;可按 statusis_hidden 过滤
PATCH /api/v1/admin/clouds/status 批量修改审核状态
PATCH /api/v1/admin/clouds/visibility 批量修改隐藏状态
POST /api/v1/admin/clouds/batch-delete 批量删除云图与本地文件

环境变量

所有可调配置均可在 .env.example 中找到。首次运行先复制该文件:

cp .env.example .env

其中关键配置包括:

变量 说明
DATABASE_URL 异步 SQLAlchemy 连接串,PostgreSQL 使用 postgresql+asyncpg://
SECRET_KEY JWT 签名密钥;生产环境至少 32 个字符,勿使用示例值
ENVIRONMENT development / test / production
FRONTEND_URL 认证邮件中的前端地址
PUBLIC_BASE_URL API 和图片公开地址
CORS_ORIGINS 逗号分隔的前端来源
UPLOAD_DIR 持久化图片目录
MAX_UPLOAD_BYTES 上传大小上限(默认 20MB
EMAIL_DELIVERY_MODE consolesmtp
ADMIN_EMAIL / ADMIN_PASSWORD 执行 seed 时可选创建管理员
COOKIE_SECURE / COOKIE_SAMESITE Refresh Cookie 安全属性

生产环境还必须设置 ENVIRONMENT=productionCOOKIE_SECURE=true,并通过 HTTPS 访问。

数据库迁移(Alembic

Alembic 用于管理数据库结构版本,作用类似于数据库结构的 Git。它会记录建表、增加字段、创建索引等变更,使不同环境中的数据库结构与代码保持一致,但不会负责启动 PostgreSQL 服务。

项目中的相关文件:

  • app/models.pySQLAlchemy 数据模型,也是当前期望的数据库结构
  • alembic/versions/:按版本保存数据库迁移脚本
  • alembic/env.py:加载模型,并通过 .env 获取 DATABASE_URL
  • alembic.ini:Alembic 的基础配置,不保存数据库账号或密码

首次初始化数据库或拉取到新的迁移后,升级到最新版本:

uv run alembic upgrade head

修改 SQLAlchemy 模型后,先生成迁移脚本并检查生成内容,再执行升级:

uv run alembic revision --autogenerate -m "describe change"
uv run alembic upgrade head

查看当前数据库版本和迁移历史:

uv run alembic current
uv run alembic history

回退最近一次迁移:

uv run alembic downgrade -1

回退操作可能导致字段或数据被删除,执行前应先检查迁移脚本并备份数据库。

应用使用 SQLAlchemy 连接池,所有外键和主要画廊、地图、个人主页、审核查询均有对应索引。应用数据库账号不应使用 PostgreSQL 超级用户。

验证

uv run pytest -q
uv run python -m compileall -q app main.py
uv run alembic upgrade head --sql

测试使用临时 SQLite 与上传目录,按模块覆盖:

  • tests/test_auth.py:注册、邮箱确认、登录、刷新、登出、改密
  • tests/test_clouds.py:上传、缩略图、画廊、地图、更新与删除
  • tests/test_collections.py:图鉴解锁
  • tests/test_favorites.py:照片点赞收藏与取消、收藏列表
  • tests/test_cloud_types.py:云类型查询
  • tests/test_profiles.py:资料、头像、用户云图、用户统计
  • tests/test_admin.py:统计、用户管理(含创建)、审核与批量操作

生产环境仍建议在真实 PostgreSQL 上再跑一遍迁移与关键路径。

部署注意事项

  • data/uploads 必须挂载到持久化磁盘并纳入备份。
  • PostgreSQL 和上传目录需要分别制定备份与恢复方案。
  • 单机部署可由 FastAPI 提供 /media;高流量部署建议由 Nginx/Caddy 直接服务该目录。
  • 多实例部署时,上传目录需共享存储或改为对象存储;当前实现为本地文件系统。
  • is_hidden 与原 Supabase 公共 bucket 行为一致,只阻止页面发现,知道图片 URL 的人仍可直接访问。若需要真正私密图片,应改为鉴权下载或短期签名 URL。
  • 生产环境请使用独立的非超级用户数据库账号,并轮换 SECRET_KEY 与管理员密码。
S
Description
No description provided
Readme
240 KiB
Languages
Python 99.7%
Mako 0.3%