FastAPI 从入门到实战 — 完整学习笔记

该文章已生成可运行项目,

FastAPI 从入门到实战 — 完整学习笔记

📝 本笔记基于黑马程序员《Python Web开发 FastAPI框架 从入门到实战》课程字幕整理,涵盖了 FastAPI 基础、ORM 操作、项目实战(新闻/用户/收藏/浏览历史/缓存/AI问答)的所有核心知识点。


目录

  1. FastAPI 基础入门
    • 1.1 第一个 FastAPI 程序
    • 1.2 路由
    • 1.3 参数简介和路径参数
    • 1.4 路径参数 — Path 类型注解
    • 1.5 查询参数和 Query 类型注解
    • 1.6 请求体参数
    • 1.7 请求体参数 — Field 类型注解
    • 1.8 响应类型 — JSON 格式
    • 1.9 响应类型 — HTML 格式
    • 1.10 响应类型 — 文件格式
    • 1.11 自定义响应数据格式
    • 1.12 异常响应处理
  2. FastAPI 进阶
    • 2.1 中间件
    • 2.2 依赖注入
    • 2.3 ORM 简介及安装
    • 2.4 ORM — 建表
    • 2.5 ORM — 在路由中使用 ORM
    • 2.6 ORM — 查询数据
    • 2.7 ORM — 条件查询(比较判断)
    • 2.8 ORM — 条件查询(模糊查询、与或非、包含)
    • 2.9 ORM — 聚合查询
    • 2.10 ORM — 分页查询
    • 2.11 ORM — 查询总结
    • 2.12 ORM — 新增数据
    • 2.13 ORM — 更新数据
    • 2.14 ORM — 删除数据
    • 2.15 ORM 总结
  3. 头条项目实战
    • 3.1 项目及物料简介
    • 3.2 工程结构
    • 3.3 模块化路由
    • 3.4 数据库和 ORM 配置
    • 3.5 获取新闻分类
    • 3.6 解决跨域问题(CORS)
    • 3.7 获取新闻列表
    • 3.8 获取新闻详情 + 增加浏览量
    • 3.9 获取新闻详情 — 相关推荐
    • 3.10 用户注册
    • 3.11 封装通用成功响应格式
    • 3.12 全局异常处理器
    • 3.13 用户登录
    • 3.14 获取用户信息
    • 3.15 修改用户信息
    • 3.16 修改用户密码
    • 3.17 收藏模块
    • 3.18 浏览历史模块
    • 3.19 缓存(Redis)
    • 3.20 AI 问答功能

1. FastAPI 基础入门

1.1 第一个 FastAPI 程序

创建项目
  • 使用 PyCharm 创建 FastAPI 项目,选择 FastAPI 框架,PyCharm 会自动安装 FastAPI 及相关依赖。
  • 虚拟环境:推荐使用 virtualenv(或 conda),隔离项目依赖,避免不同项目之间的包冲突,保证全局 Python 环境干净。
运行项目

两种方式:

  1. 右上角 Run 按钮:直接点击运行
  2. 命令行:使用 Uvicorn 服务器
uvicorn main:app --reload
  • uvicorn:FastAPI 的高性能 ASGI 服务器
  • main:appmain 是文件名,app 是 FastAPI 实例名
  • --reload:代码修改后自动重启服务器,方便开发调试
自动生成的可交互式文档

FastAPI 项目运行后,访问 http://127.0.0.1:8000/docs 即可查看自动生成的 Swagger 交互式文档,可以直接在文档中测试接口。

第一个程序代码
from fastapi import FastAPI

# 创建 FastAPI 实例
app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.get("/hello/{name}")
async def say_hello(name: str):
    return {"message": f"Hello {name}"}

1.2 路由

路由是什么?

路由是URL 地址与处理函数之间的映射关系。它决定了当用户访问某个特定网址时,服务器应该执行哪段代码来返回结果。

路由的写法

FastAPI 中,路由通过装饰器将 URL 路径绑定到处理函数:

@app.get("/hello")
async def get_hello():
    return {"message": "hello world!"}
  • @app:FastAPI 实例
  • get:请求方法(还支持 post、put、delete、patch 等)
  • "/hello":请求路径
  • return:响应结果(FastAPI 自动将 Python 对象转为 JSON)
@app.get("/user/hello")
async def get_user_hello():
    return {"msg": "我正在学习 FastAPI"}

1.3 参数简介和路径参数

什么是参数?

参数是客户端发请求时附带的额外信息,可以让同一个接口根据参数不同响应不同的结果,实现接口的动态交互

参数的三种类型
参数类型出现位置作用常用方法
路径参数URL 路径的一部分定位特定资源(如某本书、某个用户)GET
查询参数URL 问号后面做筛选、排序、分页GET
请求体参数HTTP 消息体(Body)向服务器提供数据(创建、更新资源)POST、PUT
路径参数

路径参数出现在 URL 路径中,用大括号 {} 定义:

@app.get("/book/{id}")
async def get_book(id: int):
    return {"id": id, "title": f"这是第{id}本书"}
  • 路径参数的名字在路由中定义,需要在处理函数中使用同名的形参来接收
  • 可以添加 Python 原生类型注解(如 intstrbool)来约定数据类型
  • 路径参数是必填的(required),不填会报错

请求示例

  • GET /book/666{"id": 666, "title": "这是第666本书"}
  • GET /book/888{"id": 888, "title": "这是第888本书"}

练习

# 以用户 id 为路径参数设计一个 URL
@app.get("/user/{id}")
async def get_user(id: int):
    return {"id": id, "name": f"普通用户{id}"}

1.4 路径参数 — Path 类型注解

Path 函数的作用
  • Python 原生类型注解只能限制数据类型
  • 使用 Path 函数(从 fastapi 导入)可以为路径参数添加额外的校验信息,如大小限制、长度限制、描述等
常用 Path 参数
参数含义
... (三个点)必填
gtgreater than,大于
ltless than,小于
gegreater than or equal,大于等于
leless than or equal,小于等于
min_length最小长度(用于字符串)
max_length最大长度(用于字符串)
description参数描述(会显示在交互式文档中)
示例:限制 id 范围
from fastapi import FastAPI, Path

@app.get("/book/{id}")
async def get_book(id: int = Path(..., gt=0, lt=101, description="书籍的id,取值范围1-100")):
    return {"id": id, "title": f"这是第{id}本书"}
示例:限制字符串长度
@app.get("/author/{name}")
async def get_author(name: str = Path(..., min_length=2, max_length=10)):
    return {"msg": f"这是{name}的信息"}

1.5 查询参数和 Query 类型注解

查询参数是什么?

查询参数出现在 URL 的问号 ? 后面,格式为 key=value,多个参数用 & 连接。查询参数通常用于筛选、排序和分页等操作。

查询参数的写法

不在路由路径中定义的参数,FastAPI 会自动将其视为查询参数

