第5章 5.5 综合实战:博客系统后端

🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了用 ORM 连接数据库,再也不用写那些让人头疼的 SQL 语句了。但光会查数据不够——你肯定想要一个真正能用的系统,比如一个博客后端。

你有没有遇到过这种情况:写了个博客系统,数据存是存了,但代码乱成一锅粥?文章、标签、评论分开存,调用时东一块西一块,自己都看不懂了?

这一章我们把这些知识串起来,做一个完整的博客后端 API。学完你可以:

  • 用一套代码管理文章、标签、评论
  • 告别「代码能跑但看不懂」的尴尬
  • 下一章学习 class 的时候,你会有一个真实项目等着它

🧱 基础 25 分钟:核心概念

5.5.1 什么是 RESTful API?

是什么:API 就是「菜单」,RESTful 是「点菜的方式」。

你去餐厅吃饭,菜单上写着「宫保鸡丁 28元」——这就是接口文档。你说「来一份宫保鸡丁」,服务员就给你端菜来。API 也是一样:客户端发请求,服务器返回数据。

为什么要用:想\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n象一下,如果没有统一的方式,博客前台、后台、移动端都要自己写一堆乱七八糟的接口。RESTful 规定了一套「怎么说话」的规则,大家统一用,效率翻倍。

怎么用

# 这是一个简单的 Flask API 示例
from flask import Flask, jsonify

app = Flask(__name__)

# 模拟数据库里的文章
articles = [
{"id": 1, "title": "Python 入门", "content": "内容略..."},
{"id": 2, "title": "Flask 实战", "content": "内容略..."}
]

@app.route("/api/articles", methods=["GET"])
def get_articles():
"""获取所有文章 - 相当于"给我看看菜单" """
return jsonify(articles)

if __name__ == "__main__":
app.run(debug=True)

运行后访问 http://127.0.0.1:5000/api/articles,就能看到文章列表了。

5.5.2 什么是 CRUD?

是什么:CRUD = Create(创建)、Read(读取)、Update(更新)、Delete(删除)。

为什么要用:这四个字涵盖了所有对数据的基本操作。博客系统里:
- 发一篇新文章 = Create
- 看文章列表 = Read
- 修改文章 = Update
- 删除文章 = Delete

怎么用

from flask import Flask, jsonify, request

app = Flask(__name__)

# 模拟数据库(用列表代替)
articles = []

@app.route("/api/articles", methods=["POST"])
def create_article():
"""创建文章 - POST 请求"""
new_article = request.json  # 从请求体获取数据
new_article["id"] = len(articles) + 1
articles.append(new_article)
return jsonify(new_article), 201  # 201 = 创建成功

@app.route("/api/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
"""读取单篇文章 - GET 请求"""
for article in articles:
    if article["id"] == article_id:
        return jsonify(article)
return jsonify({"error": "文章不存在"}), 404

@app.route("/api/articles/<int:article_id>", methods=["PUT"])
def update_article(article_id):
"""更新文章 - PUT 请求"""
for article in articles:
    if article["id"] == article_id:
        article.update(request.json)
        return jsonify(article)
return jsonify({"error": "文章不存在"}), 404

@app.route("/api/articles/<int:article_id>", methods=["DELETE"])
def delete_article(article_id):
"""删除文章 - DELETE 请求"""
global articles
articles = [a for a in articles if a["id"] != article_id]
return jsonify({"message": "删除成功"}), 200

if __name__ == "__main__":
app.run(debug=True)

5.5.3 什么是资源关联(标签和评论)?

是什么:一篇文章可以有多个标签,一篇文章也可以有多条评论。标签和评论都「属于」某篇文章。

为什么要用:真实业务里,数据不是孤立的,是一张网。文章和标签是多对多关系(一个文章多个标签,一个标签多个文章),文章和评论是一对多关系。

怎么用

# 用字典模拟关联关系
articles_db = []

tags_db = []  # 标签表

comments_db = []  # 评论表

def get_article_with_details(article_id):
"""获取文章的同时,把标签和评论也带上"""
article = None
for a in articles_db:
    if a["id"] == article_id:
        article = a.copy()
        break

if not article:
    return None

# 筛选出这篇文章的标签
article["tags"] = [t for t in tags_db if article_id in t["article_ids"]]

# 筛选出这篇文章的评论
article["comments"] = [c for c in comments_db if c["article_id"] == article_id]

return article

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):Flask 博客 API 基础版

目标:搭建一个能跑起来的博客 API,支持查看所有文章和查看单篇文章。

