Files
opencloud-backend/README.md
T

330 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <access_token>
```
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=<uuid>
```
设置/取消请求体:
```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` 与管理员密码。