@app.get("/books")
async def get_books(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

请求示例GET /books?skip=5&limit=20

Query 函数

使用 Query 函数(从 fastapi 导入)可以为查询参数添加额外的类型注解和校验:

from fastapi import FastAPI, Query

@app.get("/books")
async def get_books(
    skip: int = Query(0, ge=0, description="跳过的记录数"),
    limit: int = Query(10, le=100, description="每页返回的最大记录数"),
    q: str = Query(None, min_length=2, alias="search-query")  # alias 为别名
):
    return {"skip": skip, "limit": limit, "query": q}
Query 常用参数
参数含义
default (第一个参数)默认值,如 None 表示可选
...必填
gt/lt/ge/le大小比较
min_length/max_length长度限制
alias别名(URL中显示的参数名)
description描述信息
title参数标题
路径参数 vs 查询参数
# 路径参数:/book/5
# 查询参数:/books?page=2&size=10

@app.get("/news")
async def get_news(
    page: int = Query(1, ge=1),
    page_size: int = Query(10, ge=1, le=100, alias="pageSize")
):
    return {"page": page, "pageSize": page_size}

1.6 请求体参数

请求体参数是什么?

请求体参数出现在HTTP 请求的消息体(body)中,不在 URL 里,相比路径参数和查询参数更加安全。通常用于向服务器提供数据,如创建用户、修改信息等。

HTTP 请求的三个部分
  1. 请求行:请求方法(GET/POST等)、请求地址、协议版本
  2. 请求头:元数据信息,如 Content-Type、Token 等
  3. 请求体:实际要发送到服务器的数据内容
请求体参数的写法(两步走)

第一步:定义类型(基于 Pydantic 的 BaseModel)

from pydantic import BaseModel

class User(BaseModel):
    username: str
    password: str

第二步:类型注解(在处理函数中使用该类型)

@app.post("/register")
async def register(user: User):
    return user
完整示例
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 定义请求体类型
class Book(BaseModel):
    title: str       # 书名
    author: str      # 作者
    publisher: str   # 出版社
    price: float     # 售价

@app.post("/book/add")
async def add_book(book: Book):
    return {"msg": "新增图书成功", "book": book}

请求体示例(JSON):

{
  "title": "Python入门",
  "author": "张三",
  "publisher": "黑马出版社",
  "price": 59.9
}

1.7 请求体参数 — Field 类型注解

Field 函数
  • 来自 pydantic(不是 fastapi),用法与 PathQuery 类似
  • 在 Python 原生类型注解的基础上,为请求体参数添加额外的校验和声明
常用 Field 参数
参数含义
...必填
default默认值
gt/lt/ge/le大小判断
min_length/max_length长度限制
description描述
示例
from pydantic import BaseModel, Field

class User(BaseModel):
    username: str = Field(default="张三", min_length=2, max_length=10,
                          description="用户名,长度2-10个字")
    password: str = Field(..., min_length=3, max_length=20)

@app.post("/register")
async def register(user: User):
    return user
练习示例
from pydantic import BaseModel, Field

class Book(BaseModel):
    title: str = Field(..., min_length=2, max_length=20, description="书名不能为空,长度2-20")
    author: str = Field(..., min_length=2, max_length=10, description="作者长度2-10")
    publisher: str = Field(default="黑马出版社")
    price: float = Field(..., gt=0, description="售价不能为空,价格大于0元")

1.8 响应类型 — JSON 格式

JSON 是 FastAPI 的默认响应格式

FastAPI 会自动将 Python 对象(dict、list 等)转换为 JSON 格式返回,不需要程序员做额外操作。

@app.get("/news")
async def get_news():
    return {"id": 1, "title": "新闻标题", "content": "新闻内容"}

FastAPI 会自动将返回的 dict 转换为 JSON 响应。

使用 response_model 定义响应格式

可以通过 response_model 参数来声明响应模型(定义响应数据的结构和类型),FastAPI 会自动进行数据校验、过滤和文档生成:

from pydantic import BaseModel

class NewsOut(BaseModel):
    id: int
    title: str
    content: str

@app.get("/news/{id}", response_model=NewsOut)
async def get_news(id: int):
    return {"id": id, "title": f"新闻{id}", "content": "这是新闻内容"}

1.9 响应类型 — HTML 格式

设置非 JSON 响应类型的方式

FastAPI 提供两种方式:

  1. 在装饰器中指定响应类response_class):适用于返回相对固定、简单的内容,如 HTML、纯文本
  2. 返回响应对象:适用于文件下载、流媒体等动态场景,更灵活
HTML 响应示例
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get("/html", response_class=HTMLResponse)
async def get_html():
    return "<h1>这是一级标题</h1>"
  • 先从 fastapi.responses 导入 HTMLResponse
  • 在装饰器的 response_class 参数中指定
  • 返回的是 HTML 代码字符串

1.10 响应类型 — 文件格式

FileResponse

使用 FileResponse 直接返回文件内容(图片、PDF、Excel、音频、视频等):

from fastapi import FastAPI
from fastapi.responses import FileResponse

app = FastAPI()

@app.get("/file")
async def get_file():
    path = "./files/1.jpeg"  # 文件路径
    return FileResponse(path)
  • 不再使用 response_class 参数
  • 直接 return FileResponse(文件路径) 返回响应对象
  • 这种方式更灵活,适用于动态响应场景

1.11 自定义响应数据格式

response_model

当响应数据是 JSON 格式时,可以使用 response_model 参数来约束响应的数据格式——定义响应包含哪些属性、是什么类型。

from pydantic import BaseModel

# 自定义响应数据格式(类型)
class News(BaseModel):
    id: int
    title: str
    content: str

@app.get("/news/{id}", response_model=News)
async def get_news(id: int):
    return {
        "id": id,
        "title": f"这是第{id}本书",
        "content": "这是一本好书"
    }
  • 响应数据必须严格符合定义的模型,多一个或少一个字段都会报错
  • response_model 实现了数据校验和格式约束
设置响应类型总结
场景方式关键代码
JSON(默认)FastAPI 自动转换return {"key": "value"}
JSON + 约定格式response_model 参数@app.get("/news", response_model=News)
HTML装饰器中 response_class@app.get("/html", response_class=HTMLResponse)
文件返回响应对象return FileResponse(path)

1.12 异常响应处理

HTTPException

当数据找不到或出现业务异常时,应响应错误信息给客户端。使用 HTTPException中断正常流程并返回错误响应。

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/news/{id}")
async def get_news(id: int):
    # 假设只有 1-6 的新闻
    id_list = [1, 2, 3, 4, 5, 6]

    if id not in id_list:
        raise HTTPException(
            status_code=404,
            detail="您查找的新闻不存在"
        )

    return {"id": id, "title": f"新闻{id}"}
HTTPException 参数
  • status_code:HTTP 状态码,必填
    • 4xx 表示客户端错误(如 404 未找到,401 未授权)
    • 5xx 表示服务器错误
  • detail:异常详情信息,可选但推荐填写,会以 JSON 格式响应给客户端
响应示例
{
  "detail": "您查找的新闻不存在"
}

2. FastAPI 进阶

2.1 中间件

中间件是什么?

中间件是一个函数,在每次请求进入 FastAPI 应用时自动执行——在请求到达路由处理函数之前之后(响应返回前)各执行一次。用于给每个请求的前后添加统一的逻辑代码

使用场景
  • 日志记录
  • 身份认证
  • 性能监控
  • 跨域处理(CORS)
  • 响应头的添加和处理
中间件的执行流程
客户端请求 → 中间件1(请求前) → 中间件2(请求前) → 路由处理函数
                                                    ↓
客户端响应 ← 中间件1(响应前) ← 中间件2(响应前) ← 返回响应

执行顺序:按代码顺序自下而上(代码中后定义的先执行)。

