Python 后端核心知识

75 阅读19分钟

1. FastAPI 的 Depends 和 Middleware 实现鉴权拦截

  • 依赖注入(Depends
  • 中间件(Middleware

1.1. 依赖注入(Depends)—— 细粒度、路由级鉴权

原理

  • Depends 声明一个函数(或可调用对象)作为依赖项,FastAPI 会在路由执行前自动解析并注入它的返回值。
  • 依赖项可以嵌套依赖(依赖树),且默认每次请求重新计算(除非用 @lru_cache 等缓存)。
  • 支持 async 依赖,也支持 yield 依赖用于资源管理(如数据库会话)。

鉴权实现

最常见的做法是定义 get_current_user 依赖,它从请求头提取 Token,验证后返回用户信息。

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

app = FastAPI()
security = HTTPBearer()

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(token, "SECRET_KEY", algorithms=["HS256"])
        user_id = payload.get("sub")
        if user_id is None:
            raise HTTPException(status_code=401, detail="Invalid token")
        return {"user_id": user_id, "role": payload.get("role")}
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

@app.get("/protected")
def protected_route(user=Depends(verify_token)):
    return {"message": f"Hello user {user['user_id']}"}

优势

  • 精确控制:可针对不同路由使用不同认证策略(如某些路由可选认证)。
  • 自动注入:依赖项可注入到其他依赖中,形成组合。
  • 测试友好:可轻松 mock 依赖。

深入机制

  • FastAPI 会构建依赖图,并缓存依赖实例(默认在请求范围内复用)。
  • 若依赖项是 async,会在异步上下文中执行。

1.2. 中间件(Middleware)—— 全局、统一鉴权

原理

中间件是 WSGI/ASGI 层面的拦截器,在请求到达路由之前和响应返回客户端之前执行。它采用“洋葱模型”:请求穿过中间件链,再穿过中间件返回。

鉴权实现

中间件适合统一拦截所有请求(或路径前缀),进行全局认证,但无法灵活区分不同路由的认证需求。

from fastapi import Request, Response, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
import jwt

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 放行健康检查路径
        if request.url.path in ("/health", "/docs", "/openapi.json"):
            return await call_next(request)

        auth_header = request.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            return Response("Unauthorized", status_code=401)

        token = auth_header.split(" ")[1]
        try:
            payload = jwt.decode(token, "SECRET_KEY", algorithms=["HS256"])
            request.state.user = payload  # 将用户信息存入请求状态
        except jwt.PyJWTError:
            return Response("Invalid token", status_code=401)

        response = await call_next(request)
        return response

app.add_middleware(AuthMiddleware)

优势

  • 全局生效,无需在每个路由重复认证代码。
  • 可统一处理响应头、日志等。

深入机制

  • 中间件是 ASGI 应用的一部分,dispatch 方法接收 request,调用 call_next 传递请求给下一个中间件或路由。
  • 中间件可以修改请求(添加属性)或提前返回响应。

3. 对比与选择

特性Depends 依赖注入中间件
粒度路由级,可精细控制全局,统一拦截
易用性依赖项可复用,易于组合需要编写类,配置稍复杂
异常处理在依赖中抛 HTTPException,自动转为 4xx 响应需手动返回 ResponseJSONResponse
访问请求体依赖可访问请求体(需 Body中间件访问请求体需额外处理(流式)
性能每次请求解析依赖树,轻量每次请求经过中间件链,性能略有损耗
最佳实践推荐用于业务逻辑的鉴权、授权推荐用于全局日志、限流、CORS、健康检查绕过

4. 高级:结合两者实现“通用+精细”鉴权

一种常见模式是:

  • 中间件:负责解析 Token,将用户信息存入 request.state(但若失败只记录错误,不直接拦截)。
  • 依赖项:从 request.state 读取用户信息,并执行具体的权限校验(如角色检查)。

这样既实现了全局 Token 解析(减少重复代码),又保留了路由级别的权限控制。

# 依赖项
def get_current_user(request: Request):
    user = getattr(request.state, "user", None)
    if user is None:
        raise HTTPException(401, "Unauthorized")
    return user

@app.get("/admin")
def admin_route(user=Depends(get_current_user)):
    if user.get("role") != "admin":
        raise HTTPException(403, "Not enough permissions")
    return {"status": "ok"}

5. 依赖注入的高级特性

5.1 类依赖

class CommonHeaders:
    def __init__(self, user_agent: str = Header(...)):
        self.user_agent = user_agent

@app.get("/items")
def read_items(headers: CommonHeaders = Depends()):
    return {"ua": headers.user_agent}

5.2 Yield 依赖(资源管理)

async def get_db():
    db = await connect_db()
    try:
        yield db
    finally:
        await db.close()

@app.get("/users")
def get_users(db=Depends(get_db)):
    return db.query("SELECT * FROM users")

5.3 全局依赖

appAPIRouter 上添加 dependencies 列表,会对该路由下所有端点生效。


6. 中间件的注意事项

  • 请求体访问:中间件读取请求体可能需要 request.stream(),但会消耗流,导致路由无法读取。解决方案:将请求体缓存或使用 request.body()(注意大小)。
  • 异常处理:在中间件中抛出 HTTPException 不会被 FastAPI 的异常处理器捕获,需手动处理并返回 Response
  • 执行顺序:中间件按添加顺序执行(先进后出)。

7. 完整鉴权示例(Middleware + Depends + JWT)

使用中间件提取 Token 并存入 request.state ,再用 Depends 函数从 request.state 获取用户并执行权限校验,这样既保持全局统一,又不失路由灵活性。

import jwt
from fastapi import FastAPI, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Optional

# ---------- 配置 ----------
SECRET_KEY = "your-secret-key"  # 生产环境应从环境变量读取
ALGORITHM = "HS256"

# ---------- 数据模型 ----------
class User(BaseModel):
    user_id: str
    role: str  # "admin" 或 "user"

# ---------- 中间件:提取 Token 并解码 ----------
class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 1. 放行不需要认证的路径(可选)
        if request.url.path in ("/health", "/docs", "/openapi.json"):
            return await call_next(request)

        # 2. 提取 Authorization 头
        auth_header = request.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            return JSONResponse(
                status_code=status.HTTP_401_UNAUTHORIZED,
                content={"detail": "Missing or invalid Authorization header"},
                headers={"WWW-Authenticate": "Bearer"},
            )

        token = auth_header.split(" ")[1]

        # 3. 解码 JWT
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            user = User(user_id=payload.get("sub"), role=payload.get("role", "user"))
            request.state.user = user  # 存入 request.state
        except jwt.PyJWTError:
            return JSONResponse(
                status_code=status.HTTP_401_UNAUTHORIZED,
                content={"detail": "Invalid token"},
                headers={"WWW-Authenticate": "Bearer"},
            )

        # 4. 继续处理请求
        response = await call_next(request)
        return response

# ---------- 依赖:从 request.state 获取当前用户 ----------
def get_current_user(request: Request) -> User:
    user = getattr(request.state, "user", None)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Not authenticated",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user

# 依赖:仅允许 admin 角色
def require_admin(current_user: User = Depends(get_current_user)) -> User:
    if current_user.role != "admin":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Insufficient permissions",
        )
    return current_user

# ---------- FastAPI 应用 ----------
app = FastAPI(title="JWT Auth Demo")

# 添加中间件
app.add_middleware(AuthMiddleware)

# ---------- 路由 ----------
@app.get("/health")
async def health():
    return {"status": "ok"}

@app.get("/me")
async def get_my_info(current_user: User = Depends(get_current_user)):
    """获取当前登录用户信息(任何有效 token 均可)"""
    return {"user_id": current_user.user_id, "role": current_user.role}

@app.get("/admin")
async def admin_only(current_user: User = Depends(require_admin)):
    """仅 admin 角色可访问"""
    return {"message": f"Welcome admin {current_user.user_id}"}

2. 全局异常处理

  • 在 FastAPI 中,全局异常处理是通过注册全局异常处理器(Exception Handler) 来实现的。
  • FastAPI(基于 Starlette)提供了 exception_handler 装饰器,可以针对特定异常类型注册处理函数。
异常类型处理器装饰器适用场景
HTTPException@app.exception_handler(HTTPException)标准 HTTP 错误(4xx/5xx)
RequestValidationError@app.exception_handler(RequestValidationError)请求参数校验失败
自定义业务异常@app.exception_handler(MyCustomError)业务逻辑错误(如库存不足、权限不足)
Exception@app.exception_handler(Exception)兜底所有未捕获异常

2.1 处理 HTTPException(FastAPI 内置)

python

from fastapi import HTTPException

@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"message": exc.detail, "success": False},
    )

