Flask 博客系统(Flask Blog System)

发布于:2025-09-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

目标:零基础也能从头搭建一个支持文章管理、评论、分类标签、搜索、用户登录的博客系统
技术栈:Flask + SQLite + SQLAlchemy + Jinja2 + HTML/CSS + Flask-Login
开发工具:VSCode
学习重点:MVC 模式、数据库操作、会话管理、表单处理

一、项目概述

本项目是一个基于 Python Web 框架 Flask 构建的轻量级个人博客系统,旨在为开发者提供一个功能完整、结构清晰、易于学习和扩展的 Web 应用范例。系统采用 MVC 设计模式 组织代码结构,使用 SQLite 作为数据库,结合 HTML/CSS 前端模板 实现用户友好的交互界面,完整实现了博客核心功能模块。

该系统不仅具备实用价值,更注重教学意义,特别适合 Python Web 开发初学者理解 Web 请求响应机制、数据库操作、用户会话管理与前后端交互流程。

二、技术栈

类别 技术
后端框架 Flask (轻量级 Python Web 框架)
数据库 SQLite(嵌入式数据库,无需额外服务)
ORM Flask-SQLAlchemy(对象关系映射,简化数据库操作)
用户认证 Flask-Login(管理用户登录状态与会话)
前端技术 HTML5 + CSS3 + Jinja2 模板引擎
安全机制 密码哈希(Werkzeug.security)、CSRF 防护(基础)
开发工具 VSCode、Python 虚拟环境

三、核心功能

  1. 文章管理

    • 支持文章的创建、编辑、删除与查看详情
    • 文章包含标题、内容、发布时间、作者信息
  2. 分类与标签系统

    • 每篇文章可归属一个分类(如“技术”、“生活”)
    • 支持多标签管理(如“Python”、“Flask”),便于内容组织与检索
  3. 用户评论功能

    • 登录用户可在文章页发表评论
    • 评论按时间排序展示,增强互动性
  4. 全文搜索

    • 支持通过关键词在文章标题和内容中进行模糊搜索
    • 搜索结果实时展示,提升用户体验
  5. 用户系统与会话管理

    • 用户注册与登录功能
    • 基于 Flask-Login 的会话管理,确保安全访问控制
    • 权限控制:仅文章作者可编辑或删除自己的文章
  6. 响应式前端界面

    • 使用原生 HTML/CSS 构建简洁美观的页面布局
    • 支持导航菜单、消息提示、表单验证等基础交互

四、架构设计(MVC 模式)

系统严格遵循 MVC(Model-View-Controller)设计模式,实现关注点分离:

  • Model(模型层):由 models.py 定义数据模型(User、Post、Comment、Category、Tag),通过 SQLAlchemy 映射到 SQLite 数据库。
  • View(视图层):使用 Jinja2 模板引擎在 templates/ 目录下渲染 HTML 页面,实现动态内容展示。
  • Controller(控制器层)routes.py 中的路由函数处理 HTTP 请求,调用模型进行数据操作,并返回对应视图。

五、项目特点

  • 零依赖外部服务:使用 SQLite,无需安装数据库服务器
  • 开箱即用:提供完整代码与依赖文件,一键运行
  • 学习友好:代码结构清晰,注释详尽,适合初学者理解 Web 开发全流程
  • 可扩展性强:模块化设计,便于后续集成 Markdown 编辑器、分页、REST API 等功能
  • 安全基础:用户密码加密存储,防止明文泄露

📁 项目目录结构

/blog
  ├── app.py                 # 主程序入口
  ├── models.py              # 数据模型定义
  ├── routes.py              # 路由与控制器逻辑
  ├── config.py              # 配置文件
  ├── requirements.txt       # 依赖包列表
  ├── instance/
  │   └── blog.db            # 自动生成的 SQLite 数据库
  ├── templates/             # HTML 模板
  │   ├── base.html          # 布局模板
  │   ├── index.html         # 首页
  │   ├── login.html         # 登录页
  │   ├── create_post.html   # 发布文章
  │   ├── post.html          # 文章详情
  │   └── register.html      # 注册页(可选)
  └── static/
      └── style.css          # 样式文件

✅ 第一步:环境准备

1. 创建项目文件夹

mkdir blog && cd blog

2. 创建虚拟环境

python -m venv venv
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate

3. 安装依赖

创建 requirements.txt 文件:

Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Werkzeug==3.0.3

安装:

pip install -r requirements.txt

✅ 第二步:配置文件 config.py

# config.py
import os