中间件的写法
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def middleware_1(request: Request, call_next):
    print("中间件1 开始了")          # 请求前
    response = await call_next(request)  # 向下传递请求
    print("中间件1 结束了")          # 响应前
    return response                   # 返回响应给客户端

@app.middleware("http")
async def middleware_2(request: Request, call_next):
    print("中间件2 开始了")
    response = await call_next(request)
    print("中间件2 结束了")
    return response
中间件要点
  • 使用 @app.middleware("http") 装饰器
  • 函数必须有两个参数:request(请求对象)和 call_next(传递请求的函数)
  • 必须 await call_next(request) 向下传递请求
  • 必须 return response 返回响应
  • 多个中间件按代码自下而上的顺序执行

2.2 依赖注入

依赖注入是什么?

依赖注入系统用于共享通用的逻辑,减少代码重复。与中间件的区别:

  • 中间件:控制所有请求,每个请求都自动执行
  • 依赖注入程序员说了算,哪个接口需要就注入到哪个接口
使用场景
  • 请求参数重用
  • 业务逻辑复用(如分页逻辑)
  • 数据库连接共享
  • 用户身份认证/权限校验
依赖注入三步走
from fastapi import FastAPI, Depends
from fastapi import Query

app = FastAPI()

# 第一步:创建依赖项(封装重用的代码)
async def common_parameters(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, le=60)
):
    return {"skip": skip, "limit": limit}

# 第二步:导入 Depends(已在上面导入)

# 第三步:声明依赖项(注入到需要的路由)
@app.get("/news/list")
async def get_news_list(commons: dict = Depends(common_parameters)):
    return commons

@app.get("/user/list")
async def get_user_list(commons: dict = Depends(common_parameters)):
    return commons
依赖注入要点
  • 依赖项通常是一个函数(或类),封装可重用的代码
  • 使用 Depends(函数名) 注入到路由处理函数的参数中
  • 注意:不要加括号调用函数,只写函数名
  • 只有注入了依赖项的接口才会执行依赖代码

2.3 ORM 简介及安装

什么是 ORM?

ORM(Object-Relational Mapping)是一种操作数据库的编程技术,让程序员可以不写 SQL 语句,通过面向对象的方式来建表和操作数据。

ORM 的优势
  • 不写重复的 SQL 代码,用面向对象方式操作
  • 代码可读性更高
  • 自动处理数据库连接、事务等操作
  • 有效防止 SQL 注入攻击
SQLAlchemy

本课程选择的 ORM 工具是 SQLAlchemy(企业中使用最多、社区最完善最活跃)。

安装

使用异步版 SQLAlchemy + MySQL 异步驱动:

pip install "sqlalchemy[asyncio]" aiomysql

Mac 系统注意给带有 [asyncio] 的包名加引号。


2.4 ORM — 建表

建表三步走
  1. 创建数据库引擎(连接数据库)
  2. 定义模型类(对应数据库表)
  3. 在 FastAPI 启动时建表
第一步:创建数据库引擎
from sqlalchemy.ext.asyncio import create_async_engine

# 数据库URL格式:数据库+驱动://用户:密码@地址:端口/数据库名
ASYNC_DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/fastapi_first?charset=utf8"

# 创建异步引擎
async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,         # 可选:输出SQL日志
    pool_size=10,      # 连接池活跃连接数
    max_overflow=20    # 额外允许的连接数(最大=10+20=30)
)
第二步:定义模型类
from sqlalchemy.orm import DeclarativeBase, mapped_column, Mapped
from sqlalchemy import String, Integer, Float, DateTime
from datetime import datetime
from sqlalchemy.sql import func

# 基类(放公共字段)
class Base(DeclarativeBase):
    pass

# 具体的表模型类
class Book(Base):
    __tablename__ = "book"  # 表名

    id: Mapped[int] = mapped_column(Integer, primary_key=True, comment="书籍id")
    book_name: Mapped[str] = mapped_column(String(255), comment="书名")
    author: Mapped[str] = mapped_column(String(255), comment="作者")
    price: Mapped[float] = mapped_column(Float, comment="价格")
    publisher: Mapped[str] = mapped_column(String(255), comment="出版社")
    create_time: Mapped[datetime] = mapped_column(
        DateTime, insert_default=func.now(), default=func.now(), comment="创建时间"
    )
    update_time: Mapped[datetime] = mapped_column(
        DateTime, insert_default=func.now(), onupdate=func.now(), comment="更新时间"
    )
第三步:建表
from fastapi import FastAPI

app = FastAPI()

async def create_tables():
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

@app.on_event("startup")
async def startup_event():
    await create_tables()

2.5 ORM — 在路由中使用 ORM

核心:依赖注入数据库会话

分两步:

  1. 创建异步会话工厂数据库会话依赖项
  2. 在路由中通过 Depends 注入数据库会话
创建会话工厂和依赖项
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

# 创建异步会话工厂
async_session_local = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False  # 提交后会话不过期,避免重新查询
)

# 创建依赖项
async def get_db():
    async with async_session_local() as session:
        try:
            yield session          # 返回数据库会话给路由
            await session.commit() # 正常提交事务
        except Exception:
            await session.rollback()  # 异常回滚
            raise
        finally:
            await session.close()     # 关闭会话,避免连接泄漏
路由中使用
from fastapi import Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

@app.get("/books")
async def get_books(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Book))
    books = result.scalars().all()
    return books

2.6 ORM — 查询数据

核心:select + execute
from sqlalchemy import select

# 查询所有
@app.get("/books")
async def get_books(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Book))
    books = result.scalars().all()  # 获取所有
    return books

# 获取单条 — scalars().first()
result = await db.execute(select(Book))
book = result.scalars().first()  # 获取第一条

# 根据主键获取 — db.get()
book = await db.get(Book, 5)  # 获取 id=5 的书
提取数据方式对比
方法用途说明
scalars().all()获取所有返回列表
scalars().first()获取第一条无数据返回 None
scalars().one_or_none()获取一个或 None最多一个结果,多个则报错
scalar_one()恰好一个0个或多个都报错
scalar()获取标量值用于聚合查询返回单个数值
db.get(Model, id)根据主键获取直接基于 db 会话

2.7 ORM — 条件查询(比较判断)

select + where
from sqlalchemy import select

# 根据 id 查找(相等比较)
@app.get("/book/get_book/{book_id}")
async def get_book_by_id(book_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(Book).where(Book.id == book_id)
    )
    book = result.scalars().one_or_none()
    return book

# 根据价格查找(大于等于比较)
@app.get("/book/search")
async def search_book(db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(Book).where(Book.price >= 200)
    )
    books = result.scalars().all()
    return books
比较运算符
写法含义
==等于
!=不等于
>大于
<小于
>=大于等于
<=小于等于

2.8 ORM — 条件查询(模糊查询、与或非、包含)

模糊查询 — like
# 查询作者以"曹"开头的书籍(%匹配任意个字符)
result = await db.execute(
    select(Book).where(Book.author.like("曹%"))
)

# 查询作者以"曹"开头且后面只有一个字符的书籍(_匹配单个字符)
result = await db.execute(
    select(Book).where(Book.author.like("曹_"))
)
通配符含义
%匹配任意个字符(包含零个)
_匹配单个字符
逻辑运算符 — 与或非
# AND:同时满足(用逗号隔开多个条件)
result = await db.execute(
    select(Book).where(Book.author.like("曹%"), Book.price > 100)
)

