# 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 邮箱确认与密码重置 - 依赖管理:`uv`(`pyproject.toml` + `uv.lock`) ## 项目结构 ```text 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) ``` ## 本地启动 复制配置并生成开发密钥: ```bash cp .env.example .env openssl rand -hex 32 ``` 把生成值写入 `.env` 的 `SECRET_KEY`,然后安装依赖: ```bash uv sync ``` 准备一个可访问的 PostgreSQL 数据库,并在 `.env` 中填写实际连接信息: ```dotenv DATABASE_URL=postgresql+asyncpg://用户名:密码@主机:5432/数据库名 ``` 应用和 Alembic 都只从 `.env` 读取数据库连接,不需要在其他配置文件中重复填写。 创建表并写入十种云类型、可选初始管理员: ```bash uv run alembic upgrade head uv run python -m app.seed ``` 启动开发服务器: ```bash uv run uvicorn app.main:app --reload ``` - API:`http://localhost:8000/api/v1` - Swagger:`http://localhost:8000/docs` - ReDoc:`http://localhost:8000/redoc` - 健康检查:`http://localhost:8000/api/v1/health` - 媒体文件:`http://localhost:8000/media/...` ## 认证约定 登录成功后响应包含有效期较短的 `access_token`(默认 15 分钟)。前端在请求头中发送: ```text Authorization: Bearer ``` Refresh Token 只保存在 HttpOnly Cookie 中(默认名 `opencloud_refresh`,有效期 30 天)。前端调用 `/api/v1/auth/refresh` 和 `/api/v1/auth/logout` 时必须允许 Cookie: ```ts 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` | 公开画廊分页;支持 `page`、`page_size`、`type_id`、`search` | | `GET` | `/api/v1/clouds/map` | 地图点位;必填 `start`/`end`,可选 `time_field`、`limit` | | `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_id` 与 `custom_cloud_type` 二选一 - `latitude` / `longitude`(成对出现,服务端保留两位小数) - `location_name`、`description`、`captured_at`、`is_hidden` 服务端会验证格式(JPEG/PNG/WebP)、限制大小与像素、重编码原图并生成 JPEG 缩略图。新图片状态始终为 `pending`,`user_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` 查他人): ```http GET /api/v1/clouds/{id}/favorite GET /api/v1/clouds/{id}/favorite?user_id= ``` 设置/取消请求体: ```json { "favorited": true } ``` 响应: ```json { "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_id`、`username`、`email`、`created_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` | 全部云图;可按 `status`、`is_hidden` 过滤 | | `PATCH` | `/api/v1/admin/clouds/status` | 批量修改审核状态 | | `PATCH` | `/api/v1/admin/clouds/visibility` | 批量修改隐藏状态 | | `POST` | `/api/v1/admin/clouds/batch-delete` | 批量删除云图与本地文件 | ## 环境变量 所有可调配置均可在 `.env.example` 中找到。首次运行先复制该文件: ```bash 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` | `console` 或 `smtp` | | `ADMIN_EMAIL` / `ADMIN_PASSWORD` | 执行 seed 时可选创建管理员 | | `COOKIE_SECURE` / `COOKIE_SAMESITE` | Refresh Cookie 安全属性 | 生产环境还必须设置 `ENVIRONMENT=production`、`COOKIE_SECURE=true`,并通过 HTTPS 访问。 ## 数据库迁移(Alembic) Alembic 用于管理数据库结构版本,作用类似于数据库结构的 Git。它会记录建表、增加字段、创建索引等变更,使不同环境中的数据库结构与代码保持一致,但不会负责启动 PostgreSQL 服务。 项目中的相关文件: - `app/models.py`:SQLAlchemy 数据模型,也是当前期望的数据库结构 - `alembic/versions/`:按版本保存数据库迁移脚本 - `alembic/env.py`:加载模型,并通过 `.env` 获取 `DATABASE_URL` - `alembic.ini`:Alembic 的基础配置,不保存数据库账号或密码 首次初始化数据库或拉取到新的迁移后,升级到最新版本: ```bash uv run alembic upgrade head ``` 修改 SQLAlchemy 模型后,先生成迁移脚本并检查生成内容,再执行升级: ```bash uv run alembic revision --autogenerate -m "describe change" uv run alembic upgrade head ``` 查看当前数据库版本和迁移历史: ```bash uv run alembic current uv run alembic history ``` 回退最近一次迁移: ```bash uv run alembic downgrade -1 ``` 回退操作可能导致字段或数据被删除,执行前应先检查迁移脚本并备份数据库。 应用使用 SQLAlchemy 连接池,所有外键和主要画廊、地图、个人主页、审核查询均有对应索引。应用数据库账号不应使用 PostgreSQL 超级用户。 ## 验证 ```bash 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` 与管理员密码。