本文分享一个基于 Flask 框架开发的个人博客系统后端项目,涵盖用户注册登录、文章发布、分类管理、评论功能等核心模块。适合初学者学习和中小型博客系统开发。
一、项目结构
blog
│ app.py
│ forms.py
│ models.py
│
├───instance
│ blog.db
│
├───static
│ qu.jpg
│ styles.css
│ wc.jpg
│
├───templates
│ base.html
│ categories.html
│ category.html
│ edit_post.html
│ first.html
│ index.html
│ login.html
│ post.html
│ register.html
│ welcome.html
app.py:Flask 应用主程序,负责路由定义、请求处理和业务逻辑。
forms.py:定义所有 Web 表单类,负责用户输入验证。
models.py:定义数据库模型,映射用户、文章、分类和评论表。
instance/blog.db:SQLite 数据库文件,存储所有数据。
static/:静态资源文件夹,存放图片、CSS 样式等。
templates/:HTML 模板文件夹,使用 Jinja2 模板引擎渲染页面。
二、主要功能
1. 用户管理
- 注册:用户通过注册表单输入用户名、邮箱和密码,密码经过哈希加密存储,保证安全。
- 登录/登出:支持用户登录验证,登录后可访问受保护页面,登出后清除会话。
- 用户认证:集成 Flask-Login,管理用户会话和权限。
2. 文章管理
- 发布文章:登录用户可以创建新文章,填写标题、内容并选择分类。
- 文章展示:首页展示所有文章列表,点击进入文章详情页查看完整内容和评论。
- 新建文章:文章新建。
3. 分类管理
- 分类列表:展示所有文章分类,方便用户浏览不同主题文章。
- 分类详情:查看某个分类下的所有文章。
4. 评论功能
- 添加评论:用户可以在文章详情页发表评论。
- 评论展示:文章详情页显示所有评论,增强互动性。
三、运行测试
环境准备:pip install flask flask_sqlalchem 等等
启动项目:运行 python app.py
访问 http://127.0.0.1:5000/
进入博客首页
数据库初始化:程序启动时自动创建数据库和默认分类,无需手动操作
四、code
1、python code
app.py
import logging
from flask import Flask, render_template, redirect, url_for, flash, request
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from models import db, User, Post, Category, Comment
from forms import RegistrationForm, LoginForm, PostForm, CategoryForm
from datetime import datetime# 初始化日志配置
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db.init_app(app)login_manager = LoginManager()
login_manager.init_app(app)@login_manager.user_loader
def load_user(user_id):logger.debug(f"加载用户,ID={user_id}")return db.session.get(User, int(user_id))@app.route('/')
def first():logger.info("访问首页 /")return render_template('first.html')@app.route('/welcome')
def welcome():logger.info("访问欢迎页 /welcome")return render_template('welcome.html')@app.route('/index')
def index():logger.info("访问文章列表页 /index")posts = Post.query.all()logger.info(f"查询到 {len(posts)} 篇文章")return render_template('index.html', posts=posts)@app.route('/register', methods=['GET', 'POST'])
def register():form = RegistrationForm()if form.validate_on_submit():# 先检查邮箱是否已存在existing_user = User.query.filter_by(email=form.email.data).first()if existing_user:flash('该邮箱已被注册,请使用其他邮箱。', 'warning')logger.warning(f"注册失败,邮箱已存在: {form.email.data}")return render_template('register.html', form=form)# 也可以检查用户名是否已存在,防止重复existing_username = User.query.filter_by(username=form.username.data).first()if existing_username:flash('该用户名已被使用,请选择其他用户名。', 'warning')logger.warning(f"注册失败,用户名已存在: {form.username.data}")return render_template('register.html', form=form)hashed_password = generate_password_hash(form.password.data, method='pbkdf2:sha256')new_user = User(username=form.username.data, email=form.email.data, password=hashed_password)db.session.add(new_user)try:db.session.commit()flash('注册成功!', 'success')logger.info(f"用户注册成功,用户名: {new_user.username}, 邮箱: {new_user.email}")return redirect(url_for('login'))except IntegrityError as e:db.session.rollback()flash('注册失败,用户名或邮箱已存在。', 'danger')logger.error(f"数据库错误,注册失败: {e}")return render_template('register.html', form=form)elif request.method == 'POST':logger.warning(f"注册表单验证失败,错误: {form.errors}")return render_template('register.html', form=form)@app.route('/login', methods=['GET', 'POST'])
def login():form = LoginForm()if form.validate_on_submit():logger.info(f"登录尝试,用户名: {form.username.data}")user = User.query.filter_by(username=form.username.data).first()if user and check_password_hash(user.password, form.password.data):login_user(user)logger.info(f"用户登录成功,用户名: {user.username}")return redirect(url_for('welcome'))else:logger.warning("登录失败,用户名或密码错误")flash('登录失败,请检查用户名和密码', 'danger')elif request.method == 'POST':logger.warning(f"登录表单验证失败,错误: {form.errors}")return render_template('login.html', form=form)@app.route('/logout')
@login_required
def logout():logger.info(f"用户登出,用户名: {current_user.username}")logout_user()return render_template('base.html')@app.route('/post/new', methods=['GET', 'POST'])
@login_required
def new_post():form = PostForm()form.category_id.choices = [(c.id, c.name) for c in Category.query.all()]if form.validate_on_submit():logger.info(f"用户 {current_user.username} 创建新文章,标题: {form.title.data}")new_post = Post(title=form.title.data, content=form.content.data, category_id=form.category_id.data)db.session.add(new_post)db.session.commit()logger.info(f"文章创建成功,ID: {new_post.id}")flash('文章已创建!', 'success')return redirect(url_for('index'))elif request.method == 'POST':logger.warning(f"文章表单验证失败,错误: {form.errors}")return render_template('edit_post.html', form=form)@app.route('/post/<int:post_id>')
def post(post_id):logger.info(f"访问文章详情页,文章ID: {post_id}")post = Post.query.get_or_404(post_id)comments = Comment.query.filter_by(post_id=post.id).all()logger.info(f"文章 {post_id} 有 {len(comments)} 条评论")return render_template('post.html', post=post, comments=comments)@app.route('/post/<int:post_id>/comment', methods=['POST'])
def add_comment(post_id):post = Post.query.get_or_404(post_id)author = request.form.get('author')content = request.form.get('content')logger.info(f"新增评论,文章ID: {post_id}, 作者: {author}")new_comment = Comment(post_id=post.id, author=author, content=content)db.session.add(new_comment)db.session.commit()logger.info(f"评论添加成功,评论ID: {new_comment.id}")return redirect(url_for('post', post_id=post.id))@app.route('/categories')
@login_required
def categories():logger.info(f"用户 {current_user.username} 访问分类列表")categories = Category.query.all()form = CategoryForm()return render_template('categories.html', categories=categories, form=form)@app.route('/category/<int:category_id>')
@login_required
def category(category_id):logger.info(f"用户 {current_user.username} 访问分类详情,分类ID: {category_id}")category = Category.query.get_or_404(category_id)form = CategoryForm()return render_template('category.html', category=category, form=form)if __name__ == '__main__':with app.app_context():db.create_all()if not Category.query.first():categories = ['开发', '测试', '人生感悟', '杂项']for category_name in categories:category = Category(name=category_name)db.session.add(category)db.session.commit()logger.info("初始化默认分类数据")logger.info("数据库表已创建或已存在。")app.run(debug=True)
forms.py
import logging
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SubmitField, SelectField
from wtforms.validators import DataRequired, Length# 初始化日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)class RegistrationForm(FlaskForm): # 注册表单username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=150)]) # 用户名字段email = StringField('邮箱', validators=[DataRequired()]) # 邮箱字段password = PasswordField('密码', validators=[DataRequired()]) # 密码字段submit = SubmitField('注册') # 提交按钮def validate(self, *args, **kwargs):rv = super().validate(*args, **kwargs)if not rv:logger.warning("RegistrationForm 验证失败")else:logger.info(f"RegistrationForm 验证成功,用户名: {self.username.data}")return rvclass LoginForm(FlaskForm): # 登录表单username = StringField('用户名', validators=[DataRequired()]) # 用户名字段password = PasswordField('密码', validators=[DataRequired()]) # 密码字段submit = SubmitField('登录') # 提交按钮def validate(self, *args, **kwargs):rv = super().validate(*args, **kwargs)if not rv:logger.warning("LoginForm 验证失败")else:logger.info(f"LoginForm 验证成功,用户名: {self.username.data}")return rvclass CategoryForm(FlaskForm): # 分类表单name = StringField('分类', validators=[DataRequired(), Length(max=50)]) # 分类名称字段def validate(self, *args, **kwargs):rv = super().validate(*args, **kwargs)if not rv:logger.warning("CategoryForm 验证失败")else:logger.info(f"CategoryForm 验证成功,分类名称: {self.name.data}")return rvclass PostForm(FlaskForm): # 文章表单title = StringField('标题', validators=[DataRequired()]) # 文章标题字段content = TextAreaField('内容', validators=[DataRequired()]) # 文章内容字段category_id = SelectField('分类', coerce=int, validators=[DataRequired()]) # 分类选择字段submit = SubmitField('提交') # 提交按钮def validate(self, *args, **kwargs):rv = super().validate(*args, **kwargs)if not rv:logger.warning("PostForm 验证失败")else:logger.info(f"PostForm 验证成功,标题: {self.title.data}")return rv
models.py
import logging
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from flask_login import UserMixin
#初始化日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
db = SQLAlchemy() # 创建 SQLAlchemy 实例
class User(db.Model, UserMixin): # 用户模型id = db.Column(db.Integer, primary_key=True) # 用户 IDusername = db.Column(db.String(150), unique=True, nullable=False) # 用户名password = db.Column(db.String(150), nullable=False) # 密码email = db.Column(db.String(150), unique=True, nullable=False) # 邮箱def __repr__(self):logger.info(f"User实例被创建: {self.username}")return f"<User {self.username}>"
class Category(db.Model): # 分类模型id = db.Column(db.Integer, primary_key=True) # 分类 IDname = db.Column(db.String(100), nullable=False) # 分类名称posts = db.relationship('Post', backref='category', lazy=True) # 一对多关系,分类与文章def __repr__(self):logger.info(f"Category实例被创建: {self.name}")return f"<Category {self.name}>"
class Post(db.Model): # 文章模型id = db.Column(db.Integer, primary_key=True) # 文章 IDtitle = db.Column(db.String(150), nullable=False) # 文章标题content = db.Column(db.Text, nullable=False) # 文章内容created_at = db.Column(db.DateTime, default=datetime.now) # 创建时间updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) # 更新时间category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) # 分类 IDdef __repr__(self):logger.info(f"Post实例被创建: {self.title}")return f"<Post {self.title}>"
class Comment(db.Model): # 评论模型id = db.Column(db.Integer, primary_key=True) # 评论 IDpost_id = db.Column(db.Integer, db.ForeignKey('post.id')) # 文章 IDauthor = db.Column(db.String(100), nullable=False) # 评论作者content = db.Column(db.Text, nullable=False) # 评论内容created_at = db.Column(db.DateTime, default=datetime.now) # 创建时间def __repr__(self):logger.info(f"Comment实例被创建,作者: {self.author}")return f"<Comment by {self.author}>"
2、static files
styles.css
/* 基本重置 */
* {margin: 0;padding: 0;box-sizing: border-box;
}
/* 容器样式 */
.container {max-width: 800px; /* 最大宽度 */margin: 0 auto; /* 居中 */padding: 20px; /* 内边距 */background: white; /* 背景颜色 */border-radius: 8px; /* 圆角 */box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 阴影效果 */
}
/* 全局样式 */
body {font-family: Arial, sans-serif;line-height: 1.6; /* 行高 */background-color: #f8f9fa;color: #343a40;margin: 0;padding: 20px;
}
/* 表单样式 */
form {display: flex; /* 使用 flexbox */flex-direction: column; /* 垂直排列 */
}
form input, form select, form textarea {margin-bottom: 15px; /* 下边距 */padding: 10px; /* 内边距 */border: 1px solid #ccc; /* 边框 */border-radius: 4px; /* 圆角 */
}
/* 导航栏样式 */
.navbar {margin-bottom: 30px;
}
/* 按钮样式 */
button, input[type="submit"] {background-color: #007bff; /* 按钮背景颜色 */color: white; /* 字体颜色 */border: none; /* 去掉边框 */padding: 10px; /* 内边距 */border-radius: 4px; /* 圆角 */cursor: pointer; /* 鼠标指针 */transition: background-color 0.3s; /* 背景颜色过渡效果 */
}
button:hover, input[type="submit"]:hover {background-color: #0056b3; /* 悬停时的背景颜色 */
}
/* 标题样式 */
h1, h2, h3 {text-align: center; /* 标题居中 */margin-bottom: 20px; /* 下边距 */
}
/* 消息提示样式 */
.alert {margin-bottom: 20px; /* 下边距 */padding: 15px; /* 内边距 */border-radius: 4px; /* 圆角 */
}
.alert-danger {background-color: #f8d7da; /* 错误消息背景 */color: #721c24; /* 错误消息字体颜色 */
}
.alert-success {background-color: #d4edda; /* 成功消息背景 */color: #155724; /* 成功消息字体颜色 */
}
/* 文章列表样式 */
.list-group-item {background-color: #ffffff;border: 1px solid #dee2e6;border-radius: 5px;margin-bottom: 10px;transition: background-color 0.3s;
}.list-group-item:hover {background-color: #f1f1f1;
}/* 文章标题样式 */
h1 {font-size: 2.0rem;margin-bottom: 20px;
}/* 表单样式 */
.form-label {font-weight: bold;
}.form-control {border-radius: 5px;
}.btn-primary {background-color: #007bff;border: none;border-radius: 5px;
}.btn-primary:hover {background-color: #0056b3;
}/* 评论区样式 */
.comment-section {margin-top: 20px;
}.comment-section h3 {margin-bottom: 15px;
}.comment-section .list-group-item {background-color: #e9ecef;
}/* 页脚样式 */
footer {margin-top: 30px;text-align: center;font-size: 0.9rem;color: #6c757d;
}
/* 链接样式 */
.nav-link {color: #007bff; /* 链接颜色 */font-size: 1.0rem;
}.nav-link:hover {color: #0056b3; /* 悬停时链接颜色 */font-size: 3ch;
}
3、html files
base.html
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><!-- link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.1.3/css/bootstrap.min.css" --><link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"><title>个人博客</title>
</head>
<body><nav class="navbar navbar-expand-lg navbar-light bg-light"><a class="navbar-brand" href="{{ url_for('first') }}">博客首页</a><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto">{% if current_user.is_authenticated %}<li class="nav-item"><a class="nav-link" href="{{ url_for('index') }}">我的博客</a></li><li class="nav-item"><a class="nav-link" href="{{ url_for('new_post') }}">新建文章</a></li><li class="nav-item"><a class="nav-link" href="{{ url_for('categories') }}">分类</a></li><li class="nav-item"><a class="nav-link" href="{{ url_for('logout') }}">登出</a></li>{% else %}<li class="nav-item"><a class="nav-link" href="{{ url_for('login') }}">登录</a></li><li class="nav-item"><a class="nav-link" href="{{ url_for('register') }}">注册</a></li>{% endif %}</ul></div></nav><div class="container mt-4">{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="alert alert-{{ category }} alert-dismissible fade show">{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>{% endfor %}{% endif %}{% endwith %}{% block content %}{% endblock %}</div>
</body>
</html>
categories.html
{% extends 'base.html' %}{% block content %}
<h1>分类列表</h1>
<ul class="list-group">{% for category in categories %}<li class="list-group-item"><a href="{{ url_for('category', category_id=category.id) }}">{{ category.name }}</a><span class="badge bg-secondary">{{ category.posts|length }} 篇文章</span> <!-- 显示文章数量 --></li>{% endfor %}<a href="{{ url_for('index') }}" class="btn btn-primary mt-4">返回首页</a>
</ul>
{% endblock %}
category.html
{% extends "base.html" %}{% block content %}
<h1 class="mt-5">{{ category.name }} 分类</h1>
<p>以下是属于 "{{ category.name }}" 分类的文章:</p><div class="list-group">{% for post in category.posts %}<a href="{{ url_for('post', post_id=post.id) }}" class="list-group-item list-group-item-action"><h5 class="mb-1">{{ post.title }}</h5><p class="mb-1">{{ post.summary }}</p><small>发布于 {{ post.created_at.strftime('%Y-%m-%d') }}</small><small>分类: <a href="{{ url_for('category', category_id=post.category_id) }}">{{ post.category.name }}</a></small></a>{% else %}<div class="alert alert-info" role="alert">该分类下没有文章。</div>{% endfor %}
</div>
<a href="{{ url_for('categories') }}" class="btn btn-secondary mt-5">返回分类列表</a>
{% endblock %}
edit_post.html
{% extends 'base.html' %}{% block content %}
<h1>新建文章</h1>
<form method="POST" action="{{ url_for('new_post') }}">{{ form.hidden_tag() }}<div class="mb-3">{{ form.title.label(class="form-label") }}{{ form.title(class="form-control") }}</div><div class="mb-3">{{ form.content.label(class="form-label") }}{{ form.content(class="form-control", rows=5) }}</div><div class="mb-3">{{ form.category_id.label(class="form-label") }}{{ form.category_id(class="form-select") }}</div><button type="submit" class="btn btn-primary">提交</button>
</form>
{% endblock %}
first.html
{% extends "base.html" %}{% block content %}
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>动态时间与图片展示</title><style>#current-time {font-size: 24px;font-weight: bold;margin: 20px 0;}img {max-width: 100%;height: auto;margin-top: 20px;}</style>
</head>
<body><h1>欢迎来到个人博客系统</h1><div id="current-time"></div> <!-- 显示当前时间 --><img src="{{ url_for('static', filename='qu.jpg') }}" alt="展示图片"> <!-- 替换为你的图片路径 --><script>function updateTime() {const now = new Date();const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false };document.getElementById('current-time').innerText = now.toLocaleString('zh-CN', options);}setInterval(updateTime, 1000); // 每秒更新一次时间updateTime(); // 页面加载时立即显示时间</script>
</body>
{% endblock %}
login.html
{% extends 'base.html' %}{% block content %}
<h1>登录</h1>
<form method="POST">{{ form.hidden_tag() }}<div class="mb-3">{{ form.username.label(class="form-label") }}{{ form.username(class="form-control") }}</div><div class="mb-3">{{ form.password.label(class="form-label") }}{{ form.password(class="form-control") }}</div><button type="submit" class="btn btn-primary">登录</button>
</form>
{% endblock %}
index.html
{% extends 'base.html' %}
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}<ul class="flashes">{% for category, message in messages %}<li class="{{ category }}">{{ message }}</li>{% endfor %}</ul>{% endif %}
{% endwith %}
{% block content %}
<h1>文章列表</h1>
<ul class="list-group">{% for post in posts %}<li class="list-group-item"><a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a><small class="text-muted">创建于 {{ post.created_at.strftime('%Y-%m-%d') }}</small></li>{% endfor %}
</ul>
{% endblock %}
post.html
{% extends 'base.html' %}{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<h3>评论</h3>
<form method="POST" action="{{ url_for('add_comment', post_id=post.id) }}"><input type="text" name="author" placeholder="你的名字" required><textarea name="content" placeholder="你的评论" required></textarea><button type="submit">提交评论</button>
</form>
<ul class="list-group mt-3">{% for comment in comments %}<li class="list-group-item"><strong>{{ comment.author }}</strong>: {{ comment.content }}</li>{% endfor %}
</ul>
{% endblock %}
register.html
{% extends 'base.html' %}{% block content %}
<h1>注册</h1>
<form method="POST">{{ form.hidden_tag() }}<div class="mb-3">{{ form.username.label(class="form-label") }}{{ form.username(class="form-control") }}</div><div class="mb-3">{{ form.email.label(class="form-label") }}{{ form.email(class="form-control") }}</div><div class="mb-3">{{ form.password.label(class="form-label") }}{{ form.password(class="form-control") }}</div><button type="submit" class="btn btn-primary">注册</button>
</form>
{% endblock %}
welcome.html
<!-- templates/welcome.html -->
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"><title>欢迎</title>
</head>
<body><div class="container mt-4"><h1>欢迎, {{ current_user.username }}!</h1><p>感谢您登录到个人博客系统。</p><img src="{{ url_for('static', filename='wc.jpg') }}" alt="欢迎图片" style="max-width: 100%; height: auto;"><p><a href="{{ url_for('index') }}">我的博客</a></p><p><a href="{{ url_for('first') }}">返回首页</a></p></div>
</body>
</html>
五、效果展示