AI 掘金头条项目-新闻模块实现

1、项目介绍

1.1 配套的资料

1.2 运行前端项目

1.3 工程结构

1.4 模块化路由

模块化路由就是把每个业务功能的接口拆分 到独立文件里,再统一挂载到主应用中。

1.4.1 特点

  • 项目结构更清晰
    • 接口按模块拆分,不会混在一起,让整个项目结构更直观
  • 更易维护
    • 每个模块都负责自己对应的接口,便于快速查找和定向修改
  • 避免 main.py 爆炸
    • 把接口拆分出去后,main.py就只负责启动应用,不再堆满业务代码

在routers目录下新建一个news.py文件

python 复制代码
from fastapi import APIRouter

# 创建APIrouter实例
router = APIRouter(prefix="/api/news",tags=["news"])

# 创建路由处理函数
@router.get("/categories")
async def get_categories():
    return {"message": "获取分类成功"}

在main.py中挂载路由

python 复制代码
from fastapi import FastAPI
from routers import news

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}

# 挂载路由/注册路由
app.include_router(news.router)

接着启动项目测试,看接口是否加载成功

然后执行接口测试一下

1.4.2 小结

1、什么是模块化路由?有什么优势?

模块化路由就是把每个业务功能的接口拆分到独立文件里,再统一挂载到主应用中。

优势:项目结构更清晰、项目更易维护

2、说出下方代码的含义

python 复制代码
# 1. 从 fastapi 库中导入 APIRouter 工具
from fastapi import APIRouter

# 2. 创建一个路由对象(专门管理新闻相关的接口)
router = APIRouter(
    prefix="/api/news",  # 接口前缀:所有这个路由下的接口,URL 都会以这个开头
    tags=["news"]        # 接口文档标签:方便在文档里分类查看
)

# 3. 定义一个 GET 接口
@router.get("/categories")
async def get_categories():
    # 4. 接口返回的数据
    return {"msg": "获取分类成功"}
  • APIRouter = 接口分类管理工具
  • prefix = 给接口统一加 URL 前缀
  • @router.get = 定义 GET 接口
  • 完整接口地址:/api/news/categories
  • 返回固定的 JSON 成功消息

3、如何注册路由?

main.py 中添加 app.include_router()

2、数据库和配置ORM

2.1 创建流程

2.2 配置 ORM

MySQL 导入 SQL 文件:

PyCharm中 Database 插件 → 数据库连接 → 右键 → SQL Script → Run SQL Script → 浏览 SQL 文件 → 确认

1、安装SQLAlchemy

python 复制代码
pip install "sqlalchemy[asyncio]" aiomysql

2、ORM 配置

python 复制代码
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine

# 数据库URL
ASYNC_DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/news_app?charset=utf8mb4"

# 创建异步引擎
async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,     # 可选:输出SQL日志
    pool_size=10,  # 设置连接池中保持的持久连接数
    max_overflow=20  # 设置连接池允许创建的额外连接数
)

# 创建异步会话工厂
AsyncSessionLocal = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False
)

# 依赖项,用于获取数据库会话
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

3、项目功能开发

3.1 新闻模块

1、获取新闻分类

2、接口实现流程

1️⃣ 模块化路由(API 入口层)

作用:定义接口的 URL 结构,实现接口的模块化管理。

  • 定义 APIRouter 实例,给不同模块(如新闻、用户)创建独立路由。
  • 给路由设置 prefix(统一前缀)和 tags(文档分类)。
  • 在主程序中用 include_router() 注册路由,挂载到 FastAPI 应用。
  • 遵循接口规范文档,统一接口风格和路径设计。
2️⃣ 定义模型类(数据库映射层)

作用:用 Python 类映射数据库表结构,让 ORM 能操作数据库。

  • 继承 SQLAlchemy 的 DeclarativeBase 创建基类 Base
  • 为每个数据库表定义对应的模型类(如 CategoryNews)。
  • tablename 指定表名,用 mapped_column 定义字段、类型、约束(主键、外键、索引等)。
  • 字段设计完全参照数据库表结构,确保一一对应。