# OR:或(使用 | 符号)
result = await db.execute(
    select(Book).where(
        (Book.author.like("曹%")) | (Book.price > 100)
    )
)

# NOT:非/取反(使用 ~ 符号)
result = await db.execute(
    select(Book).where(~Book.author.like("曹%"))
)
包含查询 — in_
# 查询 id 在指定列表中的书
id_list = [1, 3, 5, 7]
result = await db.execute(
    select(Book).where(Book.id.in_(id_list))
)

2.9 ORM — 聚合查询

聚合函数

用于统计:数据总量、求和、平均值、最大值、最小值等。

from sqlalchemy import select, func

# 统计数据总量
result = await db.execute(select(func.count(Book.id)))
total = result.scalar()  # 提取单个数值(标量值)

# 求最大值
result = await db.execute(select(func.max(Book.price)))
max_price = result.scalar()

# 求最小值
result = await db.execute(select(func.min(Book.price)))
min_price = result.scalar()

# 求和
result = await db.execute(select(func.sum(Book.price)))
sum_price = result.scalar()

# 求平均值
result = await db.execute(select(func.avg(Book.price)))
avg_price = result.scalar()
常用聚合函数
函数含义
func.count(字段)统计数据总量
func.max(字段)求最大值
func.min(字段)求最小值
func.sum(字段)求和
func.avg(字段)求平均值

2.10 ORM — 分页查询

offset + limit
@app.get("/book/list")
async def get_book_list(
    page: int = 1,         # 页码,默认第一页
    page_size: int = 3,    # 每页条数
    db: AsyncSession = Depends(get_db)
):
    # offset = (页码 - 1) × 每页数量
    skip = (page - 1) * page_size

    result = await db.execute(
        select(Book).offset(skip).limit(page_size)
    )
    books = result.scalars().all()
    return books
  • offset(n):跳过前 n 条数据
  • limit(n):每页最多返回 n 条数据
  • 跳过的数量计算公式(当前页码 - 1) × 每页数量

2.11 ORM — 查询总结

select 查询的完整结构
result = await db.execute(
    select(模型类)           # 查询主体
    .where(条件)             # 筛选条件
    .order_by(字段)          # 排序
    .offset(跳过数量)        # 分页跳过
    .limit(每页数量)         # 分页限制
)
提取结果方式总结
方法适用场景
scalars().all()提取所有数据
scalars().first()提取第一条
scalars().one_or_none()最多返回一个,没有则返回 None
scalar_one()恰好返回一个,0 个或多个都报错
scalar()提取单个数值(配合聚合查询使用)
db.get(Model, id)根据主键直接查询
where 条件类型总结
类型写法
比较判断==, !=, >, <, >=, <=
模糊查询.like("曹%"), .like("曹_")
逻辑与where(条件1, 条件2)
逻辑或where((条件1) | (条件2))
逻辑非where(~条件)
包含.in_([1, 3, 5])

2.12 ORM — 新增数据

新增流程

先构造 ORM 对象 → db.add()db.commit()

@app.post("/book/add")
async def add_book(book_data: BookBase, db: AsyncSession = Depends(get_db)):
    # 构造 ORM 对象
    book_obj = Book(**book_data.model_dump())
    # 添加到事物
    db.add(book_obj)
    # 提交到数据库
    await db.commit()
    # 刷新获取最新数据
    await db.refresh(book_obj)
    return book_obj
新增关键点
  • db.add(ORM对象):只是添加到事物,并未真正提交到数据库
  • await db.commit():才算真正写入数据库
  • await db.refresh(对象):从数据库读回最新的数据(字段有更新时推荐使用)
从请求体转为 ORM 对象
# 请求体参数转为字典
book_dict = book.model_dump()  # Pydantic 类型转字典
# 构造 ORM 对象(两种方式)
book_obj = Book(**book_dict)   # 解包字典
# 或直接用 Book(**book.model_dump())

2.13 ORM — 更新数据

更新流程

先查再改,最后提交

方式一:重新赋值
@app.put("/book/update/{book_id}")
async def update_book(book_id: int, update_data: BookUpdate, db: AsyncSession = Depends(get_db)):
    # 先查找
    db_book = await db.get(Book, book_id)
    if not db_book:
        raise HTTPException(status_code=404, detail="查无此书")

    # 重新赋值
    db_book.book_name = update_data.book_name
    db_book.author = update_data.author
    db_book.price = update_data.price
    db_book.publisher = update_data.publisher

    # 提交
    await db.commit()
    return db_book
方式二:update 语句
from sqlalchemy import update

# 更新(update + where + values)
stmt = update(Book).where(Book.id == book_id).values(
    book_name=new_data.book_name,
    author=new_data.author
)
result = await db.execute(stmt)

# 检查是否命中
if result.rowcount == 0:
    raise HTTPException(status_code=404, detail="用户不存在")

await db.commit()
更新关键点
  • 必须先查到数据,确保存在再修改
  • 更新后必须 commit()
  • 建议检查 result.rowcount(命中的行数)确认更新是否生效
  • rowcount > 0 表示有数据被更新

2.14 ORM — 删除数据

删除流程

先查再删,最后提交

@app.delete("/book/delete/{book_id}")
async def delete_book(book_id: int, db: AsyncSession = Depends(get_db)):
    # 先查找
    db_book = await db.get(Book, book_id)
    if not db_book:
        raise HTTPException(status_code=404, detail="查无此书")

    # 删除
    await db.delete(db_book)

    # 提交
    await db.commit()
    return {"msg": "删除图书成功"}
删除关键点
  • 先查再删:确保要删除的数据存在
  • 不存在时抛出异常提示用户
  • 必须 commit() 才能真正删除

2.15 ORM 总结

ORM 完整使用流程
1. 安装(pip install sqlalchemy[asyncio] aiomysql)
     ↓
2. 建库建表(create database + 定义模型类 + 启动建表)
     ↓
3. 配置依赖注入(创建会话工厂 → 定义依赖项 → 路由中注入)
     ↓
4. 增删改查操作(select / add / update / delete + commit)
增删改查方法速查
操作核心方法关键步骤
select(Model).where(...)execute()scalars().all()
db.add(obj)构造 ORM 对象 → add → commit → refresh
update(Model).where(...).values(...) 或直接赋值先查 → 改 → commit → 检查 rowcount
db.delete(obj)先查 → delete → commit

3. 头条项目实战

3.1 项目及物料简介

项目功能
  • 新闻模块:新闻分类、新闻列表、新闻详情、相关推荐
  • 用户模块:登录、注册、获取/修改个人信息、修改密码
  • 收藏模块:检查收藏状态、添加/取消收藏、收藏列表、清空收藏
  • 浏览历史模块:添加/删除浏览记录、浏览历史列表、清空
  • AI 问答:接入阿里百炼千问大模型
  • 缓存:Redis 缓存热点数据
配套物料
  1. API 接口规范文档:定义所有接口的地址、方法、参数、响应格式
  2. SQL 文件:自动创建数据库、表和大量新闻测试数据
  3. 前端项目代码:Vue 框架的前端界面,用于测试后端接口

3.2 工程结构

