AI 掘金头条项目-用户模块、收藏模块以及Redis和调用大模型实现

1、用户模块

本模块涉及三个核心功能的开发

1、用户注册

1️⃣ 接口文档
  • 接口地址 : POST /api/user/register
  • 请求参数:
参数名 类型 必填 说明
username string 用户名
password string 密码
  • 请求示例:
json 复制代码
{
  "username": "example_user",
  "password": "example_password"
}
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "注册成功",
  "data": {
    "token": "用户访问令牌",
    "userInfo": {
      "id": 1,
      "username": "example_user",
      "bio": "这个人很懒,什么都没留下",
      "avatar": "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
    }
  }
}
2️⃣ 基础路由实现

先创建一个routers/user.py文件

python 复制代码
from fastapi import APIRouter

router = APIRouter(prefix="/api/user", tags=["users"])


@router.post("/register")
async def register():
    return {
        "code": 200,
        "message": "注册成功",
        "data": {
            "token": "用户访问令牌",
            "userInfo": {
                "id": 1,
                "username": "example_user",
                "bio": "这个人很懒,什么都没留下",
                "avatar": "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
            }
        }
    }

然后再main.py中进行挂载

python 复制代码
from routers import news,users

app.include_router(users.router)

接着创建一个为用户注册进行类型校验的模块

python 复制代码
# 该模块为用户注册做类型校验
from pydantic import BaseModel

class UserRequest(BaseModel):
    username: str
    password: str
python 复制代码
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from schemas.users import UserRequest

from config.db_config import get_db

router = APIRouter(prefix="/api/user", tags=["users"])


@router.post("/register")
async def register(user_data: UserRequest,db: AsyncSession= Depends(get_db)): # 用户信息 和 db
    return {
        "code": 200,
        "message": "注册成功",
        "data": {
            "token": "用户访问令牌",
            "userInfo": {
                "id": 1,
                "username": user_data.username,
                "bio": "这个人很懒,什么都没留下",
                "avatar": "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
            }
        }
    }
3️⃣ 用户注册实现

先准备用户表和Token表模型类

python 复制代码
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表示可选的,说明该字段可以为空

封装crud代码

python 复制代码
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from models.users import User
from schemas.users import UserRequest


# 根据用户名查询数据库
async def get_user_by_username(db: AsyncSession, username: str):
    query = select(User).where(User.username == username)
    result = await db.execute(query)
    return result.scalar_one_or_none()

接下来需要写创建用户的方法

创建用户的话,涉及到密码的操作,在生产环境中,密码都是加密处理的,因为这里使用Python的第三方模块来进行加密--->passlib 密码加密

powershell 复制代码
# passlib[bcrypt]==1.7.4 官⽅⻓期稳定版本
pip install "passlib[bcrypt]==1.7.4"

passlib 密码加密流程

1、安装依赖

2、创建加密上下文

3、加密或校验密码

1️⃣ 安装依赖

  • 作用:安装指定版本的 passlib 库,同时附带 bcrypt 算法支持,是后续密码加密的基础。

2️⃣ 创建加密上下文

  • 作用:初始化一个密码处理的核心对象,指定使用 bcrypt 算法,自动处理废弃算法,统一管理后续的加密和校验操作。

3️⃣ 加密或校验

  • 加密:pwd_context.hash(password)
    • 作用:把用户的明文密码,转换成不可逆的 bcrypt 哈希字符串。
  • 校验:pwd_context.verify(plain_password, hashed_password)
    • 作用:对比用户输入的明文密码,和数据库里存的哈希密码是否匹配,返回 True/False。
plain 复制代码
pip install "passlib[bcrypt]==1.7.4"
plain 复制代码
pwd_context = CryptContext(
    schemes=["bcrypt"],
    deprecated="auto")

编写密码加密模块

python 复制代码
from passlib.context import CryptContext

# 创建密码上下文对象,指定使用bcrypt算法
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 密码加密
def get_hash_password(password: str):
    return pwd_context.hash(password)

实现curd创建用户的方法

python 复制代码
# 创建用户
async def create_user(db:AsyncSession,user_data: UserRequest):
    # 先密码加密处理 -> add用户
    hashed_password = security.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)  # 从数据库中读回最新的user数据,确保拿到的数据就是写入到数据库的数据
    return user

接着在路由模块中调用这些方法

python 复制代码
@router.post("/register")
async def register(user_data: UserRequest,db: AsyncSession= Depends(get_db)): # 用户信息 和 db
    # 注册逻辑:验证用户是否存在->创建用户->生成Token->响应结果
    existing_user = await users.get_user_by_username(db, user_data.username)
    # 验证用户是否存在
    if existing_user:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 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,
                "avatar": user.avatar
            }
        }
    }

测试报错了

遇到的问题是 bcrypt 4.0+ 版本与 passlib 的兼容性问题。通过安装 bcrypt==3.2.2 这个稳定版本,成功解决了密码加密时的错误。

这里我们需要降级bcrypt依赖的版本

powershell 复制代码
pip install bcrypt==3.2.2

然后再次进行测试,现在就可以成功了

现在bcrypt和 passlib的版本如下:

这个也可以尝试其他bcrypt的版本试试,因为现在最新版本(我安装的bcrypt版本是5.0.0)的使用起来和 passlib不兼容

4️⃣ Token

Token:是服务器发给客户端的一段**字符串****,用来在后续请求中证明"你已经登录过了"**

作用:解决 HTTP 是无状态的问题,在每次请求中"自我证明身份

Token 在请求中的位置:请求头

  • Authorization:专门用来放身份信息
  • Bearer:表示"持有者令牌"
  • :真正的身份凭证