class Config:
    SECRET_KEY = 'your-secret-key-here-change-it'  # 用于 session 加密
    SQLALCHEMY_DATABASE_URI = 'sqlite:///blog.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    DATABASE_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'instance', 'blog.db')

✅ 第三步:数据库模型 models.py

# models.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime

db = SQLAlchemy()

# 关联表:文章-标签(多对多)
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)
    comments = db.relationship('Comment', backref='author', lazy=True)

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    posts = db.relationship('Post', backref='category', lazy=True)

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(120), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    
    tags = db.relationship('Tag', secondary=post_tags, backref='posts')
    comments = db.relationship('Comment', backref='post', lazy=True)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

✅ 第四步:主程序 app.py

# app.py
from flask import Flask
from models import db
from routes import bp
from config import Config
from flask_login import LoginManager

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)
    
    # 初始化数据库
    db.init_app(app)
    
    # 创建 instance 文件夹和数据库
    import os
    if not os.path.exists('instance'):
        os.makedirs('instance')
    
    with app.app_context():
        db.create_all()
    
    # 初始化登录管理
    login_manager = LoginManager()
    login_manager.login_view = 'bp.login'
    login_manager.init_app(app)
    
    from models import User
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    
    # 注册蓝图
    app.register_blueprint(bp)
    return app

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

✅ 第五步:路由与控制器 routes.py

# routes.py
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from flask_login import login_user, logout_user, login_required, current_user
from models import db, User, Post, Category, Tag, Comment
from werkzeug.security import generate_password_hash, check_password_hash

bp = Blueprint('bp', __name__)

@bp.route('/')
def index():
    search = request.args.get('q')
    if search:
        posts = Post.query.filter(
            (Post.title.contains(search)) | 
            (Post.content.contains(search))
        ).order_by(Post.created_at.desc()).all()
    else:
        posts = Post.query.order_by(Post.created_at.desc()).all()
    
    categories = Category.query.all()
    return render_template('index.html', posts=posts, categories=categories)

@bp.route('/post/<int:id>')
def post(id):
    post = Post.query.get_or_404(id)
    return render_template('post.html', post=post)

@bp.route('/post/create', methods=['GET', 'POST'])
@login_required
def create_post():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        category_id = request.form.get('category_id')
        tag_names = request.form.get('tags', '').split(',')
        
        # 获取或创建分类
        category = None
        if category_id:
            category = Category.query.get(category_id)
        
        # 处理标签
        tags = []
        for name in tag_names:
            name = name.strip()
            if name:
                tag = Tag.query.filter_by(name=name).first()
                if not tag:
                    tag = Tag(name=name)
                    db.session.add(tag)
                tags.append(tag)
        
        post = Post(
            title=title,
            content=content,
            category=category,
            tags=tags,
            author=current_user
        )
        db.session.add(post)
        db.session.commit()
        flash('文章发布成功!')
        return redirect(url_for('bp.index'))
    
    categories = Category.query.all()
    return render_template('create_post.html', categories=categories)