2.2 处理 RequestValidationError(请求参数校验失败)

当请求体/查询参数不符合 Pydantic 模型时,会抛出此异常。

from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc: RequestValidationError):
    # exc.errors() 包含详细校验错误列表
    return JSONResponse(
        status_code=422,
        content={
            "detail": "请求参数校验失败",
            "errors": exc.errors(),
            "body": exc.body,
        },
    )

2.3 处理自定义业务异常

class BusinessError(Exception):
    def __init__(self, code: int, message: str):
        self.code = code
        self.message = message

@app.exception_handler(BusinessError)
async def business_error_handler(request, exc: BusinessError):
    return JSONResponse(
        status_code=400,
        content={"code": exc.code, "message": exc.message},
    )

2.4 兜底所有未捕获异常(Exception

注意:这个处理器会捕获所有未被特定处理器覆盖的异常,应放在最后注册。

@app.exception_handler(Exception)
async def general_exception_handler(request, exc: Exception):
    # 生产环境建议记录日志,不要暴露内部细节
    return JSONResponse(
        status_code=500,
        content={"message": "服务器内部错误"},
    )

2.5. 结合日志记录

在生产环境中,异常处理器通常需要记录日志:

import logging

logger = logging.getLogger(__name__)

@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc: HTTPException):
    logger.warning(f"HTTPException: {exc.detail}", exc_info=exc)
    return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})