3️⃣ 数据库 CRUD(数据操作层)

作用:实现对数据库的增删改查操作,封装业务逻辑的数据交互。

  • 查询 :用 select(模型类) 语句,配合 await session.execute() 获取数据。
  • 新增 :用 session.add() 把模型对象加入会话,再 await session.commit() 保存。
  • 更新 :通过 update() 语句修改字段值,提交事务。
  • 删除 :通过 delete() 语句删除数据,提交事务。
  • 所有操作都通过异步 AsyncSession 完成,保证事务安全。
4️⃣ 路由调用逻辑(接口业务层)

作用:连接路由、依赖和 CRUD,处理请求并返回响应。

  • Depends(get_db) 注入数据库会话,在接口函数中拿到 session
  • 调用第 3 步封装的 CRUD 函数,完成业务逻辑(如获取新闻列表、新增分类)。
  • 处理请求参数、异常情况,返回统一格式的响应结果(如 JSON)。

3、获取新闻分类列表

1️⃣ 接口文档
  • 接口地址 : GET /api/news/categories
  • 请求参数:
参数名 类型 必填 说明
skip integer 跳过的记录数,默认为0
limit integer 返回的记录数限制,默认为100
  • 请求示例:
plain 复制代码
GET /api/news/categories
GET /api/news/categories?skip=0&limit=10
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "success",
  "data": [
    {
      "id": 1,
      "created_at": "2023-01-01T00:00:00",
      "updated_at": "2023-01-01T00:00:00",
      "name": "科技",
      "sort_order": 0
    }
  ]
}

代码实现:

python 复制代码
router = APIRouter(prefix="/api/news", tags=["news"])

@router.get("/categories")
async def get_categories(skip: int=0, limit: int=100):
    return {
    "code": 200,
    "message": "success",
    "data": "新闻分类列表"
    }

接口实现流程

  1. 模块化路由->API接口规范文档

  2. 定义模型类->数据库表(数据库设计文档)

  3. 在crud文件夹里面创建文件,封装操作数据库的方法

  4. 在路由处理函数里面调用 crud封装好的方法,响应结果

2️⃣ 定义模型类

模型类规范:

  • 基类 ,继承 DeclarativeBase
  • 数据库表模型类 ,继承基类
    • 属性及类型参照数据库表定义

模型类,就是对应表数据库表的字段

python 复制代码
from datetime import datetime

from sqlalchemy import DateTime
from sqlalchemy.orm import DeclarativeBase,Mapped, mapped_column
from sqlalchemy import Integer,String

class Base(DeclarativeBase):
    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="更新时间"
    )

class Category(Base):
    __tablename__ = "news_category"
    id: Mapped[int] = mapped_column(
        Integer,
        primary_key=True,
        autoincrement=True,
        comment="分类id"
    )
    name: Mapped[str] = mapped_column(
        String(50),
        unique=True,
        nullable=False,
        comment="分类名称"
    )
    sort_order: Mapped[int] = mapped_column(
        Integer,
        default=0,
        nullable=False,
        comment="排序"
    )
    
    def __repr__(self):
        return f"<Category(id={self.id}, name={self.name}, sort_order={self.sort_order})>"
3️⃣ 数据库 CRUD

实现增删改查的数据库方法

python 复制代码
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
# 导入模型类
from models.news import Category

async def get_categories(db: AsyncSession,skip: int=0, limit: int=100):
    stmt = select(Category).offset(skip).limit(limit)
    result = await db.execute(stmt)
    return result.scalars().all()
4️⃣ 路由调用逻辑

在模块化路由中调用实现增删改查的数据库方法

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

from config.db_config import get_db
from crud import news