python 复制代码
# 生成Token
async def create_token(db: AsyncSession, user_id:  int):
    # 生成Token + 设置过期时间->查询数据库当前用户是否有Token->有:更新;没有:添加
    token = uuid.uuid4()
    # timedelta(days=7,hours=2,minutes=30,seconds=10)
    expires_at = datetime.now() + timedelta(days=7)
    query = select(UserToken).where(UserToken.user_id == user_id)
    result = await db.execute(query)
    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
python 复制代码
@router.post("/register")
async def register(user_data: UserRequest, db: AsyncSession = Depends(get_db)):  # 用户信息 和 db
    # 注册逻辑:验证用户是否存在->创建用户->生成Token->响应结果
    existing_user = await users.get_user_by_username(db, user_data.username)
    # 验证用户是否存在
    if existing_user:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户已存在")
    user = await users.create_user(db, user_data)
    token = await users.create_token(db, user.id)
    return {
        "code": 200,
        "message": "注册成功",
        "data": {
            "token": token,
            "userInfo": {
                "id": user.id,
                "username": user.username,
                "bio": user.bio,
                "avatar": user.avatar
            }
        }
    }

接着在前端进行测试

5️⃣ 封装通用成功响应格式

为什么一定要封装一个统一的响应类

  1. 自动校验数据,防止返回错误内容

字典是 "随便写" 的:

  • 少写字段、写错名字、返回错误类型,代码不会报错
  • 前端拿到数据会直接崩溃

Pydantic 模型:

  • 必须按定义的字段返回
  • 少传、传错、类型不对,接口直接报错,提前发现 bug
  1. 自动生成接口文档,前端直接用

FastAPI 会自动生成 Swagger 文档

  • 封装了模型 → 文档里能看到每个字段的含义、类型、示例
  • 前端不用问你,直接看文档开发

直接返回字典 → 文档里看不到任何结构,全靠沟通

  1. 代码更规范、好维护、支持复用
  • 所有接口返回格式统一
  • 字段名写错会直接标红报错(字典不会)
  • 多个接口可以复用同一个响应模型
  • 后期改字段,只改一处,不用到处找字典
1、抽取响应结果

把这个响应数据注释掉,然后抽取一个公共的响应类

python 复制代码
# return {
#     "code": 200,
#     "message": "注册成功",
#     "data": {
#         "token": token,
#         "userInfo": {
#             "id": user.id,
#             "username": user.username,
#             "bio": user.bio,
#             "avatar": user.avatar
#         }
#     }
# }
python 复制代码
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder


def success_response(message: str = "success", data=None):
    content = {
        "code": 200,
        "message": message,
        "data": data
    }
    # 目标:把任何的FastAPI、Pydantic、ORM对象都要正常响应->code、message、data
    return JSONResponse(jsonable_encoder(content))
2、定义数据类型
python 复制代码
# user_info 对应的响应数据类:基础类 + Info类(id、用户名)
class UserInfoBase(BaseModel):
    """
    ⽤户信息基础数据模型
    """
    nickname: Optional[str] = Field(None, max_length=50, description="昵称")
    avatar: Optional[str] = Field(None, max_length=255, description="头像URL")
    gender: Optional[str] = Field(None, max_length=10, description="性别")
    bio: Optional[str] = Field(None, max_length=500, description="个⼈简介")


class UserInfoResponse(UserInfoBase):
    id: int
    username: str
    # 模型类配置
    model_config = ConfigDict(
        populate_by_name=True,  # alias/字段名兼容
        from_attributes=True  # 允许从ORM对象属性中取值
    )


# data 数据类型
class UserAuthResponse(BaseModel):
    token: str
    user_info: UserInfoResponse = Field(..., alias="userInfo")
    # 模型类配置
    model_config = ConfigDict(
        populate_by_name=True,  # alias/字段名兼容
        from_attributes=True  # 允许从ORM对象属性中取值
    )
3、调用函数响应结果
python 复制代码
@router.post("/register")
async def register(user_data: UserRequest, db: AsyncSession = Depends(get_db)):  # 用户信息 和 db
    # 注册逻辑:验证用户是否存在->创建用户->生成Token->响应结果
    existing_user = await users.get_user_by_username(db, user_data.username)
    # 验证用户是否存在
    if existing_user:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户已存在")
    user = await users.create_user(db, user_data)
    token = await users.create_token(db, user.id)
    # return {
    #     "code": 200,
    #     "message": "注册成功",
    #     "data": {
    #         "token": token,
    #         "userInfo": {
    #             "id": user.id,
    #             "username": user.username,
    #             "bio": user.bio,
    #             "avatar": user.avatar
    #         }
    #     }
    # }
    response_data = UserAuthResponse(token=token, user_info=UserInfoResponse.model_validate(user))
    return success_response(data=response_data)

进行测试发现报错

这个错误的原因是 类型不匹配:

  • 问题根源:在 crud/users.py 的第 31 行,create_token 函数生成的是一个 UUID 对象(uuid.uuid4())
  • 期望类型:但在 schemas/users.py 的第 35 行,UserAuthResponse 模型中的 token 字段定义为 str 类型
  • 验证失败:当你尝试将 UUID 对象赋值给需要字符串的字段时,Pydantic 验证器就会报错

只需将 uuid.uuid4() 改为 str(uuid.uuid4()) 即可解决问题。

python 复制代码
# 生成Token
async def create_token(db: AsyncSession, user_id:  int):
    # 生成Token + 设置过期时间->查询数据库当前用户是否有Token->有:更新;没有:添加
    token = str(uuid.uuid4())
    # timedelta(days=7,hours=2,minutes=30,seconds=10)
    expires_at = datetime.now() + timedelta(days=7)
    query = select(UserToken).where(UserToken.user_id == user_id)
    result = await db.execute(query)
    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️⃣ 全局异常处理器

全局异常处理器(Global Exception Handler)是注册在 FastAPI 应用级别的异常处理函数 ,用于捕获业务层、数据库层以及系统层抛出的异常,并以统一的响应格式返回给前端。

