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
相关推荐
曲幽21 小时前
FastAPI状态共享秘籍:别再让中间件、依赖和路由“各自为政”了!
python·fastapi·web·request·state·depends·middleware
浮生札记1 天前
腾讯云 COS STS 临时密钥上传
python·腾讯云·fastapi·对象存储
在屏幕前出油1 天前
04. FastAPI——响应类型
开发语言·后端·python·pycharm·fastapi
苦瓜小生1 天前
AI-TestHub:我如何从零开发一个智能测试用例生成平台
人工智能·python·测试工具·github·测试用例·fastapi
tryCbest1 天前
Python之FastAPI 开发框架(第三篇):高级特性与实战
开发语言·python·fastapi
写代码的【黑咖啡】2 天前
Python Web 开发新宠:FastAPI 全面指南
前端·python·fastapi
曲幽2 天前
FastAPI实战:WebSocket vs Socket.IO,这回真给我整明白了!
python·websocket·nginx·socket·fastapi·web·async·socketio
I'm Jie2 天前
Swagger UI 本地化部署,解决 FastAPI Swagger UI 依赖外部 CDN 加载失败问题
python·ui·fastapi·swagger·swagger ui
七夜zippoe2 天前
OpenClaw 会话管理:单聊、群聊、多模型
大数据·人工智能·fastapi·token·openclaw