六、集成SQL数据库

项目信息
结构
ticketing_system/
│
├── alembic.ini - Alembic配置文件
├── pytest.ini - pytest测试框架配置
├── requirements.txt
│
├── alembic/ - 数据库迁移管理文件夹
│ ├── env.py - Alembic环境配置
│ ├── script.py.mako - 迁移脚本模板
│ ├── README
│ └── versions/ - 数据库迁移版本文件夹
│ ├── c89449e81bb3_start_database.py
│ ├── ......
│
├── app/ - 应用核心代码文件夹
│ ├── main.py - 主应用入口
│ ├── database.py - 数据库ORM模型定义
│ ├── db_connection.py - 数据库连接管理
│ ├── operations.py - 业务逻辑层(CRUD操作)
│ └── security.py - 安全认证功能
│
└── tests/ - 测试文件夹
├── conftest.py - pytest配置与Fixtures
├── test_client.py - 客户端相关测试
├── test_operations.py - 业务逻辑层测试
└── test_security.py - 安全功能测试
启动
uvicorn app.main:app
创建映射对象类
"一对一"
"多对一"
"多对多(通过sponsorships)"
"多对多(通过主键组合)"
"多对多(通过主键组合)"
1 1 n n n 1 n m m m Ticket
+int id
+float price
+string show
+string user
+bool sold
+int event_id
+details : TicketDetails
+event : Event
TicketDetails
+int id
+int ticket_id
+string seat
+string ticket_type
+ticket : Ticket
Event
+int id
+string name
+tickets : Ticket[]
+sponsors : Sponsor[]
Sponsor
+int id
+string name
+events : Event[]
Sponsorship
+int event_id
+int sponsor_id
+float amount
CreditCard
+int id
+string number
+string expiration_date
+string cvv
+string card_holder_name
py
# database.py
# 这些类将被用于与数据库表匹配
from sqlalchemy import ForeignKey
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
mapped_column,
relationship,
)
# 基类 基类要集成 DeclarativeBase 类
class Base(DeclarativeBase):
pass
class Ticket(Base):
# 匹配数据库中的 tickets 表
__tablename__ = "tickets"
id: Mapped[int] = mapped_column(primary_key=True)
price: Mapped[float] = mapped_column(nullable=True)
show: Mapped[str | None]
user: Mapped[str | None]
sold: Mapped[bool] = mapped_column(default=False)
# 一对一关系声明
details: Mapped["TicketDetails"] = relationship(
back_populates="ticket"
)
event_id: Mapped[int | None] = mapped_column(
ForeignKey("events.id")
)
# 一对多关系声明
event: Mapped["Event | None"] = relationship(
back_populates="tickets"
)
# 票详细信息和票是一对一的
class TicketDetails(Base):
__tablename__ = "ticket_details"
id: Mapped[int] = mapped_column(primary_key=True)
ticket_id: Mapped[int] = mapped_column(
ForeignKey("tickets.id")
)
ticket: Mapped["Ticket"] = relationship(
back_populates="details"
)
seat: Mapped[str | None]
ticket_type: Mapped[str | None]
class Event(Base):
__tablename__ = "events"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
tickets: Mapped[list["Ticket"]] = relationship(
back_populates="event"
)
sponsors: Mapped[list["Sponsor"]] = relationship(
secondary="sponsorships",
back_populates="events",
)
class Sponsor(Base):
__tablename__ = "sponsors"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(unique=True)
events: Mapped[list["Event"]] = relationship(
secondary="sponsorships",
back_populates="sponsors",
)
class Sponsorship(Base):
__tablename__ = "sponsorships"
event_id: Mapped[int] = mapped_column(
ForeignKey("events.id"), primary_key=True
)
sponsor_id: Mapped[int] = mapped_column(
ForeignKey("sponsors.id"), primary_key=True
)
amount: Mapped[float] = mapped_column(
nullable=False, default=10
)
class CreditCard(Base):
__tablename__ = "credit_cards"
id: Mapped[int] = mapped_column(primary_key=True)
number: Mapped[str]
expiration_date: Mapped[str]
cvv: Mapped[str]
card_holder_name: Mapped[str]
!IMPORTANT
ORM类的基本声明规范
基础
py# 类名采用首字母大写,对应数据库中的表 class Ticket(Base): # 显式指定数据库表名为tickets # 如果不声明,SQLAlchemy会将类名转换为小写作为表名 __tablename__ = "tickets" id: Mapped[int] = mapped_column(primary_key=True) price: Mapped[float] = mapped_column(nullable=True) show: Mapped[str | None] = mapped_column(nullable=True) user: Mapped[str | None] = mapped_column(nullable=True) sold: Mapped[bool] = mapped_column(default=False)
Mapped[int] # 映射到INTEGER类型的字段 Mapped[float] # 映射到FLOAT/NUMERIC类型的字段 Mapped[str] # 映射到VARCHAR/TEXT类型的字段 Mapped[str | None] # 可为空的字符串字段 mapped_column( primary_key=True, # 设置为主键 nullable=True, # 允许NULL值 nullable=False, # 不允许NULL值(默认) unique=True, # 唯一约束 default=value, # 默认值 server_default="...", # 数据库端默认值 )高级
关系声明
py# Mapped["TicketDetails"] TicketDetails 是上文中定义的模型类 # 表示这个属性最终是一个 TicketDetails 对象 # relationship(...) 表示这是一个数据关系 而不是数据列 details: Mapped["TicketDetails"] = relationship( # 双向关系配置 # 指向 TicketDetails 类中的 "ticket" 属性 back_populates="ticket" )
上述语句实现了 类属性注册 ├─ 将 "details" 注册为 Ticket 类的属性 ├─ 类型为 TicketDetails 对象 └─ 标记为关系属性(不是数据库列) 外键推断 ├─ 扫描 TicketDetails 类定义 ├─ 查找指向 Ticket 的外键 │ (即 TicketDetails.ticket_id) ├─ 自动建立关联规则 └─ 无需显式指定外键位置 双向关系绑定 ├─ 找到 TicketDetails 中的 ticket 属性 ├─ 建立双向映射关系 └─ 确保: ticket.details = x 时 自动触发 x.ticket = ticket 加载策略设置 ├─ 默认延迟加载(lazy loading) ├─ 访问 ticket.details 时才查询数据库 └─ 避免一次性加载所有关联数据外键定义
pyevent_id: Mapped[int | None] = mapped_column( # 添加了一个外键约束 ForeignKey("events.id") )外键即一个表中的列,其值必须来自另一个表的主键。
tickets表中的event_id是外键,event_id的值必须存在于events表的id列中,或者为NULL(如果允许),这样保证了两个表之间的数据完整性。
创建抽象层
引擎管理数据库连接并执行SQL语句,而会话则允许在事务上下文中查询、插入、更新和删除数据,确保一致性和原子性。会话绑定到引擎以与数据库通信。
py
# db_connection.py
# 实现了一个完整的异步数据库连接框架
from sqlalchemy.ext.asyncio import (
# 异步数据库会话类,用于执行异步数据库操作
AsyncSession,
# 创建异步数据库引擎的工厂函数
create_async_engine,
)
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = (
# 使用SQLite数据库,其中操作将通过支持asyncio库的aiosqlite异步库进行
"sqlite+aiosqlite:///.database.db"
)
# 异步引擎创建
# 每次调用都会返回一个新的引擎
def get_engine():
return create_async_engine(
# echo=True 启用SQL语句日志打印,便于调试
SQLALCHEMY_DATABASE_URL, echo=True
)
# 使用会话工厂指定异步会话
AsyncSessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=get_engine(),
class_=AsyncSession,
)
async def get_db_session():
async with AsyncSessionLocal() as session:
yield session
端点与初始化
py
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import Base
from app.db_connection import get_db_session, get_engine
from app.operations import (
add_sponsor_to_event,
create_event,
create_sponsor,
create_ticket,
delete_ticket,
get_all_tickets_for_show,
get_ticket,
update_ticket,
update_ticket_price,
)
# 相较于之前的内容 主要是使用了异步操作进行优化
# 生成器、上下文管理器相关内容可见 FluentPython
@asynccontextmanager
async def lifespan(app: FastAPI):
engine = get_engine()
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
await engine.dispose()
app = FastAPI(lifespan=lifespan)
@app.get("/ticket/{ticket_id}")
async def read_ticket(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
ticket_id: int,
):
ticket = await get_ticket(db_session, ticket_id)
if ticket is None:
raise HTTPException(
status_code=404, detail="Ticket not found"
)
return ticket
class TicketRequest(BaseModel):
price: float | None
show: str | None
user: str | None = None
@app.post("/ticket", response_model=dict[str, int])
async def create_ticket_route(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
ticket: TicketRequest,
):
ticket_id = await create_ticket(
db_session,
ticket.show,
ticket.user,
ticket.price,
)
return {"ticket_id": ticket_id}
class TicketDetailsUpateRequest(BaseModel):
seat: str | None = None
ticket_type: str | None = None
class TicketUpdateRequest(BaseModel):
price: float | None = Field(None, ge=0)
@app.put("/ticket/{ticket_id}")
async def update_ticket_route(
ticket_id: int,
ticket_update: TicketUpdateRequest,
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
):
update_dict_args = ticket_update.model_dump(
exclude_unset=True
)
updated = await update_ticket(
db_session, ticket_id, update_dict_args
)
if not updated:
raise HTTPException(
status_code=404, detail="Ticket not found"
)
return {"detail": "Price updated"}
@app.put("/ticket/{ticket_id}/price/{new_price}")
async def update_ticket_price_route(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
ticket_id: int,
new_price: float,
):
updated = await update_ticket_price(
db_session, ticket_id, new_price
)
if not updated:
raise HTTPException(
status_code=404, detail="Ticket not found"
)
return {"detail": "Price updated"}
@app.delete("/ticket/{ticket_id}")
async def delete_ticket_route(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
ticket_id: int,
):
ticket = await delete_ticket(db_session, ticket_id)
if not ticket:
raise HTTPException(
status_code=404, detail="Ticket not found"
)
return {"detail": "Ticket removed"}
class TicketResponse(TicketRequest):
id: int
@app.get(
"/tickets/{show}",
response_model=list[TicketResponse],
)
async def get_tickets_for_show(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
show: str,
):
tickets = await get_all_tickets_for_show(
db_session, show
)
return tickets
@app.post("/event", response_model=dict[str, int])
async def create_event_route(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
event_name: str,
nb_tickets: int | None = 0,
):
event_id = await create_event(
db_session, event_name, nb_tickets
)
return {"event_id": event_id}
@app.post(
"/sponsor/{sponsor_name}",
response_model=dict[str, int],
responses={
200: {
"description": "Successful Response",
"content": {
"application/json": {
"example": {"sponsor_id": 12345}
}
},
}
},
)
async def register_sponsor(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
sponsor_name: str,
):
sponsor_id = await create_sponsor(
db_session, sponsor_name
)
if not sponsor_id:
raise HTTPException(
status_code=400,
detail="Sponsor not created",
)
return {"sponsor_id": sponsor_id}
@app.post("/event/{event_id}/sponsor/{sponsor_id}")
async def register_sponsor_amount_contribution(
db_session: Annotated[
AsyncSession, Depends(get_db_session)
],
sponsor_id: int,
event_id: int,
amount: float | None = 0,
):
registered = await add_sponsor_to_event(
db_session, event_id, sponsor_id, amount
)
if not registered:
raise HTTPException(
status_code=400,
detail="Contribution not registered",
)
return {"detail": "Contribution registered"}
!NOTE
上下文管理器的实战应用
py# 生成器、上下文管理器相关内容可见 FluentPython # 装饰器将生成器函数变成上下文管理器 @asynccontextmanager async def lifespan(app: FastAPI): # ========== 启动阶段(对应 __aenter__)========== # 创建引擎 engine = get_engine() async with engine.begin() as conn: # 开启事务 await conn.run_sync(Base.metadata.create_all) # 创建所有表 yield # 前面是 __aenter__,后面是 __aexit__ # ========== 关闭阶段(对应 __aexit__)========== # 释放数据库连接池 await engine.dispose() app = FastAPI(lifespan=lifespan)
CRUD操作
py
# operation.py(1)
from sqlalchemy import (
and_,
delete,
select,
text,
update,
)
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, load_only
from app.database import (
Event,
Sponsor,
Sponsorship,
Ticket,
TicketDetails,
)
# 添加票证到数据库
async def create_ticket(
db_session: AsyncSession,
show_name: str,
user: str = None,
price: float = None,
) -> int:
ticket = Ticket(
show=show_name,
user=user,
price=price,
details=TicketDetails(),
)
async with db_session.begin():
db_session.add(ticket)
await db_session.flush()
ticket_id = ticket.id
await db_session.commit()
return ticket_id
# 获取票证
async def get_ticket(
db_session: AsyncSession, ticket_id: int
) -> Ticket | None:
query = (
select(Ticket)
.where(Ticket.id == ticket_id)
.options(joinedload(Ticket.details))
)
async with db_session as session:
tickets = await session.execute(query)
return tickets.scalars().first()
async def get_all_tickets_for_show(
db_session: AsyncSession, show: str
) -> list[Ticket]:
async with db_session as session:
result = await session.execute(
select(Ticket).filter(Ticket.show == show)
)
tickets = result.scalars().all()
return tickets
# 删除票证
async def delete_ticket(
db_session: AsyncSession, ticket_id
) -> bool:
async with db_session as session:
tickets_removed = await session.execute(
delete(Ticket).where(Ticket.id == ticket_id)
)
await session.commit()
if tickets_removed.rowcount == 0:
return False
return True
# 仅更新票证价格
async def update_ticket_price(
db_session: AsyncSession,
ticket_id: int,
new_price: float,
) -> bool:
query = (
update(Ticket)
.where(Ticket.id == ticket_id)
.values(price=new_price)
)
async with db_session as session:
ticket_updated = await session.execute(query)
await session.commit()
if ticket_updated.rowcount == 0:
return False
return True
async def update_ticket(
db_session: AsyncSession,
ticket_id: int,
update_ticket_dict: dict,
) -> bool:
ticket_query = update(Ticket).where(
Ticket.id == ticket_id
)
updating_ticket_values = update_ticket_dict.copy()
if updating_ticket_values == {}:
return False
ticket_query = ticket_query.values(
**updating_ticket_values
)
async with db_session as session:
result = await session.execute(ticket_query)
await session.commit()
if result.rowcount == 0:
return False
return True
async def update_ticket_details(
db_session: AsyncSession,
ticket_id: int,
updating_ticket_details: dict,
) -> bool:
ticket_query = update(TicketDetails).where(
TicketDetails.ticket_id == ticket_id
)
if updating_ticket_details == {}:
return False
ticket_query = ticket_query.values(
**updating_ticket_details
)
async with db_session as session:
result = await session.execute(ticket_query)
await session.commit()
if result.rowcount == 0:
return False
return True
async def create_event(
db_session: AsyncSession,
event_name: str,
nb_tickets: int | None = 0,
) -> int:
async with db_session.begin():
event = Event(name=event_name)
db_session.add(event)
await db_session.flush()
event_id = event.id
tickets = [
Ticket(
show=event_name,
details=TicketDetails(seat=f"{n}A"),
event_id=event_id,
)
for n in range(nb_tickets)
]
db_session.add_all(tickets)
await db_session.commit()
return event_id
async def create_sponsor(
db_session: AsyncSession,
sponsor_name: str,
) -> int:
async with db_session.begin():
sponsor = Sponsor(name=sponsor_name)
db_session.add(sponsor)
try:
await db_session.flush()
except IntegrityError:
return
sponsor_id = sponsor.id
await db_session.commit()
return sponsor_id
async def add_sponsor_to_event(
db_session: AsyncSession,
event_id: int,
sponsor_id: int,
amount: float,
) -> bool:
query = text(
"INSERT INTO sponsorships "
"(event_id, sponsor_id, amount) "
"VALUES (:event_id, :sponsor_id, :amount) "
"ON CONFLICT (event_id, sponsor_id) "
"DO UPDATE SET amount = "
"sponsorships.amount + EXCLUDED.amount"
)
params = {
"event_id": event_id,
"sponsor_id": sponsor_id,
"amount": amount,
}
async with db_session.begin():
result = await db_session.execute(query, params)
await db_session.commit()
if result.rowcount == 0:
return False
return True
async def get_event(
db_session: AsyncSession, event_id: int
) -> Event | None:
query = (
select(Event)
.where(Event.id == event_id)
.options(
joinedload(Event.sponsors)
) # check to remove select in load
)
async with db_session as session:
result = await session.execute(query)
event = result.scalars().first()
return event
async def sell_ticket_to_user(
db_session: AsyncSession, ticket_id: int, user: str
) -> bool:
ticket_query = (
update(Ticket)
.where(
and_(
Ticket.id == ticket_id,
Ticket.sold == False,
)
)
.values(user=user, sold=True)
)
async with db_session as session:
result = await session.execute(ticket_query)
await session.commit()
if result.rowcount == 0:
return False
return True
SQL 性能优化
避免 N+1 查询
N+1 查询问题的场景:
假设有 5 个 Event,每个 Event 有多个 Sponsor
❌ 不优化的方式(N+1 查询):
第1次查询:获取所有 Event
SELECT * FROM events
└─ 结果:5 个 Event 对象
第2-6次查询:为每个 Event 查询其 Sponsor
SELECT * FROM sponsors
WHERE event_id = 1
SELECT * FROM sponsors
WHERE event_id = 2
SELECT * FROM sponsors
WHERE event_id = 3
SELECT * FROM sponsors
WHERE event_id = 4
SELECT * FROM sponsors
WHERE event_id = 5
└─ 结果:共 5 次额外查询
总查询数:1 + 5 = 6 次(N+1)
py
# operation.py(2)
# (SQL性能优化)避免 N+1 查询
async def get_events_with_sponsors(
db_session: AsyncSession,
) -> list[Event]:
query = select(Event).options(
# 关键优化
joinedload(Event.sponsors)
)
async with db_session as session:
result = await session.execute(query)
events = result.scalars().all()
return events
SQL
# 使用了 joinedload 后实际生成的 SQL
SELECT
events.id, events.name,
sponsors.id, sponsors.name
FROM events
LEFT JOIN sponsorships ON events.id = sponsorships.event_id
LEFT JOIN sponsors ON sponsorships.sponsor_id = sponsors.id
# 只需 1 次查询 一次性获取所有数据
# 结果在内存中组装关系
谨慎使用连接语句
py
# 获取某个活动中赞助商名称及其捐款金额的列表,按金额从高到低排序
async def get_event_sponsorships_with_amount(
db_session: AsyncSession, event_id: int
):
query = (
# 只检索需要的字段 减少传输量
select(Sponsor.name, Sponsorship.amount)
.join(
# 连接 Sponsorship 表与 Sponsor 表
Sponsorship,
# 连接条件是赞助商 ID 必须相等
Sponsorship.sponsor_id == Sponsor.id,
)
.where(Sponsorship.event_id == event_id)
.order_by(Sponsorship.amount.desc())
)
async with db_session as session:
result = await session.execute(query)
sponsor_contributions = result.fetchall()
return sponsor_contributions
最小化获取数据
py
# 获取特定是事件的所有票务
async def get_events_tickets_with_user_price(
db_session: AsyncSession, event_id: int
) -> list[Ticket]:
query = (
select(Ticket)
.where(Ticket.event_id == event_id)
.options(
# 指定只加载特定字段
load_only(
Ticket.id, Ticket.user, Ticket.price
)
)
)
async with db_session as session:
result = await session.execute(query)
tickets = result.scalars().all()
return tickets
数据迁移
数据库迁移用于管理数据库的结构定义 (schema),对数据库模式进行版本控制,并使其在不同环境中保持一致。有助于自动化数据库变更的部署,并跟踪模式演变的历史。
为了避免如下情况:
"我给users表加了phone字段"
"我给users表加了address字段"
小明部署
小红部署
发布时
开发者小明
本地数据库
开发者小红
本地数据库
测试环境
缺少address字段
缺少phone字段
生产环境
两个字段都缺失,服务崩溃
bash
# 安装 alembic
pip install alembic
# 设置alembic
# 创建一个配置文件 alembic.ini 和对应文件夹
alembic init alembic
# 对应文件夹的目录结构
alembic/
├── env.py # 用于创建数据库迁移的变量
├── script.py.mako # 迁移脚本示例
├── README
└── versions/ (包含多个迁移文件)
├── c89449e66666_start_database.py
├── ......
# 设置 alembic.ini 中的内容
# 指定正在使用 SQLite数据库
sqlalchemy.url = sqlite:///.database.db
py
# env.py
# 找到 target_metadata 变量,并设置为应用程序的元数据
from app.database import Base
target_metadata = Base.metadata
bash
# 在项目配置完成后,生成初始数据库结构迁移脚本
# 自动创建一个迁移脚本,放置在alembic/versions文件夹中
alembic revision --autogenerate -m "Start database"
# 每次修改数据库模型(增删改字段/表/索引)后,立即生成迁移脚本
alembic revision --autogenerate -m "add a ticket"
# 进行迁移
# 自动重建 .database.db
# 新环境初始化以及更新代码后运行
alembic upgrade head
命令
修正索引/默认值等
命令
是
命令
命令
否
紧急情况
命令
项目初始化
配置 alembic.ini & env.py
定义 SQLAlchemy 模型
生成首个迁移脚本
alembic revision --autogenerate -m 'initial'
人工审查脚本
应用迁移
alembic upgrade head
开发新功能
修改了数据库模型?
生成新迁移脚本
alembic revision --autogenerate -m 'add_ticket'
审查并修正脚本
应用新迁移
alembic upgrade head
回滚迁移
alembic downgrade -1
数据保护
上文已经建立了 CreditCard类
python
# security.py(1)
from cryptography.fernet import Fernet
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import CreditCard
# 生成一个对称(加密和解密用同一个密钥)的加密密钥
key = Fernet.generate_key()
# 使用密钥初始化 Fernet 对象
# 可以使用 .encrypt() 和 .decrypt() 方法
cypher_suite = Fernet(key)
# 加密函数
def encrypt_credit_card_info(card_info: str) -> str:
return cypher_suite.encrypt(
card_info.encode()
).decode()
# .encode() 将字符串转化成字节
# .decode() 将字节码转化成字符串
输入:
"4532-1111-2222-3333"(明文信用卡号)
↓
.encode()
↓
b'4532-1111-2222-3333'(字节)
↓
Fernet.encrypt()
↓
b'gAAAAAB1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h5'(加密字节)
↓
.decode()
↓
输出:
'gAAAAAB1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h5'(密文字符串)
py
# 解密函数
def decrypt_credit_card_info(
encrypted_card_info: str,
) -> str:
return cypher_suite.decrypt(
encrypted_card_info.encode()
).decode()
输入:
'gAAAAAB1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h5'(密文字符串)
↓
.encode()
↓
b'gAAAAAB1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h5'(密文字节)
↓
Fernet.decrypt()
↓
b'4532-1111-2222-3333'(明文字节)
↓
.decode()
↓
输出:
'4532-1111-2222-3333'(明文字符串)
py
# 存储信用卡信息
async def store_credit_card_info(
db_session: AsyncSession,
card_number: str,
card_holder_name: str,
expiration_date: str,
cvv: str,
):
# 加密敏感信息
encrypted_card_number = encrypt_credit_card_info(
card_number
)
# cvv 是卡片验证值 也属于敏感信息
encrypted_cvv = encrypt_credit_card_info(cvv)
# 创建 ORM 对象(存储的是密文)
credit_card = CreditCard(
number=encrypted_card_number,
card_holder_name=card_holder_name,
expiration_date=expiration_date,
cvv=encrypted_cvv,
)
async with db_session.begin():
db_session.add(credit_card)
await db_session.flush()
credit_card_id = credit_card.id
await db_session.commit()
return credit_card_id
# 检索信用卡信息
async def retrieve_credit_card_info(
db_session: AsyncSession, credit_card_id: int
):
query = select(CreditCard).where(
CreditCard.id == credit_card_id
)
async with db_session as session:
result = await session.execute(query)
credit_card = result.scalars().first()
credit_card_number = decrypt_credit_card_info(
credit_card.number
)
cvv = decrypt_credit_card_info(credit_card.cvv)
return {
"card_number": credit_card_number,
"card_holder_name": credit_card.card_holder_name,
"expiration_date": credit_card.expiration_date,
"cvv": cvv,
}
"数据库" "database.py(CreditCard)" "security.py" "API路由/控制器" "客户端" "数据库" "database.py(CreditCard)" "security.py" "API路由/控制器" "客户端" "提交信用卡信息" "调用 store_credit_card_info(...)" "encrypt_credit_card_info(number)" "encrypt_credit_card_info(cvv)" "构造 CreditCard(number加密,cvv加密,...)" "插入记录" "成功" "返回新记录ID" "返回ID" "查询信用卡信息" "调用 retrieve_credit_card_info(id)" "按ID查询 CreditCard" "返回加密记录" "decrypt_credit_card_info(number)" "decrypt_credit_card_info(cvv)" "返回{card_number, cvv, ...}" "响应解密后的信息"