异常:

  • SQL 错误
  • 外键关联失败
  • 数据库连接异常
  • 提交事务失败
  • ...
FastAPI中实现全局异常处理器步骤

step1:定义异常处理器(函数)

step2 :全局注册异常处理器,app.add **exception **handler()

注册的顺序:

  1. 业务异常
  2. 数据约束异常(子类)
  3. 数据库异常(父类)
  4. 所有异常
python 复制代码
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
        }
    )

定义一个函数,一次性全部注册上面这四个异常处理器

python 复制代码
from fastapi import HTTPException
from pymysql import IntegrityError
from sqlalchemy.exc import SQLAlchemyError

from utils.exception import http_exception_handler, integrity_error_handler, sqlalchemy_error_handler, \
    general_exception_handler


def register_exception_handler(app):
    """
    注册全局异常处理,子类在前,父类在后;具体的在前面,抽象在后
    :param app: 
    :return: 
    """
    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) # 其他异常处理,兜底的方案

在main.py里面调用实现注册

python 复制代码
from utils.exception_handler import register_exception_handler

app = FastAPI()
register_exception_handler(app)

2、用户登录

1️⃣ 接口文档
  • 接口地址 : POST /api/user/login
  • 请求参数:
参数名 类型 必填 说明
username string 用户名
password string 密码
  • 请求示例:
json 复制代码
{
  "username": "example_user",
  "password": "example_password"
}
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "登录成功",
  "data": {
    "token": "用户访问令牌",
    "userInfo": {
      "id": 1,
      "username": "example_user",
      "nickname": null,
      "avatar": "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg",
      "bio": "这个人很懒,什么都没留下"
    }
  }
}
2️⃣ 功能实现

实现crud方法

python 复制代码
# 验证登录
async def authenticate_user(db: AsyncSession, username: str, password: str):
    user = await get_user_by_username(db, username)
    if not user:
        return None
    if not security.verify_password(password, user.password):
        return None
    return user

实现验证密码

python 复制代码
# 验证密码:verify返回值是布尔值
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

路由调用

python 复制代码
@router.post("/login")
async def login(user_data: UserRequest, db: AsyncSession = Depends(get_db)):
    # 登录逻辑:验证用户是否存在->验证密码--->生成Token->响应结果
    user = await users.authenticate_user(db, user_data.username, user_data.password)
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误")
    token = await users.create_token(db, user.id)
    response_data = UserAuthResponse(token=token, user_info=UserInfoResponse.model_validate(user))
    return success_response(message="登录成功", data=response_data)

3、获取用户信息

1️⃣ 接口文档
  • 接口地址 : GET /api/user/info
  • 请求头: 需要认证
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "username": "example_user",
    "nickname": null,
    "avatar": "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg",
    "gender": "unknown",
    "bio": "这个人很懒,什么都没留下"
  }
}
2️⃣ 功能实现

**HTTP 头部参数:**请求头(HTTP Header)主要包含身份信息、请求控制信息、数据格式说明和客户端环境信息。Header 用来从 HTTP 请求头中获取数据,是 FastAPI 提供的参数依赖工具

这里因为实现获取个人信息/修改信息/密码呀都需要登录,因此抽取一个工具模块

先封装crud代码

python 复制代码
# 根据Token查询用户:验证Token->查询用户
async def get_user_by_token(db: AsyncSession, token: str):
    query = select(UserToken).where(UserToken.token == token)
    result = await db.execute(query)
    db_token = result.scalar_one_or_none()
    # 验证Token是否过期以及Token是否存在,如果过期/不存在则返回None
    if not db_token or db_token.expires_at < datetime.now():
        return None
    query = select(User).where(User.id == db_token.user_id)
    result = await db.execute(query)
    return result.scalar_one_or_none()

封装工具函数->验证登录的函数

python 复制代码
from fastapi import Header, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status

from config.db_config import get_db
from crud import users


#  整合 根据Token查询用户,返回用户
async def get_current_user(
        authorization: str = Header(...,alias="Authorization"),
        db: AsyncSession = Depends(get_db)
):
    # Token格式:Bearer xxxxxxxxxxxxx
    # 获取Token的值:方法一
    # token = authorization.split(" ")[1]
    # 获取Token的值:方法二
    token = authorization.replace("Bearer ","")
    user = await users.get_user_by_token(db,token)
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的令牌或已经过期的令牌")
    return user

路由调用

python 复制代码
# 查Token查用户->封装crud->功能整合成一个工具函数->路由导入使用:依赖注入
@router.get("/info")
async def get_user_info(user: User = Depends(get_current_user)):
    return success_response(message="获取用户信息成功", data=UserInfoResponse.model_validate(user))

4、修改用户信息

1️⃣ 接口文档
  • 接口地址 : PUT /api/user/update
  • 请求头: 需要认证
  • 请求参数:
参数名 类型 必填 说明
nickname string 昵称
avatar string 头像URL
gender string 性别
bio string 个人简介
phone string 手机号
  • 请求示例:
json 复制代码
{
  "bio": "这是我的个人简介"
}
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "更新成功",
  "data": {
    "id": 1,
    "username": "example_user",
    "nickname": null,
    "avatar": "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg",
    "gender": "unknown",
    "bio": "这是我的个人简介"
  }
}
2️⃣功能实现

定义Pydantic类型

python 复制代码
# 更新用户信息的模型类
class UserUpdateRequest(BaseModel):
    nickname: str = None
    avatar: str = None
    gender: str = None
    bio: str = None
    phone: str = None

封装crud代码