项目目录结构(按软件功能拆分)
头条backend/
├── main.py              # 主入口文件(FastAPI实例、中间件、注册路由)
├── config/              # 配置相关(数据库配置、缓存配置)
│   └── db_config.py
├── models/              # 数据库模型类(ORM模型)
│   ├── news.py
│   └── users.py
├── routers/             # 路由层(模块化路由)
│   ├── news.py
│   └── users.py
├── crud/                # 数据库增删改查逻辑
│   ├── news.py
│   └── users.py
├── schemas/             # 数据验证模型(Pydantic类型)
│   └── users.py
├── utils/               # 工具函数(通用逻辑)
│   ├── security.py
│   ├── response.py
│   └── auth.py
└── cache/               # 缓存相关
    └── news_cache.py
各目录职责
目录存放内容
config/数据库配置、缓存配置等
models/数据库 ORM 模型类
routers/按模块拆分的路由文件
crud/数据库增删改查封装的函数
schemas/Pydantic 数据校验模型
utils/工具函数(加密、认证、响应格式等)
cache/缓存读写操作封装

3.3 模块化路由

什么是模块化路由?

将路由代码拆分到独立的文件中(按模块),再在主入口文件中挂载注册。这么做的好处:

  • 项目结构更清晰
  • 代码更容易维护
  • 主入口文件不会过于臃肿
三步实现模块化路由

第一步:模块化目录结构(创建 routers/ 文件夹)

第二步:写路由文件(使用 APIRouter)

# routers/news.py
from fastapi import APIRouter

# 创建 APIRouter 实例
router = APIRouter(
    prefix="/api/news",   # 路由前缀
    tags=["news"]         # 分组标签(在交互式文档中体现)
)

@router.get("/categories")
async def get_categories():
    return {"msg": "获取分类成功"}

第三步:主入口挂载注册

# main.py
from fastapi import FastAPI
from routers import news

app = FastAPI()

# 挂载路由
app.include_router(news.router)
APIRouter 参数
参数含义
prefix路由前缀,该模块下所有接口路径的前半部分
tags分组标签,交互式文档中可按分组折叠/展开接口
路由文件的装饰器变化
# 原来:@app.get("/hello")
# 模块化路由后:@router.get("/hello")  # router 是 APIRouter 实例名

3.4 数据库和 ORM 配置

导入 SQL 文件

通过 PyCharm 的 Database 插件导入配套的 SQL 文件,自动创建数据库 news_app 和 8 张表并填充新闻测试数据。

ORM 配置(config/db_config.py)
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker

# 数据库URL
ASYNC_DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/news_app?charset=utf8"

# 创建异步引擎
async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,
    pool_size=10,
    max_overflow=20
)

# 创建异步会话工厂
async_session_local = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False
)

# 数据库会话依赖项
async def get_db():
    async with async_session_local() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

3.5 获取新闻分类

接口实现流程(适用于所有接口)
1. 写模块化路由(参照 API 接口规范文档)
       ↓
2. 定义模型类(参照数据库表结构)
       ↓
3. 封装 CRUD 方法(数据库增删改查)
       ↓
4. 路由中调用 CRUD 方法 + 响应结果
模型类定义示例(models/news.py)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer, DateTime
from datetime import datetime
from sqlalchemy.sql import func

class Base(DeclarativeBase):
    pass

class NewsCategory(Base):
    __tablename__ = "news_category"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="分类id")
    name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="分类名称")
    sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="排序")
    create_at: Mapped[datetime] = mapped_column(DateTime, insert_default=func.now(), default=func.now())
    update_at: Mapped[datetime] = mapped_column(DateTime, insert_default=func.now(), onupdate=func.now())

    def __repr__(self):
        return f"NewsCategory(id={self.id}, name={self.name})"
CRUD 方法示例(crud/news.py)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models.news import NewsCategory

async def get_categories(db: AsyncSession, skip: int = 0, limit: int = 100):
    result = await db.execute(
        select(NewsCategory).offset(skip).limit(limit)
    )
    return result.scalars().all()
路由中调用(routers/news.py)
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from crud import news as news_crud
from config.db_config import get_db

router = APIRouter(prefix="/api/news", tags=["news"])

@router.get("/categories")
async def get_categories(
    skip: int = 0,
    limit: int = 100,
    db: AsyncSession = Depends(get_db)
):
    categories = await news_crud.get_categories(db, skip, limit)
    return {"code": 200, "message": "success", "data": categories}

3.6 解决跨域问题(CORS)

什么是跨域?
  • CORS(Cross-Origin Resource Sharing):浏览器的安全机制
  • 同源需同时满足三个条件:协议、域名、端口号全部相同
  • 任一不同即为跨域,浏览器会拦截请求
本项目跨域原因
  • 前端 Vue:http://localhost:5173
  • 后端 FastAPI:http://127.0.0.1:8000
  • 域名不同(localhost ≠ 127.0.0.1),端口也不同 → 跨域
解决方案:添加 CORS 中间件
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 全局配置 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],          # 允许访问的源(开发用*,生产环境指定具体域名)
    allow_credentials=True,       # 允许携带 Cookie
    allow_methods=["*"],          # 允许的请求方法
    allow_headers=["*"],          # 允许的请求头
)

⚠️ 生产环境应将 allow_origins 替换为具体的允许域名列表,不要使用 *


3.7 获取新闻列表

接口要求
  • 参数:category_id(分类id)、page(页码)、page_size(每页数量)
  • 响应数据:新闻列表 list + 总量 total + 是否还有更多 has_more
模型类中创建索引(models/news.py)
class News(Base):
    __tablename__ = "news"
    __table_args__ = (
        Index("idx_category_id", "category_id"),       # 按分类查询,高频场景
        Index("idx_publish_time", "publish_time"),      # 按发布时间排序,刚需场景
    )
    # ...字段定义

索引相当于书的目录——按索引查询/排序更快,避免全表扫描。

分页查询 + 聚合计算 CRUD
# crud/news.py
from sqlalchemy import select, func

async def get_news_list(db: AsyncSession, category_id: int, skip: int, limit: int):
    result = await db.execute(
        select(News)
        .where(News.category_id == category_id)
        .offset(skip)
        .limit(limit)
    )
    news_list = result.scalars().all()
    return news_list

async def get_news_count(db: AsyncSession, category_id: int):
    result = await db.execute(
        select(func.count(News.id)).where(News.category_id == category_id)
    )
    return result.scalar_one()  # 恰好一个结果
计算"是否还有更多"
# 当前已显示的数量 = offset + 当前列表长度
has_more = (offset + len(news_list)) < total
别名处理(参数名下划线 vs 驼峰)
@router.get("/list")
async def get_news_list(
    category_id: int = Query(..., alias="categoryId"),
    page: int = Query(1, ge=1),
    page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
    db: AsyncSession = Depends(get_db)
):
    offset = (page - 1) * page_size
    news_list = await news_crud.get_news_list(db, category_id, offset, page_size)
    total = await news_crud.get_news_count(db, category_id)
    has_more = (offset + len(news_list)) < total
    return {
        "code": 200, "message": "success",
        "data": {"list": news_list, "total": total, "has_more": has_more}
    }

3.8 获取新闻详情 + 增加浏览量

获取新闻详情
# crud/news.py
async def get_news_detail(db: AsyncSession, news_id: int):
    result = await db.execute(
        select(News).where(News.id == news_id)
    )
    return result.scalars().one_or_none()  # 有可能没有
