FastAPI 从入门到实战 — 完整学习笔记
📝 本笔记基于黑马程序员《Python Web开发 FastAPI框架 从入门到实战》课程字幕整理,涵盖了 FastAPI 基础、ORM 操作、项目实战(新闻/用户/收藏/浏览历史/缓存/AI问答)的所有核心知识点。
目录
- 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 异常响应处理
- 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.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 环境干净。
运行项目
两种方式:
- 右上角 Run 按钮:直接点击运行
- 命令行:使用 Uvicorn 服务器
uvicorn main:app --reload
uvicorn:FastAPI 的高性能 ASGI 服务器main:app:main是文件名,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 原生类型注解(如
int、str、bool)来约定数据类型 - 路径参数是必填的(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 参数
| 参数 | 含义 |
|---|---|
... (三个点) | 必填 |
gt | greater than,大于 |
lt | less than,小于 |
ge | greater than or equal,大于等于 |
le | less 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 请求的三个部分
- 请求行:请求方法(GET/POST等)、请求地址、协议版本
- 请求头:元数据信息,如 Content-Type、Token 等
- 请求体:实际要发送到服务器的数据内容
请求体参数的写法(两步走)
第一步:定义类型(基于 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),用法与Path、Query类似 - 在 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 提供两种方式:
- 在装饰器中指定响应类(
response_class):适用于返回相对固定、简单的内容,如 HTML、纯文本 - 返回响应对象:适用于文件下载、流媒体等动态场景,更灵活
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 — 建表
建表三步走
- 创建数据库引擎(连接数据库)
- 定义模型类(对应数据库表)
- 在 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
核心:依赖注入数据库会话
分两步:
- 创建异步会话工厂和数据库会话依赖项
- 在路由中通过
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 缓存热点数据
配套物料
- API 接口规范文档:定义所有接口的地址、方法、参数、响应格式
- SQL 文件:自动创建数据库、表和大量新闻测试数据
- 前端项目代码: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 收藏模块
收藏模块接口
- 检查收藏状态 —
GET /api/favorite/check?newsId=xxx - 添加收藏 —
POST /api/favorite/add - 取消收藏 —
DELETE /api/favorite/remove?newsId=xxx - 获取收藏列表 —
GET /api/favorite/list - 清空收藏列表 —
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 /add | POST /add |
| 删除记录 | DELETE /remove | DELETE /delete/{history_id} |
| 获取列表 | GET /list | GET /list |
| 清空列表 | DELETE /clear | DELETE /clear |
实现思路也是一致的:模块化路由 → 模型类 → CRUD 封装 → 路由调用。
添加浏览记录的逻辑
检查当前用户是否已浏览过该新闻
↓
已浏览 → 更新浏览时间
未浏览 → 新增一条浏览记录
↓
返回结果
3.19 缓存(Redis)
为什么需要缓存?
- 降低数据库压力:高频访问的数据不必每次都查数据库
- 提高响应速度:Redis 是内存级存储,读写速度远高于硬盘(数据库)
- 数据来源:数据库(硬盘,存得住) + 缓存(内存,取得快)
Redis 使用四步走
- 安装 Redis 服务端
- 安装和配置 Redis 客户端
- 封装缓存操作方法
- 设计缓存策略
安装 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_encoder或model_dump()转换 ORM/Pydantic 对象 - 从缓存读出来的数据如果是 ORM 对象,需要用
News(**data)或model_validate()反序列化
3.20 AI 问答功能
实现方式
前端 Vue 项目直接调用阿里云百炼千问大模型的 API。
步骤
- 登录阿里云百炼模型广场
- 选择模型(如通义千问3)→ 点击 API 参考
- 选择对话功能 → 获取 HTTP 请求地址
- 创建并复制 API Key
- 在前端项目代码中配置(
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 |
| 请求体参数 | Body | Field() | pydantic |
| 请求头参数 | Headers | Header() | 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 |
响应类型速查
| 场景 | 方式 |
|---|---|
| 默认 JSON | FastAPI 自动转换 |
| 约束 JSON 格式 | response_model=类型 |
| HTML | response_class=HTMLResponse |
| 文件 | return FileResponse(路径) |
缓存策略速查
| 策略 | 核心原则 |
|---|---|
| 旁路策略 | 先查缓存 → 没有再查库 → 写入缓存 |
| K 设计 | 确保唯一性(前缀+业务ID+参数) |
| 过期时间 | 数据越稳定,缓存越持久;不同类型设不同时间防止雪崩 |

6万+

被折叠的 条评论
为什么被折叠?