python 复制代码
# 更新用户信息:update更新->检查是否命中->获取更新后的用户返回
async def update_user_info(db: AsyncSession, username: str, user_data: UserRequest):
    # update(User).where(User.username == username).values(字段=值,字段=值)
    # user_data 是一个Pydantic类型,需要得到的数据类型是字典->**解包  使用model_dump()把数据转换成字典
    # 没有设置值的不更新
    query = update(User).where(User.username == username).values(**user_data.model_dump(
        exclude_unset=True,
        exclude_none= True
    ))
    result = await db.execute(query)
    await db.commit()
    # 检查更新
    if result.rowcount == 0:
        return HTTPException(status_code=404, detail="用户不存在")
    # 获取一下更新后的用户
    updated_user = await get_user_by_username(db, username)
    return updated_user

路由调用

python 复制代码
# 修改用户信息:验证Token->更新(用户输入数据,put提交->请求体参数->定义Pydantic类型)->响应结果
# 参数:用户输入的+验证Token的+db(调用更新的方法)
@router.put("/update")
async def update_user_info(user_data: UserUpdateRequest, user: User = Depends(get_current_user),
                           db: AsyncSession = Depends(get_db)):
    user = await users.update_user_info(db, user.username, user_data)
    return success_response(message="修改用户信息成功", data=UserInfoResponse.model_validate(user))

5、修改密码

1️⃣ 接口文档
  • 接口地址 : PUT /api/user/password
  • 请求头: 需要认证
  • 请求参数:
参数名 类型 必填 说明
oldPassword string 当前密码
newPassword string 新密码
  • 请求示例:
json 复制代码
{
  "oldPassword": "current_password",
  "newPassword": "new_password"
}
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "密码修改成功",
  "data": null
}
2️⃣ 功能实现

先定义Pydantic模型类

python 复制代码
class UserChangePasswordRequest(BaseModel):
    old_password: str = Field(..., alias="oldPassword", description="旧密码")
    new_password: str = Field(..., min_length=6, alias="newPassword", description="新密码")

封装crud代码

python 复制代码
# 修改密码:验证旧密码->新密码加密->修改密码
async def change_password(db: AsyncSession, user: User, old_password: str, new_password: str):
    if not security.verify_password(old_password, user.password):
        return False
    hashed_new_password = security.get_hash_password(new_password)
    user.password = hashed_new_password
    # 更新:由SQLAlchemy真正接管这个User对象,确保可以commit
    # 规避 session 过期或关闭导致的不能提交的问题
    db.add(user)
    await db.commit()
    await db.refresh(user)
    return True

路由调用

python 复制代码
@router.put("/password")
async def update_password(
        password_data: UserChangePasswordRequest,
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    if not await users.change_password(db, user, password_data.old_password, password_data.new_password):
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="修改密码失败,请稍后再试")
    return success_response(message="修改密码成功")

6、总结

1、封装通用成功响应格式

封装函数,返回统一响应格式

② 定义模型类

③ 调用函数,响应结果

2、全局异常处理器:用于捕获业务层、数据库层以及系统层抛出的异常,并以统一的响应格式返回给前端。

① 定义异常处理器(函数)

② 全局注册异常处理器:app.add **exception **handler(异常类, 异常处理函数名)

3、passlib 加密

① 安装 passlib:passlib[bcrypt]==1.7.4

② 创建加密上下文:pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

③ 加密或校验

  • pwd_context.hash(password)
  • pwd_context.verify(plain_password, hashed_password)

4、用户访问令牌 Token

Token:是服务器发给客户端的一段字符串,用来在后续请求中证明"你已经登录过了"

作用:解决 HTTP 是无状态的问题,在每次请求中"自我证明身份"

① 后端生成 Token:str( uuid.uuid4() )

② 从请求头获取 Token:authorization: str=Header(...)

2、收藏模块

1、检查新闻收藏状态

1️⃣ 接口文档
  • 接口地址 : GET /api/favorite/check
  • 请求头: 需要认证
  • 请求参数:
参数名 类型 必填 说明
newsId integer 新闻ID
  • 请求示例:
plain 复制代码
GET /api/favorite/check?newsId=1
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "isFavorite": true
  }
}
2️⃣ 功能实现

收藏表模型类

python 复制代码
from datetime import datetime
from sqlalchemy import UniqueConstraint, Index, Integer, ForeignKey, DateTime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

from models.news import News
from models.users import User


class Base(DeclarativeBase):
    pass


