396 lines
11 KiB
Markdown
396 lines
11 KiB
Markdown
# 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 根据项目源码自动生成。*
|