AI 掘金头条-新闻模块

一、AI 掘金头条项目内容说明

  • 项目功能
    • 新闻模块
    • 用户模块
    • 收藏和浏览历史
  • 设计说明文档
    • 用来系统说明整个后端系统的架构、模块划分、数据设计和接口规范
  • 配套物料
    • 前端代码:项目前端工程采用 Vue 框架,实现了页面结构、组件交互和接口调用等核心逻辑,能够在浏览器端呈现完整界面并与后端服务对接,从而展示整个应用的完整功能
    • 接口规范文档:项目的接口规范文档,定义了各接口的请求方式、参数格式、响应结构和错误码等关键规则,为前后端提供统一的交互标准,确保系统各模块能够准确对接并稳定协同工作。
    • 数据库 SQL 文件:项目的 MySQL SQL 文件, 包含了数据库的表结构定义、初始化数据和必要的约束规则,用于在数据库中快速搭建完整的数据存储环境,为后端服务提供稳定可靠的数据基础。

二、运行前端项目

1.安装 Node:

  • 运行构建工具(Vite/Webpack)、开启开发服务器、编译代码

2.运行项目:

  • 项目根目录打开终端
npm run dev

三、模块化路由

工程结构如下:

image

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

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

模块化路由实现流程:

  • ①.模块化目录结构

image

  • ②.编写独立路由模块,news.py部分内容
from fastapimport APIRouter
# 创建 APIRouter 实例 router = APIRouter(prefix="/api/news",tags=["news"])
@router.get(
"/categories") async def get_categories():   return {"msg": "获取分类成功"}
  • ③.在 main.py 中挂载路由,也就是注册路由
app.include_router(news.router)

四、配置 ORM

4.1.ORM配置布置

⑴.安装

安装所需要模块:

# 安装sqlalchemy[asyncio],如果是mac上安装加上引号
pip install sqlalchemy[asyncio]
# 异步数据库驱动
pip install aiomysql

⑵.建表

  1. create_async_engine创建异步引擎
  2. 定义模型基类(继承DeclartiveBase)和 模型类
  3. run_sync(Base.metadta.create_al)建表

⑶.操作数据

  1. 路由处理函数注入数据库会话依赖项 Depnds
  2. 查询数据:select()
  3. 增加数据:add()
  4. 更新数据:重新赋值
  5. 删除数据:delete()

4.2.MySQL 导入 SQL 文件

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

image

导入后查询表

image

4.3.创建异步引擎

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

# 1.创建异步数据库连接 URL (MySQL + aiomysql 驱动)
# 格式:mysql+aiomysql://用户名:密码@主机:端口/数据库?charset=utf8mb4
ASYNC_DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/fastapi_test?charset=utf8mb4"

# 创建异步引擎 async_engine = create_async_engine( ASYNC_DATABASE_URL, echo=True, # 可选:输出SQL日志输出(调试用,生产环境需要关闭) pool_size=10, # 设置连接池中保持的持久连接数(默认5) max_overflow=20, # 设置连接池运行创建的额外连接数, 超过pool_size的连接数, 创建新的连接数,超过max_overflow的连接数,则拒绝连接 ) # 2.创建异步会话工厂 AsyncSessionLocal = async_sessionmaker( bind=async_engine, # 绑定异步数据库连接引擎 class_=AsyncSession, # 指定会话类 expire_on_commit=False, # 会话对象如果不过期,则不重新查询数据库 ) # 3.创建依赖项 async def get_database(): async with AsyncSessionLocal() as session: try: yield session # 返回数据库会话给路由处理函数 await session.commit() # 提交事务 except Exception as e: await session.rollback() # 遇到异常,回滚事务 raise e finally: await session.close() # 关闭会话

五、跨域资源共享

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

5.1.什么是“源”(Origin)?

一个源由以下三部分组成:
  • 协议(scheme):如 httphttps
  • 主机名(host):如 example.com
  • 端口(port):如 80443
例如:
  • https://api.example.com:443 和 https://www.example.com:443 是不同源(主机名不同)
  • http://example.com 和 https://example.com 是不同源(协议不同)
  • https://example.com:8080 和 https://example.com:443 是不同源(端口不同)
只要上述任一不同,就被视为跨域