3. 数据库SQL

3.1 mysql 数据库

MySQL 由三层组成:

  1. 连接层:客户端和服务器端建立连接,客户端发送 SQL至服务器端;
  2. SQL 层:对 SQL 语句进行查询处理;
  3. 存储引擎层:与数据库文件打交道,负责数据的存储和读取。

设计原则 - 三少一多

  • 数据表的个数越少越好
  • 数据表中的字段个数越少越好
  • 数据表中联合主键的字段个数越少越好
  • 使用主键和外键越多越好

基本操作

-- 1. 确认你当前在哪个数据库
SELECT DATABASE();

-- 2. 如果不在 heros 库,先切换
USE heros;

-- 3. 查看 heros 库下现在有哪些表
SHOW TABLES;

-- 4. 创建表 heros
CREATE TABLE `heros` (
    -- 核心标识(建议添加自增主键)
    `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键',
    `name` VARCHAR(50) NOT NULL UNIQUE COMMENT '英雄名称',
    `hp_max` INT DEFAULT 0 COMMENT '最大生命',
    `mp_max` INT DEFAULT 0 COMMENT '最大法力',
    `role_main` VARCHAR(20) DEFAULT '' COMMENT '主要定位(如:战士、法师)'   
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='英雄属性表';

-- 5. 插入数据
INSET INTO 表(字段名) VALUES(字段值)
INSERT INTO heros (name, hp_max, mp_max, role_main) VALUES
('吕布', 3500, 0, '战士'),
('诸葛亮', 2500, 800, '法师'),
('亚瑟', 3200, 200, '战士'),
('后羿', 2800, 0, '射手'),
('安琪拉', 2300, 750, '法师');

-- 6. 查看表 heros 的name列
SELECT name FROM heros

-- 7. 表增加一列
ALTER TABLE 表名 ADD 列名 数据类型 [约束条件] [位置];
ALTER TABLE heros ADD skin_count INT DEFAULT 0 COMMENT '皮肤数量';

-- 8. 删除一列
ALTER TABLE 表名 DROP COLUMN 列名;
ALTER TABLE heros DROP COLUMN skin_count;

-- 9. 查看表的信息
desc heros

where 查询

-- 查询所有hp_max在 5399 到 6811 之间的数据
SELECT name, hp_max FROM heros WHERE hp_max BETWEEN 5399 AND 6811

-- heros 表中的`hp_max`字段有没有存在空值的数据
SELECT name, hp_max FROM heros WHERE hp_max IS NULL

-- 筛选最大生命值大于 6000,最大法力大于 1700 的英雄,然后按照最大生命值和最大法力值之和从高到低进行排序
SELECT name, hp_max, mp_max FROM heros WHERE hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC

LIKE 模糊匹配

LIKE 配合两个通配符:

  • % :代表任意多个字符(包括 0 个)。
  • _ :代表单个任意字符。
-- 1. 查询名字中带“亚”的英雄(任意位置)
SELECT * FROM heros WHERE name LIKE '%亚%';

-- 2. 查询名字以“亚”开头的英雄(前缀匹配,可以利用索引,速度最快)
SELECT * FROM heros WHERE name LIKE '亚%';

-- 3. 查询名字以“亚”结尾的英雄
SELECT * FROM heros WHERE name LIKE '%亚';

-- 4. 查询名字是“亚”字开头,且总共只有 2 个字的英雄(如“亚瑟”)
-- 一个 _ 代表一个字,两个 _ 代表总共两个字
SELECT * FROM heros WHERE name LIKE '亚_';

-- 5. 查询主要定位是“战士”或“法师”(模糊包含)
SELECT * FROM heros WHERE role_main LIKE '%战士%' OR role_main LIKE '%法师%';

SQL的聚集函数

函数作用
COUNT(*)统计记录的总行数
COUNT(DISTINCT 列)统计该列不同值的个数(去重)
SUM(列)计算该列的总和
AVG(列)计算该列的平均值
MAX(列)求该列的最大值
MIN(列)求该列的最小值
-- 查询最大生命值大于 6000 的英雄数量
SELECT COUNT(*) FROM heros WHERE hp_max > 6000

-- 查询最大生命值大于 6000,且有次要定位的英雄数量
SELECT COUNT(role_assist) FROM heros WHERE hp_max > 6000

-- 射手(主要定位或者次要定位是射手)的英雄数、平均最大生命值、法力最大值的最大值、攻击最大值的最小值,以及这些英雄总的防御最大值等汇总数据
SELECT COUNT(*), AVG(hp_max), MAX(mp_max), MIN(attack_max), SUM(defense_max) FROM heros WHERE role_main = '射手' or role_assist = '射手'

子查询

-- 查询哪个球员的身高最高,最高身高是多少
SELECT player_name, height FROM player WHERE height = (SELECT max(height) FROM player)

-- 查找每个球队中大于平均身高的球员有哪些,并显示他们的球员姓名、身高以及所在球队 ID。
SELECT player_name, height, team_id FROM player AS a WHERE height > (SELECT avg(height) FROM player AS b WHERE a.team_id = b.team_id)

Python 使用mysql-connector-python操作 mysql

import mysql.connector

# 建立数据库连接
conn = mysql.connector.connect(user='root', password='123', database='test')
# 创建游标对象(用于执行查询)
# 游标相当于在数据库会话中开辟的一个“临时工作区”
cursor = conn.cursor()
# 执行参数化查询(防止 SQL 注入)
cursor.execute("SELECT id, name FROM users WHERE age > %s", (18,))
# 从结果集中获取所有行
rows = cursor.fetchall()
# 释放游标资源
cursor.close()
# 关闭数据库连接
conn.close()

FastAPI 使用sqlalchemy操作 mysql

类似插件有 FastAPI 作者基于 ORM 框架 sqlalchemy 封装的 SQLModel

from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy import select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

# ---------- 数据库配置 ----------
DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/test_db"
engine = create_async_engine(DATABASE_URL, echo=True, pool_size=5, max_overflow=10)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

# ---------- ORM 基类与模型 ----------
class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    name: Mapped[str] = mapped_column(length=100)
    email: Mapped[str] = mapped_column(length=255, unique=True, index=True)

# ---------- Pydantic Schemas ----------
class UserCreate(BaseModel):
    name: str
    email: EmailStr

class UserUpdate(BaseModel):
    name: str | None = None
    email: EmailStr | None = None

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    class Config:
        from_attributes = True

# ---------- 依赖:获取数据库会话 ----------
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session

# ---------- 生命周期:创建表 ----------
@asynccontextmanager
async def lifespan(app: FastAPI):
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    await engine.dispose()

app = FastAPI(lifespan=lifespan)

# ---------- CRUD 路由 ----------
@app.post("/users/", response_model=UserResponse)
async def create_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
    """创建用户"""
    # 检查邮箱是否已存在
    stmt = select(User).where(User.email == user_data.email)
    result = await db.execute(stmt)
    if result.scalar_one_or_none():
        raise HTTPException(status_code=400, detail="Email already registered")
    new_user = User(name=user_data.name, email=user_data.email)
    db.add(new_user)
    await db.commit()
    await db.refresh(new_user)
    return new_user

@app.get("/users/", response_model=list[UserResponse])
async def list_users(db: AsyncSession = Depends(get_db)):
    """获取所有用户"""
    result = await db.execute(select(User))
    return result.scalars().all()

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    """根据 ID 获取用户"""
    user = await db.get(User, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

@app.put("/users/{user_id}", response_model=UserResponse)
async def update_user(user_id: int, user_data: UserUpdate, db: AsyncSession = Depends(get_db)):
    """更新用户(部分更新)"""
    user = await db.get(User, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    # 更新字段
    if user_data.name is not None:
        user.name = user_data.name
    if user_data.email is not None:
        # 若修改邮箱,检查是否已被其他用户使用
        if user_data.email != user.email:
            stmt = select(User).where(User.email == user_data.email)
            result = await db.execute(stmt)
            if result.scalar_one_or_none():
                raise HTTPException(status_code=400, detail="Email already in use")
            user.email = user_data.email
    db.add(user)
    await db.commit()
    await db.refresh(user)
    return user

@app.delete("/users/{user_id}")
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db)):
    """删除用户"""
    user = await db.get(User, user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    await db.delete(user)
    await db.commit()
    return {"message": "User deleted successfully"}

3.2 Redis 数据库

典型使用场景

  • String:缓存单值(如 Session、计数器、限流令牌)。
  • Hash:存储对象(如用户资料、配置项),便于单独修改字段。
  • List:消息队列(LPUSH + RPOP)、最新动态列表(LPUSH + LTRIM)。
  • Set:标签系统、共同好友(交集)、去重集合。
  • Sorted Set:排行榜、带权重的任务队列、延时队列(按时间分数排序)。
  • Bitmap:每日签到、用户在线状态。
  • HyperLogLog:独立访客 UV 统计。
  • Stream:可靠消息队列,支持消费者组和消息持久化。

基本操作

import redis
r = redis.Redis(host='localhost', port=6379, db=0)

# String
r.set('name', 'Alice', ex=60)          # 设置并过期
print(r.get('name'))                   # b'Alice'

# Hash
r.hset('user:1', mapping={'name': 'Bob', 'age': 25})
print(r.hgetall('user:1'))             # {b'name': b'Bob', b'age': b'25'}

# List
r.rpush('queue', 'task1', 'task2')
print(r.lpop('queue'))                 # b'task1'

# Set
r.sadd('tags', 'python', 'redis')
print(r.smembers('tags'))              # {b'python', b'redis'}

# ZSet
r.zadd('rank', {'Alice': 100, 'Bob': 90})
print(r.zrange('rank', 0, -1, withscores=True))

Python 使用 Redis

在 Python 中提供了两种连接 Redis 的方式,第一种是直接连接,使用下面这行命令即可。

r = redis.Redis(host='localhost', port= 6379)

第二种是连接池方式。

pool = redis.ConnectionPool(host='localhost', port=6379)

r = redis.Redis(connection_pool=pool)

: 推荐使用连接池方式。连接池机制可以避免频繁创建和释放连接,提升整体的性

FastAPI 使用 Redis

# 安装依赖 pip install fastapi uvicorn redis
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from redis.asyncio import Redis

# ---------- Redis 连接配置 ----------
REDIS_URL = "redis://localhost:6379/0"  # 默认数据库 0
redis_client: Redis | None = None

# ---------- 生命周期管理 ----------
@asynccontextmanager
async def lifespan(app: FastAPI):
    global redis_client
    # 启动时创建 Redis 连接池
    redis_client = Redis.from_url(REDIS_URL, decode_responses=True)
    await redis_client.ping()  # 测试连接
    print("✅ Connected to Redis")
    yield
    # 关闭时释放连接
    await redis_client.close()
    print("🔒 Redis connection closed")

app = FastAPI(lifespan=lifespan)

# ---------- 路由 ----------
@app.get("/ping")
async def ping_redis():
    """测试 Redis 连接"""
    if redis_client is None:
        raise HTTPException(500, "Redis not initialized")
    await redis_client.ping()
    return {"status": "ok"}

@app.get("/set/{key}/{value}")
async def set_key(key: str, value: str):
    """设置键值对"""
    if redis_client is None:
        raise HTTPException(500, "Redis not initialized")
    await redis_client.set(key, value)
    return {"key": key, "value": value, "action": "set"}

@app.get("/get/{key}")
async def get_key(key: str):
    """获取键的值"""
    if redis_client is None:
        raise HTTPException(500, "Redis not initialized")
    value = await redis_client.get(key)
    if value is None:
        raise HTTPException(404, f"Key '{key}' not found")
    return {"key": key, "value": value}

@app.get("/delete/{key}")
async def delete_key(key: str):
    """删除键"""
    if redis_client is None:
        raise HTTPException(500, "Redis not initialized")
    deleted = await redis_client.delete(key)
    return {"key": key, "deleted": deleted > 0}

@app.get("/expire/{key}/{seconds}")
async def expire_key(key: str, seconds: int):
    """设置键的过期时间(秒)"""
    if redis_client is None:
        raise HTTPException(500, "Redis not initialized")
    result = await redis_client.expire(key, seconds)
    if not result:
        raise HTTPException(404, f"Key '{key}' not found")
    return {"key": key, "expire_seconds": seconds}

# ---------- 可选:带过期时间的设置 ----------
@app.get("/setex/{key}/{seconds}/{value}")
async def setex_key(key: str, seconds: int, value: str):
    """设置键值对并指定过期时间"""
    if redis_client is None:
        raise HTTPException(500, "Redis not initialized")
    await redis_client.setex(key, seconds, value)
    return {"key": key, "value": value, "expire_seconds": seconds}

3.3 全文搜索引擎 Elasticsearch

Elasticsearch(简称 ES)是一个开源的分布式搜索与分析引擎,基于 Apache Lucene 构建。它擅长对海量文本做近实时的全文检索,搜索引擎是数据库检索中的重要应用。搜索引擎的优势在于采用了全文搜索的技术,核心原理是“倒排索引”。

from elasticsearch import Elasticsearch
import os

# 连接本地 ES
es = Elasticsearch("http://localhost:9200")

# 或连接 Elastic Cloud
# es = Elasticsearch(
#     cloud_id=os.environ['ELASTIC_CLOUD_ID'],
#     api_key=os.environ['ELASTIC_API_KEY']
# )

# 验证连接
print(es.info())

# 索引文档
doc = {
    "name": "iPhone 15 Pro",
    "brand": "Apple",
    "price": 7999.00,
    "in_stock": True
}
es.index(index="products", id=1, document=doc)

# 搜索文档
resp = es.search(
    index="products",
    query={"match": {"name": "iPhone"}}
)
for hit in resp['hits']['hits']:
    print(hit['_source'])

# 聚合分析
resp = es.search(
    index="products",
    size=0,
    aggs={"by_brand": {"terms": {"field": "brand.keyword"}}}
)
print(resp['aggregations'])

4. 基于角色的访问控制(RBAC)

RBAC 的三大核心要素(数据库表关系)

在后端数据库里,RBAC 通常由五张表构成:

表名前端类比存储内容
用户表(User)登录账号存储 user_idusername(比如张三)
角色表(Role)用户组/权限组存储 role_idrole_name(比如超级管理员、普通会员、访客)
权限表(Permission)路由地址/API接口存储 perm_idperm_code(比如 user:deleteorder:read
用户-角色关联表(多对多)用户属于哪个组张三 -> 关联到“普通会员”
角色-权限关联表(多对多)这个组能访问哪些路由“普通会员” -> 关联 order:read,不关联 user:delete

python 里的实现

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

......

# ---------- 配置 ----------
DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/test_db"

# ---------- 数据库 ----------
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

class Base(DeclarativeBase):
    pass

# 关联表
user_role = Table(
    "user_role",
    Base.metadata,
    Column("user_id", ForeignKey("users.id"), primary_key=True),
    Column("role_id", ForeignKey("roles.id"), primary_key=True),
)
role_permission = Table(
    "role_permission",
    Base.metadata,
    Column("role_id", ForeignKey("roles.id"), primary_key=True),
    Column("permission_id", ForeignKey("permissions.id"), primary_key=True),
)

# 模型
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True)
    password_hash: Mapped[str] = mapped_column(String(255))  # 实际应存哈希
    roles: Mapped[list["Role"]] = relationship(secondary=user_role, back_populates="users")

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50), unique=True)
    users: Mapped[list["User"]] = relationship(secondary=user_role, back_populates="roles")
    permissions: Mapped[list["Permission"]] = relationship(secondary=role_permission, back_populates="roles")

class Permission(Base):
    __tablename__ = "permissions"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100), unique=True)  # 如 "user:list"
    roles: Mapped[list["Role"]] = relationship(secondary=role_permission, back_populates="roles")

# Pydantic 模型
class UserLogin(BaseModel):
    username: str
    password: str

class TokenResponse(BaseModel):
    access_token: str
    token_type: str

# ---------- 应用生命周期 ----------
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 建表
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    # 初始化数据(仅首次运行)
    async with AsyncSessionLocal() as db:
        # 检查是否已有角色
        result = await db.execute(select(Role))
        if not result.scalars().first():
            # 创建权限
            perm_user_list = Permission(name="user:list")
            perm_user_create = Permission(name="user:create")
            db.add_all([perm_user_list, perm_user_create])
            # 创建角色
            admin_role = Role(name="admin", permissions=[perm_user_list, perm_user_create])
            viewer_role = Role(name="viewer", permissions=[perm_user_list])
            db.add_all([admin_role, viewer_role])
            # 创建用户(密码未哈希,仅演示)
            admin_user = User(username="admin", password_hash="admin123", roles=[admin_role])
            viewer_user = User(username="viewer", password_hash="viewer123", roles=[viewer_role])
            db.add_all([admin_user, viewer_user])
            await db.commit()
    yield
    await engine.dispose()

app = FastAPI(lifespan=lifespan)
......

5. 分布式任务队列 Celery

  • 当任务可能失败需要重试、耗时长于 5 秒、或并发量增大时,再升级为 Celery
  • 它的核心作用是处理“耗时”且“需要可靠执行”的后台任务,让你可以把这些任务从主业务流程(如 Web 请求)中剥离出来异步执行。

Celery 三大部件

Celery 不是一个“单体”程序,而是由三个核心组件协同工作的系统:

  • Broker(消息代理) :任务“传达室”。它接收应用发出的任务请求(如“帮我算 10+20”),并将任务放入队列。常用 Redis 或 RabbitMQ。
  • Worker(工作进程) :任务“打工人”。它会持续监控 Broker 中的队列,发现有任务就取走并执行。
  • Backend(结果后端) :任务“档案室”。任务执行完毕后,Worker 会将结果存储在这里,供应用查询。常用 Redis 或数据库。

FastAPI + Celery 实现实例

# 安装依赖 pip install fastapi uvicorn celery redis
from fastapi import FastAPI, HTTPException
from celery import Celery
from celery.result import AsyncResult
import time

# ---------- Celery 配置 ----------
celery_app = Celery(
    "tasks",
    broker="redis://localhost:6379/0",
    backend="redis://localhost:6379/0",
)
celery_app.conf.update(
    task_serializer="json",
    accept_content=["json"],
    result_serializer="json",
    timezone="UTC",
    enable_utc=True,
)

# ---------- 任务定义 ----------
@celery_app.task(name="add")
def add(x: int, y: int) -> int:
    return x + y

@celery_app.task(name="long_task")
def long_task(seconds: int) -> str:
    time.sleep(seconds)
    return f"Slept for {seconds} seconds"

# ---------- FastAPI 应用 ----------
app = FastAPI(title="FastAPI + Celery Demo")

@app.post("/add/{x}/{y}")
async def run_add(x: int, y: int):
    task = add.delay(x, y)
    return {"task_id": task.id, "status": "PENDING"}

@app.post("/long-task/{seconds}")
async def run_long_task(seconds: int):
    task = long_task.delay(seconds)
    return {"task_id": task.id, "status": "PENDING"}

@app.get("/task/{task_id}")
async def get_task_status(task_id: str):
    task_result = AsyncResult(task_id, app=celery_app)
    if task_result.failed():
        raise HTTPException(status_code=500, detail="Task failed")
    if task_result.ready():
        return {
            "task_id": task_id,
            "status": "SUCCESS",
            "result": task_result.result,
        }
    else:
        return {"task_id": task_id, "status": task_result.state}