预计阅读时间:24 分钟
🚀 超前科技感个人博客 — Python 全栈架构设计
一、技术栈总览
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare CDN │
└────────────────────────┬────────────────────────────────────┘
│
┌────▼────┐
│ Nginx │ 反向代理 / SSL / 限流
└────┬────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 前端 SPA │ │ 后端 API│ │ 管理后台 │
│ React + │ │FastAPI │ │ React │
│ Vite │◄──►│+ Uvicorn│ │ Admin │
│ :3000 │ │ :8000 │ │ :3001 │
└─────────┘ └────┬────┘ └─────────┘
│
┌────────────┼──────────────┐
│ │ │
┌─────▼───┐ ┌────▼────┐ ┌──────▼──────┐
│PostgreSQL│ │ Redis │ │ MinIO/S3 │
│ 主数据库 │ │ 缓存/队列│ │ 对象存储 │
└──────────┘ └─────────┘ └─────────────┘
| 层级 | 技术选型 | 版本 |
|---|---|---|
| 前端 | React 19 + Vite 6 + TypeScript | 展示层,科技感 UI |
| 后端 | FastAPI + Uvicorn + Pydantic v2 | RESTful API + WebSocket |
| 数据库 | PostgreSQL 16 | 主数据存储 |
| ORM | SQLAlchemy 2.0 (async) + Alembic | 异步 ORM + 迁移 |
| 缓存 | Redis 7 | 缓存 / Session / 消息队列 |
| 任务队列 | Celery + Redis | 异步任务(邮件、数据聚合) |
| 存储 | MinIO / 阿里云OSS | 图片 / 附件 |
| 网关 | Nginx | 反向代理 / SSL / 静态文件 |
二、Monorepo 项目结构
blog/
├── packages/ # 共享包(仅前端)
│ └── shared/ # 前端侧共享 TS 类型
│ ├── src/
│ │ ├── types/
│ │ └── constants/
│ └── package.json
│
├── apps/
│ ├── client/ # ===== 前端 (React + Vite) =====
│ │ ├── public/
│ │ │ ├── fonts/ # JetBrains Mono 等自定义字体
│ │ │ ├── images/ # 静态图片
│ │ │ └── models/ # 3D 模型文件 (.glb)
│ │ │
│ │ ├── src/
│ │ │ ├── main.tsx
│ │ │ ├── App.tsx # 路由 + 全局 Provider
│ │ │ │
│ │ │ ├── pages/ # 路由页面
│ │ │ │ ├── Home/ # 首页(Terminal 入口)
│ │ │ │ ├── Posts/ # 文章列表 + 详情
│ │ │ │ ├── Projects/ # 项目墙
│ │ │ │ ├── About/ # 关于页 + 技能星盘
│ │ │ │ ├── Guestbook/ # 留言板
│ │ │ │ ├── Snippets/ # 代码片段
│ │ │ │ └── NotFound/ # 404
│ │ │ │
│ │ │ ├── components/ # 组件
│ │ │ │ ├── ui/ # 基础组件(Button/Card/Input/Badge/Modal/Toast)
│ │ │ │ ├── layout/ # 布局(Navbar/Footer/Sidebar/MobileNav)
│ │ │ │ ├── effects/ # 特效(粒子背景/网格光效/故障文字/打字机/CRT/极光)
│ │ │ │ ├── blog/ # 博客组件(PostCard/CodeBlock/CommentSection/TOC)
│ │ │ │ └── home/ # 首页组件(Hero/TerminalIntro/FeaturedPosts/StatsBoard)
│ │ │ │
│ │ │ ├── hooks/ # 自定义 Hooks
│ │ │ │ ├── useMousePosition.ts
│ │ │ │ ├── useScrollProgress.ts
│ │ │ │ ├── useIntersectionObserver.ts
│ │ │ │ └── useApi.ts
│ │ │ │
│ │ │ ├── services/ # API 调用层
│ │ │ │ ├── http.ts # Axios 实例(拦截器 + 鉴权注入)
│ │ │ │ ├── postService.ts # 文章模块
│ │ │ │ ├── projectService.ts # 项目模块
│ │ │ │ ├── commentService.ts # 评论模块
│ │ │ │ ├── authService.ts # 认证模块
│ │ │ │ └── guestbookService.ts # 留言模块
│ │ │ │
│ │ │ ├── stores/ # Zustand 状态管理
│ │ │ │ ├── useThemeStore.ts # 主题
│ │ │ │ ├── useUserStore.ts # 用户
│ │ │ │ └── useUIStore.ts # UI 状态
│ │ │ │
│ │ │ ├── styles/ # 样式
│ │ │ │ ├── globals.css
│ │ │ │ ├── animations.css
│ │ │ │ └── markdown.css
│ │ │ │
│ │ │ └── utils/
│ │ │ └── cn.ts
│ │ │
│ │ ├── index.html
│ │ ├── vite.config.ts
│ │ ├── tailwind.config.ts
│ │ ├── tsconfig.json
│ │ └── package.json
│ │
│ ├── server/ # ===== 后端 (FastAPI + Python) =====
│ │ ├── app/
│ │ │ ├── __init__.py
│ │ │ ├── main.py # 入口:FastAPI 实例 + 生命周期
│ │ │ ├── config.py # Pydantic Settings 配置
│ │ │ │
│ │ │ ├── api/ # API 路由层
│ │ │ │ ├── __init__.py
│ │ │ │ ├── v1/ # API v1 版本
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── router.py # 主路由聚合
│ │ │ │ │ ├── posts.py # 文章接口
│ │ │ │ │ ├── projects.py # 项目接口
│ │ │ │ │ ├── comments.py # 评论接口
│ │ │ │ │ ├── guestbook.py # 留言板接口
│ │ │ │ │ ├── auth.py # 认证接口
│ │ │ │ │ ├── users.py # 用户接口
│ │ │ │ │ ├── tags.py # 标签接口
│ │ │ │ │ ├── search.py # 搜索接口
│ │ │ │ │ ├── analytics.py # 统计接口
│ │ │ │ │ └── upload.py # 文件上传
│ │ │ │ └── deps.py # 依赖注入(DB Session / 当前用户)
│ │ │ │
│ │ │ ├── models/ # SQLAlchemy 数据模型
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py # 基类(时间戳、UUID 主键)
│ │ │ │ ├── user.py # 用户模型
│ │ │ │ ├── post.py # 文章模型
│ │ │ │ ├── comment.py # 评论模型
│ │ │ │ ├── project.py # 项目模型
│ │ │ │ ├── tag.py # 标签模型
│ │ │ │ ├── guestbook.py # 留言模型
│ │ │ │ ├── like.py # 点赞模型
│ │ │ │ ├── session.py # 会话模型
│ │ │ │ └── site_stats.py # 站点统计
│ │ │ │
│ │ │ ├── schemas/ # Pydantic 请求/响应 Schema
│ │ │ │ ├── __init__.py
│ │ │ │ ├── post.py # 文章 DTO
│ │ │ │ ├── user.py # 用户 DTO
│ │ │ │ ├── comment.py
│ │ │ │ ├── project.py
│ │ │ │ ├── auth.py
│ │ │ │ └── common.py # 通用(分页/响应包装)
│ │ │ │
│ │ │ ├── services/ # 业务逻辑层
│ │ │ │ ├── __init__.py
│ │ │ │ ├── post_service.py
│ │ │ │ ├── auth_service.py
│ │ │ │ ├── comment_service.py
│ │ │ │ ├── project_service.py
│ │ │ │ ├── search_service.py
│ │ │ │ ├── analytics_service.py
│ │ │ │ └── upload_service.py
│ │ │ │
│ │ │ ├── core/ # 核心基础设施
│ │ │ │ ├── __init__.py
│ │ │ │ ├── database.py # 异步 DB 引擎 + Session 工厂
│ │ │ │ ├── redis.py # Redis 客户端
│ │ │ │ ├── security.py # JWT / 密码哈希
│ │ │ │ ├── cache.py # 缓存装饰器(Cache-Aside)
│ │ │ │ ├── celery_app.py # Celery 异步任务
│ │ │ │ └── minio.py # MinIO 客户端
│ │ │ │
│ │ │ ├── middleware/ # 中间件
│ │ │ │ ├── __init__.py
│ │ │ │ ├── cors.py # CORS
│ │ │ │ ├── rate_limit.py # 限流
│ │ │ │ ├── request_log.py # 请求日志
│ │ │ │ └── process_time.py # 响应时间
│ │ │ │
│ │ │ └── tasks/ # Celery 异步任务
│ │ │ ├── __init__.py
│ │ │ ├── email_tasks.py # 发送邮件
│ │ │ ├── stats_tasks.py # 统计数据聚合
│ │ │ └── cleanup_tasks.py # 清理过期数据
│ │ │
│ │ ├── alembic/ # 数据库迁移
│ │ │ ├── env.py
│ │ │ └── versions/ # 迁移版本文件
│ │ │
│ │ ├── tests/ # 测试
│ │ │ ├── conftest.py # Fixtures(测试 DB / 客户端)
│ │ │ ├── test_posts.py
│ │ │ ├── test_auth.py
│ │ │ └── test_comments.py
│ │ │
│ │ ├── alembic.ini
│ │ ├── pyproject.toml
│ │ ├── requirements.txt
│ │ ├── Dockerfile
│ │ └── .env.example
│ │
│ └── admin/ # ===== 管理后台(React) =====
│ ├── src/
│ │ ├── pages/ # 页面
│ │ │ ├── Dashboard/
│ │ │ ├── Posts/ # 文章管理(MD 编辑器)
│ │ │ ├── Comments/ # 评论审核
│ │ │ ├── Users/
│ │ │ ├── Settings/
│ │ │ └── Analytics/
│ │ ├── components/
│ │ │ ├── RichEditor.tsx
│ │ │ ├── ImageUploader.tsx
│ │ │ └── DataTable.tsx
│ │ ├── services/
│ │ └── hooks/
│ ├── vite.config.ts
│ └── package.json
│
├── docker/ # Docker 编排
│ ├── docker-compose.yml # 一键启动全部服务
│ ├── Dockerfile.client
│ ├── Dockerfile.server
│ └── nginx/
│ ├── nginx.conf
│ └── ssl/
│
├── scripts/
│ ├── dev.sh # 本地开发启动脚本
│ └── deploy.sh
│
├── .env.example
├── .gitignore
└── README.md
三、数据库设计 (SQLAlchemy 2.0)
# app/models/base.py
"""所有模型的基类"""
import uuid
from datetime import datetime
from sqlalchemy import DateTime, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
class Base(DeclarativeBase):
pass
class TimestampMixin:
"""自动时间戳 Mixin"""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class UUIDMixin:
"""UUID 主键 Mixin"""
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
# app/models/user.py
"""用户模型"""
from sqlalchemy import String, Enum as SAEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship
import enum
from app.models.base import Base, TimestampMixin, UUIDMixin
class UserRole(str, enum.Enum):
USER = "USER"
ADMIN = "ADMIN"
class User(Base, TimestampMixin, UUIDMixin):
__tablename__ = "users"
username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255))
avatar: Mapped[str | None] = mapped_column(String(500))
bio: Mapped[str | None] = mapped_column(String(500))
role: Mapped[UserRole] = mapped_column(
SAEnum(UserRole), default=UserRole.USER
)
github_id: Mapped[str | None] = mapped_column(
String(100), unique=True, nullable=True
)
# 关系
posts = relationship("Post", back_populates="author")
comments = relationship("Comment", back_populates="author")
guestbooks = relationship("Guestbook", back_populates="author")
likes = relationship("Like", back_populates="user")
# app/models/post.py
"""文章模型"""
from sqlalchemy import String, Text, Boolean, Integer, ForeignKey, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from app.models.base import Base, TimestampMixin, UUIDMixin
# 多对多关联表
post_tags = Table(
"post_tags",
Base.metadata,
Column("post_id", ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True),
Column("tag_id", ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True),
)
class Post(Base, TimestampMixin, UUIDMixin):
__tablename__ = "posts"
title: Mapped[str] = mapped_column(String(200))
slug: Mapped[str] = mapped_column(String(200), unique=True, index=True)
excerpt: Mapped[str | None] = mapped_column(String(500))
content: Mapped[str] = mapped_column(Text)
cover_image: Mapped[str | None] = mapped_column(String(500))
published: Mapped[bool] = mapped_column(Boolean, default=False)
pinned: Mapped[bool] = mapped_column(Boolean, default=False)
views: Mapped[int] = mapped_column(Integer, default=0)
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
author_id: Mapped[int] = mapped_column(
ForeignKey("users.id"), index=True
)
author = relationship("User", back_populates="posts")
tags = relationship("Tag", secondary=post_tags, back_populates="posts")
comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
likes = relationship("Like", back_populates="post", cascade="all, delete-orphan")
# app/models/comment.py
"""评论模型(支持嵌套回复)"""
from sqlalchemy import String, Text, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class Comment(Base, TimestampMixin, UUIDMixin):
__tablename__ = "comments"
content: Mapped[str] = mapped_column(Text)
likes: Mapped[int] = mapped_column(Integer, default=0)
post_id: Mapped[int] = mapped_column(
ForeignKey("posts.id", ondelete="CASCADE"), index=True
)
author_id: Mapped[int] = mapped_column(
ForeignKey("users.id"), index=True
)
parent_id: Mapped[int | None] = mapped_column(
ForeignKey("comments.id", ondelete="CASCADE"), nullable=True
)
post = relationship("Post", back_populates="comments")
author = relationship("User", back_populates="comments")
replies = relationship("Comment", backref="parent", remote_side="Comment.id", cascade="all, delete-orphan")
四、API 路由设计
4.1 路由注册
# app/api/v1/router.py
from fastapi import APIRouter
from app.api.v1 import posts, projects, comments, guestbook, auth, users, tags, search, analytics, upload
router = APIRouter(prefix="/api/v1")
router.include_router(posts.router, prefix="/posts", tags=["文章"])
router.include_router(projects.router, prefix="/projects", tags=["项目"])
router.include_router(comments.router, prefix="/comments", tags=["评论"])
router.include_router(guestbook.router, prefix="/guestbook", tags=["留言板"])
router.include_router(auth.router, prefix="/auth", tags=["认证"])
router.include_router(users.router, prefix="/users", tags=["用户"])
router.include_router(tags.router, prefix="/tags", tags=["标签"])
router.include_router(search.router, prefix="/search", tags=["搜索"])
router.include_router(analytics.router, prefix="/analytics", tags=["统计"])
router.include_router(upload.router, prefix="/upload", tags=["上传"])
4.2 文章模块示例
# app/api/v1/posts.py
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, get_current_user
from app.schemas.post import PostCreate, PostUpdate, PostResponse, PostListResponse
from app.schemas.common import PaginationParams
from app.services.post_service import PostService
from app.models.user import User
router = APIRouter()
@router.get("", response_model=PostListResponse)
async def list_posts(
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=50),
tag: str | None = None,
published: bool = True,
sort: str = "published_at_desc",
db: AsyncSession = Depends(get_db),
):
"""文章列表(分页 + 筛选 + 排序)"""
service = PostService(db)
return await service.get_paginated(page=page, limit=limit, tag=tag,
published=published, sort=sort)
@router.get("/{slug}", response_model=PostResponse)
async def get_post(
slug: str,
db: AsyncSession = Depends(get_db),
):
"""文章详情"""
service = PostService(db)
post = await service.get_by_slug(slug)
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
return post
@router.post("", response_model=PostResponse, status_code=201)
async def create_post(
data: PostCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user(required_role="ADMIN")),
):
"""创建文章(仅管理员)"""
service = PostService(db)
return await service.create(data, author_id=current_user.id)
@router.post("/{post_id}/views")
async def increment_views(
post_id: str,
db: AsyncSession = Depends(get_db),
):
"""阅读量 +1"""
service = PostService(db)
await service.increment_views(post_id)
return {"ok": True}
4.3 API 接口总览
| 方法 | 路径 | 说明 | 鉴权 |
|---|---|---|---|
| GET | /api/v1/posts |
文章列表(分页/标签/排序) | 公开 |
| GET | /api/v1/posts/{slug} |
文章详情 | 公开 |
| POST | /api/v1/posts |
创建文章 | ADMIN |
| PUT | /api/v1/posts/{id} |
更新文章 | ADMIN |
| DELETE | /api/v1/posts/{id} |
删除文章 | ADMIN |
| POST | /api/v1/posts/{id}/views |
阅读量 +1 | 公开 |
| POST | /api/v1/posts/{id}/like |
点赞/取消 | USER |
| GET | /api/v1/comments?post_id= |
评论列表 | 公开 |
| POST | /api/v1/comments |
发表评论 | USER |
| GET | /api/v1/guestbook |
留言列表 | 公开 |
| POST | /api/v1/guestbook |
发表留言 | USER |
| GET | /api/v1/projects |
项目列表 | 公开 |
| POST | /api/v1/auth/register |
注册 | 公开 |
| POST | /api/v1/auth/login |
登录(JWT) | 公开 |
| POST | /api/v1/auth/github |
GitHub OAuth | 公开 |
| GET | /api/v1/search?q= |
全站搜索 | 公开 |
| POST | /api/v1/upload |
文件上传 | USER |
| GET | /api/v1/analytics |
站点统计 | ADMIN |
| WS | /ws/online |
实时在线人数 | — |
五、核心代码文件详解
5.1 FastAPI 入口
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from app.core.database import engine, Base
from app.core.redis import redis_client
from app.core.celery_app import celery_app
from app.api.v1.router import router as api_router
from app.middleware.rate_limit import RateLimitMiddleware
from app.middleware.request_log import RequestLogMiddleware
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期"""
# 启动时: 检查数据库连接 / Redis 连通性
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) # 开发用,生产用 Alembic
await redis_client.ping()
yield
# 关闭时: 释放连接
await engine.dispose()
await redis_client.close()
app = FastAPI(
title="Tech Blog API",
version="1.0.0",
lifespan=lifespan,
docs_url="/docs", # Swagger UI
redoc_url="/redoc", # ReDoc
)
# 中间件注册(顺序重要)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
app.add_middleware(RequestLogMiddleware)
app.add_middleware(RateLimitMiddleware)
# 路由注册
app.include_router(api_router)
# 静态文件(用户上传的图片等)
app.mount("/static", StaticFiles(directory="static"), name="static")
5.2 配置管理
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# 应用
APP_NAME: str = "Tech Blog"
DEBUG: bool = False
# 数据库
DATABASE_URL: str = "postgresql+asyncpg://blog:password@localhost:5432/blog"
# Redis
REDIS_URL: str = "redis://:password@localhost:6379/0"
# JWT
JWT_SECRET: str = "change-this-in-production"
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRES_MINUTES: int = 10080 # 7 天
# GitHub OAuth
GITHUB_CLIENT_ID: str = ""
GITHUB_CLIENT_SECRET: str = ""
# 对象存储(MinIO / S3)
OSS_ENDPOINT: str = ""
OSS_ACCESS_KEY: str = ""
OSS_SECRET_KEY: str = ""
OSS_BUCKET: str = "blog-assets"
# Celery
CELERY_BROKER_URL: str = "redis://:password@localhost:6379/1"
CELERY_RESULT_BACKEND: str = "redis://:password@localhost:6379/1"
model_config = {"env_file": ".env", "case_sensitive": True}
settings = Settings()
5.3 缓存装饰器
# app/core/cache.py
"""Cache-Aside 缓存装饰器"""
import json
import hashlib
from functools import wraps
from app.core.redis import redis_client
def cache(ttl: int = 300):
"""
缓存装饰器,自动执行 Cache-Aside 模式
用法:
@cache(ttl=600)
async def get_post(slug: str, db: AsyncSession):
...
缓存 Key: f"blog:{func_name}:{hash(args+kwargs)}"
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 生成缓存 Key
key_base = f"blog:{func.__name__}"
arg_hash = hashlib.md5(
json.dumps({"args": str(args), "kwargs": str(kwargs)},
default=str).encode()
).hexdigest()[:12]
cache_key = f"{key_base}:{arg_hash}"
# 读缓存
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 查 DB
result = await func(*args, **kwargs)
# 写回缓存
await redis_client.setex(cache_key, ttl, json.dumps(
result, default=str
))
return result
return wrapper
return decorator
5.4 业务逻辑层示例
# app/services/post_service.py
"""文章业务逻辑"""
from sqlalchemy import select, func, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.post import Post, post_tags
from app.models.tag import Tag
from app.schemas.post import PostCreate, PostUpdate, PostResponse
from app.core.cache import cache
class PostService:
def __init__(self, db: AsyncSession):
self.db = db
@cache(ttl=3600) # 文章详情缓存 1 小时
async def get_by_slug(self, slug: str) -> Post | None:
query = select(Post).where(Post.slug == slug)
result = await self.db.execute(query)
return result.scalar_one_or_none()
async def get_paginated(
self, page: int, limit: int, tag: str | None = None,
published: bool = True, sort: str = "published_at_desc"
) -> dict:
query = select(Post).where(Post.published == published)
if tag:
query = query.join(post_tags).join(Tag).where(Tag.name == tag)
# 排序
if sort == "published_at_desc":
query = query.order_by(desc(Post.published_at))
elif sort == "views_desc":
query = query.order_by(desc(Post.views))
# 分页
offset = (page - 1) * limit
query = query.offset(offset).limit(limit)
result = await self.db.execute(query)
posts = result.scalars().all()
# 总数
count_query = select(func.count()).select_from(Post).where(
Post.published == published
)
total = await self.db.scalar(count_query)
return {
"items": posts,
"total": total,
"page": page,
"limit": limit,
"pages": (total + limit - 1) // limit,
}
async def create(self, data: PostCreate, author_id: str) -> Post:
post = Post(**data.model_dump(), author_id=author_id)
self.db.add(post)
await self.db.commit()
await self.db.refresh(post)
return post
async def increment_views(self, post_id: str) -> None:
query = select(Post).where(Post.id == post_id)
result = await self.db.execute(query)
post = result.scalar_one_or_none()
if post:
post.views += 1
await self.db.commit()
六、依赖注入
# app/api/deps.py
"""FastAPI 依赖注入"""
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from jose import jwt, JWTError
from app.core.database import async_session
from app.core.security import decode_token
from app.models.user import User, UserRole
security = HTTPBearer()
async def get_db() -> AsyncSession:
"""数据库会话依赖"""
async with async_session() as session:
try:
yield session
finally:
await session.close()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db),
required_role: UserRole | None = None,
) -> User:
"""获取当前登录用户(可选角色校验)"""
token = credentials.credentials
payload = decode_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="无效的 Token")
user_id = payload.get("sub")
query = select(User).where(User.id == user_id)
result = await db.execute(query)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=401, detail="用户不存在")
if required_role and user.role != required_role:
raise HTTPException(status_code=403, detail="权限不足")
return user
七、数据流 & 缓存策略
请求进入 → FastAPI Middleware(日志/限流)
│
┌─────▼─────┐
│ API Route │
└─────┬─────┘
│
┌─────▼──────┐
│ Service │ ← @cache 装饰器自动查 Redis
│ 层 │
└─────┬──────┘
│
┌────▼────┐ ┌──────────┐
│ Cache │ ← hit →│ Redis │
│ Check │ └──────────┘
└────┬────┘
miss│
┌────▼────┐
│ DB │ ← SQLAlchemy async session
│ Query │
└────┬────┘
│
┌────▼────┐
│ 写回缓存 │ → Redis setex
└─────────┘
缓存策略:
Cache-Aside: 读 → 先 Redis → miss → DB → 写回 + TTL
写 → DB 更新 → 删除缓存 → 下次读重建
TTL:
文章详情: 3600s (1h)
文章列表: 1800s (30min)
标签/项目: 3600s (1h)
统计数据: 600s (10min)
缓存 Key 格式:
blog:post:{slug}
blog:posts:page:{n}:tag:{tag}
blog:projects
blog:tags
blog:analytics
八、异步任务 (Celery)
# app/core/celery_app.py
from celery import Celery
from app.config import settings
celery_app = Celery(
"blog_tasks",
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
)
celery_app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="Asia/Shanghai",
task_track_started=True,
beat_schedule={
"aggregate-daily-stats": {
"task": "app.tasks.stats_tasks.aggregate_daily_stats",
"schedule": 3600, # 每小时聚合一次
},
"cleanup-expired-sessions": {
"task": "app.tasks.cleanup_tasks.cleanup_expired_sessions",
"schedule": 86400, # 每天清理一次
},
},
)
# app/tasks/stats_tasks.py
"""统计数据聚合任务"""
from app.core.celery_app import celery_app
from app.core.database import sync_session
from app.models.post import Post
from app.models.comment import Comment
from app.models.site_stats import SiteStats
@celery_app.task
def aggregate_daily_stats():
"""每小时聚合站点统计数据"""
with sync_session() as db:
total_posts = db.query(Post).count()
total_comments = db.query(Comment).count()
total_views = db.query(func.sum(Post.views)).scalar() or 0
stats = db.query(SiteStats).first()
if not stats:
stats = SiteStats(id="main")
db.add(stats)
stats.total_posts = total_posts
stats.total_comments = total_comments
stats.total_views = total_views
db.commit()
九、WebSocket 实时在线
# 嵌入 app/main.py 或单独 websocket_manager.py
from fastapi import WebSocket, WebSocketDisconnect
import json
class ConnectionManager:
"""WebSocket 连接管理"""
def __init__(self):
self.active_connections: dict[str, WebSocket] = {}
async def connect(self, websocket: WebSocket, client_id: str):
await websocket.accept()
self.active_connections[client_id] = websocket
await self.broadcast_online_count()
def disconnect(self, client_id: str):
self.active_connections.pop(client_id, None)
async def broadcast_online_count(self):
count = len(self.active_connections)
for conn in self.active_connections.values():
try:
await conn.send_json({
"type": "online_count",
"data": count
})
except Exception:
pass
async def broadcast_post_update(self, post_id: str, action: str):
"""文章更新时推送(新评论、阅读量变化)"""
for conn in self.active_connections.values():
try:
await conn.send_json({
"type": "post:updated",
"data": {"post_id": post_id, "action": action}
})
except Exception:
pass
manager = ConnectionManager()
# WebSocket 路由
@app.websocket("/ws/online")
async def websocket_endpoint(websocket: WebSocket):
client_id = str(id(websocket))
await manager.connect(websocket, client_id)
try:
while True:
data = await websocket.receive_text()
# 处理客户端消息(可选)
except WebSocketDisconnect:
manager.disconnect(client_id)
await manager.broadcast_online_count()
十、扩展能力设计
10.1 模块扩展 —— 加新功能只需三步
想给博客加一个「读书笔记」模块:
① app/models/booknote.py ← 数据模型
② app/schemas/booknote.py ← Pydantic Schema
③ app/api/v1/booknotes.py ← 路由(CRUD 约 40 行)
④ app/services/booknote_service.py ← 业务逻辑
⑤ router.include_router(booknotes.router) ← 注册到主路由
每个模块完全解耦,不碰其他模块一行代码。
10.2 可插拔功能清单
| 功能 | 实现方式 | 影响范围 |
|---|---|---|
| 文章推荐 | services/recommend_service.py |
新增1文件 |
| RSS 订阅 | api/v1/rss.py |
新增1路由 |
| 邮件通知 | tasks/email_tasks.py |
新增 Celery 任务 |
| 阅读排行榜 | services/analytics_service.py + 1行 |
方法追加 |
| Markdown 导入 | services/import_service.py |
新增1文件 |
| 友情链接 | models/link.py + api/v1/links.py |
2个文件 |
| 音乐播放器 | 前端组件 + api/v1/music.py |
纯前端+1路由 |
| 相册集 | models/album.py + api/v1/albums.py |
2个文件 |
| 数据分析 | 前端图表 + api/v1/analytics + Celery |
3个文件 |
10.3 后期可接入的 AI 功能
| 功能 | 需要 | 代码量 |
|---|---|---|
| 文章自动标签 | jieba + TF-IDF | \~30行 |
| 阅读量预测 | sklearn LinearRegression | \~50行 |
| 文章摘要生成 | 调用 LLM API | \~40行 |
| 相似文章推荐 | cosine similarity | \~30行 |
| 评论情感分析 | transformers pipeline | \~20行 |
因为这些全部是 Python 生态,不需要换栈或起新服务。
十一、部署 & 开发流程
11.1 本地开发
# 后端
cd apps/server
python -m venv .venv
pip install -r requirements.txt
alembic upgrade head # 数据库迁移
uvicorn app.main:app --reload # 热重载开发 → http://localhost:8000
# Swagger 自动生成 → http://localhost:8000/docs
# 前端
cd apps/client
pnpm install
pnpm dev # → http://localhost:5173
# Celery 任务队列(另开终端)
cd apps/server
celery -A app.core.celery_app worker -l info
celery -A app.core.celery_app beat -l info
# Redis + PostgreSQL + MinIO
docker compose up -d postgres redis minio
11.2 docker-compose.yml
version: "3.8"
services:
client:
build:
context: ./apps/client
dockerfile: ../../docker/Dockerfile.client
ports:
- "3000:80"
depends_on:
- server
server:
build:
context: ./apps/server
dockerfile: ../../docker/Dockerfile.server
ports:
- "8000:8000"
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
admin:
build:
context: ./apps/admin
dockerfile: ../../docker/Dockerfile.client
ports:
- "3001:80"
depends_on:
- server
celery_worker:
build:
context: ./apps/server
dockerfile: ../../docker/Dockerfile.server
command: celery -A app.core.celery_app worker -l info
env_file: .env
depends_on:
- server
- redis
postgres:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: blog
POSTGRES_USER: blog
POSTGRES_PASSWORD: ${DB_PASSWORD:?err}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U blog"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
command: redis-server --requirepass ${REDIS_PASSWORD:?err}
minio:
image: minio/minio
ports:
- "9000:9000"
- "9001:9001"
volumes:
- miniodata:/data
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${OSS_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${OSS_SECRET_KEY}
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./docker/nginx/ssl:/etc/nginx/ssl
depends_on:
- client
- server
- admin
volumes:
pgdata:
redisdata:
miniodata:
11.3 依赖清单
# apps/server/requirements.txt
# === Web 框架 ===
fastapi==0.115.*
uvicorn[standard]==0.34.*
pydantic==2.*
pydantic-settings==2.*
# === 数据库 ===
sqlalchemy[asyncio]==2.0.*
asyncpg==0.30.*
alembic==1.14.*
# === 认证 ===
python-jose[cryptography]==3.3.*
passlib[bcrypt]==1.7.*
python-multipart==0.0.*
# === 缓存 / 队列 ===
redis[hiredis]==5.*
celery[redis]==5.*
# === 存储 ===
minio==7.*
# === 工具 ===
httpx==0.28.*
python-slugify==8.*
Pillow==11.*
十二、与 NestJS 版对比总结
| 维度 | NestJS (TS) | FastAPI (Python) | 对你 |
|---|---|---|---|
| 学习成本 | 高(DI/IoC/装饰器体系) | 低(3 小时上手) | ✅ |
| 开发效率 | 编译 + 重启 | 热重载 + 自动 docs | ✅ |
| 数据库 ORM | Prisma | SQLAlchemy 2.0 | 功能对等 |
| 类型安全 | TS 编译时 | Pydantic 运行时 | ✅ 够用 |
| AI/ML 扩展 | ❌ 需另外起 Python 服务 | 原生支持 ✅ | ✅ |
| 秋招加成 | Node 后端岗位 | Python 后端岗位更多 | ✅ |
| 微信生态 | ❌ | ✅ wechaty/itchat 等 | ✅ 你之前玩过 |
结论: Python 版对成品效果没有显著提升或下降,但对你个人——备考压力小、秋招面更广、后期加 AI 功能不需要换栈,是更划算的选择。
本文由 admin 原创,转载请注明出处。
评论
0