5.2.为什么需要 CORS?

出于安全考虑,浏览器默认禁止网页向不同源的服务器发起某些类型的请求(尤其是带凭据或非简单请求)。如果没有 CORS,前端应用就无法安全地与第三方 API 通信。
CORS 通过在 HTTP 响应中添加特定的头部字段,告诉浏览器:“我允许这个源访问我的资源”。

5.3.CORS请求类型

5.3.1.简单请求(Simple Request)

满足以下条件的请求被视为“简单请求”,浏览器直接发送,不预先发预检请求:
  • 方法为 GETPOST 或 HEAD
  • 请求头仅包含以下字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 无自定义请求头
  • 无读取响应头的特殊要求
对于简单请求,浏览器自动在请求中加上 Origin 头,服务器返回 Access-Control-Allow-Origin 即可。

5.3.2.预检请求(Preflight Request)

对于非简单请求(如 PUTDELETE、带自定义头、Content-Type: application/json 等),浏览器会先发送一个 OPTIONS 请求(预检),询问服务器是否允许该实际请求。
预检请求包含:
  • Origin
  • Access-Control-Request-Method:实际请求的方法
  • Access-Control-Request-Headers:实际请求将携带的自定义头
服务器需在响应中返回:
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • (可选)Access-Control-Max-Age:缓存预检结果时间(秒)

5.4.关键 CORS 响应头

响应头说明
Access-Control-Allow-Origin 允许的源,如 https://example.com 或 *(但 * 不能用于带凭据的请求)
Access-Control-Allow-Methods 允许的 HTTP 方法,如 GET, POST, PUT
Access-Control-Allow-Headers 允许的请求头字段
Access-Control-Allow-Credentials 是否允许携带凭据(如 cookies),设为 true
Access-Control-Expose-Headers 允许前端 JS 读取的响应头(默认只能读取简单响应头)
Access-Control-Max-Age 预检请求的缓存时间(单位:秒)

5.5.CORSMiddleware 的作用

它自动为你的 FastAPI 应用添加必要的 CORS 响应头,让浏览器知道:“这个跨域请求是被允许的”。例如,它会在响应中加入:
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

CORSMiddleware 作用:让后端主动告诉浏览器,这个前端“允许访问”。在main.py配置如下:

image

1.allow_origins=["*"]

  • 表示 允许所有域名 访问你的 API。
  • ⚠️ 开发阶段方便,但生产环境非常危险!
  • 生产建议写成具体域名,例如:
    allow_origins=[
        "https://your-frontend.com",
        "https://www.your-frontend.com"
    ]

2.allow_credentials=True

  • 允许前端请求携带 Cookie、Authorization 头 等凭证。
  • ⚠️ 如果设为 Trueallow_origins 不能是 "*"!必须指定具体域名。
    • 否则浏览器会报错:Credentials flag is 'true', but the 'Access-Control-Allow-Origin' header is '*'.
✅ 正确做法(带凭证时):
allow_origins=["https://your-frontend.com"],
allow_credentials=True,

3.allow_methods=["*"]

  • 允许所有 HTTP 方法(GET、POST、PUT、DELETE、PATCH 等)。
  • 也可以限制,如 ["GET", "POST"]

4.allow_headers=["*"]

  • 允许所有请求头(如 Content-TypeAuthorization)。
  • 生产环境可按需限制。

说明如果你正在开发前后端分离项目,这个中间件几乎是必需的。但在上线前,务必收紧配置以保障安全

配置项作用安全建议
CORSMiddleware 解决跨域问题,让其他网站能安全调用你的 API 必须配置,否则前端可能无法调用
allow_origins=["*"] 允许任意网站跨域访问 仅限开发!生产必须指定域名
allow_credentials=True 允许带 Cookie/Token 的请求 若开启,allow_origins 不能为 "*"

六、新闻模块实现

新闻模块核心功能

image

6.1.接口实现流程

⑴.模块化路由

  • 定义 APIRouter 实例
  • 注册路由include_router()
  • 参照接口规范文档

⑵.定义模型类

  • 参照数据库表
class Base(DeclarativeBase):
    pass

class Category(Base):
    _tablename_="news_category"

⑶.数据库CRUD

  • select(模型类)
  • add()
  • update()
  • delete()

