AI 掘金头条-新闻模块
一、AI 掘金头条项目内容说明
- 项目功能
- 新闻模块
- 用户模块
- 收藏和浏览历史
- 设计说明文档
- 用来系统说明整个后端系统的架构、模块划分、数据设计和接口规范
- 配套物料
- 前端代码:项目前端工程采用 Vue 框架,实现了页面结构、组件交互和接口调用等核心逻辑,能够在浏览器端呈现完整界面并与后端服务对接,从而展示整个应用的完整功能
- 接口规范文档:项目的接口规范文档,定义了各接口的请求方式、参数格式、响应结构和错误码等关键规则,为前后端提供统一的交互标准,确保系统各模块能够准确对接并稳定协同工作。
- 数据库 SQL 文件:项目的 MySQL SQL 文件, 包含了数据库的表结构定义、初始化数据和必要的约束规则,用于在数据库中快速搭建完整的数据存储环境,为后端服务提供稳定可靠的数据基础。
二、运行前端项目
1.安装 Node:
- 运行构建工具(Vite/Webpack)、开启开发服务器、编译代码
2.运行项目:
- 项目根目录打开终端
npm run dev
三、模块化路由
工程结构如下:

模块化路由就是把每个业务功能的接口拆分到独立文件里,再统一挂载到主应用中。
- 项目结构更清晰:接口按模块拆分,不会混在一起,让整个项目结构更直观
- 更易维护:每个模块都负责自己对应的接口,便于快速查找和定向修改
- 避免 main.py 爆炸:把接口拆分出去后,main.py就只负责启动应用,不再堆满业务代码
模块化路由实现流程:
- ①.模块化目录结构

- ②.编写独立路由模块,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
⑵.建表
- create_async_engine创建异步引擎
- 定义模型基类(继承DeclartiveBase)和 模型类
- run_sync(Base.metadta.create_al)建表
⑶.操作数据
- 路由处理函数注入数据库会话依赖项 Depnds
- 查询数据:select()
- 增加数据:add()
- 更新数据:重新赋值
- 删除数据:delete()
4.2.MySQL 导入 SQL 文件
PyCharm中 Database 插件 → 数据库连接 → 右键 → SQL Script → Run SQL Script → 浏览 SQL 文件 → 确认

导入后查询表

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):如
http、https - 主机名(host):如
example.com - 端口(port):如
80、443
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?
5.3.CORS请求类型
5.3.1.简单请求(Simple Request)
- 方法为
GET、POST或HEAD - 请求头仅包含以下字段:
AcceptAccept-LanguageContent-LanguageContent-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)
- 无自定义请求头
- 无读取响应头的特殊要求
对于简单请求,浏览器自动在请求中加上Origin头,服务器返回Access-Control-Allow-Origin即可。
5.3.2.预检请求(Preflight Request)
PUT、DELETE、带自定义头、Content-Type: application/json 等),浏览器会先发送一个 OPTIONS 请求(预检),询问服务器是否允许该实际请求。OriginAccess-Control-Request-Method:实际请求的方法Access-Control-Request-Headers:实际请求将携带的自定义头
Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-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 的作用
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配置如下:

1.allow_origins=["*"]
- 表示 允许所有域名 访问你的 API。
- ⚠️ 开发阶段方便,但生产环境非常危险!
- 生产建议写成具体域名,例如:
allow_origins=[ "https://your-frontend.com", "https://www.your-frontend.com" ]
2.allow_credentials=True
- 允许前端请求携带 Cookie、Authorization 头 等凭证。
- ⚠️ 如果设为
True,allow_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-Type,Authorization)。 - 生产环境可按需限制。
说明:如果你正在开发前后端分离项目,这个中间件几乎是必需的。但在上线前,务必收紧配置以保障安全
| 配置项 | 作用 | 安全建议 |
|---|---|---|
CORSMiddleware |
解决跨域问题,让其他网站能安全调用你的 API | 必须配置,否则前端可能无法调用 |
allow_origins=["*"] |
允许任意网站跨域访问 | 仅限开发!生产必须指定域名 |
allow_credentials=True |
允许带 Cookie/Token 的请求 | 若开启,allow_origins 不能为 "*" |
六、新闻模块实现
新闻模块核心功能

6.1.接口实现流程
⑴.模块化路由
- 定义 APIRouter 实例
- 注册路由include_router()
- 参照接口规范文档
⑵.定义模型类
- 参照数据库表
class Base(DeclarativeBase): pass class Category(Base): _tablename_="news_category"
⑶.数据库CRUD
- select(模型类)
- add()
- update()
- delete()
⑷.路由调用逻辑
- Depnds 注入数据库依赖
- 调用逻辑
- 响应结果
6.2.接口说明
6.2.1.接口文档-获取新闻分类列表
根据下面的接口文档编写接口:

接口编写:

6.2.2.接口文档-获取新闻列表
根据下面的接口文档编写接口:

接口编写:

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

6.2.3.接口文档-获取新闻详情
根据下面的接口文档编写接口:

接口编写:

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

七、新闻模块实现
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)

浙公网安备 33010602011771号