FastAPI 学习规划大纲
一. 基础知识补充
HTTP 请求方法(GET, POST, PUT, DELETE)。
HTTP 状态码及其含义(200, 400, 401, 404, 500 等)。
RESTful API 设计原则。
-
HTTP 请求方法
| 方法 | 描述 | 示例 |
|---|---|---|
| GET | 获取资源,不会对服务器数据产生影响 | GET /users/1 |
| POST | 创建资源,向服务器提交新数据 | POST /users |
| PUT | 更新资源,修改已有数据 | PUT /users/1 |
| DELETE | 删除资源 | DELETE /users/1 |
-
HTTP 状态码
| 状态码 | 含义 | 适用场景 |
|---|---|---|
| 200 OK | 请求成功 | 获取用户信息时返回 |
| 201 Created | 资源创建成功 | 创建用户时返回 |
| 400 Bad Request | 请求参数错误 | 缺少必填参数 |
| 401 Unauthorized | 未授权访问 | 未提供正确的 API 令牌 |
| 403 Forbidden | 权限不足 | 用户无权访问某资源 |
| 404 Not Found | 资源未找到 | 查询不存在的用户 |
| 500 Internal Server Error | 服务器内部错误 | 数据库异常 |
-
RESTful API 设计原则
| 原则 | 说明 |
|---|---|
| 资源(Resource) | 通过 URL 定义资源,如 /users/{id} 表示用户资源 |
| 无状态(Stateless) | 每次请求都应包含所有必要信息,服务器不存储会话状态 |
| 使用 HTTP 方法 | 根据操作选择 GET、POST、PUT、DELETE |
| 一致的响应格式 | 统一 JSON 格式,例如 { "id": 1, "name": "Alice" } |
| 使用状态码 | 通过 HTTP 状态码传达 API 结果 |
| 分页与过滤 | 大量数据时提供分页支持,如 /users?page=2&size=10 |
| API 版本管理 | 通过 URL 管理版本,如 /api/v1/users |
| 安全性 | 采用 JWT、OAuth2 进行身份认证 |
二. FastAPI 简介
2.1 FastAPI 概述
FastAPI 的核心特性
| 特性 | 说明 |
|---|---|
| 高性能 | 基于 Starlette 和 Pydantic,使用 Python 的 异步(async) 特性,比 Flask 更快,性能接近 Node.js 和 Go。 |
| 基于类型注解 | 使用 Python 的类型提示(Type Hints),确保代码的可读性、可维护性,并提供自动数据验证。 |
| 自动生成 API 文档 | 内置支持 Swagger UI 和 ReDoc,访问 /docs 或 /redoc 即可查看 API 文档。 |
| 数据校验 | 依赖 Pydantic 进行数据验证,确保请求参数符合预期格式。 |
| 异步支持 | 原生支持 async/await,适用于高并发的异步应用,如 WebSocket、背景任务等。 |
| 依赖注入 | 内置依赖注入(Dependency Injection)机制,方便管理数据库连接、认证等。 |
| 与标准 Python 兼容 | 兼容标准 Python 代码,可与 Flask、Django、SQLAlchemy 等无缝集成。 |
示例:自动生成 API 文档
-
访问
http://127.0.0.1:8000/docs查看 Swagger UI -
访问
http://127.0.0.1:8000/redoc查看 ReDoc 文档
FastAPI 的应用场景
| 应用场景 | 说明 |
|---|---|
| Web API | 适用于 RESTful API 和 GraphQL API 开发,如用户管理系统。 |
| 微服务 | 由于 FastAPI 轻量且高性能,适用于微服务架构。 |
| 异步处理 | 内置 async 支持,可用于 WebSocket、任务队列等高并发应用。 |
| 机器学习 & AI | 可与 TensorFlow、PyTorch 等结合,构建 AI 服务,如图像识别 API。 |
| 自动化系统 | 适用于 IoT 设备管理、自动化任务、数据采集系统等。 |
2.2 FastAPI 与 Flask/Django 的对比
(1) FastAPI 为什么比 Flask 快?
| 对比项 | FASTAPI | FLASK |
|---|---|---|
| 性能 | 高性能(基于 ASGI,异步支持) | 低于 FastAPI(基于 WSGI,默认同步) |
| 异步支持 | 原生支持 async/await |
需手动集成,如 Quart 或 Flask-async |
| 类型安全 | 使用 Python 类型注解,自动校验 | 需手动校验请求数据 |
| 自动文档 | 内置 OpenAPI(Swagger)和 ReDoc | 需手动配置,如 Flask-RESTful |
| 数据校验 | 内置 Pydantic 进行强类型数据校验 | 需手动使用 marshmallow 或 pydantic |
-
FastAPI 采用 ASGI(Asynchronous Server Gateway Interface),比 Flask 传统的 WSGI(Web Server Gateway Interface) 更适合高并发应用
(2) FastAPI 与 Django Rest Framework(DRF)区别
| 对比项 | FASTAPI | DJANGO REST FRAMEWORK(DRF) |
|---|---|---|
| 框架类型 | 轻量级 Web API 框架 | 基于 Django,适用于大型项目 |
| 性能 | 高性能,异步支持 | 性能较低,默认同步 |
| 数据模型 | Pydantic 进行数据校验 | Django ORM 进行数据管理 |
| 文档生成 | 内置 Swagger UI 和 ReDoc | 需手动安装 drf-yasg 生成 API 文档 |
| 开发复杂度 | 简单、现代、基于类型注解 | 需要 Django 生态,学习曲线较陡 |
FastAPI 适用于 高性能异步 API,而 DRF 更适合 大型 Django 项目,如管理后台、企业级应用。
FastAPI 结合了 Flask 的轻量和 Django 的结构化,同时提升了 性能、异步支持、自动文档 等特性,非常适合现代 Web API 开发!
三. 安装与使用
3.1 安装 FastAPI
"""
FastAPI 依赖 Python 3.7+,请确保你的 Python 版本符合要求。这是使用python3.11 版本
"""
#01 安装 FastAPI
pip install fastapi
#02 安装 ASGI 服务器(uvicorn)
pip install uvicorn
提示**:`uvicorn` 是一个高性能的 ASGI 服务器,它能让你的 FastAPI 应用跑起来。
#03 验证安装
python -c "import fastapi; print(fastapi.__version__)"
3.2 运行FastAPI 应用
(1) 启动服务 重点
"""
现在,我们来写一个简单的 FastAPI 应用,让它返回 `"Hello, FastAPI!"`。
文件名称:
01-第一个自定义书写的应用程序.py
uvicorn 01-第一个自定义书写的应用程序:app --reload # 终端运行
API文档生成地址:
Swagger UI:http://127.0.0.1:8000/docs
ReDoc 文档:http://127.0.0.1:8000/redoc
"""
from fastapi import FastAPI
import uvicorn
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}"}
if __name__ == "__main__":
uvicorn.run("01-第一个自定义书写的应用程序:app",port=8000,host="0.0.0.0",reload=True)
#02 访问 http://127.0.0.1:8000/
{"message": "Hello, FastAPI!"}
#02 传参数访问
http://127.0.0.1:8000/hello/guoguo
{
"message": "Hello guoguo"
}
-
API 文档
(2) 终端运行 FastAPI
#01 在终端运行以下命令:
uvicorn main:app --reload
uvicorn 01-第一个自定义书写的应用程序:app --reload
#02 解释下:
--reload :开启热重载,修改代码后不需要手动重启服务器。
#03 如果运行成功,终端会输出类似的信息:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
四. 路由与请求
4.1 定义路由
| 方法 | 描述 | 示例请求路径 | 示例操作 |
|---|---|---|---|
| GET | 获取资源 | /items/{item_id} |
获取某个资源的信息 |
| POST | 创建资源 | /create_item/ |
提交表单数据,创建新资源 |
| PUT | 更新资源(全量更新) | /update_item/{id} |
更新现有资源 |
| DELETE | 删除资源 | /delete_item/{id} |
删除指定资源 |
-
GET、POST、PUT、DELETE 的基本使用。
"""
@app.get()
@app.post()
@app.put()
@app.delete()
"""
from fastapi import FastAPI
import uvicorn
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}"}
@app.get("/index")
async def index():
return {"message": "get"}
@app.post("/index")
async def index():
return {"message": "post"}
@app.put("/index/{id}")
async def index(id: int):
return {"message": f"put {id}"}
@app.delete("/index/{id}")
async def index(id: int):
return {"message": f"delete {id}"}
if __name__ == "__main__":
uvicorn.run("02-路径操作和装饰器方法:app",port=8000,reload=True)
4.2 路径装饰器参数
"""
@app.get('/items',tags=["定义标签:items测试接口 考勤功能"],
summary="提示信息:这是测试items接口 考勤打卡相关",
description="详细信息:这是个测试的详情内容:用于考勤不卡 需要传递参数 xxx xxx ",
response_description="返回结果温馨提示:返回值是: 200",
deprecated=True # 接口失效
)
"""
from fastapi import FastAPI
import uvicorn
app = FastAPI()
@app.get('/items',tags=["定义标签:items测试接口 考勤功能"],
summary="提示信息:这是测试items接口 考勤打卡相关",
description="详细信息:这是个测试的详情内容:用于考勤不卡 需要传递参数 xxx xxx ",
response_description="返回结果温馨提示:返回值是: 200",
deprecated=True # 接口失效
)
def test():
return {"message": "home get"}
if __name__ == "__main__":
uvicorn.run("03-装饰器路径参数:app",port=8000,reload=True)
-
图一
-
图二
4.3 路由分发 include_router
"""
路由分发语法
子路由:
from fastapi import APIRouter
shop = APIRouter()
总路由引用:
from app01.urls import shop
app = FastAPI()
app.include_router(shop,prefix='/shop',tags=['用户购物接口'])
解释下: prefix=/shop 这是前缀
我们在apps 下方创建两个包文件,对应两个路由
/shop
/user
└── apps
├── __init__.py
├── app01
│ ├── __init__.py
│ └── urls.py
├── app02
│ ├── __init__.py
│ └── urls.py
└── main.py
"""
#01 app01.urls文件内容
from fastapi import APIRouter
shop = APIRouter()
@shop.get("/data")
def shopP_data():
return {"code": 200, "msg": "app01 shop_data "}
@shop.get("/reg")
def shop_reg():
return {"code": 200, "msg": "app01 shop_reg "}
#02 app02.urls文件内容
from fastapi import APIRouter
user = APIRouter()
@user.get("/login")
def user_data():
return {"code": 200, "msg": "app02 user_login "}
@user.get("/regis")
def user_reg():
return {"code": 200, "msg": "app02 user_regis "}
#03 main.py 文件内容
import uvicorn
from fastapi import FastAPI
from app01.urls import shop
from app02.urls import user
app = FastAPI()
app.include_router(shop,prefix='/shop',tags=['用户购物接口'])
app.include_router(user,prefix='/user',tags=['用户管理接口'])
if __name__ == '__main__':
uvicorn.run('main:app',port=8000,reload=True)
4.4 路径参数
"""
#01 定义单个参数参数
@app.get('/user/{id}
def user(id:int):
pass
#02 多个参数
@app.get('/user/{id}/{user}')
def user(id:int,user:str):
pass
注意:如果出现重复 url 会按照查找顺序
"""
from fastapi import FastAPI
import uvicorn
app = FastAPI()
# 如果访问 /user/1 会按照顺序查找
@app.get('/user/1')
def get_user():
return {'username': '1'}
@app.get('/user/{id}/{user}',tags=["人员信息查询接口"],
summary="人员信息相关",
description="需要传入 id值"
)
def user(id:int,user:str):
print(f'id {id}',type(id))
print(f'user {user}',type(user))
return {
"code":"200",
"message": [id,user]
}
if __name__ == "__main__":
uvicorn.run("05-路径参数:app",port=8000,reload=True)
4.5 查询参数(请求参数)
"""
## 只要路径参数中没有的 都是查询参数
"""
from typing import Union
import uvicorn
from fastapi import FastAPI
app = FastAPI()
# 01 查询参数,只要路径参数中没有的 都是查询参数(xl,gj)
@app.get('/user/{id}', tags=['用户查询参数'])
async def read_user(
id: int, # 指定默认类型 id: int (没有指定默认值为 必选项)
xl: Union[str, int] = None, # 指定多个默认类型,并指定默认值(指定默认值后为 可选项目)
gj=None):
return {
'id': id,
'xl': xl,
'gj': gj
}
if __name__ == "__main__":
uvicorn.run("06-请求参数设置:app", port=8000, reload=True)
4.6 请求体数据校验 Pydantic
"""
# 01导入包
from pydantic import BaseModel, Field, validator, ValidationError
"""
from datetime import date
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel, Field, validator, ValidationError
app03 = APIRouter()
# 生成校验数据模型
class User(BaseModel):
name: str = Field(..., pattern="^[a-zA-Z0-9-]") # 允许字母、数字和破折号
age: int = Field(default=0, gt=0, lt=100) # 默认值是0,数据需要大于0 小于100
birth: Union[date, None] = None # 默认值是None,类型是date,或者空
guo: List[int] = [] # 默认值是空列表
jun: Optional[str] = None # 默认值是None,类型是字符串
test: str # 使用下面方式验证数据,长度大于5
@validator("test")
def name_ll(cls, v):
if len(v) <= 5:
raise ValueError('长度小于5') # 抛出异常
return v
# 接收数据并返回
@app03.post("/data")
async def data(user: User):
print(user, type(user)) # 接收数据,打印 User 实例
print(user.dict()) # 转为字典并打印
return user # FastAPI 会自动将 User 对象转成字典格式返回
#02 嵌套模型
class Data(BaseModel):
data: List[User] # 继承user模型
@app03.post("/data_user")
async def data_user(data: Data):
print(data, type(data))
return data
4.7 form 表单数据
"""
语法使用:
(1)导入包
from fastapi import APIRouter, Form
(2)使用方式
async def app04_post(username: str = Form(...), password: str = Form(...)):
"""
from fastapi import APIRouter, Form
app04 = APIRouter()
## 接收From表单格式
@app04.post("/app04")
async def app04_post(username: str = Form(...), password: str = Form(...)): # 接收form表单格式数据
print(username, password)
return {"username": username, "password": password}
4.8 文件上传
| 特性 | FILE |
UPLOADFILE |
|---|---|---|
| 文件存储方式 | 文件内容加载到内存 | 使用临时文件,不直接加载到内存中 |
| 适用场景 | 适用于小文件 | 适用于大文件 |
| 文件读取方式 | 直接通过 bytes 获取 |
使用异步方法,通过 read() 获取 |
| 文件元数据获取 | 无 | 可以获取文件名、文件类型等信息 |
| 性能 | 内存消耗较大(对于大文件不推荐) | 内存消耗较小,更适合大文件上传 |
-
小文件上传:可以使用
File,操作简单。 -
大文件上传:推荐使用
UploadFile,以避免内存占用过高。
"""
语法结构
(1) 导入包 File UploadFile
from fastapi import APIRouter, File, UploadFile
(2) 使用语法
async def app05_file(file: UploadFile = File(...)):
"""
import os
from fastapi import APIRouter, File, UploadFile
from typing import List
app05 = APIRouter()
# 01 单个小文件上传 放在内存里
@app05.post("/file", summary="上传单个小文件")
async def app05_file(file: bytes = File(...)):
"""
处理单个小文件上传,文件内容存储在内存中
:param file: 传入的文件数据
:return: 返回文件的大小
"""
print(file) # 打印文件内容(字节流)
return {
"file": len(file) # 返回文件的字节数
}
# 02 批量上传小文件
@app05.post("/files", summary="批量上传小文件")
async def app05_files(files: List[bytes] = File(...)):
"""
处理批量上传文件,文件内容存储在内存中
:param files: 传入的多个文件数据
:return: 返回上传的文件数
"""
for file in files:
print(len(file)) # 打印每个文件的字节数
return {
"files": len(files) # 返回上传的文件数量
}
# 定义存储文件的目录
UPLOAD_DIR = "uploaded_files" # 可以根据需要更改目录路径
# 确保上传目录存在,如果不存在则创建
os.makedirs(UPLOAD_DIR, exist_ok=True)
# 03 单个小文件上传 放在本地
@app05.post("/uploadfile", summary="上传单个文件并保存")
async def app05_file(file: UploadFile = File(...)):
"""
处理单个文件上传并保存到本地磁盘
:param file: 上传的文件对象
:return: 返回上传文件的信息,如文件大小和名称
"""
file_location = os.path.join(UPLOAD_DIR, file.filename) # 拼接文件存储路径
with open(file_location, "wb") as f:
f.write(await file.read()) # 将上传的文件写入本地文件系统
return {
"code": 200, # 状态码
"文件大小": f'{os.path.getsize(file_location) // (1024 * 1024)}MB', # 获取文件大小并转化为MB
"文件名称": file.filename # 返回文件的名称
}
# 04 批量上传小文件 保存到本地
@app05.post("/uploadfiles", summary="批量上传并保存")
async def app05_files(files: List[UploadFile] = File(...)):
"""
处理批量上传文件并保存到本地磁盘
:param files: 上传的多个文件对象
:return: 返回多个文件的信息,包括文件名和大小
"""
saved_files = [] # 用于存储所有已保存文件的状态信息
for file in files:
file_location = os.path.join(UPLOAD_DIR, file.filename) # 拼接每个文件的存储路径
with open(file_location, "wb") as buffer:
buffer.write(await file.read()) # 将文件内容写入本地
saved_files.append(
{
"code": 200, # 状态码
"文件名称": file.filename, # 文件名称
"文件大小": f'{os.path.getsize(file_location) // (1024 * 1024)}MB' # 文件大小(MB)
}
)
return {"files": saved_files} # 返回所有保存文件的状态信息
4.9 Requests 对象
from fastapi import APIRouter, Request
app06 = APIRouter()
# 01 获取 request 对象中的数据
@app06.get("/app06")
async def app06_get(request: Request):
# 获取请求的 URL 和客户端信息
print("Url", request.url) # 完整的请求 URL(包括协议、主机、路径、查询参数等)
print("客户端IP地址", request.client.host) # 客户端的 IP 地址
print("客户端浏览器类型", request.headers.get("user-agent")) # 获取请求头中的 User-Agent(客户端浏览器信息)
print("Cookies", request.cookies) # 获取请求中的所有 cookies
# 获取请求的路径和查询参数
print("请求路径", request.url.path) # 只获取 URL 的路径部分
print("查询参数", request.query_params) # 获取 URL 查询参数,返回一个 MultiDict
# 获取请求方法(如 GET、POST 等)
print("请求方法", request.method)
# 获取请求头中的所有字段
print("请求头", dict(request.headers)) # 返回请求头的字典
# 获取请求体的内容类型
print("请求内容类型", request.headers.get("Content-Type"))
# 获取请求体(如 JSON 或表单)
# 注意:需要手动解析,视请求的内容类型而定
try:
# 获取 JSON 数据(如果请求体是 JSON 格式)
body = await request.json()
print("请求体 (JSON)", body)
except Exception as e:
print("非 JSON 请求体")
try:
# 获取表单数据(如果请求体是表单格式)
form_data = await request.form()
print("请求体 (表单)", form_data)
except Exception as e:
print("非表单请求体")
# 返回请求相关的信息
data = {
"URL": request.url,
"IP": request.client.host,
"User-Agent": request.headers.get("user-agent"),
"Cookies": request.cookies,
"Path 路径": request.url.path,
"Query Parameters 查询参数": dict(request.query_params),
"Method 方法": request.method,
"Headers": dict(request.headers),
"Content-Type ": request.headers.get("Content-Type")
}
return data
4.9 请求静态文件
"""
暴露静态文件:
from fastapi.staticfiles import StaticFiles 引用包
app.mount("/static", StaticFiles(directory="statics"))
"""
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# 挂载静态文件路径 "/static",对应存储静态文件的目录 "/xxx/xxx/statics"
app.mount("/static", StaticFiles(directory="/xxx/xxx/statics"))
#02 目录结构
├── statics
│ ├── image.jpg
│ └── style.css
└── main.py
访问:http://localhost:8000/static/image.jpg
4.10 响应模型相关参数
"""
常用参数解释:
response_model_exclude_unset=True: 如果数据内没有值 则不会包含在响应数据中。
response_model_exclude_none=True: 如果某个字段的值是 None,则不会包含在响应数据中。
response_model_include={"name", "age"}, 只返回指定字段
response_model_exclude={"name", "age"} 排除指定字段
"""
from fastapi import APIRouter, Request, Response
from pydantic import BaseModel, EmailStr
from typing import Optional
app07 = APIRouter()
"""
接收用户名称,返回数据:
用户姓名:name
用户密码:password 该字段需要不返回
用户年龄:age
用户邮箱: None 默认是None
用户爱好:字符串或者空
"""
# 01 定义请求体类型:UserRequest 用于接收用户的请求数据
class UserRequest(BaseModel):
name: str # 用户姓名,必须为字符串类型
password: str # 用户密码,必须为字符串类型,通常需要加密存储,但这里只是示例
age: int # 用户年龄,必须为整数类型
email: Optional[EmailStr] = None # 用户邮箱,可选,EmailStr 类型,默认为 None
love_sea: Optional[str] = "" # 用户爱好,默认为空字符串
# 02 定义返回数据模型:UserResponse 用于返回用户的数据
class UserResponse(BaseModel):
name: str # 用户姓名
# password: str # 用户密码不返回
age: int # 用户年龄
email: Optional[EmailStr] = None # 用户邮箱,使用 EmailStr 类型或者空,默认为 None
love_sea: Optional[str] = "" # 用户爱好,默认为空字符串
# 02 定义用户集合,模拟一些用户数据
itmes = {
"果果": {"name": "果果", "age": 18},
"张宇宙": {"name": "张宇宙", "age": 20, "email": "123@qq.com", "love_sea": "爱吃"},
"小鼻嘎": {"name": "小鼻嘎", "age": 6, "email": "123@qq.com", "love_sea": None},
}
# 03 POST 请求,接收请求数据并返回响应数据
@app07.post("/app07_req/", response_model=UserResponse) # 使用返回数据模型 UserResponse,password 不会返回
async def app01_req(user: UserRequest):
"""
处理请求数据,并返回用户响应数据:
接收 UserRequest 请求数据,返回 UserResponse 类型的数据。
用户密码不会在响应数据中返回(注释掉的字段)。
"""
print(user) # 打印接收到的请求数据
return user # FastAPI 会自动将 UserRequest 转换为 UserResponse,并返回
# 04 根据 URL 路径参数查询用户数据
@app07.post("/app07_get/{itmes_id}",
response_model=UserResponse, # 使用返回数据模型 UserResponse
# response_model_exclude_unset=True, # 如果数据库内 itmes[itmes_id] 没有的值,则不返回
# response_model_exclude_none=True, # 不返回为 None 的值
# response_model_include={"name", "age"}, # 只返回指定字段
response_model_exclude={"name", "age"} # 排除指定字段
)
async def app01_req(itmes_id: str):
"""
根据传入的 itmes_id 查询对应的用户数据并返回:
如果 itmes_id 对应的用户数据中没有某些值(如 email 或 love_sea 为 None),
可以通过 response_model_exclude_none 参数过滤掉这些值。
"""
print(itmes[itmes_id]) # 打印查询到的用户数据
return itmes[itmes_id] # 返回对应的用户数据
五. jinjia2 模块
5.1 变量使用
(1) 后端使用jinjia2
from fastapi import FastAPI, Request
import uvicorn
from fastapi.templating import Jinja2Templates
app = FastAPI()
# 定义模版文件
template = Jinja2Templates(directory="templates")
@app.get('/06_index')
async def index(request: Request):
name = "果果" # 字符串类型
age = 18 # int类型
list_data = ["金xx", "西游记", "三国演义", "红楼梦"] # 列表类型
dice_data = {"id": 1, "test": "国色天香"}
return template.TemplateResponse(
"index.html",
{
"request": request,
"name": name,
"age": age,
"list_data": list_data,
"dice_data": dice_data
}
)
if __name__ == '__main__':
uvicorn.run('main:app', port=8000, reload=True)
(2) index文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 + Element Plus</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 引入 Element Plus 样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<!-- 引入 Element Plus 组件库 -->
<script src="https://unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h2>姓名显示:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>列表数据:{{ list_data[0] }}</h2>
<h2>字典数据:{{ dice_data.id }}</h2>
<p><button @click="incrementAge">点击年龄+1</button></p>
</el-header>
</el-container>
</div>
<script>
// 下面可以不写 测试下 vue3
const { createApp, ref } = Vue;
const app = createApp({
setup() {
// 解析 FastAPI 传入的 JSON 数据
const name = ref("{{ name }}"); // 直接字符串可以
const age = ref(parseInt("{{ age }}")); // 确保 age 是数字
const list_data = ref(JSON.parse(`{{ list_data | tojson }}`)); // 确保是数组
const dice_data = ref(JSON.parse(`{{ dice_data | tojson }}`)); // 确保是对象
// 增加年龄的方法
const incrementAge = () => {
age.value += 1;
console.log("年龄变更:", age.value);
};
return { name, age, list_data, dice_data, incrementAge };
}
});
// 挂载 Element Plus
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
5.2 过滤器
✅ 字符串处理:lower、upper、replace ✅ 数字计算:abs、round、int ✅ 列表处理:length、sort、unique ✅ 逻辑控制:default、defined ✅ 日期时间:date、time ✅ 支持自定义过滤器
"""
语法结构: 保留小说点后到三位数
<h2>过滤器pai:{{ pai |round(10) }}</h2>
"""
(1) 字符串相关过滤器
| 过滤器 | 作用 | 示例 | 输出 |
|---|---|---|---|
lower |
转换为小写 | {{ "Hello" | lower }} |
|
upper |
转换为大写 | {{ "hello" | upper }} |
|
capitalize |
首字母大写 | {{ "hello world" | capitalize }} |
|
title |
所有单词首字母大写 | {{ "hello world" | title }} |
|
trim |
去除首尾空格 | {{ " hello " | trim }} |
|
replace |
替换字符串 | {{ "hello" | replace("l", "x") }} |
(2) 数字相关过滤器
| 过滤器 | 作用 | 示例 | 输出 |
|---|---|---|---|
abs |
取绝对值 | {{ -10 | abs }} |
|
round |
四舍五入 | {{ 3.14159 | round(2) }} |
|
int |
转换为整数 | {{ "123" | int }} |
|
float |
转换为浮点数 | {{ "3.14" | float }} |
(3) 列表与字典相关过滤器
| 过滤器 | 作用 | 示例 | 输出 |
|---|---|---|---|
length |
获取长度 | {{ [1, 2, 3] | length }} |
|
first |
获取第一个元素 | {{ [1, 2, 3] | first }} |
|
last |
获取最后一个元素 | {{ [1, 2, 3] | last }} |
|
join |
用分隔符连接列表 | {{ ["a", "b", "c"] | join("-") }} |
|
sort |
排序列表 | {{ [3, 1, 2] | sort }} |
|
unique |
去重 | {{ [1, 2, 2, 3] | unique }} |
(4) 逻辑与条件过滤器
| 过滤器 | 作用 | 示例 | 输出 |
|---|---|---|---|
default |
为空时返回默认值 | {{ None | default("未定义") }} |
|
defined |
检查变量是否定义 | {% if name is defined %} Yes {% else %} No {% endif %} |
Yes/No |
none |
是否为 None | {% if value is none %} 是 None {% endif %} |
是 None |
(5) 日期与时间
| 过滤器 | 作用 | 示例 | 输出 |
|---|---|---|---|
date |
格式化日期 | {{ my_date | date("%Y-%m-%d") }} |
|
time |
获取时间部分 | {{ my_date | time("%H:%M") }} |
(6) 自定义过滤器
如果默认的过滤器不够用,你可以在 FastAPI 里自定义过滤器:
from jinja2 import Environment
def reverse_string(value):
return value[::-1] # 反转字符串
env = Environment()
env.filters["reverse"] = reverse_string # 注册过滤器
{{ "hello" | reverse }} <!-- 输出 "olleh" -->
5.3 控制结构
(1) 分支控制
-
if age >= 18:判断是否 大于等于 18,是的话就显示 "你是成年人!" -
elif age >= 12:否则,判断是否 大于等于 12,显示 "你是青少年!" -
else:如果都不满足,显示 "你还是个小朋友!"
#01 基本语法
{% if age >= 18 %} #条件为真则执行
<p>你是成年人!</p>
{% elif age >= 12 %}
<p>你是青少年!</p>
{% else %} # 否侧
<p>你还是个小朋友!</p>
{% endif %}
(2) 循环控制
Jinja2 允许你使用 for 循环 遍历列表、字典
-
遍历列表
<ul>
{% for name in names %}
<li>{{ name }}</li>
{% endfor %}
</ul>
假设 names = ["小明", "小红", "小刚"],页面输出:
<ul>
<li>小明</li>
<li>小红</li>
<li>小刚</li>
</ul>
-
遍历字典
<table>
{% for key, value in user.items() %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</table>
假设 user = {"name": "小明", "age": 18},页面输出:
<table>
<tr>
<td>name</td>
<td>小明</td>
</tr>
<tr>
<td>age</td>
<td>18</td>
</tr>
</table>
-
遍历列表套字典
<h1>书籍列表</h1>
<table border="1">
<tr>
<th>书籍ID</th>
<th>书籍名称</th>
<th>书籍价格</th>
</tr>
{% for book in books %}
<tr>
<td>{{ book.id }}</td>
<td>{{ book.books_name }}</td>
<td>{{ book.books_price }}</td>
</tr>
{% endfor %}
</table>
-
获取 for 循环索引
有时我们想知道当前循环到了第几次,Jinja2 提供了 loop.index:
{% for name in names %}
<p>第 {{ loop.index }} 个名字是:{{ name }}</p>
{% endfor %}
-
loop.index:索引(从 1 开始) -
loop.index0:索引(从 0 开始)
5.4 变量默认值
有时变量可能未定义,Jinja2 允许设置 默认值:
<p>欢迎,{{ user | default("游客") }}</p>
-
user变量未定义时,默认显示"游客"
5.5 过滤器结合判断
Jinja2 过滤器可以与判断语句结合使用:
{% if scores | length > 0 %}
<p>你有 {{ scores | length }} 条成绩记录。</p>
{% else %}
<p>暂无成绩记录。</p>
{% endif %}
5.6 宏(函数使用)
Jinja2 允许定义可复用的代码块(类似函数),使用 {% macro %}。
{% macro greet(name) %}
<p>你好,{{ name }}!</p>
{% endmacro %}
{{ greet("小明") }}
{{ greet("小红") }}
页面输出
<p>你好,小明!</p>
<p>你好,小红!</p>
六. ORM操作
-
Tortoise ORM
6.1 创建模型
我们需要定义三个表:Book(书籍)、Publisher(出版社)和 Author(作者)。同时,我们还会设计这三者之间的关系。
-
一对多关系:
Publisher和Book,一个出版社有多本书,一本书属于一个出版社。 -
多对多关系:
Book和Author,一本书可以有多个作者,一个作者可以写多本书。
from tortoise import fields
from tortoise.models import Model
class Publisher(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
class Meta:
table = "publisher"
class Author(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
class Meta:
table = "author"
class Book(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=255) # 书籍名称
publication_year = fields.IntField() # 出版年份
publisher = fields.ForeignKeyField("models.Publisher", related_name="books") # 书籍与出版社的关系(一对多)
authors = fields.ManyToManyField("models.Author", related_name="books") # 书籍与作者的关系(多对多)
class Meta:
table = "book"
6.2 Aerich迁移工具
Aerich 是 Tortoise ORM 的数据库迁移工具,类似于 Django 的迁移工具。我们将使用 Aerich 来管理数据库模式的版本,并进行迁移。
(1) 设置配置文件
config.py
#01 安装
pip install tortoise-orm aerich
pip install mysql-connector-python aiomysql
#02 配置数据库连接 config.py
# 数据库连接配置
DATABASE_CONFIG = {
"connections": {
"default": {
"engine": "tortoise.backends.mysql", # 使用 MySQL 或 MariaDB
"credentials": {
"host": "192.168.5.242", # 数据库主机
"port": 3308, # 数据库端口
"user": "root", # 数据库用户名
"password": "yunyi123", # 数据库密码
"database": "fastapi_01", # 数据库名称
"minsize": 1, # 最小连接池大小
"maxsize": 5, # 最大连接池大小
"charset": "utf8mb4", # 字符集
},
}
},
"apps": {
"models": {
"models": ["models","aerich.models"], # 指定模型路径
"default_connection": "default", # 使用默认的数据库连接
}
}
}
(2) 执行迁移命令
#01 初始化 aerich,生成一个迁移所需的配置文件
aerich init -t config.DATABASE_CONFIG
解释下: config.DATABASE_CONFIG 指定 config文件内 DATABASE_CONFIG (数据库连接信息)
#02 创建数据库迁移文件
aerich migrate
#03 根据迁移文件创建表
aerich init-db
(3) 主入口 main.py
import uvicorn
from config import DATABASE_CONFIG
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
# FastAPI 实例
app = FastAPI()
# FastAPI 通过 Tortoise 注册模型
register_tortoise(
app,
config=DATABASE_CONFIG,
generate_schemas=False
)
# 其他 FastAPI 路由和业务逻辑
if __name__ == '__main__':
uvicorn.run('main:app', port=8000, reload=True)
6.3 插入数据
## publisher 表
INSERT INTO `publisher` (`id`, `name`) VALUES (1, '东方出版社');
INSERT INTO `publisher` (`id`, `name`) VALUES (2, '新华出版社');
INSERT INTO `publisher` (`id`, `name`) VALUES (3, '果果出版社');
INSERT INTO `publisher` (`id`, `name`) VALUES (4, '宇宙出版社');
## 书籍表
INSERT INTO `fastapi_01`.`book` (`title`, `publication_year`, `publisher_id`) VALUES
('小猫咪的冒险', 1980, 1),
('糖果屋的秘密', 1985, 1),
('兔兔与月亮', 1990, 1),
('泡泡糖奇遇记', 1990, 2),
('彩虹小马', 1995, 2),
('小狐狸的冬天', 2000, 2),
('喵星人的日常', 2005, 3),
('星星与月亮的约定', 2010, 3),
('梦幻小精灵', 2015, 3),
('聊斋志异', 2000, 2),
('平凡的世界', 2005, 3),
('活着', 2010, 3),
('射雕英雄传', 2015, 3),
('蓝色的天空', 2020, 3);
## 作者表
INSERT INTO `fastapi_01`.`author` (`name`) VALUES
('果果'),
('西红柿'),
('辰东'),
('人生');
## 作者书籍关系
INSERT INTO `fastapi_01`.`book_author` (`book_id`, `author_id`) VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 3),
(3, 2),
(3, 4),
(4, 3),
(4, 4),
(5, 1),
(5, 2),
(6, 3),
(6, 4),
(7, 1),
(7, 4),
(8, 2),
(8, 3),
(9, 1),
(9, 2),
(10, 3),
(10, 4);
6.4 创建路由
(1) 路由引用
import uvicorn
from config import DATABASE_CONFIG
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
from api.books_api import books_api
# FastAPI 实例
app = FastAPI()
app.include_router(books_api, prefix="/books", tags=["书籍相关api接口"])
# FastAPI 通过 Tortoise 注册模型
register_tortoise(
app,
config=DATABASE_CONFIG,
generate_schemas=False
)
if __name__ == '__main__':
uvicorn.run('main:app', port=8000, reload=True)
(2) books api创建
"""
目录结构
├── api
│ ├── __init__.py
│ └── books_api.py # 存放改api路径
│ └── models.py
├── config.py
├── main.py
"""
from fastapi import APIRouter
"""
书籍相关所有 增删改查
"""
books_api = APIRouter()
#01 查看所有书籍
@books_api.get("/",summary="获取全部书籍")
def books_all_api():
return {
"books": "查看所有书籍"
}
#02 查看某个书籍
@books_api.get("/{books_id}",summary="获取指定书籍")
def books_get_api(books_id: int):
return {
"books": f"查看ID是:{books_id} 书籍"
}
#03 修改学生信息
@books_api.put("/{books_id}",summary="修改指定书籍")
def books_put_api(books_id: int):
return {
"books": f"修改ID是:{books_id} 书籍"
}
#03 添加书籍
@books_api.post("/{books_id}",summary="添加指定书籍")
def books_post_api(books_id: int):
return {
"books": f"添加ID是:{books_id} 书籍"
}
#05 添加书籍
@books_api.delete("/{books_id}",summary="删除指定书籍")
def books_delete_api(books_id: int):
return {
"books": f"删除ID是:{books_id} 书籍"
}
-
完整版本
from fastapi import APIRouter, Request
from starlette.templating import Jinja2Templates
from tortoise.exceptions import DoesNotExist
from models import *
from tortoise.expressions import Q
from pydantic import BaseModel, Field
from typing import List
"""
书籍相关所有 增删改查
"""
books_api = APIRouter()
# 查看所有书籍信息
@books_api.get("/", summary="获取全部书籍")
async def books_all_api():
# 01 查询全部的书籍 格式:[{'id': 1, 'title': '射雕英雄传'...}, {'id': 2, 'title': '活着'...}]
books_all = await Book.all().values()
# 02 查询指定条件书籍
books_id = await Book.filter(id=1).values()
# 03 包含查询 模糊匹配 title包含"英雄"的
books_like = await Book.filter(title__contains="英雄").values()
# 04 AND 查询(title 包含 "活着" 且 id > 1)
books_and = await Book.filter(title__contains="活着", id__gt=1).values()
# 05 OR 查询(id>1 或 title 包含 "英雄")
books_or = await Book.filter(Q(title__contains="英雄") | Q(id__gt=1)).values()
# 06 AND 查询 (publication_year >2005 或 title 包含 "红楼梦")
books_q_and = await Book.filter(Q(title__contains="红楼梦") & Q(publication_year__gt=1980)).values()
# 07 in 在列表 (id in [1,3,4])
books_in = await Book.filter(id__in=[1, 2, 3]).values()
# 08 范围 id是1-8 之间
books_between = await Book.filter(id__range=[1, 8]).values()
# 09 自定义返回字段 只返回id和title
books_val = await Book.filter(id__in=[1, 3]).values('id', 'title')
# 打印结果带提示信息
# print("01. 全部书籍查询结果:", books_all)
# print("02. 指定条件查询结果 (id=1):", books_id)
# print("03. 模糊匹配查询结果 (title 包含 '英雄'):", books_like)
# print("04. AND 查询结果 (title 包含 '活着' 且 id > 1):", books_and)
# print("05. OR 查询结果 (id>1 或 title 包含 '英雄'):", books_or)
# print("06 OR_q 查询结果 (publication_year >2005 或 title 包含 红楼梦)", books_q_and)
# print("07 in 在列表查询结果 (id in [1,3,4])", books_in)
# print("08 范围 id是1-8 之间 (id` BETWEEN '1' AND '8' )", books_between)
# print("09 自定义返回字段 只返回id和title", books_val)
return {
"books_all": books_all,
}
# 02 查看一本或者多本书籍 并返回给前端
@books_api.get("/{books_ids}", summary="获取指定书籍")
async def books_get_api(request: Request, books_ids: str):
books_id_list = [int(id) for id in books_ids.split(",")]
# 打印传入的书籍 ID
print(books_id_list)
# 从数据库中查询对应的书籍,支持多个 ID 查询
books = await Book.filter(id__in=books_id_list).values()
# 渲染模板并返回响应
templates = Jinja2Templates(directory="templates")
return templates.TemplateResponse(
"books_index.html",
{
"request": request,
"books": books
}
)
# 书籍创建 & 更新模型(用于添加和修改)
class BookCreateUpdate(BaseModel):
title: str = Field(..., max_length=255, title="书籍名称")
publication_year: int = Field(..., ge=0, title="出版年份") # 确保年份为非负数
publisher_id: int = Field(..., title="出版社ID") # 外键,指向 Publisher 表
author_ids: List[int] = Field(..., title="作者ID列表") # 多对多关系
class Config:
from_attributes = True
# 书籍返回模型(用于 API 返回的数据格式)
class BookResponse(BookCreateUpdate):
title: str
publication_year: int
publisher_id: int
author_ids: List[int]
class Config:
from_attributes = True
# 03 修改书籍信息
@books_api.put("/{books_id}",
summary="修改指定书籍",
response_model=BookResponse)
async def books_put_api(books_id: int, books_data: BookCreateUpdate):
# 获取指定书籍
book = await Book.get(id=books_id)
# 确保book对象包含author_ids
if not book:
return {"error": "Book not found"}
# 更新书籍信息
# book.title = books_data.title
# book.publication_year = books_data.publication_year
# book.publisher_id = books_data.publisher_id
# book.author_ids = books_data.author_ids # 更新 author_ids
# 使用 ** 打散的方式更新书籍字段
# 打散 books_data 中的所有字段并传递给 book 实例
for key, value in books_data.dict().items():
setattr(book, key, value)
# 保存更新后的书籍
await book.save()
# 返回更新后的 BookResponse
return BookResponse.from_orm(book)
# 04 添加书籍
@books_api.post("/", summary="添加指定书籍")
async def books_post_api(create_book: BookCreateUpdate):
# 01 创建数据 使用 ** 方式把json数据打散
book = await Book.create(**create_book.dict())
print(book.id) # 获取新插入记录的 ID
print(book.title) # 获取书籍的标题
return {
"books": f"添加ID是:{book.id} 书籍 {book.title}",
"book": book
}
# 05 删除书籍
@books_api.delete("/{books_id}", summary="删除指定书籍")
async def books_delete_api(books_id: int):
try:
#01 查询删除的书籍ID 如果没有返回错误响应
book = await Book.get(id=books_id)
# 02 book 就是这个书籍对象 直接删除即可
await book.delete()
return {
"books": f"删除ID是:{books_id} 书籍",
}
except DoesNotExist:
return {
"error": f"未找到ID为 {books_id} 的书籍",
"books_id": books_id
}
@books_api.get("/publisher_author/{books_id}",summary="根据书籍ID获取出版社和作者")
async def books_publisher_author_api(book_id: int):
book = await Book.get(id=book_id).prefetch_related("publisher", "authors")
# 获取书籍的出版社名称
publisher_name = book.publisher.name if book.publisher else None
# 获取书籍的作者名称
author_names = [author.name for author in book.authors] # 获取多对多关系中的所有作者
return {
"book_id": book.id,
"title": book.title,
"publication_year": book.publication_year,
"publisher": publisher_name,
"authors": author_names
}
6.5 单表查询操作
| 查询类 | FASTAPI 代码 | SQL 语句 |
|---|---|---|
| 查询所有书籍 | Book.all().values() |
SELECT * FROM book; |
| 指定条件查询 | Book.filter(id=1).values() |
SELECT * FROM book WHERE id = 1; |
| 模糊查询 | Book.filter(title__contains="英雄").values() |
SELECT * FROM book WHERE title LIKE '%英雄%'; |
| AND 查询 | Book.filter(title__contains="活着", id__gt=1).values() |
SELECT * FROM book WHERE title LIKE '%活着%' AND id > 1; |
| OR 查询 | Book.filter(Q(title__contains="英雄") | Q(id__gt=1)).values() |
|
| 复杂 AND/OR | Book.filter(Q(title__contains="红楼梦") & Q(publication_year__gt=1980)).values() |
SELECT * FROM book WHERE title LIKE '%红楼梦%' AND publication_year > 1980; |
| IN 查询 | Book.filter(id__in=[1,2,3]).values() |
SELECT * FROM book WHERE id IN (1,2,3); |
| 范围查询 | Book.filter(id__range=[1,8]).values() |
SELECT * FROM book WHERE id BETWEEN 1 AND 8; |
| 自定义返回列 | Book.filter(id__in=[1, 3]).values('id', 'title') |
SELECT id, title FROM book WHERE id IN (1,3); |
from fastapi import APIRouter
from tomlkit import api
from models import *
from tortoise.expressions import Q
"""
书籍相关所有 增删改查
"""
books_api = APIRouter()
@books_api.get("/", summary="获取全部书籍")
async def books_all_api():
# 01 查询全部的书籍 格式:[{'id': 1, 'title': '射雕英雄传'...}, {'id': 2, 'title': '活着'...}]
books_all = await Book.all().values()
# 02 查询指定条件书籍
books_id = await Book.filter(id=1).values()
# 03 包含查询 模糊匹配 title包含"英雄"的
books_like = await Book.filter(title__contains="英雄").values()
# 04 AND 查询(title 包含 "活着" 且 id > 1)
books_and = await Book.filter(title__contains="活着", id__gt=1).values()
# 05 OR 查询(id>1 或 title 包含 "英雄")
books_or = await Book.filter(Q(title__contains="英雄") | Q(id__gt=1)).values()
# 06 AND 查询 (publication_year >2005 或 title 包含 "红楼梦")
books_q_and = await Book.filter(Q(title__contains="红楼梦") & Q(publication_year__gt=1980)).values()
# 07 in 在列表 (id in [1,3,4])
books_in = await Book.filter(id__in=[1,2,3]).values()
# 08 范围 id是1-8 之间
books_between = await Book.filter(id__range=[1,8]).values()
# 09 自定义返回字段 只返回id和title
books_val = await Book.filter(id__in=[1, 3]).values('id','title')
# 打印结果带提示信息
print("01. 全部书籍查询结果:", books_all)
print("02. 指定条件查询结果 (id=1):", books_id)
print("03. 模糊匹配查询结果 (title 包含 '英雄'):", books_like)
print("04. AND 查询结果 (title 包含 '活着' 且 id > 1):", books_and)
print("05. OR 查询结果 (id>1 或 title 包含 '英雄'):", books_or)
print("06 OR_q 查询结果 (publication_year >2005 或 title 包含 红楼梦)", books_q_and)
print("07 in 在列表查询结果 (id in [1,3,4])", books_in)
print("08 范围 id是1-8 之间 (id` BETWEEN '1' AND '8' )", books_between)
print("09 自定义返回字段 只返回id和title", books_val)
return {
"books_all": books_all,
}
6.6 数据返回前端html
"""
传入参数:1,2,3 逗号为分隔符
通过jinjia2模版语法渲染到前端
"""
# 01 查看一本或者多本书籍 并返回给前端
@books_api.get("/{books_ids}", summary="获取指定书籍")
async def books_get_api(request: Request, books_ids: str):
books_id_list = [int(id) for id in books_ids.split(",")]
# 打印传入的书籍 ID
print(books_id_list)
# 从数据库中查询对应的书籍,支持多个 ID 查询
books = await Book.filter(id__in=books_id_list).values()
# 渲染模板并返回响应
templates = Jinja2Templates(directory="templates")
return templates.TemplateResponse(
"books_index.html",
{
"request": request,
"books": books
}
)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>书籍信息</title>
<!-- 引入 Bootstrap 样式 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="text-center mb-4">书籍详情</h2>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>id</th>
<th>书籍名称</th>
<th>出版年月</th>
<th>出版社</th>
</tr>
</thead>
<tbody>
<!-- 使用 for 循环迭代书籍列表 -->
{% for book in books %}
<tr>
<td>{{ book.id }}</td>
<td>{{ book.title }}</td>
<td>{{ book.publication_year }}</td>
<td>{{ book.publisher_id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 引入 Bootstrap 的 JavaScript (可选) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
6.7 增加操作
from fastapi import APIRouter, Request
from starlette.templating import Jinja2Templates
from tortoise.exceptions import DoesNotExist
from models import *
from tortoise.expressions import Q
from pydantic import BaseModel, Field
from typing import List
#01 书籍创建 & 更新模型(用于添加和修改)
class BookCreateUpdate(BaseModel):
title: str = Field(..., max_length=255, title="书籍名称")
publication_year: int = Field(..., ge=0, title="出版年份") # 确保年份为非负数
publisher_id: int = Field(..., title="出版社ID") # 外键,指向 Publisher 表
author_ids: List[int] = Field(..., title="作者ID列表") # 多对多关系
class Config:
from_attributes = True
# 书籍返回模型(用于 API 返回的数据格式)
class BookResponse(BookCreateUpdate):
title: str
publication_year: int
publisher_id: int
author_ids: List[int]
class Config:
from_attributes = True
# 02 添加书籍
@books_api.post("/", summary="添加指定书籍")
async def books_post_api(create_book: BookCreateUpdate):
# 01 创建数据 使用 ** 方式把json数据打散
book = await Book.create(**create_book.dict())
print(book.id) # 获取新插入记录的 ID
print(book.title) # 获取书籍的标题
return {
"books": f"添加ID是:{book.id} 书籍 {book.title}",
"book": book
}
6.8 更新操作
"""
模型文件参考:6.7 增加操作
01 定义请求和响应模型文件
02 或者修改的数据对象
03 赋值
04 保存
"""
# 01 修改书籍信息
@books_api.put("/{books_id}",
summary="修改指定书籍",
response_model=BookResponse)
async def books_put_api(books_id: int, books_data: BookCreateUpdate):
# 获取指定书籍
book = await Book.get(id=books_id)
# 确保book对象包含author_ids
if not book:
return {"error": "Book not found"}
# 更新书籍信息 如果字段多 使用下面方式
# book.title = books_data.title
# book.publication_year = books_data.publication_year
# book.publisher_id = books_data.publisher_id
# book.author_ids = books_data.author_ids # 更新 author_ids
# 使用 ** 打散的方式更新书籍字段
# 打散 books_data 中的所有字段并传递给 book 实例
for key, value in books_data.dict().items():
setattr(book, key, value)
# 保存更新后的书籍
await book.save()
# 返回更新后的 BookResponse
return BookResponse.from_orm(book)
-
图示
6.9 删除操作
"""
首先判断数据是否存在
如果存在则删除
await book.delete()
"""
# 05 删除书籍
@books_api.delete("/{books_id}", summary="删除指定书籍")
async def books_delete_api(books_id: int):
try:
#01 查询删除的书籍ID 如果没有返回错误响应
book = await Book.get(id=books_id)
# 02 book 就是这个书籍对象 直接删除即可
await book.delete()
return {
"books": f"删除ID是:{books_id} 书籍",
}
except DoesNotExist:
return {
"error": f"未找到ID为 {books_id} 的书籍",
"books_id": books_id
}
6.10 连表查询
# 连表查询
@books_api.get("/publisher_author/{books_id}",summary="根据书籍ID获取出版社和作者")
async def books_publisher_author_api(book_id: int):
book = await Book.get(id=book_id).prefetch_related("publisher", "authors")
# 获取书籍的出版社名称
publisher_name = book.publisher.name if book.publisher else None
# 获取书籍的作者名称
author_names = [author.name for author in book.authors] # 获取多对多关系中的所有作者
return {
"book_id": book.id,
"title": book.title,
"publication_year": book.publication_year,
"publisher": publisher_name,
"authors": author_names
}
###返回结果:
{
"book_id": 3,
"title": "平凡的世界",
"publication_year": 2005,
"publisher": "果果出版社",
"authors": [
"西红柿",
"人生"
]
6.11 一对多和多对多
#01 多对多基本语法
# 多对多:添加多个作者
await book.authors.add(*authors)
# 多对多:替换作者列表
await book.authors.set(authors)
#02 一对多
publisher_books = await publisher.books.all() # 获取该出版社的所有书籍
from tortoise import fields
from tortoise.models import Model
class Publisher(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
class Meta:
table = "publisher"
class Author(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
class Meta:
table = "author"
class Book(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=255) # 书籍名称
publication_year = fields.IntField() # 出版年份
publisher = fields.ForeignKeyField("models.Publisher", related_name="books") # 外键,一对多关系
authors = fields.ManyToManyField("models.Author", related_name="books") # 多对多关系
class Meta:
table = "book"
@books_api.post("/", summary="添加指定书籍")
async def books_post_api(book_data: BookCreate):
# 获取出版社
publisher = await Publisher.get(id=book_data.publisher_id)
# 获取作者列表
authors = await Author.filter(id__in=book_data.author_ids)
# 创建新书籍
book = await Book.create(
title=book_data.title,
publication_year=book_data.publication_year,
publisher=publisher
)
# 将作者关联到书籍(多对多关系)
await book.authors.add(*authors)
return {
"books": f"添加ID是:{book.id} 书籍 {book.title}",
"book": book
}
七. 中间件和CORS
7.1 中间件使用
(1) 基本语法
@app.middleware("http")
async def add_header_time(request: Request,call_next):
start_time = time.time()
# 处理请求前
response = await call_next(request) # 这是本次请求
# 处理请求后
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
(2) 计算响应时长
import time
import uvicorn
from fastapi import FastAPI,Request,Response
# FastAPI 实例
app = FastAPI()
#01 自定义中间件 计算消耗时间
@app.middleware("http")
async def add_header_time(request: Request,call_next):
start_time = time.time()
# 处理请求前
response = await call_next(request)
# 处理请求后
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get('/')
async def root():
return {'message': 'Holle word'}
if __name__ == '__main__':
# 注意 host 的正确写法是 0.0.0.0(不是 0.0.0.)
uvicorn.run(
'main:app',
host='192.168.1.109', # 允许所有网络接口访问
port=8000,
reload=True # 开发模式启用热重载
)
(3) 限制路由 /admin
import time
import uvicorn
from fastapi import FastAPI,Request
from starlette.responses import JSONResponse
#02 自定义中间件 限制路由
@app.middleware("http")
async def restrict_urls(request: Request,call_next):
blocked_path = [ # 限制的路由
"/admin",
"/server"
] # 要限制的路径
if request.url.path not in blocked_path: # 判断如果路由 如果不在则正常秩序
response = await call_next(request)
return response
return JSONResponse(
status_code=403,
content={"detail": "权限拒绝:Access forbidden to admin or guo area"}
)
(4) IP白名单
import time
import uvicorn
from fastapi import FastAPI,Request
from starlette.responses import JSONResponse
#03 自定义中间件 IP白名单
@app.middleware("http")
async def deny_access(request: Request,call_next):
# IP白名单配置
ALLOWED_IPS = ["127.0.0.1", "192.168.1.109"] # 允许的IP列表
client_id = request.client.host # 获取允许访问的IP
if client_id not in ALLOWED_IPS: # 判断如果不在白名单内 则返回
return JSONResponse(
status_code=403,
content={
"detail": "非法请求,IP不允许请求"
}
)
response = await call_next(request)
return response
7.2 CORS 中间件
import time
import uvicorn
from fastapi import FastAPI, Request
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
from pydantic import BaseModel
# FastAPI 实例
app = FastAPI()
# 01 CORS 校验模型
class UserData(BaseModel):
username: str
email: str
# 跨域中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许的源列表(生产环境建议明确指定域名)
allow_credentials=True, # 是否允许携带认证信息(如cookies) 如果前端需要携带cookies则必须开启
allow_methods=["*"], # 允许所有方法(GET/POST/PUT等)
allow_headers=["*"], # 允许所有请求头
# 其他可选参数:
# expose_headers=["X-Custom-Header"], # 允许前端访问的响应头
# max_age=600 # 预检请求缓存时间(秒)
)
@app.get("/api/data")
async def get_data():
return {"message": "GET成功", "timestamp": time.time()}
@app.post("/api/submit")
async def submit_data(data: UserData):
return {
"status": "数据已接收",
"username": data.username,
"email": data.email
}
if __name__ == '__main__':
# 注意 host 的正确写法是 0.0.0.0(不是 0.0.0.)
uvicorn.run(
'main:app',
host='0.0.0.0', # 允许所有网络接口访问
port=8000,
reload=True # 开发模式启用热重载
)
八. 集成其它数据库
8.1 集成其他数据库
"""
我们需要实现,集成其它数据库:
创建以下文件:
main.py 主文件
setting.py 配置文件
models.py 模型文件
"""
setting.py 数据库配置文件
# 数据库连接配置
DATABASE_CONFIG = {
"connections": {
"default": {
"engine": "tortoise.backends.mysql", # 使用 MySQL 或 MariaDB
"credentials": {
"host": "192.168.5.242", # 数据库主机
"port": 3308, # 数据库端口
"user": "root", # 数据库用户名
"password": "yunyi123", # 数据库密码
"database": "fastapi_01", # 数据库名称
"minsize": 1, # 最小连接池大小
"maxsize": 5, # 最大连接池大小
"charset": "utf8mb4", # 字符集
},
}
},
"apps": {
"models": {
"models": ["models","aerich.models"], # 指定模型路径
"default_connection": "default", # 使用默认的数据库连接
}
}
}
models.py 模型文件
from tortoise import fields
from tortoise.models import Model
class Test(Model):
id = fields.IntField(pk=True) # 字段
name = fields.CharField(max_length=255) # 字段
class Meta:
table = "test" # 表名称
main.py 主入口文件
实现增删改查接口
import uvicorn
from fastapi import FastAPI, HTTPException
from models import Test # 确保正确导入模型类(首字母大写)
from tortoise.contrib.fastapi import register_tortoise
from pydantic import BaseModel
from typing import Optional
from setting import *
app = FastAPI()
# 注册Tortoise ORM
register_tortoise(
app,
config=DATABASE_CONFIG,
generate_schemas=False
)
# ------------------------ 查询 ------------------------
@app.get("/", summary="查询所有数据")
async def get_all_data():
"""获取全部数据"""
data = await Test.all().values()
return {"data": data}
@app.get("/{id}", summary="根据ID查询")
async def get_data_by_id(id: int):
"""通过ID获取单条数据"""
data = await Test.filter(id=id).first().values()
if not data:
raise HTTPException(status_code=404, detail="数据不存在")
return {"data": data}
# ------------------------ 新增 ------------------------
class CreateDataRequest(BaseModel):
name: str
@app.post("/", summary="新增数据")
async def create_data(data: CreateDataRequest):
"""创建新数据"""
new_data = await Test.create(**data.dict())
return {"msg": "添加成功", "id": new_data.id}
# ------------------------ 更新 ------------------------
class UpdateDataRequest(BaseModel):
name: Optional[str] = None # 允许部分更新
@app.put("/{id}", summary="更新数据")
async def update_data(id: int, data: UpdateDataRequest):
"""通过ID更新数据"""
# 获取有效更新字段
update_fields = data.dict(exclude_unset=True)
# 执行更新操作
updated_count = await Test.filter(id=id).update(**update_fields)
if not updated_count:
raise HTTPException(status_code=404, detail="更新失败,数据不存在")
return {"msg": "更新成功", "updated_id": id}
# ------------------------ 删除 ------------------------
@app.delete("/{id}", summary="删除数据")
async def delete_data(id: int):
"""通过ID删除数据"""
# 先查询是否存在
exists = await Test.filter(id=id).exists()
if not exists:
raise HTTPException(status_code=404, detail="删除失败,数据不存在")
# 执行删除
await Test.filter(id=id).delete()
return {"msg": "删除成功", "deleted_id": id}
if __name__ == '__main__':
uvicorn.run(
'main:app',
host='0.0.0.0',
port=8000,
reload=True
)
8.2 集成多个数据库
(1) 目录结构
#01 目录结构
project/
├── main.py # 主程序入口
├── config/
│ └── database.py # 数据库配置(含DATABASE_CONFIG)
└── models/
├── __init__.py # 必须的空文件
├── db1/ # 数据库1的模型
│ ├── __init__.py
│ └── test.py # 对应 default 数据库的模型
└── db2/ # 数据库2的模型
├── __init__.py
└── user.py # 对应 db2 数据库的模型
(2) 数据配置文件
"""
多个数据依次添加即可 : config/datavase.py
"""
DATABASE_CONFIG = {
"connections": {
# 默认数据库 (fastapi_01)
"default": {
"engine": "tortoise.backends.mysql",
"credentials": {
"host": "192.168.5.242",
"port": 3308,
"user": "root",
"password": "yunyi123",
"database": "fastapi_01",
"minsize": 1,
"maxsize": 5,
"charset": "utf8mb4",
},
},
# 第二个数据库 (fas02)
"db2": {
"engine": "tortoise.backends.mysql",
"credentials": {
"host": "192.168.5.242",
"port": 3308, # 默认MySQL端口
"user": "root",
"password": "yunyi123", # 根据实际修改
"database": "fas02",
"minsize": 1,
"maxsize": 5,
"charset": "utf8mb4",
},
}
},
"apps": {
# 第一个数据库的模型
"models": {
"models": ["models.db1.test", "aerich.models"], # 模型文件路径
"default_connection": "default",
},
# 第二个数据库的模型
"fas02_models": {
"models": ["models.db2.user"], # 单独目录存放第二个库的模型
"default_connection": "db2",
}
}
}
(3) 模型文件
#01 数据库1模型文件 models/db1/test.py
from tortoise import fields
from tortoise.models import Model
class Test(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=255)
class Meta:
table = "test" # 对应数据库 fastapi_01 的表
app = "models" # 对应 DATABASE_CONFIG 中的第一个应用
#02 数据库2 模型文件 models/db2/user.py
(4) 引用 main文件
import uvicorn
from fastapi import FastAPI, HTTPException
from models.db1.test import Test # 确保正确导入模型类(首字母大写)
from models.db2.user import User # 确保正确导入模型类(首字母大写)
from tortoise.contrib.fastapi import register_tortoise
from pydantic import BaseModel
from typing import Optional
from config.database import *
app = FastAPI()
# 注册Tortoise ORM
register_tortoise(
app,
config=DATABASE_CONFIG, # 使用配置文件
generate_schemas=False, # 禁用自动生成表结构
add_exception_handlers=True # 启用ORM错误处理
)
# ------------------------ 数据库1查询 ------------------------
@app.get("/user/user_get", summary="获取所有用户")
async def get_all_users():
"""从数据库2获取用户"""
users = await User.all().values()
return {"users": users}
# ------------------------ 数据集2查询 ------------------------
@app.get("/", summary="查询所有数据")
async def get_all_data():
"""获取全部数据"""
data = await Test.all().values()
return {"data": data}
@app.get("/{id}", summary="根据ID查询")
async def get_data_by_id(id: int):
"""通过ID获取单条数据"""
data = await Test.filter(id=id).first().values()
if not data:
raise HTTPException(status_code=404, detail="数据不存在")
return {"data": data}
if __name__ == '__main__':
uvicorn.run(
'main:app',
host='0.0.0.0',
port=8000,
reload=True
)
局域网内文件下载
from fastapi import APIRouter
from models_all import *
from fastapi import APIRouter, Request, HTTPException
from starlette.templating import Jinja2Templates
from pathlib import Path
from fastapi.responses import HTMLResponse, FileResponse
import os
test_api = APIRouter()
# 配置模板目录(假设你的模板放在项目根目录的templates文件夹中)
templates = Jinja2Templates(directory="templates")
# 配置文件存储目录
FILE_DIR = Path("./files")
FILE_DIR.mkdir(parents=True, exist_ok=True)
"""
书籍相关所有 增删改查
"""
def safe_join(base_path: Path, target_path: str) -> Path:
"""安全路径校验"""
target_path = target_path.lstrip("/")
full_path = (base_path / target_path).resolve()
if base_path.resolve() not in full_path.parents and full_path != base_path.resolve():
raise HTTPException(status_code=400, detail="非法路径")
return full_path
@test_api.get("/files", response_class=HTMLResponse, summary="文件列表页面")
async def file_list_page(request: Request):
"""展示带样式的文件列表页面"""
files = []
for item in FILE_DIR.iterdir():
files.append({
"name": item.name,
"size": f"{item.stat().st_size / 1024:.2f} KB",
"is_dir": item.is_dir(),
"modified": item.stat().st_mtime
})
return templates.TemplateResponse(
"file_list.html",
{
"request": request,
"files": sorted(files, key=lambda x: x["modified"], reverse=True)
}
)
@test_api.get("/download/{file_path:path}", summary="文件下载")
async def download_file(file_path: str):
"""文件下载接口"""
try:
target_file = safe_join(FILE_DIR, file_path)
if not target_file.exists():
raise HTTPException(status_code=404, detail="文件不存在")
if target_file.is_dir():
raise HTTPException(status_code=400, detail="不能下载目录")
return FileResponse(
path=target_file,
filename=target_file.name,
media_type="application/octet-stream"
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}")
<!DOCTYPE html>
<html>
<head>
<title>文件列表</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 20px;
background-color: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.file-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.file-item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-name {
color: #333;
text-decoration: none;
}
.file-name:hover {
color: #007bff;
}
.dir-icon {
color: #6c757d;
margin-right: 8px;
}
.size {
color: #6c757d;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="header">
<h1>文件列表</h1>
<a href="/books" style="text-decoration: none; color: #007bff;">查看书籍列表 →</a>
</div>
<div class="file-list">
{% for file in files %}
<div class="file-item">
<div>
{% if file.is_dir %}
<span class="dir-icon">📁</span>
<span>{{ file.name }}</span>
{% else %}
<a href="/download/{{ file.name }}" class="file-name">📄 {{ file.name }}</a>
{% endif %}
</div>
<span class="size">{{ file.size }}</span>
</div>
{% endfor %}
</div>
</body>
</html>
















浙公网安备 33010602011771号