目标:零基础也能从头搭建一个支持文章管理、评论、分类标签、搜索、用户登录的博客系统
技术栈: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 虚拟环境 |
三、核心功能
文章管理
- 支持文章的创建、编辑、删除与查看详情
- 文章包含标题、内容、发布时间、作者信息
分类与标签系统
- 每篇文章可归属一个分类(如“技术”、“生活”)
- 支持多标签管理(如“Python”、“Flask”),便于内容组织与检索
用户评论功能
- 登录用户可在文章页发表评论
- 评论按时间排序展示,增强互动性
全文搜索
- 支持通过关键词在文章标题和内容中进行模糊搜索
- 搜索结果实时展示,提升用户体验
用户系统与会话管理
- 用户注册与登录功能
- 基于 Flask-Login 的会话管理,确保安全访问控制
- 权限控制:仅文章作者可编辑或删除自己的文章
响应式前端界面
- 使用原生 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>© 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;
}
✅ 第八步:运行项目
- 确保你在
blog
目录下 - 运行:
python app.py
浏览器访问:http://127.0.0.1:5000
✅ 第九步:使用说明
- 访问
/register
注册一个账号 - 登录后即可发布文章
- 支持分类、标签、搜索、评论
- 只有作者可编辑/删除自己的文章
✅ 学习要点总结
概念 | 项目中体现 |
---|---|
MVC | models.py (M) + routes.py (C) + templates/ (V) |
数据库操作 | SQLAlchemy ORM 实现增删改查 |
会话管理 | Flask-Login 处理登录状态 |
表单处理 | request.form 获取数据 |
模板渲染 | Jinja2 动态生成 HTML |
✅ 后续建议
- 添加 Markdown 支持
- 增加分页
- 使用 Bootstrap 美化界面
- 部署到云端(如 Render.com)