# 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 ``` 2. **Cookie Token**(可选,用于同源场景) ``` Cookie: access_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 根据项目源码自动生成。*