class Favorite(Base):
    """
    收藏表ORM模型
    """
    __tablename__ = 'favorite'

    # 创建索引
    __table_args__ = (
        # 唯一约束,当前用户只能收藏一次
        UniqueConstraint('user_id', 'news_id', name='user_news_unique'),
        Index('fk_favorite_user_idx', 'user_id'),
        Index('fk_favorite_news_idx', 'news_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")
    news_id: Mapped[int] = mapped_column(Integer, ForeignKey(News.id), nullable=False, comment="新闻ID")
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False, comment="收藏时间")

    def __repr__(self):
        return f"<Favorite(id={self.id}, user_id={self.user_id}, news_id={self.news_id}, created_at={self.created_at})>"

路由接口

python 复制代码
from fastapi import APIRouter, Query, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from config.db_config import get_db
from models.users import User
from utils.auth import get_current_user
from utils.response import success_response

router = APIRouter(prefix="/api/favorite",tags=["favorite"])

@router.get("/check")
async def check_favorite(
        user_id: int = Query(...,alias="userId"),
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    return success_response(message="检查收藏成功")

注册main.py

python 复制代码
from routers import news, users,favorite

app.include_router(favorite.router)

封装crud方法

python 复制代码
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from models.favorite import Favorite


# 检查收藏状态:当前用户 是否收藏了这一条新闻
async def is_news_favorite(
        db: AsyncSession,
        user_id: int,
        news_id: int
):
    query = select(Favorite).where(Favorite.user_id == user_id, Favorite.news_id == news_id)
    result = await db.execute(query)
    # 是否有收藏记录
    return result.scalar_one_or_none() is not None

构造响应数据的Pydantic模型类

python 复制代码
from pydantic import BaseModel,Field

class FavoriteCheckResponse(BaseModel):
    """
    收藏状态响应模型
    """
    is_favorite: bool = Field(...,alias="isFavorite")

路由调用

python 复制代码
from fastapi import APIRouter, Query, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from config.db_config import get_db
from models.users import User
from schemas.favorite import FavoriteCheckResponse
from utils.auth import get_current_user
from utils.response import success_response
from crud import favorite

router = APIRouter(prefix="/api/favorite",tags=["favorite"])

@router.get("/check")
async def check_favorite(
        user_id: int = Query(...,alias="userId"),
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    is_favorited = await favorite.is_news_favorite(db, user_id, user.id) # 得到的是一个布尔值,需要把整个对象构造出来
    return success_response(message="检查收藏成功",data=FavoriteCheckResponse(isFavorite=is_favorited))

2、添加收藏

1️⃣ 接口文档
  • 接口地址 : POST /api/favorite/add
  • 请求头: 需要认证
  • 请求参数:
参数名 类型 必填 说明
newsId integer 新闻ID
  • 请求示例:
json 复制代码
{
  "newsId": 1
}
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "收藏成功",
  "data": {
    "id": 1,
    "userId": 1,
    "newsId": 1,
    "createTime": "2023-01-01T00:00:00"
  }
}
2️⃣ 功能实现

添加收藏请求体参数的模型类型

python 复制代码
class FavoriteAddRequest(BaseModel):
    """
    收藏请求模型
    """
    news_id: int = Field(...,alias="newsId")

封装crud代码

python 复制代码
async def add_news_favorite(
        db: AsyncSession,
        user_id: int,
        news_id: int
):
    favorite = Favorite(user_id=user_id, news_id=news_id)
    db.add(favorite)
    await db.commit()
    await db.refresh(favorite)
    return favorite 

路由调用

python 复制代码
@router.post("/add")
async def add_favorite(
        data: FavoriteAddRequest,
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    result = await favorite.add_news_favorite(db, user.id,data.news_id)
    return success_response(message="收藏成功",data=result)

3、取消收藏

1️⃣接口文档
  • 接口地址 : DELETE /api/favorite/remove
  • 请求头: 需要认证
  • 请求参数:
参数名 类型 必填 说明
newsId integer 新闻ID
  • 请求示例:
plain 复制代码
DELETE /api/favorite/remove?newsId=1
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "取消收藏成功",
  "data": null
}
2️⃣ 功能实现

封装crud方法

python 复制代码
async def remove_news_favorite(
        db: AsyncSession,
        user_id: int,
        news_id: int
):
    stmt = delete(Favorite).where(Favorite.user_id == user_id, Favorite.news_id == news_id)
    result = await db.execute(stmt)
    await db.commit()
    return result.rowcount > 0

路由调用

python 复制代码
@router.delete("/remove")
async def remove_favorite(
        news_id: int = Query(...,alias="newsId"),
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    result = await favorite.remove_news_favorite(db, user.id, news_id)
    if not result:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="收藏记录不存在")
    return success_response(message="取消收藏成功")

3、收藏列表

1️⃣ 接口文档
  • 接口地址 : GET /api/favorite/list
  • 请求头: 需要认证
  • 请求参数:
参数名 类型 必填 说明
page integer 页码,默认为1
pageSize integer 每页条数,默认为10,最大值为100
  • 请求示例:
plain 复制代码
GET /api/favorite/list
GET /api/favorite/list?page=1&pageSize=10
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "title": "新闻标题",
        "description": "",
        "image": "",
        "author": "",
        "publishTime": "2023-01-01T00:00:00",
        "categoryId": 1,
        "views": 1,
        "favoriteTime": "2023-01-01T00:00:00"
      }
    ],
    "total": 1,
    "hasMore": false
  }
}
2️⃣ 功能实现

封装curd

python 复制代码
# 获取收藏列表:获取的是某个用户的收藏列表 + 分页功能
async def get_favorite_list(
        db: AsyncSession,
        user_id: int,
        page: int = 1,
        page_size: int = 10
):
    # 总量、收藏的新闻列表
    count_query = select(func.count()).where(Favorite.user_id == user_id)
    count_result = await db.execute(count_query)
    total = count_result.scalar_one()

    # 获取收藏列表 - 联表查询 join() + 按照收藏时间进行排序 + 分页
    # select(查询主体模型类,字段别名).join(联合查询的模型类,联合查询的条件).where().order_by().offset().limit()
    # 别名:Favorite.created_at.label("favorite_time")
    # rows是一个列表,列表中有一个元组[
    #   (新闻对象,收藏时间,收藏id)
    # ]
    query = (select(News, Favorite.created_at.label("favorite_time"), Favorite.id.label("favorite_id"))
             .join(Favorite, Favorite.news_id == News.id)
             .where(Favorite.user_id == user_id)
             .order_by(Favorite.created_at.desc())
             .offset((page - 1) * page_size)
             .limit(page_size))
    result = await db.execute(query)
    rows = result.all()
    return rows, total

规划两个类: 一个是新闻模型类 + 收藏的模型类

python 复制代码
# 规划两个类: 一个是新闻模型类 + 收藏的模型类
class FavoriteNewsItemResponse(NewsItemBase):
    """
    收藏列表接口响应模型类
    """
    favorite_id: int = Field(...,alias="favoriteId")
    favorite_time: datetime = Field(...,alias="favoriteTime")
    model_config = ConfigDict(
        populate_by_name=True,  # alias/字段名兼容
        from_attributes=True  # 允许从ORM对象属性中取值
    )

