Files
nanocode/personal-blog/docs/02-backend-design.md
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

396 lines
11 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.
# Personal Blog - 后端设计文档
> FastAPI + SQLAlchemy + SQLite 架构
---
## 1. 应用入口 (app.py)
### 1.1 启动流程
```
1. 创建 FastAPI 实例,配置 CORS
2. 注册静态文件服务 (uploads/)
3. 包含所有路由模块
4. 启动时自动创建数据库表 (create_all)
```
### 1.2 CORS 配置
```python
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 开发环境允许全部
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
### 1.3 路由注册
| 路由前缀 | 文件 | 说明 |
|----------|------|------|
| `/api/auth` | `routers/auth.py` | 登录、Token、管理员创建 |
| `/api/posts` | `routers/posts.py` | 文章 CRUD、搜索、统计 |
| `/api/categories` | `routers/categories.py` | 分类 CRUD |
| `/api/tags` | `routers/tags.py` | 标签 CRUD |
| `/api/images` | `routers/images.py` | 图片上传/删除 |
| `/api/settings` | `routers/settings.py` | 系统设置、自定义页面 |
| `/api/crawl` | `routers/crawl.py` | 通用网页采集 |
| `/api/wechat-crawl` | `routers/wechat_crawl.py` | 公众号文章采集 |
| `/uploads` | `app.py` | 静态图片文件服务 |
| `/` | `app.py` | 静态文件 (index.html 等) |
---
## 2. 数据库模型 (models.py)
### 2.1 ER 关系图
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Post │ │ Category │ │ Tag │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ id (PK) │──────►│ id (PK) │ │ id (PK) │
│ title │ N:1 │ name │ │ name │
│ slug (UQ) │ │ description │ └──────────────┘
│ content │ └──────────────┘ ▲
│ summary │ │
│ cover_image │ ┌──────────────┐ ┌──────┴──────┐
│ is_published │ │ post_tags │ │ post_tags │
│ is_draft │◄──────┤ post_id (FK) │ │ tag_id (FK) │
│ created_at │ N:M │ tag_id (FK) │ └─────────────┘
│ updated_at │ └──────────────┘
│ view_count │
│ category_id │ ┌──────────────┐
│ (FK) │ │ Setting │
└──────────────┘ ├──────────────┤
│ id (PK) │
│ site_name │
│ site_desc │
│ ... │
└──────────────┘
┌──────────────┐ ┌──────────────┐
│ Admin │ │ Page │
├──────────────┤ ├──────────────┤
│ id (PK) │ │ id (PK) │
│ username(UQ) │ │ title │
│ password_hash│ │ slug (UQ) │
│ created_at │ │ content │
└──────────────┘ │ is_nav │
│ nav_order │
└──────────────┘
```
### 2.2 模型详解
#### Post (文章)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Integer, PK | 主键自增 |
| `title` | String(200) | 文章标题 |
| `slug` | String(200), UQ | URL 标识,如 `hello-world` |
| `content` | Text | Markdown 格式正文 |
| `summary` | Text | 文章摘要 |
| `cover_image` | String(500) | 封面图 URL(可为空) |
| `is_published` | Boolean | 是否已发布 |
| `is_draft` | Boolean | 是否草稿 |
| `created_at` | DateTime | 创建时间 |
| `updated_at` | DateTime | 更新时间 |
| `view_count` | Integer | 浏览次数 |
| `category_id` | Integer, FK | 所属分类(可为空) |
- `is_published` 和 `is_draft` 互斥:发布后 `is_published=True, is_draft=False`
- 状态组合:
- 草稿:`is_published=False, is_draft=True`
- 已发布:`is_published=True, is_draft=False`
- 暂不支持 "已发布但隐藏"
#### Category (分类)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Integer, PK | 主键自增 |
| `name` | String(50), UQ | 分类名称 |
| `description` | Text | 分类描述 |
| `created_at` | DateTime | 创建时间 |
- 删除分类时,关联文章自动设为 `category_id=None`
#### Tag (标签)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Integer, PK | 主键自增 |
| `name` | String(30), UQ | 标签名称 |
| `created_at` | DateTime | 创建时间 |
- 与 Post 为多对多关系,通过 `post_tags` 关联表实现
- 删除标签时自动解除关联
#### Admin (管理员)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Integer, PK | 主键自增 |
| `username` | String(50), UQ | 用户名 |
| `password_hash` | String(255) | bcrypt 哈希密码 |
| `created_at` | DateTime | 创建时间 |
- 系统初始化时创建默认管理员(`admin` / `admin123`)
#### Setting (系统设置)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Integer, PK | 主键自增 |
| `site_name` | String(100) | 站点名称 |
| `site_description` | Text | 站点描述 |
| `site_logo` | String(500) | Logo URL |
| `posts_per_page` | Integer | 每页文章数 |
| `theme` | String(50) | 主题标识 |
| `custom_css` | Text | 自定义 CSS |
| `icp_number` | String(50) | 备案号 |
| `copyright` | String(200) | 版权信息 |
| `analytics_code` | Text | 统计代码 |
- 单记录表(`get_or_create_settings` 保证只有一条记录)
#### Page (自定义页面)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Integer, PK | 主键自增 |
| `title` | String(200) | 页面标题 |
| `slug` | String(200), UQ | URL 标识 |
| `content` | Text | Markdown 内容 |
| `is_nav` | Boolean | 是否在导航栏显示 |
| `nav_order` | Integer | 导航排序 |
| `created_at` | DateTime | 创建时间 |
| `updated_at` | DateTime | 更新时间 |
- 限制最多 5 个自定义页面
---
## 3. 认证机制 (routers/auth.py)
### 3.1 JWT Token
- **算法**:HS256
- **密钥**:硬编码 `your-secret-key-123`(生产环境应改为环境变量)
- **有效期**:30 天
- **签发内容**:`{"sub": "admin", "exp": timestamp}`
### 3.2 双重认证方式
1. **Bearer Token**(管理后台使用)
```
Authorization: Bearer <token>
```
2. **Cookie Token**(可选,用于同源场景)
```
Cookie: access_token=<token>
```
### 3.3 鉴权装饰器
```python
async def get_current_admin(
token: str = Depends(oauth2_scheme),
token_cookie: str = Cookie(None, alias="access_token")
)
```
- 优先读取 Bearer Token
- 如不存在则读取 Cookie
- 验证失败返回 401
---
## 4. 文章 API (routers/posts.py)
### 4.1 端点列表
| 方法 | 路径 | 说明 | 鉴权 |
|------|------|------|------|
| GET | `/api/posts` | 文章列表(含分页/搜索/筛选) | 公开 |
| GET | `/api/posts/{post_id}` | 单篇文章详情 | 公开 |
| POST | `/api/posts` | 创建文章 | 管理员 |
| PUT | `/api/posts/{post_id}` | 更新文章 | 管理员 |
| DELETE | `/api/posts/{post_id}` | 删除文章 | 管理员 |
| GET | `/api/posts/stats/overview` | 统计概览 | 管理员 |
| GET | `/api/posts/slug/{slug}` | 通过 slug 获取文章 | 公开 |
| GET | `/api/posts/category/{category_id}` | 按分类获取文章 | 公开 |
| GET | `/api/posts/tag/{tag_name}` | 按标签获取文章 | 公开 |
### 4.2 列表查询参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `skip` | int | 分页偏移,默认 0 |
| `limit` | int | 每页数量,默认 10 |
| `search` | str | 标题/摘要/内容关键词搜索(like `%xxx%`) |
| `category_id` | int | 按分类筛选 |
| `tag` | str | 按标签名筛选 |
| `status` | str | `published` / `draft` / `all`,默认 `published` |
### 4.3 响应数据结构
```json
{
"items": [
{
"id": 1,
"title": "文章标题",
"slug": "article-slug",
"summary": "摘要",
"cover_image": "/uploads/xxx.jpg",
"is_published": true,
"is_draft": false,
"created_at": "2026-04-28T04:00:00",
"updated_at": "2026-04-28T04:00:00",
"view_count": 100,
"category": {"id": 1, "name": "技术"},
"tags": [{"id": 1, "name": "Python"}]
}
],
"total": 50,
"page": 1,
"page_size": 10,
"total_pages": 5
}
```
### 4.4 搜索实现
```python
# 标题 > 摘要 > 内容,全文模糊搜索
Post.title.ilike(f"%{search}%") |
Post.summary.ilike(f"%{search}%") |
Post.content.ilike(f"%{search}%")
```
### 4.5 浏览次数
- 每次 GET 单篇文章时 `view_count += 1`
- 通过 `/api/posts/{id}?increment_view=true` 控制
---
## 5. 图片上传 (routers/images.py)
### 5.1 上传接口
```
POST /api/images/upload
Content-Type: multipart/form-data
Body: file=<二进制文件>
```
### 5.2 处理流程
1. 校验文件类型:白名单 `jpg, jpeg, png, gif, webp`
2. 生成唯一文件名:`uuid.uuid4().hex + 扩展名`
3. 保存到 `backend/uploads/`
4. 返回可访问的 URL:`/uploads/{filename}`
### 5.3 图片删除
```
DELETE /api/images/{filename}
```
- 从文件系统删除
- 同时从所有文章 `cover_image` 和 `content` 字段中移除引用
### 5.4 图片裁剪(封面图)
```
POST /api/images/{filename}/crop
Body: {"x": 0, "y": 0, "width": 800, "height": 400}
```
- 使用 Pillow 裁剪
- 直接覆盖原图
---
## 6. 数据库连接 (database.py)
### 6.1 配置
```python
# SQLite 本地文件
SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False} # 允许多线程
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
```
### 6.2 Session 获取
```python
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
```
- 通过 FastAPI `Depends` 注入到路由处理函数
---
## 7. Pydantic 模型 (schemas.py)
| Schema | 用途 |
|--------|------|
| `PostCreate` | 创建文章请求 |
| `PostUpdate` | 更新文章请求 |
| `PostResponse` | 文章响应(含 category/tags) |
| `CategoryCreate` | 创建分类请求 |
| `CategoryResponse` | 分类响应(含 post_count) |
| `TagCreate` | 创建标签请求 |
| `TagResponse` | 标签响应 |
| `SettingUpdate` | 更新设置请求 |
| `SettingResponse` | 设置响应 |
| `PageCreate` | 创建页面请求 |
| `PageUpdate` | 更新页面请求 |
| `PageResponse` | 页面响应 |
---
## 8. 错误处理
后端使用 FastAPI 的 HTTPException 统一返回错误:
```json
{
"detail": "错误描述"
}
```
常见错误码:
| 状态码 | 场景 |
|--------|------|
| 400 | 参数错误、数量超限、名称重复 |
| 401 | 未登录/Token 无效 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 422 | 请求体校验失败 |
---
*本文档由 nanobot 根据项目源码自动生成。*