Files
chunyu_project/docs/ARCHITECTURE.md
T

178 lines
12 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.
# 可乐平台 异步化架构(ADRF + Granian + PostgreSQL + Nginx)
## 架构总览
```
┌──────────┐ :8080 ┌─────────────────────┐
│ 浏览器 │──────────▶│ Nginx (frontend) │
└──────────┘ │ ├─ / → React SPA 静态产物 (build/, 动静分离)
│ ├─ /api 等前缀 ─┐
│ └─ /ws/ ─┤ 反向代理
└──────────────────┼───┘
▼
┌───────────────────────────────┐
│ Granian (Rust) :8000 │
│ Django 5.2 ASGI + ADRF │
│ ├─ 异步视图 (async def) │
│ ├─ 异步 ORM (aget/afilter) │
│ └─ Channels (WebSocket) │
└───────┬──────────────┬────────┘
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PostgreSQL 16 │ │ Redis 7 │
│ psycopg3 异步 │ │ cache/session│
│ ORM 驱动 │ │ celery/ws层 │
└──────────────┘ └──────────────┘
```
## 技术选型
| 层 | 技术 | 说明 |
|---|---|---|
| Web 框架 | Django 5.2 + DRF 3.17 | 原生异步能力 |
| 异步视图 | **ADRF 0.1.14** | 所有 API 视图跑在异步事件循环上 |
| 应用服务器 | **Granian 2.8.2 (Rust)** | 替代 Daphne,ASGI + WebSocket,吞吐量显著提升 |
| 数据库 | **PostgreSQL 16** | Django 5 异步 ORM 必备(MySQL 后端不支持异步) |
| DB 驱动 | **psycopg3 (binary) 3.2.13** | 支持异步连接 |
| 缓存/队列 | Redis 7 | 缓存、Session、Celery broker、Channels layer |
| 前端 | React 19 + Vite 8 | 独立 SPA,与 Django 模板层完全解耦 |
| 反代/静态 | Nginx | SPA 静态资源直出 + API 反向代理 |
## 关键设计决策
1. **全链路异步**:视图 `async def` → ORM `aget/afirst/aexists/acount/acreate/asave/aupdate/adelete` → PostgreSQL(psycopg3)。`CONN_MAX_AGE=0`(异步 ORM 不支持持久连接)。
2. **ADRF 兼容性**:`view_is_async` 自动检测处理器;纯 CPU 视图(图形验证码、图像压缩、文本 diff)保持同步 `def`,ADRF 自动 `sync_to_async` 线程池兜底。
3. **外部 HTTP 调用 aiohttp 化**:weather / air_quality / currency / getIpData 全部改为 `aiohttp.ClientSession`,无阻塞 I/O。
4. **Redis 缓存 / Celery / SMTP / 密码哈希**:同步客户端用 `sync_to_async` 包裹(不阻塞事件循环)。
5. **事务兜底**:`transaction.atomic` + `select_for_update` 段(钱包、任务领取)抽成同步闭包整体 `sync_to_async` 执行,保住行锁语义。
6. **数据库可切换**:`DB_ENGINE` 环境变量支持 `django.db.backends.postgresql`(默认)与 `django.db.backends.mysql`(本地过渡)。
## 前后端分离
- 开发:`pnpm dev`(Vite HMR :5173),`BACKEND_URL` 环境变量指定后端(默认 `http://127.0.0.1:8002`,Docker 后端用 `http://127.0.0.1:8000`)。API 全部走 Vite proxy,无 CORS 负担。
- 生产:`pnpm build` 产物由 Nginx 容器直出(`try_files → index.html` SPA 回退),带指纹资源 1 年 immutable 缓存,index.html no-cache。
- 前后端仅通过 RESTful API + WebSocket 交互。
## 快速开始
### 一键部署(生产形态)
```bash
docker compose up -d --build
# 前端 http://localhost:8080
# 后端 API http://localhost:8000
# Swagger http://localhost:8080/swagger/
```
### 本地开发
```bash
# 1. 启动数据层
docker compose up -d postgres redis
# 2. 后端(异步栈)
cd chunyu_project
export DB_ENGINE=django.db.backends.postgresql DB_HOST=localhost DB_PORT=5432 \
DB_NAME=chunyu DB_USER=chunyu DB_PASSWORD=chunyu_pg_pass \
REDIS_HOST=localhost
python manage.py migrate
granian --interface asgi --host 0.0.0.0 --port 8000 chunyu_project.asgi:application
# 3. 前端(Vite HMR)
cd chunyu_project_react
BACKEND_URL=http://127.0.0.1:8000 pnpm dev # http://localhost:5173
```
## 异步化转换统计
### Phase 1:视图层全异步(ADRF 事件循环)
- 覆盖 16 个应用、45 个视图文件:`api` `user` `article` `learn` `chat` `message` `history` `search` `bug` `tool` `weather` `air_quality` `currency` `shorturl` `logs` `apidirectory` `app`
- 处理器全部 `async def`(约 90+ 个);ORM 调用全部异步化
- 转换规范见 `chunyu_project/docs/ADRF_CONVERSION_GUIDE.md`
### Phase 2:serializer 层原生异步(100% 异步化收口)
- **10 个 serializer 文件**全部迁移至 `adrf.serializers`(原生异步 ModelSerializer/Serializer):
`article` `learn` `bug` `chat` `message` `history` `tool` `apidirectory` `user/serializers/user_serializers` `user/serializers/region_serializers`
- 全部 `SerializerMethodField` 的 getter 改为 `async def` + 异步 ORM(含嵌套外键改 id 访问/`select_related` 消懒加载)
- 写路径 `create/update` → `acreate/aupdate`,视图统一 `await serializer.asave(**kwargs)`(adrf 原生,kwargs 合入 validated_data)
- 序列化输出统一 `await serializer.adata`(替代同步 `.data` 与 `sync_to_async` 兜底)
- **async_cache 基建**(`utils/async_cache.py`):`redis.asyncio` 直连 + msgpack 序列化 + 同步缓存降级,验证码/缓存场景(注册/登录/改密/邮箱/手机号/IP 定位)全部切换
- 外键懒加载治理:黑名单列表 `select_related('blocked_user')`、聊天消息 `select_related('sender')`、评论父作者异步加载等
### 终态核验(服务器实跑 grep)
- serializer 层裸同步 ORM getter:**0**
- 视图层 `serializer.data` 同步残留:**0**
- `sync_to_async(serializer…)` 残留:仅 8 处 `is_valid`(**设计边界**:Django validate 钩子同步调用,且 user 系列校验器含 DB 查询/argon2 哈希)+ 已清零的 save
- `async def` 总量:**310+**
- 剩余 `sync_to_async`(约 160 处)全部为**无异步 API 的库**:SMTP/Celery `.delay`/密码哈希/文件存储/DRF 分页器求值/`transaction.atomic` 事务段(Django 5.2 限制,行锁语义必须同步闭包)
## 验证记录
- ✅ `python manage.py check` 0 错误
- ✅ URLCONF 全量导入成功(所有视图模块可加载)
- ✅ 205 个 py 文件 py_compile 0 失败
- ✅ Granian 启动 + 冒烟:`/`、`/article/articles/`、`/learn/courses/`、`/api/apidirectory/categories/`、`/api/weather/?city=Beijing`、`/api/air-quality/?city=Shanghai`、`/api/currency/rates/?base=USD`、`/api/image-captcha/`、`/api/captcha/generate/`、`/swagger.json`(331KB) 全部 200
- ✅ `docker compose config` 校验通过
- ✅ Nginx 配置语法 `nginx -t` 通过
- ✅ Phase-2 服务器回归:7/9 端点组全绿、写路径(短链/评论)异步落库、并发 240/240
- ✅ Phase-3 全链路深度回归:公开 11/11 + 认证 6/6(JWT 登录/钱包/黑名单/短链/登录记录/任务)+ 并发 240/240,评论"创建→异步序列化→PG 持久化"全链路打通
### Phase 3:全链路深度测试暴露并修复的原有 Bug(非异步化回归,均已确认在改造前就存在)
1. `ArticleCommentSerializer.created_at` 缺字段级 `read_only=True` —— DRF/adrf 的 `Meta.read_only_fields` 对显式声明字段无效,导致**评论接口一直无法用**(前端只发 content/parent)
2. `@method_decorator(never_cache, name='dispatch')` 同步包装器在 adrf 协程上崩溃(wallet/login_record/tasks)—— 新增 `utils/async_decorators.py::async_never_cache_dispatch`(兼容同步/异步 dispatch)
3. `FUser` 没有 `nickname` 字段 —— article 通知/message sender_name/bug user_name 共 6 处改用 `first_name`(前两处一直被 Bug1 掩盖,从未执行到)
> 教训:这三个 Bug 层层叠加掩盖——Bug1 让评论接口 400(走不到后面),Bug3 在 Bug1 修复后才暴露;never_cache 502 则在无认证流量时不可见。深度回归必须带认证流量。
## 服务器部署(192.168.5.7 生产/联调环境)
本地 Windows 宿主虚拟化暂时不可用(Hypervisor 未加载,见下),已将整套异步架构部署至本地 Linux 服务器:
| 项 | 值 |
|---|---|
| 服务器 | Debian,16 核(`lan-19216857` SSH profile) |
| 部署目录 | `/mnt/sda/chunyu` |
| 访问地址 | `http://192.168.5.7:18080`(前端 SPA + API 反代) |
| 后端直连 | `http://192.168.5.7:18000`(Granian) |
| PostgreSQL | `192.168.5.7:15432`(容器内 5432) |
| Redis | `192.168.5.7:16379`(容器内 6379) |
端口重映射原因:服务器既有服务占用 5432/6379/8080,覆盖文件 `docker-compose.server.yml` 将宿主端口改为 15432/16379/18000/18080,容器间通信仍走内部网络不受影响。
部署命令:
```bash
cd /mnt/sda/chunyu
docker compose -f docker-compose.yml -f docker-compose.server.yml up -d --build
```
服务器端到端验证记录(全部通过):
- ✅ 四容器运行:chunyu-backend(healthy) / chunyu-frontend / chunyu-postgres(healthy) / chunyu-redis(healthy)
- ✅ SPA 静态资源 Nginx 直出 200;`/api/apidirectory/categories/`、`/article/articles/`、`/swagger.json` 经反代 200
- ✅ aiohttp 外部 API(天气)经反代 200
- ✅ 异步 ORM 写路径:短链创建成功落库(PostgreSQL 66 张迁移表)
- ✅ Redis PONG(缓存/Session 层)
- ✅ WebSocket:Granian + Channels,JWT 认证后 accept 成功(匿名拒绝为原有业务设计,ChatConsumer 强制认证)
- ✅ 并发冒烟:80 并发 × 5 轮 = 400 请求,**400/400 成功,187.9 RPS**(穿透 Nginx → Granian → PostgreSQL 全链路)
- ✅ Windows 局域网访问 `http://192.168.5.7:18080/*` 全部 200
> 说明:`DJANGO_DEBUG` 插值默认 True(局域网无 TLS;False 会触发 `SECURE_SSL_REDIRECT` 301)。正式上 TLS 后设 `DJANGO_DEBUG=False`。
> 本地 Windows 宿主:2026-09-05 22:22 重启后 HypervisorPresent=False(BCD hypervisorlaunchtype 被关),Docker Desktop 不可用;管理员执行 `bcdedit /set "{current}" hypervisorlaunchtype auto` 重启后可恢复本地 Docker。
## 环境变量
见 `docker.env.example`。核心项:
| 变量 | 默认 | 说明 |
|---|---|---|
| `DB_ENGINE` | postgresql | django.db.backends.postgresql / mysql |
| `DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD` | postgres/5432/chunyu/chunyu | 数据库连接 |
| `REDIS_HOST/REDIS_PORT/REDIS_PASSWORD` | redis/6379 | Redis 连接 |
| `POSTGRES_PASSWORD` | chunyu_pg_pass | PostgreSQL root 密码 |
| `DJANGO_DEBUG` | False | 生产必须 False |
| `CORS_ALLOWED_ORIGINS` | localhost:8080/5173 | 前端来源 |
## 已知限制
1. **serializer 层仍有同步 ORM**(经 sync_to_async 兜底,功能正确但该段走线程池):`chat/serializers.py` ConversationSerializer 的 MethodField、`bug` 列表 get_images_count、若干 `serializer.create()`。后续可对 serializer 原生异步化。
2. **Celery worker 不在默认编排内**(views 已适配:同步调用失败自动降级 submit_task 异步队列)。如需启用:`docker compose up -d` 后手动 `docker compose run backend celery -A chunyu_project worker -l info`。
3. Channels ASGI Lifespan 在 Granian 下有 warning(`asginl` 接口可消除),不影响 WebSocket 功能。
4. 纯 CPU 视图(验证码/压缩/diff)保持同步,ADRF 自动线程池兜底。