⑷.路由调用逻辑

  1. Depnds 注入数据库依赖
  2. 调用逻辑
  3. 响应结果

6.2.接口说明

6.2.1.接口文档-获取新闻分类列表

根据下面的接口文档编写接口:

image

接口编写:

image

6.2.2.接口文档-获取新闻列表

根据下面的接口文档编写接口:

image

接口编写:

image

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

image

6.2.3.接口文档-获取新闻详情

根据下面的接口文档编写接口:

image

接口编写:

image

实现思路:

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

image

七、新闻模块实现

7.1.创建实体类

在models\news.py下创建新闻分类表、新闻表的实体类:

注意先创建基类

from datetime import datetime
from sqlalchemy import DateTime, String, func, Integer, Text, Index

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    """
    声明式基类,所有数据模型都集成此类
    """
    created_at: Mapped[datetime] = mapped_column(DateTime, insert_default=func.now(), default=datetime.now, comment="创建时间")
    updated_at: Mapped[datetime] = mapped_column(DateTime, insert_default=func.now(), default=datetime.now, onupdate=datetime.now(), comment="更新时间")


class NewsCategory(Base):
    """
    新闻分类表
    """
    __tablename__ = "news_category"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, comment="编号")
    name: Mapped[str] = mapped_column(String(50), unique=True,nullable=False, comment="分类名称")
    sort_order: Mapped[str] = mapped_column(Integer, default=0, nullable=False, comment="排序顺序")

    # repr 方法:这是 Python 的特殊方法,用于定义对象的字符串表示形式
    # 作用:当你打印对象或在控制台中查看对象时,会显示这个方法返回的字符串
    def __repr__(self):
        return f"<NewsCategory(id={self.id}, name={self.name}, sort_order={self.sort_order})>"


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(primary_key=True, autoincrement=True, comment="新闻id")
    title: Mapped[str] = mapped_column(String(255), nullable=False, comment="新闻标题")
    description: Mapped[str] = mapped_column(String(500), comment="新闻简介")
    content: Mapped[str] = mapped_column(Text, nullable=False, comment="新闻内容")
    image: Mapped[str] = mapped_column(String(255), comment="封面图片URL")
    author: Mapped[str] = mapped_column(String(50), comment="作者")
    category_id: Mapped[int] = mapped_column(Integer, 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, nullable=False, comment="发布时间")

    def __repr__(self):
        return f"<News(id={self.id}, title={self.title}, views={self.views})>"

7.2.接口开发

在 routers\news.py下创建接口:

from fastapi import APIRouter, Query, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession

from config.db_conf import get_database
from crud import news

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


@router.get("/categories")
async def get_news_categories(
        skip: int = Query(0, description="跳过的数量"),
        limit: int = Query(10, le=100, description="返回的记录数限制"),
        db: AsyncSession = Depends(get_database)
):
    """
    获取新闻分类列表
    :param skip: 跳过的数量
    :param limit: 返回的记录数限制
    :param db: 数据库连接对象
    :return:
    """
    news_categories = await news.get_categories(db, skip, limit)
    return {"code": 200, "message": "获取新闻分类列表成功", "data": news_categories}


@router.get("/list")
async def get_news_list(
        category_id: int = Query(..., alias="categoryId", description="分类ID"),
        page: int = Query(1, description="页码"),
        page_size: int = Query(10, le=100, alias="pageSize", description="每页显示的新闻最大数量"),
        db: AsyncSession = Depends(get_database)
):
    """
    获取新闻列表
    :param category_id:
    :param page:
    :param page_size:
    :param db: 数据库连接对象
    :return:
    """

    # 处理分页:查询新闻列表--->计算总数---> 计算是否还有更多
    offset = (page - 1) * page_size

    # 根据id,分页查询新闻列表
    news_list = await news.get_news_list(db, category_id, offset, page_size)

    # 统计某个分类下的新闻数量
    total = await news.get_news_count(db, category_id)

    # 判断是否还有更多:跳过的数量+当前页面的新闻数量 小于总数量,则还有更多
    has_more = (offset + len(news_list)) < total

    return {
        "code": 200,
        "message": "获取新闻列表成功",
        "data": {
            "list": news_list,
            "total": total,
            "hasMore": has_more
        }
    }