增加浏览量(使用 update 语句)
from sqlalchemy import update

async def increase_news_views(db: AsyncSession, news_id: int):
    stmt = (
        update(News)
        .where(News.id == news_id)
        .values(views=News.views + 1)
    )
    result = await db.execute(stmt)
    await db.commit()

    # 检查是否命中数据
    return result.rowcount > 0  # 返回 True/False

# 路由中调用
@app.get("/news/detail")
async def get_news_detail(news_id: int = Query(..., alias="id"), db: AsyncSession = Depends(get_db)):
    news = await news_crud.get_news_detail(db, news_id)
    if not news:
        raise HTTPException(status_code=404, detail="新闻不存在")

    # 增加浏览量
    views_result = await news_crud.increase_news_views(db, news_id)
    if not views_result:
        raise HTTPException(status_code=404, detail="新闻不存在")

    return {"code": 200, "message": "success", "data": news}

3.9 获取新闻详情 — 相关推荐

相关推荐 = 同类的其他新闻
  • 同类category_id 相同
  • 其他:排除当前这条新闻
使用 order_by 排序
async def get_related_news(db: AsyncSession, news_id: int, category_id: int, limit: int = 5):
    result = await db.execute(
        select(News)
        .where(News.category_id == category_id, News.id != news_id)
        .order_by(News.views.desc(), News.publish_time.desc())  # 按浏览量和发布时间降序
        .limit(limit)
    )
    return result.scalars().all()
order_by 排序规则
  • 默认升序(ASC),加 .desc() 表示降序
  • 可以逗号隔开多个排序规则(先按浏览量降序,再按时间降序)
列表推导式提取核心数据(过滤不需要的字段)
# 只返回需要的字段
related_news_list = [
    {
        "id": news.id,
        "title": news.title,
        "image": news.image,
        "author": news.author,
        "publish_time": news.publish_time,
        "category_id": news.category_id,
        "views": news.views,
    }
    for news in related_news
]

3.10 用户注册

注册流程
用户输入用户名密码
    ↓
检查用户名是否已存在 → 存在则报错
    ↓
密码加密(使用 passlib)
    ↓
创建用户(写入数据库)
    ↓
生成 Token(使用 uuid)
    ↓
返回 Token + 用户信息
密码加密(utils/security.py)
from passlib.context import CryptContext

# 创建加密上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 明文 → 密文(注册时用)
def get_hash_password(password: str) -> str:
    return pwd_context.hash(password)

# 验证明文与密文是否一致(登录时用)
def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

安装:pip install passlib[bcrypt]==1.7.4

生成 Token(使用 uuid)
import uuid
from datetime import datetime, timedelta

async def create_token(db: AsyncSession, user_id: int):
    # 生成临时 token
    token = str(uuid.uuid4())

    # 设置过期时间(7天)
    expires_at = datetime.now() + timedelta(days=7)

    # 查询是否已有 token
    result = await db.execute(
        select(UserToken).where(UserToken.user_id == user_id)
    )
    db_token = result.scalars().one_or_none()

    if db_token:
        # 已有 token,更新
        db_token.token = token
        db_token.expires_at = expires_at
    else:
        # 没有 token,新增
        new_token = UserToken(user_id=user_id, token=token, expires_at=expires_at)
        db.add(new_token)

    await db.commit()
    return token
Token 是什么?
  • Token 是一个登录凭证(字符串)
  • HTTP 是无状态的,每次请求时前端在请求头中携带 Token 来证明"已经登录过"
  • 前端在请求头中按标准写法携带:Authorization: Bearer <token值>

3.11 封装通用成功响应格式

为什么封装?

项目中所有接口的成功响应格式都是 {code, message, data},只有 data 内容不同。封装成工具函数可以避免每次手写字典。

utils/response.py
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder

def success_response(message: str = "success", data=None):
    content = {
        "code": 200,
        "message": message,
        "data": data
    }
    return JSONResponse(content=jsonable_encoder(content))
使用
from utils.response import success_response

@router.post("/register")
async def register(...):
    # ... 注册逻辑 ...
    return success_response(message="注册成功", data=response_data)
定义 data 对应的 Pydantic 模型(schemas/users.py)
from pydantic import BaseModel, Field, ConfigDict

class UserInfoBase(BaseModel):
    """用户基础可选信息"""
    nick_name: Optional[str] = Field(None, max_length=50)
    avatar: Optional[str] = None
    gender: Optional[str] = None
    bio: Optional[str] = Field(None, max_length=200)
    phone: Optional[str] = None

class UserInfoResponse(UserInfoBase):
    """用户关键信息(响应用)"""
    id: int
    username: str
    model_config = ConfigDict(from_attributes=True)  # 允许从ORM对象取值

class UserOutResponse(BaseModel):
    """注册/登录的完整响应数据"""
    token: str
    user_info: UserInfoResponse = Field(..., alias="userInfo")
    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True  # 别名和字段名兼容
    )
关键配置说明
  • from_attributes=True:允许从 ORM 对象属性中提取值
  • populate_by_name=True:别名(alias)和字段名可以互相兼容
  • model_validate(orm_obj):从 ORM 对象中提取指定属性值,不包含在模型中的字段(如 password)会被自动忽略

3.12 全局异常处理器

为什么需要全局异常处理?

项目中每个接口都可能产生异常,如果在每个接口中都写 try-except 会违反工程化原则。全局异常处理器可以统一捕获和处理所有异常,返回统一格式的错误信息。

四大类异常处理器(utils/exception.py)
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError, SQLAlchemyError

DEBUG_MODE = True  # 开发模式返回详细错误信息

# 1. 业务层面异常
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"code": exc.status_code, "message": exc.detail, "data": None}
    )

# 2. 数据库完整性约束异常(如唯一性、外键冲突)
async def integrity_error_handler(request: Request, exc: IntegrityError):
    detail = "数据库约束冲突"
    if DEBUG_MODE:
        detail = str(exc.orig)
    return JSONResponse(
        status_code=400,
        content={"code": 400, "message": detail, "data": None}
    )

# 3. SQLAlchemy 数据库错误
async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
    detail = "数据库操作失败"
    if DEBUG_MODE:
        detail = str(exc)
    return JSONResponse(
        status_code=500,
        content={"code": 500, "message": detail, "data": None if not DEBUG_MODE else {"error": str(exc)}}
    )

# 4. 兜底异常(其他未捕获的异常)
async def general_exception_handler(request: Request, exc: Exception):
    detail = "服务器内部错误"
    if DEBUG_MODE:
        detail = str(exc)
    return JSONResponse(
        status_code=500,
        content={"code": 500, "message": detail, "data": None}
    )
注册全局异常处理器(utils/exception_handlers.py)
from fastapi import FastAPI
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from utils.exception import (
    http_exception_handler,
    integrity_error_handler,
    sqlalchemy_error_handler,
    general_exception_handler,
)

def register_exception_handlers(app: FastAPI):
    """注册顺序:子类在前,父类在后;具体的在前,抽象的在后"""
    app.add_exception_handler(HTTPException, http_exception_handler)
    app.add_exception_handler(IntegrityError, integrity_error_handler)
    app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler)
    app.add_exception_handler(Exception, general_exception_handler)  # 兜底