# 创建APIrouter实例
# prefix 路由前缀(API 接口规范文档)
# tags 分组 标签
router = APIRouter(prefix="/api/news",tags=["news"])

# 接口实现流程
# 1. 模块化路由->API接口规范文档
# 2. 定义模型类->数据库表(数据库设计文档)
# 3. 在crud文件夹里面创建文件,封装操作数据库的方法
# 4. 在路由处理函数里面调用 crud封装好的方法,响应结果

# 创建路由处理函数
@router.get("/categories")
async def get_categories(skip: int=0, limit: int=100, db: AsyncSession = Depends(get_db)):
    # 先获取数据库里面的新闻分类数据->先定义模型类->封装查询数据的方法
    categories = await news.get_categories(db,skip,limit)
    return {
    "code": 200,
    "message": "success",
    "data": categories
    }

接口测试

4、跨域资源共享 CORS

跨域资源共享(CORS)是一种浏览器安全机制 ,用于允许运行在一个源(Origin)的 Web 应用,通过浏览器向另一个源的服务器发起跨域 HTTP 请求,并在服务器授权的前提下获取资源。

同源的三个条件

  • 协议
  • 域名
  • 端口
1️⃣ CORS 中间件

CORS:让后端主动告诉浏览器:这个前端"允许访问"。

python 复制代码
from fastapi.middleware.cors import CORSMiddleware

# 允许的来源(可以是域名列表)
origins = [
    "http://localhost",
    "http://localhost:3000",
    "https://your-frontend-domain.com"
]

# 添加 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],       # 允许访问的源,开发时候可以使用*,项目上线就不能使用了,需要写一个能访问的列表
    allow_credentials=True,    # 允许携带 Cookie
    allow_methods=["*"],       # 允许所有请求方法
    allow_headers=["*"],       # 允许所有请求头
)
2️⃣ 跨域代码实现
python 复制代码
from fastapi import FastAPI
from routers import news
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许的源,开发的时候允许所有源,生产环境需要指定源
    allow_credentials=True,  # 允许的凭证cookie
    allow_methods=["*"],  # 允许的请求方法
    allow_headers=["*"],  # 允许的请求头
)

@app.get("/")
async def root():
    return {"message": "Hello World"}

# 挂载路由/注册路由
app.include_router(news.router)

现在启动前后端代码进行前后端联调测试

现在我们可以发现,财经这个分类出现了

3️⃣ 小结

1、分析下图浏览器报错的原因?

浏览器向另一个源 的服务器发起了跨域 HTTP 请求(浏览器的安全机制,只允许同源请求

2、同源的三个条件是什么?

  • 协议
  • 域名
  • 端口

3、如何解决跨域问题?

全局配置 CORS 中间件

5、获取新闻列表

1️⃣ 接口文档
  • 接口地址 : GET /api/news/list
  • 请求参数:
参数名 类型 必填 说明
categoryId integer 分类ID
page integer 页码,默认为1
pageSize integer 每页显示的新闻数量,最大值为100,默认为10
  • 请求示例:
plain 复制代码
GET /api/news/list?categoryId=1
GET /api/news/list?categoryId=1&page=2&pageSize=20
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "publish_time": "2023-01-01T00:00:00",
        "created_at": "2023-01-01T00:00:00",
        "updated_at": "2023-01-01T00:00:00",
        "category": null,
        "title": "新闻标题",
        "description": "新闻简介",
        "content": "新闻内容",
        "image": null,
        "author": null,
        "category_id": 1,
        "views": 0
      }
    ],
    "total": 100,
    "hasMore": true
  }
}
2️⃣ 代码实现
python 复制代码
@router.get("/list")
async def get_news_list(
        category_id: int = Query(..., alias="categoryId"),
        page: int = 1,
        page_size: int = Query(..., alias="pageSize", le=100),
        db: AsyncSession = Depends(get_db)
):
    return {
        "code": 200,
        "message": "success",
        "data": {
            "list": "新闻列表",
            "total": "总量",
            "hasMore": "是否有更多"
        }
    }