# 收藏列表接口响应模型类
class FavoriteListResponse(BaseModel):
    """
    收藏列表接口响应模型类
    """
    list: list[FavoriteNewsItemResponse]
    total: int
    has_more: bool = Field(alias="hasMore")

    model_config = ConfigDict(
        populate_by_name=True,  # alias/字段名兼容
        from_attributes=True  # 允许从ORM对象属性中取值
    )
python 复制代码
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, Field, ConfigDict


class NewsItemBase(BaseModel):
    id: int
    title: str
    description: Optional[str] = None
    image: Optional[str] = None
    author: Optional[str] = None
    category_id: int = Field(alias="categoryId")
    views: int
    publish_time: Optional[datetime] = Field(None, alias="publishedTime")

    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True
    )

路由调用

python 复制代码
# 获取收藏列表
@router.get("/list")
async def get_favorite_list(
        page: int  = Query(1,ge=1),
        page_size: int = Query(10,ge=1,le=100,alias="pageSize"),
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    rows,total = await favorite.get_favorite_list(db, user.id, page, page_size)
    favorite_list = [{
        **news.__dict__,
        "favorite_time": favorite_time.isoformat(),
        "favorite_id": favorite_id
    } for news,favorite_time,favorite_id in rows]
    has_more = total > page * page_size
    data = FavoriteListResponse(list=favorite_list,total=total,hasMore=has_more)
    return success_response(message="获取收藏列表成功",data=data)

4、清空收藏列表

1️⃣ 接口文档
  • 接口地址 : DELETE /api/favorite/clear
  • 请求头: 需要认证
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "成功删除1条收藏记录",
  "data": null
}
2️⃣功能实现

封装crud代码

python 复制代码
# 清空收藏列表:清空某个用户的收藏列表
async def remove_all_favorite(
        db: AsyncSession,
        user_id: int
):
    stmt = delete(Favorite).where(Favorite.user_id == user_id)
    result = await db.execute(stmt)
    await db.commit()
    # 返回一个删除的数量
    return result.rowcount or 0

路由调用

python 复制代码
@router.delete("/clear")
async def clear_favorite(
        user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db)
):
    count = await favorite.remove_all_favorite(db, user.id)
    return success_response(message=f"清空了{count}条收藏记录")

5、小结

1、ORM 联表查询的写法

select ( 查询的主体模型类, 字段重命名 ).join( jion 的模型类, join 的条件)

2、模型类中,UniqueConstraint 的作用

唯一约束

3、缓存和模型调用

1、Redis

缓存:高速的临时数据存储机制

1️⃣ 数据缓存

缓存是一种存储机制,用于临时存储数据或计算结果,当再次需要这些数据时,可以快速从缓存中检索,而不是重新进行耗时或昂贵的获取和计算过程。

在网站开发中,缓存(Cache)是一个非常重要的概念,其核心作用是提高性能、降低延迟和减轻服务器负载。

主要优势:

  1. 提升性能和用户体验
  2. 减轻服务器/数据库负载
  3. 降低网络延迟
  4. 节省资源和成本
2️⃣ 项目中的缓存流程
3️⃣ Redis

Redis 是一种高性能的 Key-Value** **** 存储系统**,它将数据存储在内存中,因此读写速度极快,非常适合作为应用层的缓存服务。

在 FastAPI 这样的后端框架中,通常在应用层使用像Redis 这样的内存**** ****数据存储作为缓存。

4️⃣ 安装 Redis 服务端
5️⃣ 配置 Redis 客户端

1、安装 Redis 客户端

python 复制代码
pip install redis

2、配置 Redis 客户端

import redis.asyncio as redis

redis.Redis(...)

  • host: Redis 服务器地址
  • port: 端口号 6379
  • db: 数据库编号(0~15)
  • decode_responses: 是否将返回的数据从字节流解码为字符串
python 复制代码
import redis.asyncio as redis

REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0

# 创建Redis的连接对象
redis_client = redis.Redis(
    host=REDIS_HOST,  # Redis服务器的IP地址
    port=REDIS_PORT,  # Redis服务端口号
    db=REDIS_DB,      # Redis数据库编号,0-15,默认为0
    decode_responses=True  # 是否自动将返回值解码为字符串
)
6️⃣ 封装缓存操作

缓存操作就是围绕 Redis 做"存、取、删、判断、过期"等操作,让数据访问更快、数据库压力更小。

Redis 存储数据:key - value

python 复制代码
import json
from typing import Any

import redis.asyncio as redis

REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0

# 创建Redis的连接对象
redis_client = redis.Redis(
    host=REDIS_HOST,  # Redis服务器的IP地址
    port=REDIS_PORT,  # Redis服务端口号
    db=REDIS_DB,      # Redis数据库编号,0-15,默认为0
    decode_responses=True  # 是否自动将返回值解码为字符串
)

# 设置和读取(字符串和列表或字典)"[{}]"
# 读取:字符串
async def get_cache(key: str):
    # return await redis_client.get( key)
    try:
        return await redis_client.get(key)
    except Exception as e:
        print(f"获取缓存失败: {e}")
        return None

# 读取:列表或字典
async def get_json_cache(key: str):
    try:
        data = await redis_client.get(key)
        if data:
            return json.loads(data)  # 序列化
        return None
    except Exception as e:
        print(f"获取 JSON 缓存失败: {e}")
        return None

# 设置缓存 setex(key, expire, value)
async def set_cache(key: str, value: Any, expire: int = 3600):
    try:
        if isinstance(value, (dict,list)):
            # 转字符串
            value = json.dumps(value,ensure_ascii= False)  # 中文正常保存
        await redis_client.setex(key, expire, value)
        return True
    except Exception as e:
        print(f"设置缓存失败: {e}")
        return False
7️⃣ 设计缓存策略