在 main.py 中调用
from utils.exception_handlers import register_exception_handlers

register_exception_handlers(app)

注册后,路由中只需要写好成功的业务逻辑,异常会由全局异常处理器自动接管。


3.13 用户登录

登录流程
用户输入用户名、密码
    ↓
查询用户是否存在 → 不存在则报错
    ↓
验证密码(明文 vs 密文)→ 不一致则报错
    ↓
生成 Token
    ↓
返回 Token + 用户信息
验证用户名和密码(crud/users.py)
from utils.security import verify_password

async def authenticate_user(db: AsyncSession, username: str, password: str):
    # 根据用户名查询用户
    result = await db.execute(
        select(User).where(User.username == username)
    )
    user = result.scalars().one_or_none()

    if not user:
        return None  # 用户不存在

    # 验证密码
    if not verify_password(password, user.password):
        return None  # 密码不一致

    return user
路由中调用
@router.post("/login")
async def login(
    user_data: UserRequest,
    db: AsyncSession = Depends(get_db)
):
    # 验证用户
    user = await users_crud.authenticate_user(db, user_data.username, user_data.password)
    if not user:
        raise HTTPException(status_code=401, detail="用户名或密码错误")

    # 生成 token
    token = await users_crud.create_token(db, user.id)

    # 构造响应
    response_data = UserOutResponse(
        token=token,
        user_info=UserInfoResponse.model_validate(user)
    )
    return success_response(message="登录成功", data=response_data)

3.14 获取用户信息

核心:从请求头获取 Token 进行认证
使用 Header 参数获取请求头数据
from fastapi import Header

@router.get("/info")
async def get_user_info(
    authorization: str = Header(..., alias="Authorization"),
    db: AsyncSession = Depends(get_db)
):
    # 解析 token(格式:Bearer <token值>)
    token = authorization.replace("Bearer ", "")

    # 根据 token 验证用户
    user = await users_crud.get_user_by_token(db, token)
    # ...
封装认证工具函数(utils/auth.py)
from fastapi import Depends, Header, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from config.db_config import get_db
from crud import users as users_crud

async def get_current_user(
    authorization: str = Header(..., alias="Authorization"),
    db: AsyncSession = Depends(get_db)
):
    # 解析 token(去掉 "Bearer " 前缀)
    token = authorization.replace("Bearer ", "")

    # 根据 token 查用户
    user = await users_crud.get_user_by_token(db, token)
    if not user:
        raise HTTPException(status_code=401, detail="无效的令牌或已过期的令牌")

    return user
在路由中通过依赖注入使用
from utils.auth import get_current_user

@router.get("/info")
async def get_user_info(user: User = Depends(get_current_user)):
    data = UserInfoResponse.model_validate(user)
    return success_response(message="获取个人信息成功", data=data)
  • 使用 Depends(get_current_user) 依赖注入,可以直接获取认证信息、逻辑和返回值
  • 多个接口需要认证时,直接复用即可
Token 验证流程(crud/users.py)
async def get_user_by_token(db: AsyncSession, token: str):
    # 查 token 记录
    result = await db.execute(
        select(UserToken).where(UserToken.token == token)
    )
    db_token = result.scalars().one_or_none()

    # 没有 token 或已过期
    if not db_token or db_token.expires_at < datetime.now():
        return None

    # 根据 token 关联的 user_id 查用户
    result = await db.execute(
        select(User).where(User.id == db_token.user_id)
    )
    return result.scalars().one_or_none()

3.15 修改用户信息

修改流程
验证 Token(是否登录)
    ↓
获取用户输入的新数据(请求体参数)
    ↓
更新数据库
    ↓
检查是否命中(rowcount > 0)
    ↓
返回更新后的用户信息
model_dump:Pydantic 转字典
# 将用户输入的 Pydantic 类型转为字典,便于更新数据库
update_dict = user_data.model_dump(exclude_unset=True, exclude_none=True)
  • exclude_unset=True:只包含用户真正设置了值的字段(未设置的不包含)
  • exclude_none=True:排除值为 None 的字段
  • 两个参数配合使用,做到**“没有设置值的不更新”**,避免把默认头像覆盖为 None
更新方法(crud/users.py)
from sqlalchemy import update

async def update_user(db: AsyncSession, username: str, user_data: UserUpdateRequest):
    # model_dump + 解包
    stmt = (
        update(User)
        .where(User.username == username)
        .values(**user_data.model_dump(exclude_unset=True, exclude_none=True))
    )
    result = await db.execute(stmt)
    await db.commit()

    # 检查命中
    if result.rowcount == 0:
        raise HTTPException(status_code=404, detail="用户不存在")

    # 获取更新后的用户
    updated_user = await get_user_by_username(db, username)
    return updated_user