3️⃣ News模型类
python 复制代码
class News(Base):
    __tablename__ = "news"

    # 创建索引: 提升查询速度->添加目录
    __table_args__ = (
        Index('fk_news_category_idx', 'category_id'), # 高频查询场景
        Index('idx_publish_time', 'publish_time')  # 按发布时间排序
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="新闻ID")
    title: Mapped[str] = mapped_column(String(255), nullable=False, comment="新闻标题")
    description: Mapped[Optional[str]] = mapped_column(String(500), comment="新闻简介")
    content: Mapped[str] = mapped_column(Text, nullable=False, comment="新闻内容")
    image: Mapped[Optional[str]] = mapped_column(String(255), comment="封面图片URL")
    author: Mapped[Optional[str]] = mapped_column(String(50), comment="作者")
    category_id: Mapped[int] = mapped_column(Integer, ForeignKey('news_category.id'), nullable=False, comment="分类ID")
    views: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="浏览量")
    publish_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="发布时间")

    def __repr__(self):
        return f"<News(id={self.id}, title='{self.title}', views={self.views})>"
4️⃣ 查询功能

查询功能响应结果:当前分类新闻列表、新闻总量、是否还有更多新闻

实现数据库的crud方法

python 复制代码
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()

路由实现

python 复制代码
@router.get("/list")
async def get_news_list(
        category_id: int = Query(..., alias="categoryId"),
        page: int = 1,
        page_size: int = Query(..., alias="pageSize", le=100),
        db: AsyncSession = Depends(get_db)
):
    # 思路:处理分页规则->查询新闻列表->计算总量->计算是否还有更多
    offset = (page - 1) * page_size
    news_list = await news.get_news_list(db, category_id, offset, page_size)
    return {
        "code": 200,
        "message": "success",
        "data": {
            "list": news_list,
            "total": "总量",
            "hasMore": "是否有更多"
        }
    }

这里运行的时候,接口测试如果出现了报错,报错中含有这个cryptography

python 复制代码
aise RuntimeError( RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods

这个错误是因为你的 MySQL 数据库使用了 caching_sha2_password 认证方式(MySQL 8.0 默认的认证方式),但是缺少了必需的 cryptography 包。

python 复制代码
pip install cryptography

在crud当中实现查询指定分类下的所有新闻列表的总量

python 复制代码
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()  # 只能有一个结果,否则报错

完整的路由方法

python 复制代码
@router.get("/list")
async def get_news_list(
        category_id: int = Query(..., alias="categoryId"),
        page: int = 1,
        page_size: int = Query(..., alias="pageSize", le=100),
        db: AsyncSession = Depends(get_db)
):
    # 思路:处理分页规则->查询新闻列表->计算总量->计算是否还有更多
    offset = (page - 1) * page_size
    news_list = await news.get_news_list(db, category_id, offset, page_size)
    total = await news.get_news_count(db, category_id)
    # (跳过的 + 当前列表里面的数量) < 总数量
    has_more = len(news_list) + offset < total
    return {
        "code": 200,
        "message": "success",
        "data": {
            "list": news_list,
            "total": total,
            "hasMore": has_more
        }
    }

如果发现total可以显示总量,而且hasmore是否还有更多为true,就说明接口测试成功

前端测试,现在发现,现在已经出现了列表

6、获取新闻详情

1️⃣ 接口文档
  • 接口地址 : GET /api/news/detail
  • 请求参数:
参数名 类型 必填 说明
id integer 新闻ID
  • 请求示例:
plain 复制代码
GET /api/news/detail?id=1
  • 响应示例:
json 复制代码
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "title": "新闻标题",
    "content": "新闻内容",
    "image": null,
    "author": null,
    "publishTime": "2023-01-01T00:00:00",
    "categoryId": 1,
    "views": 1,
    "relatedNews": []
  }
}
2️⃣ 代码实现

