AI头条项目二用户模块的实现,全局异常处理器

全局异常处理器:

全局异常处理器是注册在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
相关推荐
李昊哲小课6 小时前
fastapi sse websocket 奶茶店实时订单看板
人工智能·python·websocket·网络协议·fastapi·sse
李昊哲小课15 小时前
fastapi sse websocket 智能家居实时控制台
python·websocket·智能家居·fastapi·sse
赵广陆16 小时前
SQLAlchemy ORM 超全教程
python·fastapi
为啥全要学18 小时前
fastapi websocket全双工通信
websocket·网络协议·fastapi
这就是佬们吗18 小时前
企业Agent落地为什么需要RAG?
人工智能·python·fastapi
鲨鱼辣钊2 天前
【FastAPI 筑基 - Day2】彩色日志封装 + Python 装饰器精讲
fastapi
Muselit2 天前
Python 泛型:把 list[User] 讲明白
python·fastapi
赵广陆2 天前
FastAPI零基础完整实战
前端·chrome·fastapi
0566462 天前
agent学习Day15——SQLAlchemy 查询过滤、分页与历史列表接口
python·学习·fastapi
淼澄研学3 天前
Python构建AI应用:从环境配置到FastAPI部署的5个实操步骤
人工智能·python·fastapi