旁路缓存策略(Cache-Aside) 是一种常见 的缓存策略,其核心概念是应用程序主动管理缓存,在读取数据时先检查缓存 ,如果缓存中没有数据 ,则从数据库或其他**** ****数据源 加载数据,并将数据存入缓存 ;当数据更新删除 时,应用程序也负责更新或删除缓存中的数据

不同类型的数据,缓存时间不同,否则会出现缓存雪崩

数据越稳定,缓存越久;数据变化越快,缓存越短

python 复制代码
# 新闻相关的缓存方法:新闻分类的读取和写入
# key - value
from typing import List, Dict, Any

from config.cache_conf import get_json_cache, set_cache

CATEGORIES_KEY = "news:categories"

# 获取新闻分类缓存
async def get_cached_categories():
    return await get_json_cache(CATEGORIES_KEY)

# 写入新闻分类缓存:缓存的数据,过期时间
# 分类、配置 7200;列表:600;详情:1800;验证码:120 - 数据越稳定,缓存时间越持久
# 避免所有key同时过期 引起缓存雪崩
async def set_cached_categories(data: List[Dict[str,Any]], expire: int = 7200):
    return await set_cache(CATEGORIES_KEY, data, expire)

改造新闻的crud代码,加入缓存

python 复制代码
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession

from cache.news_cache import get_cached_categories, set_cached_categories
# 导入模型类
from models.news import Category, News


async def get_categories(db: AsyncSession, skip: int = 0, limit: int = 100):
    # 先尝试从缓存中获取数据
    cached_categories = await get_cached_categories()
    if cached_categories:
        return cached_categories

    stmt = select(Category).offset(skip).limit(limit)
    result = await db.execute(stmt)
    # return result.scalars().all()
    categories = result.scalars().all()  # ORM结果
    # 写入缓存
    if categories:
        categories =jsonable_encoder(categories)
        await set_cached_categories(categories)
    # 返回数据
    return categories


async def get_news_list(db: AsyncSession, category_id: int, skip: int = 0, limit: int = 10):
    # 查询指定分类下的所有新闻,limit指定每页展示的数量
    stmt = select(News).where(News.category_id == category_id).offset(skip).limit(limit)
    result = await db.execute(stmt)
    return result.scalars().all()


async def get_news_count(db: AsyncSession, category_id: int):
    # 查询指定分类下的所有新闻的总量
    stmt = select(func.count(News.id)).where(News.category_id == category_id)
    result = await db.execute(stmt)
    return result.scalar_one()  # 只能有一个结果,否则报错


async def get_news_detail(db: AsyncSession, news_id: int):
    # 根据新闻ID查询新闻
    stmt = select(News).where(News.id == news_id)
    result = await db.execute(stmt)
    return result.scalar_one_or_none()


async def increase_news_views(db: AsyncSession, news_id: int):
    # 增加新闻的浏览量
    stmt = update(News).where(News.id == news_id).values(views=News.views + 1)
    result = await db.execute(stmt)
    # 更新之后,立刻提交事务
    await db.commit()

    # 更新->检查数据库是否真的命中了数据->命中了返回True
    return result.rowcount > 0


async def get_related_news(db: AsyncSession, news_id: int, category_id: int, limit: int = 5):
    # 获取新闻的关联新闻,order_by 排序->浏览量和发布时间排序
    stmt = select(News).where(
        News.id != news_id,
        News.category_id == category_id
    ).order_by(
        News.views.desc(),  # 默认是升序,desc是降序
        News.publish_time.desc()
    ).limit(limit)
    result = await db.execute(stmt)
    # return result.scalars().all()  # 这个方法会返回所有的字段
    related_news = result.scalars().all()
    # 这里使用一个列表推导式,推导出新闻的核心数据(字段),然后再return
    return [{
        "id": news_detail.id,
        "title": news_detail.title,
        "content": news_detail.content,
        "image": news_detail.image,
        "author": news_detail.author,
        "publishTime": news_detail.publish_time,
        "categoryId": news_detail.category_id,
        "views": news_detail.views,
    } for news_detail in related_news]
缓存新闻列表
  • 缓存 Key(唯一):news:list:分类 ID:页码:每页数量
  • 缓存 Value:新闻列表

先实现缓存的方法

python 复制代码
# 新闻相关的缓存方法:新闻分类的读取和写入
# key - value
from typing import List, Dict, Any, Optional

from config.cache_conf import get_json_cache, set_cache

CATEGORIES_KEY = "news:categories"
NEWS_LIST_PREFIX = "news_list:"

# 获取新闻分类缓存
async def get_cached_categories():
    return await get_json_cache(CATEGORIES_KEY)

# 写入新闻分类缓存:缓存的数据,过期时间
# 分类、配置 7200;列表:600;详情:1800;验证码:120 - 数据越稳定,缓存时间越持久
# 避免所有key同时过期 引起缓存雪崩
async def set_cached_categories(data: List[Dict[str,Any]], expire: int = 7200):
    return await set_cache(CATEGORIES_KEY, data, expire)

# 写入缓存-新闻列表 key = new_list:分类id:页码:每页数量   +  列表数据 + 过期时间
async def set_cache_news_list(category_id: Optional[int], page: int, size: int, new_list: List[Dict[str, Any]], expire: int = 1800):
    # 调用封装Redis设置的方法,存新闻列表到缓存
    category_part = "all" if category_id is None else category_id
    key = f"{NEWS_LIST_PREFIX}{category_part}:{page}:{size}"
    return await set_cache(key, new_list, expire)

# 读取缓存-新闻列表
async def get_cache_news_list(category_id: Optional[int], page: int, size: int):
    category_part = "all" if category_id is None else category_id
    key = f"{NEWS_LIST_PREFIX}{category_part}:{page}:{size}"
    return await get_json_cache(key)

接着调用封装crud

python 复制代码
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession

from cache.news_cache import get_cached_categories, set_cached_categories, get_cache_news_list, set_cache_news_list
# 导入模型类
from models.news import Category, News
from schemas.base import NewsItemBase


async def get_categories(db: AsyncSession, skip: int = 0, limit: int = 100):
    # 先尝试从缓存中获取数据
    cached_categories = await get_cached_categories()
    if cached_categories:
        return cached_categories

    stmt = select(Category).offset(skip).limit(limit)
    result = await db.execute(stmt)
    # return result.scalars().all()
    categories = result.scalars().all()  # ORM结果
    # 写入缓存
    if categories:
        categories = jsonable_encoder(categories)
        await set_cached_categories(categories)
    # 返回数据
    return categories


async def get_news_list(db: AsyncSession, category_id: int, skip: int = 0, limit: int = 10):
    # 先尝试从缓存中获取数据
    # 跳过的数量skip = (页码-1) * 每页数量 -> 页码 = 跳过的数量 // 每页数量 + 1
    # await get_cache_news_list(分类id,页码,每页数量)
    page = skip // limit + 1
    cached_list = await get_cache_news_list(category_id, page, limit)  # 缓存数据 json
    if cached_list:
        # return cached_news_list  # 这里要的是ORM,不是 json
        return [News(**item) for item in cached_list]

    # 查询指定分类下的所有新闻,limit指定每页展示的数量
    stmt = select(News).where(News.category_id == category_id).offset(skip).limit(limit)
    result = await db.execute(stmt)
    # return result.scalars().all()
    news_list = result.scalars().all()
    # 写入缓存
    if news_list:
        # 先把ORM数据转换为字典才能写入缓存
        # ORM转成Pydantic,再转成字典
        # by_alias=True 不适用别名,保存Python风格,因为Redis数据是给后端用的
        new_data = [NewsItemBase.model_validate(item).model_dump(mode="json", by_alias=True) for item in news_list]
        await set_cache_news_list(category_id, page, limit, new_data)

    return news_list


async def get_news_count(db: AsyncSession, category_id: int):
    # 查询指定分类下的所有新闻的总量
    stmt = select(func.count(News.id)).where(News.category_id == category_id)
    result = await db.execute(stmt)
    return result.scalar_one()  # 只能有一个结果,否则报错


async def get_news_detail(db: AsyncSession, news_id: int):
    # 根据新闻ID查询新闻
    stmt = select(News).where(News.id == news_id)
    result = await db.execute(stmt)
    return result.scalar_one_or_none()


async def increase_news_views(db: AsyncSession, news_id: int):
    # 增加新闻的浏览量
    stmt = update(News).where(News.id == news_id).values(views=News.views + 1)
    result = await db.execute(stmt)
    # 更新之后,立刻提交事务
    await db.commit()

    # 更新->检查数据库是否真的命中了数据->命中了返回True
    return result.rowcount > 0


async def get_related_news(db: AsyncSession, news_id: int, category_id: int, limit: int = 5):
    # 获取新闻的关联新闻,order_by 排序->浏览量和发布时间排序
    stmt = select(News).where(
        News.id != news_id,
        News.category_id == category_id
    ).order_by(
        News.views.desc(),  # 默认是升序,desc是降序
        News.publish_time.desc()
    ).limit(limit)
    result = await db.execute(stmt)
    # return result.scalars().all()  # 这个方法会返回所有的字段
    related_news = result.scalars().all()
    # 这里使用一个列表推导式,推导出新闻的核心数据(字段),然后再return
    return [{
        "id": news_detail.id,
        "title": news_detail.title,
        "content": news_detail.content,
        "image": news_detail.image,
        "author": news_detail.author,
        "publishTime": news_detail.publish_time,
        "categoryId": news_detail.category_id,
        "views": news_detail.views,
    } for news_detail in related_news]

2、调用 QWen 大模型

1️⃣ QWen 模型

在前端页面中有模型请求的接口指定,支持千问(Qwen)系列模型的使用,可以直接指定对应的API和key

使用步骤:

登录阿里云百炼模型广场 → 选择模型(通义千问3-Max)→ API 参考 → 获取 HTTP 请求地址 + API Key → 前端调用

2️⃣ 前端调用模型

前端项目 → src/ → config/ → api.js → 替换 大模型 HTTP 请求地址 + API Key + 模型

阿里云百炼模型广场

3、小结

Redis 是一种高性能的 Key-Value 存储系统,它将数据存储在内存中,因此读写速度极快,非常适合作为应用层的

缓存服务。

相关推荐
yexuhgu8 小时前
Golang如何做贪心算法_Golang贪心算法教程【速学】
jvm·数据库·python
EnCi Zheng8 小时前
M4-更新日志v0.1.3-Mermaid图表支持 [特殊字符]
python
2401_831419448 小时前
Redis如何实现多维度权重排序_利用ZSet分数计算进行优先级排列
jvm·数据库·python
TickDB8 小时前
MCP、WebSocket 与 Agentic Wallet:AI 自主交易的三把钥匙,同时转动了
人工智能·python·websocket
qq_349317488 小时前
CSS如何实现全屏背景图固定_background-attachment fixed
jvm·数据库·python
qq_460978408 小时前
Go语言如何做环境变量配置_Go语言环境变量管理教程【详解】
jvm·数据库·python
小张同学8248 小时前
-RAG检索增强生成让智能体拥有企业级专属知识库
开发语言·python·架构·pycharm
Ulyanov8 小时前
《现代 Python 桌面应用架构实战:PySide6 + QML 从入门到工程化》:实时时钟与数据驱动 UI —— 从“事件回调”到“状态绑定”的范式跃迁
开发语言·python·qt·ui·架构·交互
wuxinyan1238 小时前
大模型学习之路02:提示工程从入门到精通(第二篇)
人工智能·python·学习