响应结果:当前新闻详情 + 增加1次浏览量 + 相关新闻(同分类 id 的新闻)

实现数据库的crud方法

python 复制代码
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()

路由请求实现

python 复制代码
@router.get("/detail")
async def get_news_detail(news_id: int=Query(..., alias="id"), db: AsyncSession = Depends(get_db)):
    # 获取新闻详情 + 浏览量+1 + 相关新闻
    news_detail = await news.get_news_detail(db, news_id)
    if not news_detail:
        raise HTTPException(status_code=404, detail="新闻不存在")
    return {
        "code": 200,
        "message": "success",
        "data": {
            "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,
            "relatedNews": []
        }
    }
3️⃣ 实现浏览量+1

数据库的crud实现

python 复制代码
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

路由实现

python 复制代码
@router.get("/detail")
async def get_news_detail(news_id: int=Query(..., alias="id"), db: AsyncSession = Depends(get_db)):
    # 获取新闻详情 + 浏览量+1 + 相关新闻
    news_detail = await news.get_news_detail(db, news_id)
    if not news_detail:
        raise HTTPException(status_code=404, detail="新闻不存在")

    views_res = await news.increase_news_views(db, news_detail.id)
    if not views_res:
        raise HTTPException(status_code=404, detail="新闻不存在")
    return {
        "code": 200,
        "message": "success",
        "data": {
            "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,
            "relatedNews": []
        }
    }
4️⃣ 实现同类相关新闻查询

封装一个crud方法

python 复制代码
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()

路由代码实现

python 复制代码
@router.get("/detail")
async def get_news_detail(news_id: int=Query(..., alias="id"), db: AsyncSession = Depends(get_db)):
    # 获取新闻详情 + 浏览量+1 + 相关新闻
    news_detail = await news.get_news_detail(db, news_id)
    if not news_detail:
        raise HTTPException(status_code=404, detail="新闻不存在")

    views_res = await news.increase_news_views(db, news_detail.id)
    if not views_res:
        raise HTTPException(status_code=404, detail="新闻不存在")

    related_news = await news.get_related_news(db, news_detail.id, news_detail.category_id)
    return {
        "code": 200,
        "message": "success",
        "data": {
            "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,
            "relatedNews": related_news
        }
    }

这时我们会发现,推荐的相关新闻返回的数据太多,我们只需要返回关键的信息即可,比如更新时间呀什么的,是不需要返回的

应该直接像这样,只返回关键的

更新代码

python 复制代码
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]
相关推荐
William Dawson1 小时前
【MySQL触发器超详细实战教程|从零基础到项目生产可用(避坑+案例+跨库+逗号拆分)】
数据库·mysql
许彰午2 小时前
我手写了一个 Java 内存数据库(四):索引引擎、SQL 解析与总结
java·数据库·sql
czlczl200209252 小时前
MySQL 中为什么我们要避免“多个范围查询”
数据库·mysql
若兰幽竹2 小时前
【HCIE-openGauss数据库认证】01 准备阶段:实验环境深度剖析与搭建指南
数据库·hcie-opengauss·华为专家级认证
杨云龙UP2 小时前
Oracle 19c多租户架构下设置用户密码永不过期及登录锁定策略说明_20260430
linux·运维·服务器·数据库·oracle
qiuyunoqy2 小时前
MySQL - 4 - mysqldump/mysqladmin/mysqlshow讲解
数据库·mysql
TO_ZRG2 小时前
Android Broadcast Receiver完全入门指南
java·后端·spring
PaperData2 小时前
2014-2026.3应届生网络招聘大数据
大数据·数据库·人工智能·数据分析·经管
Knight_AL2 小时前
使用 CyclicBarrier + 自定义线程池实现 SpringBoot 并行报表(完整性能对比)
java·spring boot·后端