# blog_api_v1.py
from flask import Flask, jsonify

app = Flask(__name__)

# 模拟数据库
articles = [
{"id": 1, "title": "Python 环境搭建", "content": "首先安装 Python..."},
{"id": 2, "title": "Flask 快速入门", "content": "Flask 是一个轻量级框架..."},
{"id": 3, "title": "SQLAlchemy 基础", "content": "ORM 让数据库操作更简单..."}
]

@app.route("/api/articles", methods=["GET"])
def list_articles():
"""获取文章列表"""
return jsonify(articles)

@app.route("/api/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
"""获取单篇文章"""
for article in articles:
    if article["id"] == article_id:
        return jsonify(article)
return jsonify({"error": "文章不存在"}), 404

if __name__ == "__main__":
print("博客 API 已启动,访问 http://127.0.0.1:5000/api/articles")
app.run(debug=True, port=5000)

运行方式

python blog_api_v1.py

预期输出

博客 API 已启动,访问 http://127.0.0.1:5000/api/articles
* Running on http://127.0.0.1:5000

访问 http://127.0.0.1:5000/api/articles 看到 JSON 格式的文章列表。

一句话解释:这个项目用 Flask 搭了一个最简单的 API,访问 /api/articles 就返回所有文章。


项目 2(15 分钟):完整 CRUD + 标签功能

目标:在项目 1 基础上,加上创建、修改、删除文章的功能,以及标签管理。

# blog_api_v2.py
from flask import Flask, jsonify, request

app = Flask(__name__)

# 数据库模拟
articles = [
{"id": 1, "title": "Python 环境搭建", "content": "首先安装 Python..."},
{"id": 2, "title": "Flask 快速入门", "content": "Flask 是一个轻量级框架..."}
]

tags = [
{"id": 1, "name": "Python", "article_ids": [1, 2]},
{"id": 2, "name": "Flask", "article_ids": [2]}
]

comments = []

# ===== 文章 CRUD =====
@app.route("/api/articles", methods=["GET"])
def list_articles():
return jsonify(articles)

@app.route("/api/articles", methods=["POST"])
def create_article():
data = request.json
new_article = {
    "id": max([a["id"] for a in articles]) + 1 if articles else 1,
    "title": data.get("title", ""),
    "content": data.get("content", "")
}
articles.append(new_article)
return jsonify(new_article), 201

@app.route("/api/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
for article in articles:
    if article["id"] == article_id:
        # 带上这篇文章的标签
        article_tags = [t for t in tags if article_id in t["article_ids"]]
        result = article.copy()
        result["tags"] = article_tags
        return jsonify(result)
return jsonify({"error": "文章不存在"}), 404

@app.route("/api/articles/<int:article_id>", methods=["PUT"])
def update_article(article_id):
for article in articles:
    if article["id"] == article_id:
        data = request.json
        article["title"] = data.get("title", article["title"])
        article["content"] = data.get("content", article["content"])
        return jsonify(article)
return jsonify({"error": "文章不存在"}), 404

@app.route("/api/articles/<int:article_id>", methods=["DELETE"])
def delete_article(article_id):
global articles, tags, comments
articles = [a for a in articles if a["id"] != article_id]
# 清理关联的标签
for tag in tags:
    if article_id in tag["article_ids"]:
        tag["article_ids"].remove(article_id)
# 清理关联的评论
comments = [c for c in comments if c["article_id"] != article_id]
return jsonify({"message": "删除成功"})

# ===== 标签管理 =====
@app.route("/api/tags", methods=["GET"])
def list_tags():
return jsonify(tags)

@app.route("/api/tags", methods=["POST"])
def create_tag():
data = request.json
new_tag = {
    "id": max([t["id"] for t in tags]) + 1 if tags else 1,
    "name": data.get("name", ""),
    "article_ids": data.get("article_ids", [])
}
tags.append(new_tag)
return jsonify(new_tag), 201

@app.route("/api/tags/<int:tag_id>/articles", methods=["GET"])
def get_articles_by_tag(tag_id):
"""获取某个标签下的所有文章"""
for tag in tags:
    if tag["id"] == tag_id:
        tag_articles = [a for a in articles if a["id"] in tag["article_ids"]]
        return jsonify({"tag": tag["name"], "articles": tag_articles})
return jsonify({"error": "标签不存在"}), 404

# ===== 评论功能 =====
@app.route("/api/articles/<int:article_id>/comments", methods=["GET"])
def list_comments(article_id):
article_comments = [c for c in comments if c["article_id"] == article_id]
return jsonify(article_comments)

@app.route("/api/articles/<int:article_id>/comments", methods=["POST"])
def add_comment(article_id):
# 检查文章是否存在
if not any(a["id"] == article_id for a in articles):
    return jsonify({"error": "文章不存在"}), 404

data = request.json
new_comment = {
    "id": max([c["id"] for c in comments]) + 1 if comments else 1,
    "article_id": article_id,
    "author": data.get("author", "匿名"),
    "content": data.get("content", "")
}
comments.append(new_comment)
return jsonify(new_comment), 201

if __name__ == "__main__":
print("博客 API v2 已启动")
print("测试命令:curl http://127.0.0.1:5000/api/articles")
app.run(debug=True, port=5000)

测试方法(在另一个终端运行):

# 获取所有文章
curl http://127.0.0.1:5000/api/articles

# 创建新文章
curl -X POST http://127.0.0.1:5000/api/articles \
 -H "Content-Type: application/json" \
 -d '{"title": "新文章", "content": "这是内容"}'

# 给文章添加评论
curl -X POST http://127.0.0.1:5000/api/articles/1/comments \
 -H "Content-Type: application/json" \
 -d '{"author": "小明", "content": "写得真好!"}'

预期输出:创建文章后会返回新文章的信息,包含自动生成的 id。

一句话解释:这个版本完整实现了文章和标签的增删改查,还加了评论功能,是一个小而全的博客后端。


项目 3(15 分钟):数据导入导出工具

目标:把博客数据导出成 JSON 文件,也能从 JSON 文件导入。

# blog_data_tool.py
import json
from flask import Flask, jsonify, request

app = Flask(__name__)

# 数据库
articles = []
tags = []
comments = []

DATA_FILE = "blog_data.json"

def save_to_file():
"""把所有数据保存到 JSON 文件"""
data = {
    "articles": articles,
    "tags": tags,
    "comments": comments
}
with open(DATA_FILE, "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)
print(f"数据已保存到 {DATA_FILE}")

def load_from_file():
"""从 JSON 文件加载数据"""
global articles, tags, comments
try:
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        data = json.load(f)
        articles = data.get("articles", [])
        tags = data.get("tags", [])
        comments = data.get("comments", [])
    print(f"已从 {DATA_FILE} 加载数据")
except FileNotFoundError:
    print("没有找到数据文件,将从空数据开始")

# 启动时加载数据
load_from_file()

# 现有的 CRUD API(简化版,省略部分代码)
@app.route("/api/articles", methods=["GET"])
def list_articles():
return jsonify(articles)

@app.route("/api/articles", methods=["POST"])
def create_article():
data = request.json
new_article = {
    "id": max([a["id"] for a in articles]) + 1 if articles else 1,
    "title": data.get("title", ""),
    "content": data.get("content", "")
}
articles.append(new_article)
save_to_file()  # 自动保存
return jsonify(new_article), 201

@app.route("/api/articles/<int:article_id>", methods=["DELETE"])
def delete_article(article_id):
global articles
articles = [a for a in articles if a["id"] != article_id]
save_to_file()  # 自动保存
return jsonify({"message": "删除成功"})

# ===== 导入导出 API =====
@app.route("/api/export", methods=["GET"])
def export_data():
"""导出所有数据为 JSON"""
save_to_file()  # 先保存最新数据
with open(DATA_FILE, "r", encoding="utf-8") as f:
    data = f.read()
return data, 200, {"Content-Type": "application/json; charset=utf-8"}

@app.route("/api/import", methods=["POST"])
def import_data():
"""从 JSON 导入数据(会覆盖现有数据)"""
global articles, tags, comments
data = request.json
articles = data.get("articles", [])
tags = data.get("tags", [])
comments = data.get("comments", [])
save_to_file()
return jsonify({"message": "导入成功", "count": len(articles)})

if __name__ == "__main__":
print("数据导入导出工具已启动")
app.run(debug=True, port=5000)

使用场景
- 定期备份博客数据
- 从测试环境迁移数据到生产环境
- 批量导入预先准备好的文章

一句话解释:这个工具让数据持久化到文件,再也不怕程序重启后数据丢失了。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:JSON 数据没加 Content-Type

# ❌ 错误:返回 JSON 但没告诉浏览器
return jsonify({"error": "找不到"})

# ✅ 正确:显式设置类型
return jsonify({"error": "找不到"}), 404

Flask 的 jsonify 会自动设置正确的 Content-Type,但如果直接返回字符串就要手动加。

坑 2:删除时没清理关联数据

# ❌ 错误:删了文章,但标签和评论还在
articles = [a for a in articles if a["id"] != article_id]

# ✅ 正确:把关联数据一起清理
articles = [a for a in articles if a["id"] != article_id]
tags = [t for t in tags if article_id not in t["article_ids"]]
comments = [c for c in comments if c["article_id"] != article_id]

数据关联就像绳结,解开一个要记得把其他结也解开。

坑 3:POST 请求没验证数据

# ❌ 危险:直接用,不检查有没有 title
@app.route("/api/articles", methods=["POST"])
def create_article():
new_article = request.json
new_article["id"] = len(articles) + 1
articles.append(new_article)
return jsonify(new_article), 201

# ✅ 安全:先检查必要字段
@app.route("/api/articles", methods=["POST"])
def create_article():
data = request.json
if not data.get("title"):
    return jsonify({"error": "标题不能为空"}), 400
# ... 继续创建

坑 4:用列表存数据,生产环境会丢

# ❌ 开发没问题,生产会哭
articles = []

# ✅ 真实项目用数据库
# from flask_sqlalchemy import SQLAlchemy
# db = SQLAlchemy(app)

内存里的列表程序一关就没了。练手可以,真做项目必须上数据库。

坑 5:调试模式开太久

# ❌ 生产环境开 debug=True,会被黑
app.run(debug=True)

# ✅ 生产环境关掉
app.run(debug=False, host="0.0.0.0")

性能小贴士:加索引

# 如果用 SQLAlchemy,给常用查询字段加索引
class Article(db.Model):
__tablename__ = "articles"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), index=True)  # 给 title 加索引

索引就像书的目录,让查询快 10 倍不止。

调试技巧:打印请求日志

@app.before_request
def log_request():
print(f"[{request.method}] {request.path} - {request.json}")

每次请求来都会打印,方便追踪数据流。


✏️ 练习题 + 作业题

练习题(5 道,10 分钟)

练习 1(2 分钟):添加一个统计接口
- 输入:访问 /api/articles/count
- 预期输出:{"count": 3}
- 提示:用 len(articles) 直接数

练习 2(2 分钟):给文章列表加标题筛选
- 输入:访问 /api/articles?title=Python
- 预期输出:只返回标题含 "Python" 的文章
- 提示:用 request.args.get("title") 获取查询参数

练习 3(2 分钟):评论加个时间戳
- 输入:创建评论时
- 预期输出:评论里多个 "created_at": "2024-01-15 10:30:00"
- 提示:import datetime,然后 datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

练习 4(2 分钟):标签解耦
- 输入:删除一个标签
- 预期输出:该标签被删除,但不删除关联的文章
- 提示:只删标签本身,不动文章的 tag_ids 列表

练习 5(2 分钟):找出报错原因
- 输入:运行以下代码

@app.route("/api/articles/<int:id>", methods=["GET"])
def get_article(id):
return jsonify(articles[id])  # 报错!
  • 预期输出:{"error": "文章不存在"}
  • 提示:列表下标从 0 开始,但 id 从 1 开始,不能直接用 articles[id]

作业题(30 分钟 - 2 小时)

作业:做一个「博客评论管理工具」

需求描述
做一个评论管理小工具,可以查看、添加、删除评论。

功能点
1. 查看某篇文章的所有评论(GET)
2. 添加评论(POST),评论者名字不能为空
3. 删除评论(DELETE)
4. 统计每篇文章的评论数

加分项
1. 评论按时间倒序(最新的在前)
2. 支持回复评论(用 parent_id 实现嵌套)

验收标准
- 能跑起来
- curl 命令能测试通过
- 代码有注释

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

本章核心

  1. RESTful API:用 HTTP 方法(GET/POST/PUT/DELETE)操作资源(文章/标签/评论)
  2. CRUD 模式:Create 创建、Read 读取、Update 更新、Delete 删除
  3. 数据关联:文章、标签、评论三者之间的关系要一起管理

延伸资源

互动钩子

你在做博客系统的时候,数据关联是怎么处理的?有没有遇到过「删了文章但评论还在」的坑?评论区聊聊,老粉优先回复!


下章预告

现在我们用列表、字典模拟了数据存储,但代码越写越乱……文章、标签、评论各有一套操作逻辑,函数名都快记不住了。下一章我们要学习 class 与命名空间,用它来把代码组织得更清晰。准备好了吗?

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。