FastAPI基础项目:仿头条新闻的web项目,实现基本的新闻列表页和详情页查看功能

文章目录

    • 前言
    • 一、环境准备
      • [1.1 项目结构](#1.1 项目结构)
      • [1.2 安装Python库](#1.2 安装Python库)
      • [1.3 准备MySQL数据库](#1.3 准备MySQL数据库)
    • 二、编写代码
      • [2.1 创建案例数据](#2.1 创建案例数据)
      • [2.2 编写主应用文件 `main.py`](#2.2 编写主应用文件 main.py)
      • [2.3 创建新闻列表页 `templates/index.html`](#2.3 创建新闻列表页 templates/index.html)
      • [2.4 创建新闻详情页 `templates/detail.html`](#2.4 创建新闻详情页 templates/detail.html)
      • [2.5 运行和测试](#2.5 运行和测试)

前言

该项目页面打开是新闻列表页,截图如下:

点击详情可以进入详情页,截图如下:

该项目属于基础入门项目。

一、环境准备

1.1 项目结构

复制代码
news-app/
├── main.py              # FastAPI应用主文件
├── requirements.txt      # 项目依赖
├── .env                 # 环境变量文件
├── create_sample_data.py # 脚本:用于创建示例数据
└── templates/
    ├── index.html       # 新闻列表页
    └── detail.html      # 新闻详情页

1.2 安装Python库

bash 复制代码
pip install fastapi "uvicorn[standard]" jinja2 sqlalchemy pymysql python-dotenv

1.3 准备MySQL数据库

确保MySQL服务正在运行,并且有相应数据库,然后在项目根目录创建.env文件,存储数据库连接信息。

ini 复制代码
DATABASE_URL="mysql+pymysql://your_username:your_password@127.0.0.1:3306/news_app"

your_usernameyour_password 替换为MySQL凭据。

二、编写代码

2.1 创建案例数据

1、编写 create_sample_data.py :这个脚本会帮助我们创建数据库表并插入一些示例新闻,方便我们测试。

python 复制代码
import os
import random
from datetime import datetime, timedelta
from dotenv import load_dotenv
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 加载环境变量
load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


# 新闻模型
class News(Base):
    __tablename__ = "fastapi_news"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(255), nullable=False)
    content = Column(Text, nullable=False)
    author = Column(String(100), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)


# 创建表
Base.metadata.create_all(bind=engine)


# 生成示例数据
def generate_sample_data():
    db = SessionLocal()

    # 如果已有数据,则不重复生成
    if db.query(News).count() > 0:
        print("数据库中已有数据,跳过生成。")
        db.close()
        return

    titles = [
        "FastAPI框架:下一代Python Web开发的王者?",
        "深入理解Python异步编程:asyncio完全指南",
        "MySQL性能优化实战:从慢查询到毫秒响应",
        "Docker容器化部署:让应用开发如虎添翼",
        "人工智能浪潮下,程序员的未来在哪里?",
        "前端框架大比拼:Vue vs React vs Angular",
        "微服务架构设计:如何构建高可用系统",
        "数据科学入门:Pandas与NumPy核心技巧",
        "网络安全新挑战:如何防范常见的Web攻击",
        "区块链技术原理与应用场景深度剖析",
        "云计算时代:AWS、Azure、Google Cloud如何选择?",
        "机器学习项目实战:从数据清洗到模型部署",
        "TypeScript:大型JavaScript项目的必备利器",
        "Kubernetes入门:容器编排的艺术",
        "代码审查最佳实践:提升团队代码质量",
        "DevOps文化:打通开发与运维的任督二脉",
        "图数据库Neo4j:探索关系型数据的新维度",
        "Rust语言:系统级编程的未来之星?",
        "量子计算离我们还有多远?技术前沿展望",
        "设计模式精髓:写出优雅可维护的代码"
    ]

    authors = ["科技前沿", "开发者日报", "AI观察员", "架构师之道", "代码诗人"]

    print("正在生成50条示例新闻数据...")
    for i in range(50):
        title = random.choice(titles) + f" (第{i + 1}期)"
        content = f"""
        <p>这是关于 <strong>{title}</strong> 的详细报道。</p>
        <p>在当今快速发展的技术世界中,{random.choice(['创新', '变革', '演进', '突破'])}是永恒的主题。
        本文将深入探讨相关的技术细节、市场趋势以及未来的发展方向。我们采访了多位行业专家,
        他们分享了宝贵的见解和经验。</p>
        <p>首先,我们需要了解其核心原理。{random.choice(['从架构设计来看', '从算法层面分析', '从用户体验角度出发'])},
        这项技术为我们带来了前所未有的可能性。它不仅提高了效率,还降低了成本。</p>
        <p>然而,挑战依然存在。{random.choice(['如何规模化应用', '如何保证安全性', '如何进行人才培养'])}
        是我们需要共同面对的问题。未来,我们期待看到更多的{random.choice(['创新应用', '跨界合作', '开源贡献'])}。</p>
        <p>总之,{title}代表了当前技术发展的一个重要方向,值得我们持续关注和深入研究。</p>
        """
        author = random.choice(authors)
        # 生成过去30天内的随机时间
        created_at = datetime.utcnow() - timedelta(days=random.randint(0, 30), hours=random.randint(0, 23))

        news_item = News(title=title, content=content, author=author, created_at=created_at)
        db.add(news_item)

    db.commit()
    print("50条示例数据生成成功!")
    db.close()


if __name__ == "__main__":
    generate_sample_data()

2、运行脚本生成数据

在终端中运行此脚本:

bash 复制代码
python create_sample_data.py

2.2 编写主应用文件 main.py

这是整个应用的核心,整合了API、数据库操作和页面渲染。

python 复制代码
# main.py
import os
import math
import uvicorn
from typing import List, Optional

from dotenv import load_dotenv
from fastapi import FastAPI, Request, Depends, HTTPException, status
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, func
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from datetime import datetime

# --- 1. 基础配置 ---
load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
    raise ValueError("DATABASE_URL not found in .env file")

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

app = FastAPI(title="News App with MySQL")
templates = Jinja2Templates(directory="templates")


# --- 2. 数据库模型 ---
class News(Base):
    __tablename__ = "fastapi_news"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(255), nullable=False)
    content = Column(Text, nullable=False)
    author = Column(String(100), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)


# --- 3. 依赖注入 ---
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# --- 4. 分页辅助类 ---
class Pagination:
    def __init__(self, query, page: int, per_page: int):
        self.query = query
        self.page = page
        self.per_page = per_page
        self.total = query.count()
        self.items = query.offset((page - 1) * per_page).limit(per_page).all()

    @property
    def pages(self):
        return int(math.ceil(self.total / float(self.per_page)))

    def has_prev(self):
        return self.page > 1

    def has_next(self):
        return self.page < self.pages

    def iter_pages(self, left_edge=2, left_current=2, right_current=3, right_edge=2):
        last = 0
        for num in range(1, self.pages + 1):
            if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) \
                    or num > self.pages - right_edge:
                if last + 1 != num:
                    yield None
                yield num
                last = num


# --- 5. 页面路由 ---

@app.get("/", response_class=HTMLResponse)
def read_news_list(request: Request, page: int = 1, db: Session = Depends(get_db)):
    per_page = 10  # 每页显示10条新闻
    news_query = db.query(News).order_by(News.created_at.desc())
    pagination = Pagination(news_query, page, per_page)

    return templates.TemplateResponse("index.html", {
        "request": request,
        "news_list": pagination
    })


@app.get("/news/{news_id}", response_class=HTMLResponse)
def read_news_detail(request: Request, news_id: int, db: Session = Depends(get_db)):
    news = db.query(News).filter(News.id == news_id).first()
    if not news:
        raise HTTPException(status_code=404, detail="News not found")

    return templates.TemplateResponse("detail.html", {
        "request": request,
        "news": news
    })


# --- 6. API 路由 (可选,用于未来扩展) ---
@app.get("/api/news", response_model=List[dict])
def api_get_all_news(db: Session = Depends(get_db)):
    news = db.query(News).order_by(News.created_at.desc()).all()
    return [
        {"id": n.id, "title": n.title, "author": n.author, "created_at": n.created_at}
        for n in news
    ]


@app.get("/api/news/{news_id}", response_model=dict)
def api_get_news(news_id: int, db: Session = Depends(get_db)):
    news = db.query(News).filter(News.id == news_id).first()
    if not news:
        raise HTTPException(status_code=404, detail="News not found")
    return {
        "id": news.id,
        "title": news.title,
        "content": news.content,
        "author": news.author,
        "created_at": news.created_at
    }


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

2.3 创建新闻列表页 templates/index.html

html 复制代码
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>今日头条 - FastAPI新闻</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f4f5f6; color: #222; }
        .container { max-width: 800px; margin: 20px auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
        header { text-align: center; padding-bottom: 20px; border-bottom: 2px solid #ed4040; margin-bottom: 20px; }
        header h1 { color: #ed4040; margin: 0; font-size: 2.5em; }
        .news-item { border-bottom: 1px solid #eee; padding: 15px 0; cursor: pointer; transition: background-color 0.2s; }
        .news-item:hover { background-color: #f9f9f9; }
        .news-item:last-child { border-bottom: none; }
        .news-title { font-size: 1.2em; font-weight: bold; color: #333; margin-bottom: 5px; }
        .news-meta { font-size: 0.9em; color: #888; }
        .news-meta span { margin-right: 15px; }
        .pagination { display: flex; justify-content: center; align-items: center; margin-top: 30px; gap: 10px; }
        .pagination a, .pagination span { padding: 8px 12px; border: 1px solid #ddd; text-decoration: none; color: #333; border-radius: 4px; }
        .pagination a:hover { background-color: #ed4040; color: white; border-color: #ed4040; }
        .pagination .current { background-color: #ed4040; color: white; border-color: #ed4040; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>今日头条</h1>
        </header>
        <main>
            {% for news in news_list.items %}
            <div class="news-item" onclick="location.href='/news/{{ news.id }}'">
                <div class="news-title">{{ news.title }}</div>
                <div class="news-meta">
                    <span>作者: {{ news.author }}</span>
                    <span>时间: {{ news.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
                </div>
            </div>
            {% endfor %}
        </main>
        <div class="pagination">
            {% if news_list.has_prev %}
                <a href="/?page={{ news_list.prev_num }}"><< 上一页</a>
            {% else %}
                <span style="color: #ccc;"><< 上一页</span>
            {% endif %}
            
            {% for page_num in news_list.iter_pages() %}
                {% if page_num %}
                    {% if page_num != news_list.page %}
                        <a href="/?page={{ page_num }}">{{ page_num }}</a>
                    {% else %}
                        <span class="current">{{ page_num }}</span>
                    {% endif %}
                {% else %}
                    <span>...</span>
                {% endif %}
            {% endfor %}
            {% if news_list.has_next %}
                <a href="/?page={{ news_list.next_num }}">下一页 >></a>
            {% else %}
                <span style="color: #ccc;">下一页 >></span>
            {% endif %}
        </div>
    </div>
</body>
</html>

2.4 创建新闻详情页 templates/detail.html

html 复制代码
<!-- templates/detail.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ news.title }} - 今日头条</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f4f5f6; color: #222; line-height: 1.6; }
        .container { max-width: 800px; margin: 20px auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
        header { text-align: center; padding-bottom: 20px; border-bottom: 2px solid #ed4040; margin-bottom: 20px; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #ed4040; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
        .news-title { font-size: 2.2em; font-weight: bold; color: #333; margin-bottom: 10px; }
        .news-meta { font-size: 0.9em; color: #888; margin-bottom: 25px; }
        .news-meta span { margin-right: 20px; }
        .news-content { font-size: 1.1em; }
        .news-content p { margin-bottom: 1em; }
        .news-content strong { color: #000; }
    </style>
</head>
<body>
    <div class="container">
        <a href="/" class="back-link">← 返回新闻列表</a>
        <header>
            <h1 class="news-title">{{ news.title }}</h1>
            <div class="news-meta">
                <span>作者: {{ news.author }}</span>
                <span>发布时间: {{ news.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
            </div>
        </header>
        <main class="news-content">
            {{ news.content | safe }}
        </main>
    </div>
</body>
</html>

2.5 运行和测试

  1. 启动应用
    news-app目录下,运行:

    bash 复制代码
    uvicorn main:app --reload
  2. 访问应用

    • 新闻列表页 :打开浏览器访问 http://127.0.0.1:8000。你将看到新闻列表,底部有分页导航。
    • 新闻详情页:点击任意一条新闻的标题,会跳转到详情页。

该功能完整的新闻网站,具备了现代Web应用的核心特性,并且结构清晰,易于扩展。

相关推荐
2501_941111252 小时前
自动化与脚本
jvm·数据库·python
wc_xue_fei_le2 小时前
11.11DNS主从服务器
linux·服务器·前端
女生寝室0382 小时前
《Chrome》 [142.0.7444.60][绿色便携版] 下载
前端·chrome
进击的野人2 小时前
JavaScript原型与原型链:深入理解面向对象编程的基石
前端·javascript·面试
yannick_liu2 小时前
wangeditor自定义扩展设置图片宽高
前端
呵阿咯咯2 小时前
Vue3项目记录
前端·vue.js
yigenhuochai2 小时前
Trae Solo 开发体验:从零到完整考试备考平台的奇妙之旅
前端·trae
n***63272 小时前
Python大数据可视化:基于大数据技术的共享单车数据分析与辅助管理系统_flask+hadoop+spider
大数据·python·信息可视化
夏目友人爱吃豆腐2 小时前
uniapp源码解析(Vue3/Vite版)
前端·vue.js·uni-app