简介:面向 Python Web 毕业设计场景,基于 Flask 的博客系统完整项目,适合毕业设计或课程设计学生参考与复用。项目已获导师指导并以高分通过,围绕用户管理、标签管理、分类管理、文章管理、公告资讯管理、举报信息管理、消息通知管理与系统管理等核心模块展开,前后端分工明确。压缩包共五百七十一个文件、二十一点四八 MB,含四十三个 Python 后端源码、一百一十一个 Vue 组件、JS/CSS 前端资源、SQL 数据库脚本、开发文档和启动脚本;图片与 SVG 图标用于页面展示,目录结构分后端、前端、脚本和文档,便于定位。目前已有 135 人下载学习。除完整源码和数据库外,还附带运行教程与逻辑讲解,有助于理解 Flask 与 MySQL 的接口实现、Vue 页面与后端联动、权限控制和消息通知机制;开发文档覆盖需求分析、数据库设计、接口设计、部署配置,便于二次开发和毕业答辩。下载后可直接运行,作为课设提交、毕设展演或 Flask 框架学习的实用素材。
1. 基于Flask的博客系统这个毕业设计题目到底在考察什么
看到「Python毕业设计-基于Flask的博客系统(源码+数据库+开发文档).zip」这类资源包,很多人第一反应是「这题太老了」。但换个角度,能把源码、数据库、开发文档打包成一套三件套的题目,恰恰是Flask方向最典型的毕业设计形态:它覆盖了Python Web开发的完整闭环。从安装Python环境、把Flask框架跑起来,到建表、写路由、渲染模板,再到把项目整理成开发文档交付,每一步都有明确的产出物。这套题适合三类人:急着在短时间内完成作品并准备论文的同学、刚学完Python基础想用Flask框架练手的开发者,以及需要一套能现场演示的python源码项目去应对面试的初级程序员。选这个题,等于选了一条完成度可以被看见的路线。
2. Flask博客系统的技术选型与工程骨架搭建
2.1 Flask框架优于Django和FastAPI的毕业设计理由
先解决选型。博客系统的核心业务就是用户登录、文章分类、评论这些数据库增删改查操作,数据量和并发都不高,技术栈的每一项都可以展开讲清楚。Django自带Admin后台和完整ORM,开发速度确实快,但答辩时容易陷入「框架替我做了大部分事」的被动,评审问到底层机制时很难接住。FastAPI的异步特性和类型校验在这个项目里没有非用不可的场景,反而要把async/await、协程模型讲透才不露怯。Flask框架的定位是微框架:路由由你注册,ORM由Flask-SQLAlchemy接入,登录状态用session自己管理,每一层都暴露在代码里。评审老师问「请求在Flask框架里是怎么走的」,你可以从WSGI服务器讲到视图函数返回值,这是选型时最能体现理解深度的地方。
第二个理由和开发文档强相关。毕业设计的开发文档通常要求包含需求分析、系统设计、数据库设计、接口说明和测试结果,Flask项目的模块边界清晰,文档章节可以直接对应到蓝图和模型文件。Django的「约定优于配置」写出来的文档容易变成框架说明书。所以这类资源包里出现Flask,不是因为Flask比Django简单,而是因为它能被完整地讲清楚。横向对比可以浓缩成一张表:
| 维度 | Django | Flask | FastAPI |
|---|---|---|---|
| 学习曲线 | 陡,概念多 | 平缓,按需引入 | 中等,需懂异步 |
| 答辩可讲深度 | 框架封装多 | 各层可见 | 需讲清楚协程 |
| 文档章节对应 | 一般 | 高 | 中等 |
2.2 用app工厂模式搭建Flask博客的最小目录结构
既然选了Flask框架,工程结构就要按可扩展的方式组织。常见做法是把项目拆成入口、配置、应用包和测试目录,我一般会这样建:
blog_system/ ├── run.py # 应用入口,python run.py 启动 ├── config.py # 开发/测试/生产三套配置 ├── requirements.txt # 依赖清单 ├── app/ │ ├── __init__.py # create_app 工厂函数 │ ├── models.py # SQLAlchemy 模型 │ ├── auth/ # 认证蓝图 │ │ ├── __init__.py │ │ └── views.py │ ├── blog/ # 文章蓝图 │ │ ├── __init__.py │ │ └── views.py │ └── templates/ │ ├── base.html │ └── index.html └── tests/ └── test_blog.pyapp/init.py里的工厂函数是项目地基,写法如下:
from flask import Flask from flask_sqlalchemy import SQLAlchemy from config import Config db = SQLAlchemy() def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) db.init_app(app) from app.auth import bp as auth_bp from app.blog import bp as blog_bp app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(blog_bp) with app.app_context(): db.create_all() return app三个参数值得展开。config_class是可替换的配置类,测试时传入testing配置,SQLALCHEMY_DATABASE_URI就能指向内存SQLite,业务代码不用改一行。url_prefix='/auth'给认证蓝图的所有路由自动加前缀,login视图的完整URL变成/auth/login,文章模块的路由因此可以保持一级路径。db.create_all()必须在app_context里调用,因为SQLAlchemy的元数据需要应用上下文才能绑定数据库;但它只能建新表、不能改旧表,正式开发要用迁移工具。requirements.txt里锁定flask、flask-sqlalchemy、flask-migrate三个核心依赖的版本号,避免换机器后跑不起来。
2.3 蓝图Blueprint拆分认证与文章模块的注册细节
蓝图解决的是Flask框架最常见的痛点:单文件路由越写越长。认证相关路由放进app/auth/,文章相关放进app/blog/,每个蓝图在自己的__init__.py里创建并导入:
from flask import Blueprint bp = Blueprint('auth', __name__) from app.auth import views # 必须放在bp创建之后导入顺序有坑:views.py里的@bp.route装饰器引用bp对象,如果from import放在文件顶部,导入views时bp还是None,启动直接AttributeError。把导入语句放到蓝图创建之后,是官方也认可的标准写法。开发文档画系统架构时,直接按「请求进入Flask框架后分发到哪个蓝图」来画,每条分发线对应一个url_prefix,评审看到这里基本不会再往深问。
提示:config.py里至少要声明SECRET_KEY和SQLALCHEMY_DATABASE_URI两个配置项,前者不设置,session功能会直接运行报错。
3. 博客系统的数据库设计:从表结构到SQLAlchemy模型落地
3.1 博客系统四张核心表:用户、文章、分类、评论
博客系统的数据关系不复杂,但数据库设计的评审标准是「范式正确、关系完整」。最少需要四张表:
| 表名 | 关键字段 | 与其他表的关系 |
|---|---|---|
| user | id, username, password_hash | 一对多:一个用户写多篇文章 |
| category | id, name | 一对多:一个分类下多篇文章 |
| post | id, title, body, created_at, user_id, category_id | 多对一:文章归属用户与分类 |
| comment | id, body, created_at, post_id, user_id | 多对一:评论归属文章与用户 |
password_hash字段存的是哈希值而不是明文密码,这是答辩必问点。werkzeug的generate_password_hash默认使用pbkdf2算法加盐,即使数据库泄露也不能直接反推出密码。外键user_id和category_id用db.ForeignKey声明,配合relationship让ORM直接通过post.author拿到用户对象,省去手写联表查询。四张表刚好覆盖一对多和多对一两类关系,足够展示对数据库课程设计里「关系建模」的理解。
3.2 用Flask-SQLAlchemy定义模型:外键、relationship与级联
models.py里四个模型的完整写法如下:
from datetime import datetime from app import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) posts = db.relationship('Post', backref='author', lazy='dynamic') 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='dynamic') class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(120), nullable=False) body = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan') class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) body = 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)几个参数展开说。lazy='dynamic'让posts成为查询对象而不是列表,调post.author.posts.count()时不会把全部数据加载进内存;如果只是遍历某分类下全部文章,改成lazy='select'反而更省事,因为查询更直接。cascade='all, delete-orphan'表示删除文章时自动删除其下所有评论,避免孤儿数据,这个配置在开发文档的数据完整性章节必须写。db.Column里的nullable=False配合表单校验,可以在数据库层挡住空标题。datetime.utcnow是函数引用而不是调用结果,这样每次插入记录时才取当前时间,写成datetime.utcnow()就会变成启动时刻的固定值。
3.3 用Flask-Migrate迁移表结构和准备种子数据
create_all()不能修改已存在的表,开发中每改一次字段就要迁移一次,标准做法是Flask-Migrate:
pip install flask-migrate flask --app run.py db init flask --app run.py db migrate -m "create user category post comment tables" flask --app run.py db upgrademigrate命令会扫描models.py与当前数据库的差异,生成migrations/versions/下的迁移脚本,upgrade才真正执行。开发文档的数据库设计章节里直接贴迁移脚本,能证明表结构是可持续演进的,而不是删库重建。种子数据单独写成一个命令,方便答辩时一键演示:
import click from flask.cli import with_appcontext from werkzeug.security import generate_password_hash @click.command('seed') @with_appcontext def seed_command(): from app import db from app.models import User, Category if not User.query.filter_by(username='admin').first(): db.session.add(User(username='admin', password_hash=generate_password_hash('admin123'))) db.session.add(Category(name='Python')) db.session.add(Category(name='Flask')) db.session.commit()click的with_appcontext让这个命令能访问数据库,注册进app.cli.add_command(seed_command)后,一条flask seed就能把演示数据准备好。种子逻辑判断「没有admin才创建」,重复执行不会产生重复数据。
4. 用Flask实现博客核心功能:登录、文章增删改查与Markdown渲染
4.1 用werkzeug做密码哈希与session登录控制
先写认证。注册和登录共用同一套密码处理逻辑,登录路由如下:
from flask import Blueprint, render_template, request, session from flask import redirect, url_for, flash from werkzeug.security import check_password_hash from app import db from app.models import User bp = Blueprint('auth', __name__) @bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '') user = User.query.filter_by(username=username).first() if user and check_password_hash(user.password_hash, password): session['user_id'] = user.id session['username'] = user.username return redirect(request.args.get('next') or url_for('blog.index')) flash('用户名或密码错误') return render_template('login.html')check_password_hash用盐值重新计算哈希再比对,即使两条记录密码相同,密文也不一样。session['user_id']写入后,后续请求都能读到当前用户;登出就是session.pop('user_id', None)。注意登录校验必须同时满足「用户存在」和「密码匹配」,不要把两种情况分开提示,否则被用来探测用户名是否存在。受保护路由用一个装饰器统一拦截:
from functools import wraps def login_required(view): @wraps(view) def wrapped(*args, **kwargs): if 'user_id' not in session: flash('请先登录') return redirect(url_for('auth.login', next=request.path)) return view(*args, **kwargs) return wrapped装饰器保留原视图的函数名,url_for生成URL时才能正确匹配,少了@wraps会报Endpoint错误。
4.2 文章列表分页与增删改查路由的参数设计
文章列表页是博客门面,分页是必考参数:
@bp.route('/') def index(): page = request.args.get('page', 1, type=int) pagination = Post.query.order_by(Post.created_at.desc()).paginate( page=page, per_page=5, error_out=False) return render_template('index.html', posts=pagination.items, pagination=pagination)paginate的参数作用如下:
| 参数 | 类型 | 作用 | 建议值 |
|---|---|---|---|
| page | int | 当前页码 | 1 |
| per_page | int | 每页条数 | 5或10 |
| error_out | bool | 越界时是否抛404 | False |
page从哪里来?request.args.get('page', 1, type=int)从URL的?page=2读取,type=int让非数字输入直接回落成默认值1,不用手动try except。error_out=False让越界页码返回空列表而不是404,用户体验更好。模板里遍历pagination.items渲染文章卡片,底部根据pagination.has_prev和pagination.has_next生成上一页下一页链接。
文章的创建、编辑、删除路由结构类似,以创建为例:
@bp.route('/post/new', methods=['GET', 'POST']) @login_required def new_post(): if request.method == 'POST': title = request.form.get('title', '').strip() body = request.form.get('body', '').strip() category_id = request.form.get('category_id', type=int) if title and body and category_id: post = Post(title=title, body=body, category_id=category_id, user_id=session['user_id']) db.session.add(post) db.session.commit() return redirect(url_for('blog.post_detail', post_id=post.id)) flash('标题、正文和分类不能为空') categories = Category.query.all() return render_template('new_post.html', categories=categories)POST成功后必须redirect而不是直接render_template,这是PRG模式,防止刷新页面重复提交表单。编辑和删除在URL里带post_id参数,删除用POST方法而不是GET,避免被预加载或爬虫触发。
4.3 Markdown渲染与代码高亮:fenced_code和codehilite扩展
博客正文用Markdown编辑是这类项目的标配,渲染用python-markdown库:
import markdown from markupsafe import Markup @blog_bp.app_template_filter('markdown') def render_markdown(text): return Markup(markdown.markdown(text, extensions=['fenced_code', 'codehilite']))注册成模板过滤器后,模板里写{{ post.body | markdown }}即可。两个扩展参数很关键:fenced_code让```围栏代码块生效,否则多行代码会粘连成一坨;codehilite负责代码高亮,配合Pygments生成的CSS使用,具体是先运行pygmentize -S default -f html生成样式文件放进static目录。
注意:markdown.markdown默认不清理HTML标签,用户文章里写