Files
GX-gp-notify/docs/superpowers/specs/2026-05-09-fastapi-docker-migration-design.md
T

391 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.
# FastAPI + Docker 迁移设计文档
**日期**: 2026-05-09
**版本**: 1.0
**状态**: 设计阶段
---
## 1. 背景与目标
将现有的广西政府采购网公告监控系统从 Flask CLI 架构迁移到 FastAPI + Docker 部署方案,同时提升代码质量、安全性和可维护性。
### 核心目标
- FastAPI 替代 Flask 回调服务器 + argparse CLI,统一为一个 Web 应用
- Docker 单容器部署,连接现有 PostgreSQL
- SQLAlchemy ORM 替代原始 psycopg2 操作
- APScheduler 内置定时任务
- 环境变量管理配置,敏感信息不提交 git
- 异步改造(httpx + asyncpg
---
## 2. 新项目结构
```
gx-gp-notify/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 应用入口 + lifespan
│ ├── config.py # Pydantic Settings (环境变量驱动)
│ ├── api/
│ │ ├── __init__.py
│ │ ├── router.py # 统一路由注册
│ │ ├── deps.py # 依赖注入 (get_db, get_scheduler)
│ │ ├── announcements.py # 公告 CRUD 端点
│ │ ├── crawl.py # 爬取触发端点
│ │ └── wechat.py # 企业微信回调端点
│ ├── models/
│ │ ├── __init__.py
│ │ ├── announcement.py # SQLAlchemy 模型
│ │ └── schemas.py # Pydantic 请求/响应模型
│ ├── crawler/
│ │ ├── __init__.py
│ │ ├── spider.py # 爬虫核心
│ │ ├── dahuagov_spider.py # 大化县专用爬虫
│ │ └── parsers.py # 数据解析器
│ ├── services/
│ │ ├── __init__.py
│ │ ├── crawl_service.py # 爬取业务流程
│ │ ├── filter_service.py # 筛选逻辑
│ │ └── notification_service.py # 通知服务
│ ├── wechat/
│ │ ├── __init__.py
│ │ ├── handler.py # 消息处理器
│ │ ├── menu.py # 菜单管理
│ │ ├── crypto.py # WXBizMsgCrypt 加解密
│ │ └── client.py # 企业微信 API 客户端
│ └── scheduler/
│ ├── __init__.py
│ └── jobs.py # APScheduler 定时任务
├── tests/
│ ├── __init__.py
│ ├── conftest.py # pytest fixtures
│ ├── test_crawler/ # 爬虫解析器测试
│ ├── test_api/ # API 端点测试
│ └── test_services/ # 业务逻辑测试
├── alembic/ # 数据库迁移
│ └── versions/
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── pyproject.toml # 项目元数据 + 依赖 + 工具配置
├── .env.example # 环境变量模板
├── .gitignore
└── logs/ # 日志目录 (gitignore)
```
---
## 3. API 端点设计
### 3.1 公告查询
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/api/v1/announcements` | 查询公告列表(分页、来源筛选、日期筛选、关键词搜索) |
| `GET` | `/api/v1/announcements/{id}` | 获取单条公告详情 |
| `GET` | `/api/v1/announcements/today` | 获取今日公告 |
| `GET` | `/api/v1/announcements/stats` | 获取公告统计信息 |
### 3.2 爬取控制
| 方法 | 路径 | 说明 |
|------|------|------|
| `POST` | `/api/v1/crawl/trigger` | 手动触发一次爬取(支持关键词/来源参数) |
| `GET` | `/api/v1/crawl/status` | 查看最近爬取状态 |
| `GET` | `/api/v1/crawl/sources` | 获取已配置的公告来源列表 |
### 3.3 定时任务
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/api/v1/scheduler/jobs` | 查看活跃的定时任务 |
| `POST` | `/api/v1/scheduler/pause/{job_id}` | 暂停某个定时任务 |
| `POST` | `/api/v1/scheduler/resume/{job_id}` | 恢复某个定时任务 |
### 3.4 企业微信
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` `POST` | `/api/v1/wechat/callback` | 企业微信回调入口(URL验证 + 消息接收) |
### 3.5 系统
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/health` | 健康检查(包含数据库连通性) |
### 3.6 通用特性
- FastAPI 自动生成 Swagger UI (`/docs`) 和 ReDoc (`/redoc`)
- 统一分页格式:`{ total, page, page_size, items }`
- 统一错误响应:`{ detail: "error message" }`
- 依赖注入:数据库 session、配置、调度器均通过 FastAPI `Depends()` 注入
- CORS 默认关闭,通过 `CORS_ORIGINS` 环境变量可选开启
---
## 4. 数据模型
### 4.1 公告表 (announcements)
合并现有的 `announcements``auto_announcements``manual_announcements` 三张表为一张,通过 `crawl_mode` 字段区分自动/手动。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Integer | 主键 |
| title | String(500) | 公告标题 |
| publish_date | DateTime | 发布时间 |
| purchase_name | String(200) | 发布单位 |
| content_url | Text | 内容链接 |
| source_code | String(50) | 来源代码 |
| source_name | String(100) | 来源名称 |
| announcement_type | String(50) | 公告类型枚举值 |
| content_hash | String(64) | SHA256 内容哈希,UNIQUE |
| crawl_mode | String(20) | "auto" / "manual" |
| is_new | Boolean | 是否新公告 |
| is_today | Boolean | 是否今日公告 |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 更新时间 |
### 4.2 大化县公告表 (dahuagov_announcements)
保留独立表,因为推送逻辑不同(全部推送,不筛选关键词)。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Integer | 主键 |
| title | String(500) | 公告标题 |
| publish_date | DateTime | 发布时间 |
| purchase_name | String(200) | 发布单位 |
| content_url | Text | 内容链接 |
| source_code | String(50) | 默认 "dahuagov" |
| source_name | String(100) | 默认 "大化县政府网采购公告" |
| content_hash | String(64) | SHA256 内容哈希,UNIQUE |
| is_new | Boolean | 是否新公告(未推送) |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 更新时间 |
### 4.3 变更说明
- `content_hash` 从 MD5 (32位) 改为 SHA256 (64位)
- 去掉 `announcement_sources` 表,来源信息从环境变量配置动态读取
- 去掉 `crawl_results` 表,爬取结果通过日志记录
- 去掉多余的索引表(keyword_matched / date_filtered 字段),筛选在应用层处理
- 使用 Alembic 管理数据库迁移
---
## 5. 配置管理
### 5.1 pydantic-settings
所有配置通过 `pydantic-settings``BaseSettings` 加载,来源优先级:环境变量 > `.env` 文件 > 默认值。
### 5.2 环境变量
```bash
# 应用
DEBUG=false
LOG_LEVEL=INFO
# 数据库(连接现有 PostgreSQL
DATABASE_URL=postgresql+asyncpg://gx-gp-notify:password@10.10.10.14:5432/gx-gp-notify
# 爬虫
CRAWLER_BASE_URL=https://zfcg.gxzf.gov.cn
CRAWLER_KEYWORDS=["大化"]
CRAWLER_MAX_PAGES=10
CRAWLER_TIMEOUT=30
# 企业微信
WECHAT_ENABLED=true
WECHAT_CORP_ID=ww69e8e44636f47780
WECHAT_AGENT_ID=1000007
WECHAT_SECRET=<secret>
WECHAT_TOKEN=<token>
WECHAT_ENCODING_AES_KEY=<aes_key>
# 定时任务
SCHEDULER_ENABLED=true
SCHEDULER_CRON=0 8,14,18 * * *
# Markdown 输出
MARKDOWN_OUTPUT_FILE=onu.md
```
### 5.3 安全措施
- `.env` 文件加入 `.gitignore`
- `config.yaml` 中的敏感信息不再提交到仓库
- 使用 SHA256 替代 MD5 做内容哈希
- 建议清理 git 历史中的敏感信息
---
## 6. Docker 部署
### 6.1 架构
```
docker-compose.yml
┌─────────────────────────────┐
│ app (FastAPI) │
│ - uvicorn 服务器 │
│ - APScheduler (内置) │
│ - 端口 8000 │
│ - 挂载: ./logs │
└──────────┬──────────────────┘
│ TCP 连接
┌──────────▼──────────────────┐
│ 外部 PostgreSQL (现有) │
│ 10.10.10.14:5432 │
└─────────────────────────────┘
```
### 6.2 Dockerfile
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir .
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### 6.3 docker-compose.yml
```yaml
services:
app:
build: .
ports:
- "8000:8000"
env_file:
- .env
volumes:
- ./logs:/app/logs
restart: unless-stopped
```
### 6.4 启动步骤
```bash
cp .env.example .env # 编辑 .env 填入真实配置
docker compose up -d # 启动服务
docker compose logs -f app # 查看日志
```
---
## 7. 技术栈
| 组件 | 当前 | 迁移后 |
|------|------|--------|
| Web 框架 | Flask (仅回调用) | FastAPI |
| CLI | argparse | 不再需要,API 替代 |
| 数据库驱动 | psycopg2 (同步) | asyncpg (异步) + SQLAlchemy 2.0 |
| HTTP 客户端 | requests (同步) | httpx (异步) |
| 定时任务 | schedule 库 | APScheduler |
| 配置 | PyYAML + config.yaml | pydantic-settings + .env |
| 加密 | pycryptodome + cryptography | 保留 |
| 部署 | 裸机 + uWSGI | Docker + uvicorn |
| 代码检查 | 无 | ruff |
| 测试 | 无 | pytest + httpx |
| 迁移 | 手动 CREATE TABLE | Alembic |
---
## 8. 额外改进
### 8.1 异步改造
- 爬虫网络请求:`httpx.AsyncClient` 替代 `requests`
- 数据库:`asyncpg` + SQLAlchemy async engine
- 通知发送:`httpx.AsyncClient`
- API 端点使用 `async def`
### 8.2 自动化测试
- 爬虫解析器单元测试(Mock 网页响应,验证解析逻辑)
- 筛选逻辑单元测试
- API 端点集成测试(用 `httpx.AsyncClient` + 测试数据库)
- pytest + pytest-asyncio + pytest-cov
### 8.3 日志改进
- 结构化日志(JSON 格式)
- Docker 环境下输出到 stdout
- 保留文件日志供宿主机查看(挂载 `./logs` 目录)
### 8.4 错误处理
- 统一异常处理中间件
- 健康检查 `/health` 含数据库连通性验证
- 关键操作结构化日志记录
### 8.5 代码质量
- `ruff` 替代 flake8 + black
- `pyproject.toml` 统一管理依赖和工具配置
- 全面 Type Hints
### 8.6 .gitignore
```gitignore
.env
logs/
*.log
__pycache__/
*.pyc
.venv/
.ruff_cache/
.pytest_cache/
*.egg-info/
```
---
## 9. 迁移策略
### 分步执行
1. 创建新项目结构骨架(FastAPI app + config
2. 数据库模型 + Alembic 迁移
3. 迁移爬虫模块(异步改造)
4. 迁移服务层(筛选、通知、Markdown)
5. 迁移 WeChat 回调(Flask → FastAPI route
6. 添加 APScheduler 定时任务
7. 编写测试
8. Docker 化
9. 清理旧文件
### 向后兼容
- 保留 `dahuagov_announcements` 表结构不变
- 企业微信回调 URL 路径保持不变 (`/api/v1/wechat/callback`)
- 数据库迁移使用 Alembic,不丢失现有数据
---
## 10. 未包含的内容
- 不做多用户认证/授权(个人使用)
- 不做 Celery 分布式任务(个人使用,APScheduler 足够)
- 不做 Redis 缓存
- 不做前端 UI
---
## 11. 风险与缓解
| 风险 | 缓解 |
|------|------|
| 数据库迁移丢失数据 | Alembic 自动生成迁移脚本,先在测试环境验证 |
| 异步爬虫被目标站限流 | 保留现有延迟/重试机制,httpx 支持同样的超时配置 |
| 企业微信回调兼容性 | 回调路径不变,加解密逻辑完全复用现有代码 |
| Docker 网络访问 10.10.10.14 | 确认 Docker 宿主机可访问该 IP,必要时用 `host.docker.internal` |