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
相关推荐
海市公约42 分钟前
FastAPI中间件与依赖系统实战指南
中间件·fastapi·依赖注入·异步编程·web架构·代码复用·拦截器模式
Li emily2 小时前
股票api接口类型全解:实时行情、历史数据与技术指标
人工智能·api·fastapi
Li emily13 小时前
外汇api实战:如何获取实时汇率数据并处理
人工智能·api·fastapi
深兰科技1 天前
深兰科技与宝武集团旗下钢友汇达成国际市场合作,俄罗斯、巴西、阿根廷市场同步推进
人工智能·django·fastapi·pygame·httpx·视觉大模型·深兰科技
qq_283720052 天前
Python Celery + FastAPI + Vue 全栈异步任务实战
vue.js·python·fastapi
呱牛do it2 天前
企业级绩效考核系统设计与实现:基于FastAPI + Vue3的全栈解决方案
python·fastapi
曲幽2 天前
我用fastapi-scaff搭了个项目,两天工期缩到两小时,老板以为我开挂了
python·api·fastapi·web·celery·cli·db·alembic·fastapi-scaff
时光不写代码2 天前
修复 pytest-asyncio 事件循环冲突:完整解决方案
python·pytest·fastapi
chushiyunen2 天前
python fastapi使用、uvicorn
开发语言·python·fastapi
Flying pigs~~2 天前
检索增强生成RAG项目tools_04:flask➕fastapi➕高并发
数据库·python·flask·大模型·fastapi·异步