@router.get("/detail")
async def get_news_detail(
        id: int = Query(..., description="新闻ID"),
        db: AsyncSession = Depends(get_database)
):
    """
    获取新闻详情、浏览量+1、相关新闻
    :param id: 新闻id
    :param db:  数据库连接对象
    :return:
    """
    # 1.获取新闻详情
    news_detail = await news.get_news_detail(id, db)

    # 判断新闻是否存在
    if not news_detail:
        raise HTTPException(status_code=404, detail="新闻不存在")

    # 2.新闻的浏览点击数加1, 返回的是布尔值
    views_boolean = await news.increase_news_views(news_detail.id, db)
    if not views_boolean:
        raise HTTPException(status_code=404, detail="新闻不存在,浏览点击数增加失败")

    # 3.获取相关新闻
    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  # 相关5条新闻
        }
    }

7.3.创建crud

在crud\news.py中创建对于上述接口的改、查方法:

from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession

from models.news import News, NewsCategory


async def get_categories(db: AsyncSession, skip: int = 0, limit: int = 10):
    """
    获取新闻分类列表
    :param db: 数据库连接对象
    :param skip: 跳过的数量
    :param limit: 返回的记录数限制
    :return:
    """
    stmt = select(NewsCategory).offset(skip).limit(limit)
    result = await db.execute(stmt)
    return result.scalars().all()


async def get_news_list(db: AsyncSession, category_id: int, skip: int = 0, limit: int = 10):
    """
    根据新闻id, 分页查询新闻列表
    :param db: 数据库连接对象
    :param category_id: 新闻分类
    :param skip: 跳过的页面数量
    :param limit: 页面的新闻数量
    :return:
    """
    # 查询制定分类下的所有新闻
    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)
    # 执行SQL
    result = await db.execute(stmt)
    return result.scalar_one()  # 返回数量, scalar_one() 返回单个结果


async def get_news_detail(id: int, db: AsyncSession):
    """
    根据id获取新闻详情
    :param id: 新闻id
    :param db: 数据库连接对象
    :return:
    """
    stmt = select(News).where(News.id == id)
    result = await db.execute(stmt)
    return result.scalar_one_or_none()


async def increase_news_views(id: int, db: AsyncSession):
    """
    修改更新新闻浏览量:新闻内容浏览一次,浏览量+1
    :param id: 新闻id
    :param db: 数据库引擎
    :return:
    """
    stmt = update(News).where(News.id == id).values(views=News.views + 1)
    result = await db.execute(stmt)  # 返回受影响的行数
    await db.commit()  # 提交事务

    return result.rowcount > 0  # 返回是否修改成功, 返回布尔值, True: 修改成功, False: 修改失败


async def get_related_news(db: AsyncSession, news_id: int, category_id: int, limit: int = 5):
    """
    获取相关新闻
    :param db:
    :param category_id:
    :return:
    """

    stmt = select(News).where(
        News.category_id == category_id,  # 新闻分类
        News.id != news_id  # 排除当前新闻
    ).order_by(
        News.views.desc(),  # 默认是升序,这里设置为按照浏览量降序排序
        News.publish_time.desc()  # 按照发布时间降序排序
    ).limit(limit)

    # 执行SQL
    result = await db.execute(stmt)
    # 返回结果
    related_news = result.scalars().all()

    return [{
        "id": news.id,
        "title": news.title,
        "content": news.content,
        "image": news.image,
        "author": news.author,
        "publishTime": news.publish_time,
        "categoryId": news.category_id,
        "views": news.views,
    }for news in related_news]

7.4.注册路由

在 main.py 中挂载路由,也就是注册路由,将上面创建的新闻模块接口路由添加到:

from fastapi import FastAPI
from routers import news
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许的源,开发阶段允许所有的源,生产环境需要制定源, 👈 明确指定前端地址例如: ["http://localhost:3000"]
    allow_credentials=True,  # 允许携带cookie
    allow_methods=["*"],  # 允许的请求方法
    allow_headers=["*"]  # 允许的请求头
)


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


# 挂载路由/注册路由
app.include_router(news.router)
posted @ 2026-01-26 10:51  酒剑仙*  阅读(2)  评论(0)    收藏  举报