全局异常处理器:
全局异常处理器是注册在FastAPI应用级别的异常处理函数,用于捕获业务层,数据库以及系统层抛出的异常,并以统一的响应格式返回给前端
from http.client 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):
"""
注册全局异常处理
"""
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) #兜底
import traceback
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette import status
# 开发模式:返回详细错误信息
# 生产模式:返回简化错误信息
DEBUG_MODE = True # 教学项目保持开启
async def http_exception_handler(request: Request, exc: HTTPException):
"""
处理 HTTPException 异常
"""
# HTTPException 通常是业务逻辑主动抛出的,data 保持 None
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"data": None
}
)
async def integrity_error_handler(request: Request, exc: IntegrityError):
"""
处理数据库完整性约束错误
"""
error_msg = str(exc.orig)
# 判断具体的约束错误类型
if "username_UNIQUE" in error_msg or "Duplicate entry" in error_msg:
detail = "用户名已存在"
elif "FOREIGN KEY" in error_msg:
detail = "关联数据不存在"
else:
detail = "数据约束冲突,请检查输入"
# 开发模式下返回详细错误信息
error_data = None
if DEBUG_MODE:
error_data = {
"error_type": "IntegrityError",
"error_detail": error_msg,
"path": str(request.url)
}
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"code": 400,
"message": detail,
"data": error_data
}
)
async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
"""
处理 SQLAlchemy 数据库错误
"""
# 开发模式下返回详细错误信息
error_data = None
if DEBUG_MODE:
error_data = {
"error_type": type(exc).__name__,
"error_detail": str(exc),
# 格式化异常信息为字符串,方便日志记录和调试
"traceback": traceback.format_exc(),
"path": str(request.url)
}
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"code": 500,
"message": "数据库操作失败,请稍后重试",
"data": error_data
}
)
async def general_exception_handler(request: Request, exc: Exception):
"""
处理所有未捕获的异常
"""
# 开发模式下返回详细错误信息
error_data = None
if DEBUG_MODE:
error_data = {
"error_type": type(exc).__name__,
"error_detail": str(exc),
# 格式化异常信息为字符串,方便日志记录和调试
"traceback": traceback.format_exc(),
"path": str(request.url)
}
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"code": 500,
"message": "服务器内部错误",
"data": error_data
}
)
#注册全局异常处理器
register_exception_handlers(app)


models
from datetime import datetime
from typing import Optional
from sqlalchemy import Index, Integer, String, Enum, DateTime, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
"""
用户信息表ORM模型
"""
__tablename__ = 'user'
# 创建索引
__table_args__ = (
Index('username_UNIQUE', 'username'),
Index('phone_UNIQUE', 'phone'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="用户ID")
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="用户名")
password: Mapped[str] = mapped_column(String(255), nullable=False, comment="密码(加密存储)")
nickname: Mapped[Optional[str]] = mapped_column(String(50), comment="昵称")
avatar: Mapped[Optional[str]] = mapped_column(String(255), comment="头像URL",
default='https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg')
gender: Mapped[Optional[str]] = mapped_column(Enum('male', 'female', 'unknown'), comment="性别", default='unknown')
bio: Mapped[Optional[str]] = mapped_column(String(500), comment="个人简介", default='这个人很懒,什么都没留下')
phone: Mapped[Optional[str]] = mapped_column(String(20), unique=True, comment="手机号")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now(), comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now(), onupdate=datetime.now(),
comment="更新时间")
def __repr__(self):
return f"<User(id={self.id}, username='{self.username}', nickname='{self.nickname}')>"
class UserToken(Base):
"""
用户令牌表ORM模型
"""
__tablename__ = 'user_token'
# 创建索引
__table_args__ = (
Index('token_UNIQUE', 'token'),
Index('fk_user_token_user_idx', 'user_id'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="令牌ID")
user_id: Mapped[int] = mapped_column(Integer, ForeignKey(User.id), nullable=False, comment="用户ID")
token: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, comment="令牌值")
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="过期时间")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now(), comment="创建时间")
def __repr__(self):
return f"<UserToken(id={self.id}, user_id={self.user_id}, token='{self.token}')>"
Optional[参数类型]: Optional 是啥呢,就是传参的时候,不是必填的,记住不是必填的就上,或者看见数据库字段中 有 noll 的时候就填上
然后我们的请求体参数来接收账号和密码
因为密码要加密,并且有可能很多的功能模块都要使用所以,我们定义一个utils
from passlib.context import CryptContext # 创建密码上下文 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def get_hash_password(password: str): return pwd_context.hash(password)这个schemes这个后面跟的是加密算法,deprecated 则是当schemes有多个加密算法时,它会判断那个加密算法版本问题,或者能不能正常使用,如果不能,他会打上标记
schemas:
class UserRequest(BaseModel):
username: str
password: str
routers:
@router.post("/register") async def register( user_data:UserRequest, db:AsyncSession = Depends(get_db), ): existing_user = await users.get_user_by_username(db,user_data.username) if existing_user: raise HTTPException(status_code=400, detail="用户已存在") user = await users.create_user(db,user_data) return { "code":200, "message": "注册成功", "data":{ "token":"用户访问令牌", "userInfo":{ "id":user.id, "username": user.username, "bio":user.bio, "avater": user.avatar } } }
这个业务的是实现,首先就是我们在创建账号的时候,我们不可能注册相同的用户名,所以我们在注册的时候,我们就需要先根据用户名查数据库里是否存在,如果不存在,我们才能使用,如果存在,那么我们就抛个异常处理
async def get_user_by_username(db:AsyncSession,username:str):
result = await db.execute(select(User).where(User.username == username))
return result.scalar_one_or_none()
然后如果用户名没有异常,那么就是创建用户,创建用户,我们不能在数据库能查出来原本的密码,应该查出来的是加密的密码
我们用到的加密工具在上面
#创建用户:
async def create_user(db:AsyncSession,user_data:UserRequest):
# 先密码加密处理 -> add
hashed_password = get_hash_password(user_data.password)
user = User(username=user_data.username,password=hashed_password)
db.add(user)
await db.commit()
await db.refresh(user) # 确保从数据库里拿到最新1的数据
return user
Token:
Token: 是服务器发给客户端的一段字符串,用来在后续请求中证明"你已经登录过了"
作用: 解决HTTP是无状态的问题,在每次请求中"自我证明省份"
通俗的来说
就是如果我们访问其他的请求时,HTTP是不知道我们登录过的,既然没登陆过,怎么能访问其他请求呢,所以我们要弄个token,登录成功后,我就给你一个临时token,每次访问其他请求的时候,先验证token,如果token没啥问题,你就能登录

Token在请求中的位置: 请求头
Authorization: Bearer<token>
Authorization: 专门用来放身份信息
Bearer: 表示"持有者令牌"
<token>: 真正的身份凭证
我们怎么创建tooken令牌
我们直接用uuid 来生成一个令牌,然后我们的表 id和tooken绑着,如果能根据id能找打这个人,我们就更新tooken,如果没找到,我们执行新增操作
async def creat_token(db:AsyncSession,user_id:int): token = str(uuid.uuid4()) expires_at = datetime.now() + timedelta(days=7) result=await db.execute(select(UserToken).where(UserToken.user_id == user_id)) user_token = result.scalar_one_or_none() if user_token: user_token.token = token user_token.expires_at = expires_at else: user_token = UserToken(user_id=user_id, token=token, expires_at=expires_at) db.add(user_token) await db.commit() return token