@bp.route('/post/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit_post(id):
    post = Post.query.get_or_404(id)
    if post.author != current_user:
        flash('你没有权限编辑此文章。')
        return redirect(url_for('bp.post', id=id))
    
    if request.method == 'POST':
        post.title = request.form['title']
        post.content = request.form['content']
        post.category_id = request.form.get('category_id')
        
        # 更新标签
        tag_names = request.form.get('tags', '').split(',')
        post.tags.clear()
        for name in tag_names:
            name = name.strip()
            if name:
                tag = Tag.query.filter_by(name=name).first()
                if not tag:
                    tag = Tag(name=name)
                    db.session.add(tag)
                post.tags.append(tag)
        
        db.session.commit()
        flash('文章已更新!')
        return redirect(url_for('bp.post', id=id))
    
    categories = Category.query.all()
    tag_str = ', '.join([t.name for t in post.tags])
    return render_template('create_post.html', post=post, categories=categories, tag_str=tag_str)

@bp.route('/post/<int:id>/delete', methods=['POST'])
@login_required
def delete_post(id):
    post = Post.query.get_or_404(id)
    if post.author != current_user:
        flash('你没有权限删除此文章。')
        return redirect(url_for('bp.post', id=id))
    
    db.session.delete(post)
    db.session.commit()
    flash('文章已删除。')
    return redirect(url_for('bp.index'))

@bp.route('/comment/<int:post_id>', methods=['POST'])
@login_required
def add_comment(post_id):
    content = request.form['content']
    post = Post.query.get_or_404(post_id)
    comment = Comment(content=content, post=post, author=current_user)
    db.session.add(comment)
    db.session.commit()
    flash('评论已发布!')
    return redirect(url_for('bp.post', id=post_id))

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = User.query.filter_by(username=username).first()
        if user and check_password_hash(user.password, password):
            login_user(user)
            flash('登录成功!')
            return redirect(url_for('bp.index'))
        else:
            flash('用户名或密码错误。')
    return render_template('login.html')

@bp.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if User.query.filter_by(username=username).first():
            flash('用户名已存在。')
        else:
            hashed = generate_password_hash(password)
            user = User(username=username, password=hashed)
            db.session.add(user)
            db.session.commit()
            flash('注册成功,请登录。')
            return redirect(url_for('bp.login'))
    return render_template('register.html')

@bp.route('/logout')
@login_required
def logout():
    logout_user()
    flash('已退出登录。')
    return redirect(url_for('bp.index'))

✅ 第六步:HTML 模板

1. 基础布局 templates/base.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的博客{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <header>
        <h1><a href="{{ url_for('bp.index') }}">我的博客</a></h1>
        <nav>
            {% if current_user.is_authenticated %}
                <span>欢迎, {{ current_user.username }}!</span>
                <a href="{{ url_for('bp.create_post') }}">发布文章</a>
                <a href="{{ url_for('bp.logout') }}">退出</a>
            {% else %}
                <a href="{{ url_for('bp.login') }}">登录</a>
                <a href="{{ url_for('bp.register') }}">注册</a>
            {% endif %}
        </nav>
    </header>

    <main>
        {% with messages = get_flashed_messages() %}
            {% if messages %}
                <ul class="flashes">
                    {% for message in messages %}
                        <li>{{ message }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        {% endwith %}

        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>&copy; 2025 我的博客系统</p>
    </footer>
</body>
</html>

2. 首页 templates/index.html

<!-- templates/index.html -->
{% extends "base.html" %}

{% block content %}
<h2>文章列表</h2>

<!-- 搜索框 -->
<form method="get" class="search-form">
    <input type="text" name="q" placeholder="搜索文章..." value="{{ request.args.get('q', '') }}">
    <button type="submit">搜索</button>
</form>

<!-- 文章列表 -->
{% for post in posts %}
<article class="post-preview">
    <h3><a href="{{ url_for('bp.post', id=post.id) }}">{{ post.title }}</a></h3>
    <p class="meta">
        发布于 {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
        {% if post.category %} | 分类: {{ post.category.name }}{% endif %}
    </p>
    <p>{{ post.content[:200] }}...</p>
    {% if post.tags %}
        <div class="tags">
            {% for tag in post.tags %}
                <span class="tag">{{ tag.name }}</span>
            {% endfor %}
        </div>
    {% endif %}
</article>
{% else %}
<p>暂无文章。</p>
{% endfor %}
{% endblock %}

3. 文章详情 templates/post.html

<!-- templates/post.html -->
{% extends "base.html" %}

{% block content %}
<article class="post">
    <h1>{{ post.title }}</h1>
    <p class="meta">
        作者: {{ post.author.username }} |
        发布于 {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
        {% if post.category %} | 分类: {{ post.category.name }}{% endif %}
    </p>
    
    <div class="content">{{ post.content }}</div>
    
    {% if post.tags %}
        <div class="tags">
            {% for tag in post.tags %}
                <span class="tag">{{ tag.name }}</span>
            {% endfor %}
        </div>
    {% endif %}

    <!-- 编辑/删除 -->
    {% if current_user.is_authenticated and current_user == post.author %}
        <p>
            <a href="{{ url_for('bp.edit_post', id=post.id) }}">编辑</a> |
            <a href="#" onclick="if(confirm('确定删除?')) document.getElementById('delete-form').submit()">删除</a>
        </p>
        <form id="delete-form" action="{{ url_for('bp.delete_post', id=post.id) }}" method="post" style="display:none;"></form>
    {% endif %}

    <!-- 评论 -->
    <h3>评论 ({{ post.comments|length }})</h3>
    {% if current_user.is_authenticated %}
        <form method="post" action="{{ url_for('bp.add_comment', post_id=post.id) }}">
            <textarea name="content" placeholder="写下你的评论..." required></textarea>
            <button type="submit">发表评论</button>
        </form>
    {% else %}
        <p><a href="{{ url_for('bp.login') }}">登录</a>后可发表评论。</p>
    {% endif %}

    {% for comment in post.comments %}
        <div class="comment">
            <strong>{{ comment.author.username }}</strong>
            <span class="date">{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
            <p>{{ comment.content }}</p>
        </div>
    {% endfor %}
</article>
{% endblock %}

4. 发布/编辑文章 templates/create_post.html

<!-- templates/create_post.html -->
{% extends "base.html" %}

{% block content %}
<h2>{% if post %}编辑文章{% else %}发布新文章{% endif %}</h2>

<form method="post">
    <label>标题 *</label>
    <input type="text" name="title" value="{{ post.title if post }}" required>

    <label>内容 *</label>
    <textarea name="content" rows="10" required>{{ post.content if post }}</textarea>

    <label>分类</label>
    <select name="category_id">
        <option value="">无分类</option>
        {% for cat in categories %}
            <option value="{{ cat.id }}" {% if post and post.category_id == cat.id %}selected{% endif %}>{{ cat.name }}</option>
        {% endfor %}
    </select>

    <label>标签(多个用逗号分隔)</label>
    <input type="text" name="tags" value="{{ tag_str if tag_str else '' }}" placeholder="如:Python,Flask">

    <button type="submit">{% if post %}更新文章{% else %}发布文章{% endif %}</button>
</form>

<a href="{{ url_for('bp.index') }}">返回首页</a>
{% endblock %}

6. 注册页 templates/register.html

<!-- templates/register.html -->
{% extends "base.html" %}

{% block content %}
<h2>注册</h2>
<form method="post">
    <label>用户名 *</label>
    <input type="text" name="username" required>

    <label>密码 *</label>
    <input type="password" name="password" required>

    <button type="submit">注册</button>
</form>
<p>已有账号?<a href="{{ url_for('bp.login') }}">去登录</a></p>
{% endblock %}

✅ 第七步:CSS 样式 static/style.css

/* static/style.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    line-height: 1.6;
    color: #333;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-bottom: 20px;
    border-bottom: 1px solid #eee;
    margin-bottom: 30px;
}

header h1 a {
    text-decoration: none;
    color: #0056b3;
}

nav a {
    margin-left: 15px;
    color: #0056b3;
    text-decoration: none;
}

nav a:hover {
    text-decoration: underline;
}

.flashes {
    background: #d4edda;
    color: #155724;
    padding: 10px;
    border-radius: 4px;
    margin-bottom: 20px;
}

.post-preview {
    margin-bottom: 30px;
    padding-bottom: 20px;
    border-bottom: 1px solid #eee;
}

.post-preview h3 {
    margin-bottom: 5px;
}

.post-preview h3 a {
    color: #0056b3;
    text-decoration: none;
}

.meta {
    color: #666;
    font-size: 0.9em;
    margin-bottom: 10px;
}

.tags {
    margin-top: 10px;
}

.tag {
    display: inline-block;
    background: #0056b3;
    color: white;
    padding: 2px 8px;
    border-radius: 12px;
    font-size: 0.8em;
    margin-right: 5px;
}

form {
    margin: 20px 0;
}

label {
    display: block;
    margin: 10px 0 5px;
    font-weight: bold;
}

input[type="text"], input[type="password"], textarea, select {
    width: 100%;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
}

button {
    background: #0056b3;
    color: white;
    padding: 10px 15px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 10px;
}

button:hover {
    background: #003d82;
}

.search-form {
    display: flex;
    margin-bottom: 30px;
}

.search-form input {
    flex: 1;
    margin-right: 10px;
}

.comment {
    border: 1px solid #eee;
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 4px;
}

.comment .date {
    color: #666;
    font-size: 0.8em;
    margin-left: 10px;
}

footer {
    text-align: center;
    margin-top: 50px;
    color: #666;
    font-size: 0.9em;
}

✅ 第八步:运行项目

  1. 确保你在 blog 目录下
  2. 运行:
python app.py

浏览器访问:http://127.0.0.1:5000

✅ 第九步:使用说明

  1. 访问 /register 注册一个账号
  2. 登录后即可发布文章
  3. 支持分类、标签、搜索、评论
  4. 只有作者可编辑/删除自己的文章

✅ 学习要点总结

概念 项目中体现
MVC models.py(M) + routes.py(C) + templates/(V)
数据库操作 SQLAlchemy ORM 实现增删改查
会话管理 Flask-Login 处理登录状态
表单处理 request.form 获取数据
模板渲染 Jinja2 动态生成 HTML

✅ 后续建议

  • 添加 Markdown 支持
  • 增加分页
  • 使用 Bootstrap 美化界面
  • 部署到云端(如 Render.com)


网站公告

今日签到

点亮在社区的每一天
去签到