路由
@router.put("/update")
async def update_user_info(
    user_data: UserUpdateRequest,
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    updated_user = await users_crud.update_user(db, user.username, user_data)
    data = UserInfoResponse.model_validate(updated_user)
    return success_response(message="更新信息成功", data=data)

3.16 修改用户密码

修改密码流程
验证 Token(是否登录)
    ↓
验证旧密码是否正确 → 错误则报错
    ↓
新密码加密
    ↓
更新数据库
    ↓
返回成功
密码修改方法(crud/users.py)
from utils.security import verify_password, get_hash_password

async def change_password(db: AsyncSession, user: User, old_password: str, new_password: str):
    # 验证旧密码
    if not verify_password(old_password, user.password):
        return False

    # 新密码加密
    hashed_new_pwd = get_hash_password(new_password)

    # 更新密码
    user.password = hashed_new_pwd

    # ⚠️ 关键:add 一下确保由 SQLAlchemy 真正接管对象
    # 规避 session 过期/关闭导致 commit 失败的问题
    db.add(user)
    await db.commit()
    await db.refresh(user)

    return True

⚠️ 在 commit 之前 db.add(user) 的作用是确保 SQLAlchemy 真正接管 user 对象,规避因 session 过期或关闭导致的提交失败问题。


3.17 收藏模块

收藏模块接口
  1. 检查收藏状态GET /api/favorite/check?newsId=xxx
  2. 添加收藏POST /api/favorite/add
  3. 取消收藏DELETE /api/favorite/remove?newsId=xxx
  4. 获取收藏列表GET /api/favorite/list
  5. 清空收藏列表DELETE /api/favorite/clear
唯一约束(模型类中)
class Favorite(Base):
    __tablename__ = "favorite"
    __table_args__ = (
        UniqueConstraint("user_id", "news_id"),  # 当前用户 + 当前新闻 只能收藏一次
        Index("idx_user_id", "user_id"),
    )
检查收藏状态
async def is_news_favorite(db: AsyncSession, user_id: int, news_id: int) -> bool:
    result = await db.execute(
        select(Favorite).where(Favorite.user_id == user_id, Favorite.news_id == news_id)
    )
    return result.scalars().one_or_none() is not None  # 返回布尔值
联表查询(收藏列表)
# 查询主体是 News,联合 Favorite 表
result = await db.execute(
    select(News, Favorite.create_at.label("favorite_time"), Favorite.id.label("favorite_id"))
    .join(Favorite, Favorite.news_id == News.id)   # 联表条件
    .where(Favorite.user_id == user_id)            # 筛选当前用户
    .order_by(Favorite.create_at.desc())           # 按收藏时间降序
    .offset(skip)
    .limit(limit)
)
联表查询关键知识
  • select(主体模型类) → 查询的主体
  • .join(联合模型类, 联合条件) → 联表查询
  • 字段冲突时使用 .label("别名") 起别名
  • 返回结果是元组列表[(新闻对象, 收藏时间, 收藏id), ...]
清空收藏列表
async def remove_all_favorites(db: AsyncSession, user_id: int):
    result = await db.execute(
        delete(Favorite).where(Favorite.user_id == user_id)
    )
    await db.commit()
    return result.rowcount  # 返回删除数量

3.18 浏览历史模块

浏览历史模块的功能与收藏模块非常类似

功能收藏模块浏览历史模块
添加记录POST /addPOST /add
删除记录DELETE /removeDELETE /delete/{history_id}
获取列表GET /listGET /list
清空列表DELETE /clearDELETE /clear

实现思路也是一致的:模块化路由 → 模型类 → CRUD 封装 → 路由调用。

添加浏览记录的逻辑
检查当前用户是否已浏览过该新闻
    ↓
已浏览 → 更新浏览时间
未浏览 → 新增一条浏览记录
    ↓
返回结果

3.19 缓存(Redis)

为什么需要缓存?
  • 降低数据库压力:高频访问的数据不必每次都查数据库
  • 提高响应速度:Redis 是内存级存储,读写速度远高于硬盘(数据库)
  • 数据来源:数据库(硬盘,存得住) + 缓存(内存,取得快)
Redis 使用四步走
  1. 安装 Redis 服务端
  2. 安装和配置 Redis 客户端
  3. 封装缓存操作方法
  4. 设计缓存策略
安装 Redis 服务端
# Mac
brew install redis
redis-server  # 测试是否安装成功

# Windows:双击安装包安装
配置 Redis 客户端(config/cache_config.py)
import redis.asyncio as redis

REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0

# 创建 Redis 连接对象
redis_client = redis.Redis(
    host=REDIS_HOST,
    port=REDIS_PORT,
    db=REDIS_DB,
    decode_responses=True  # 将字节解码为字符串
)
封装缓存操作方法
import json
from typing import Any
from config.cache_config import redis_client

# 读取缓存(普通字符串)
async def get_cache(key: str) -> str | None:
    try:
        return await redis_client.get(key)
    except Exception as e:
        print(f"获取缓存失败: {e}")
        return None

# 读取缓存(列表/字典,自动序列化)
async def get_json_cache(key: str):
    try:
        data = await redis_client.get(key)
        if data:
            return json.loads(data)
        return None
    except Exception as e:
        print(f"获取JSON缓存失败: {e}")
        return None

# 设置缓存
async def set_cache(key: str, value: Any, expire: int = 3600) -> bool:
    try:
        # 如果 value 是字典或列表,转为 JSON 字符串存储
        if isinstance(value, (dict, list)):
            value = json.dumps(value, ensure_ascii=False)
        await redis_client.setex(key, expire, value)
        return True
    except Exception as e:
        print(f"设置缓存失败: {e}")
        return False
缓存策略 — 旁路策略(Cache Aside)
读操作:先查缓存 → 有则直接返回 → 无则查数据库 → 写入缓存 → 返回
写操作:更新数据库 → 删除/更新对应缓存
缓存 K 的设计原则
  • 保证唯一性,避免缓存数据冲突
  • 新闻分类:news:categories
  • 新闻列表:news:list:{category_id}:{page}:{page_size}
新闻分类缓存示例
CATEGORIES_KEY = "news:categories"

# 获取缓存的分类
async def get_cached_categories():
    return await get_json_cache(CATEGORIES_KEY)

# 写入分类缓存(过期时间7200秒 = 2小时)
async def set_cached_categories(data, expire=7200):
    return await set_cache(CATEGORIES_KEY, data, expire)

# CRUD 中应用旁路策略
async def get_categories(db, skip, limit):
    # 1. 先查缓存
    cached = await get_cached_categories()
    if cached:
        return cached

    # 2. 缓存没有,查数据库
    categories = await db.execute(select(NewsCategory).offset(skip).limit(limit))
    categories = categories.scalars().all()

    # 3. 写入缓存(为下一次请求提速)
    if categories:
        # ORM 对象转 JSON 兼容格式
        data = jsonable_encoder(categories)
        await set_cached_categories(data)

    return categories
过期时间设计原则
数据类型过期时间原因
分类/配置7200s(2小时)数据稳定,变化少
列表600s(10分钟)数据变化较快
详情1800s(30分钟)中等
验证码120s(2分钟)极短时效

⚠️ 不同类型的数据设置不同的过期时间,可以避免所有缓存同时过期导致缓存雪崩

Redis 相关操作注意事项
  • Redis 不支持直接存储 ORM 对象,需要先转成字典/列表/字符串
  • 使用 jsonable_encodermodel_dump() 转换 ORM/Pydantic 对象
  • 从缓存读出来的数据如果是 ORM 对象,需要用 News(**data)model_validate() 反序列化

3.20 AI 问答功能

实现方式

前端 Vue 项目直接调用阿里云百炼千问大模型的 API。

步骤
  1. 登录阿里云百炼模型广场
  2. 选择模型(如通义千问3)→ 点击 API 参考
  3. 选择对话功能 → 获取 HTTP 请求地址
  4. 创建并复制 API Key
  5. 在前端项目代码中配置(src/config/api.js
// src/config/api.js
export const OPENAI_API_KEY = "sk-xxxxxxxxxxxxxx";  // 替换为自己的 API Key
export const OPENAI_API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1";
export const MODEL_NAME = "qwen-plus";
API 调用要点(了解)
  • 请求方法:POST

  • 消息格式(messages):列表套字典

    [
      {"role": "system", "content": "你是一个有帮助的助手"},
      {"role": "user", "content": "用户的问题"}
    ]
    
  • role:消息角色(system=系统设定,user=用户消息,assistant=模型回复)

⚠️ API Key 需要实名认证后才能创建,一个账号最多创建 20 个密钥。


附录:FastAPI 核心知识速查表

参数类型速查

参数类型出现位置类型注解函数来源
路径参数URL 路径Path()fastapi
查询参数URL ? 后面Query()fastapi
请求体参数BodyField()pydantic
请求头参数HeadersHeader()fastapi

ORM 操作速查

操作SQLAlchemy 写法
查询所有select(Model)scalars().all()
条件查询select(Model).where(...)
聚合select(func.count(Model.id))scalar()
排序.order_by(Model.field.desc())
分页.offset(skip).limit(limit)
新增db.add(obj) + commit() + refresh()
更新update(Model).where(...).values(...) 或直接赋值 + commit()
删除db.delete(obj)delete(Model).where(...) + commit()
联表select(ModelA).join(ModelB, 条件)
别名.label("别名")
检查命中result.rowcount > 0

响应类型速查

场景方式
默认 JSONFastAPI 自动转换
约束 JSON 格式response_model=类型
HTMLresponse_class=HTMLResponse
文件return FileResponse(路径)

缓存策略速查

策略核心原则
旁路策略先查缓存 → 没有再查库 → 写入缓存
K 设计确保唯一性(前缀+业务ID+参数)
过期时间数据越稳定,缓存越持久;不同类型设不同时间防止雪崩
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值