简介:Python Flask 个人博客网站毕业设计源码包,是一个注重内容创作的轻博客系统,面向计算机相关专业学生的毕设、课设及 Flask 全栈学习,也可作为课程设计演示和 Web 入门进阶的参考项目。项目采用 Flask 框架与 Bootstrap4 模板,集成 flask-wtf、flask-sqlalchemy、markdown、mysql 等技术,覆盖文章发布与管理、分类标签、推荐文章、内置图床、网站设置、会员注册与邀请码等完整功能,支持 Markdown 写作与图片管理,后台结构清晰。压缩包共 494 个文件,2.66MB,包含 15 个 Python 后端源码、140 个 HTML 页面、197 个 JS 与 62 个 CSS 前端资源、SQL 数据库文件及 Markdown/TXT 说明文档,目录结构清晰,便于按模块阅读、调试和二次开发。管理员与用户账号已内置(admin/admin、dashen/123456),代码经过测试运行成功,答辩评价较高;目前已有 349 人学习下载,适合快速启动项目。对希望快速搭建个人博客或掌握 Flask 项目完整开发流程的读者,这份源码提供了可直接运行的项目原型和参考实现,也可根据需求扩展模块,降低从零起步的实现成本。
1. 这个 Python Flask 博客项目,拆开来看是什么
“基于 Flask 框架的个人博客网站 + 源代码 + 文档说明 + 数据库”这种打包标题,在课程设计和毕业设计里出现频率很高,但它真正的价值不在“博客”两个字,而在于它是 Web 开发里最标准的练手骨架:Python 做后端、Flask 做路由和视图、SQLite 或 MySQL 做持久化,前端用 Jinja2 模板直接渲染。它的核心问题永远是这四件套能不能对上一套:源代码放哪、数据库表怎么建、文档里说的启动方式是否有效、预置数据能不能直接跑起来。这篇文章顺着这四个文件讲清楚 Flask 博客从模型设计到部署验证的完整路径,适合刚学完 Flask 基础、准备动手写课程设计的人,也适合想快速搭内部知识库或作品集的在职开发。
2. Flask 博客项目骨架与数据模型:先把目录定成能长个子的形状
拿到代码包后别先运行,先看目录。这类项目最常见的组织方式是 app 工厂模式加蓝图拆模块。Flask 自身不限制目录结构,但一个博客要同时承载前台文章列表、后台管理、登录认证、评论回复,如果所有路由堆在一个 app.py 里,三个月后你自己都改不动。我一般会按“入口、配置、应用包、迁移脚本、静态文件”五块来摆放。
2.1 用 app 工厂模式组织项目,启动脚本只保留入口
把项目的可扩展性放在第一位,第一件事是让 run.py 只负责创建实例和启动服务:
blog/ ├── run.py # 启动入口 ├── config.py # 所有配置项集中管理 ├── requirements.txt # 依赖清单 ├── README.md # 文档说明 ├── project.db # SQLite 数据库文件 ├── app/ │ ├── __init__.py # create_app 工厂 │ ├── models.py # 数据模型 │ ├── views/ │ │ ├── __init__.py │ │ ├── auth.py # 登录登出蓝图 │ │ └── blog.py # 文章、分类、后台蓝图 │ ├── templates/ │ └── static/ └── migrations/ # Flask-Migrate 生成的迁移目录核心工厂函数如下,注意注册蓝图时才导入视图,原因是要等 app 初始化完再绑定路由,否则会出现循环导入的报错。
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = 'auth.login' def create_app(config_class=None): app = Flask(__name__) if config_class: app.config.from_object(config_class) else: app.config['SECRET_KEY'] = 'dev-change-me' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///project.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) login_manager.init_app(app) from .views.auth import auth_bp from .views.blog import blog_bp app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(blog_bp) return app这里有几个参数值得说明:SECRET_KEY直接决定了 session 的安全性,代码包里往往写着明文,上生产前必须换成环境变量注入;SQLALCHEMY_DATABASE_URI决定数据库落盘位置,相对路径写法容易受工作目录影响,通常建议用os.path.join(BASE_DIR, 'project.db')拼出绝对路径;login_view告诉 Flask-Login 未登录用户该跳转到哪个端点。调试时如果发现登录后跳转地址不对,优先检查这个配置项是否与蓝图的url_prefix对齐。
2.2 用 SQLAlchemy 定义文章、分类、标签和用户四张表
博客模型的核心是一对多和多对多关系。文章属于一个分类,分类有一对多文章;文章和标签之间是多对多,需要用关联表。用户表可以简化为只存管理员账号,但字段设计要为后续加评论留余地。
from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from . import db, login_manager post_tags = db.Table( 'post_tags', db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True), db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True) ) class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, nullable=False) password_hash = db.Column(db.String(256), nullable=False) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Category(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True, nullable=False) posts = db.relationship('Post', backref='category', lazy='dynamic') class Tag(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True, nullable=False) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(128), nullable=False) slug = db.Column(db.String(128), unique=True, index=True) content = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) is_published = db.Column(db.Boolean, default=True) views = db.Column(db.Integer, default=0) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) tags = db.relationship('Tag', secondary=post_tags, backref=db.backref('posts', lazy='dynamic'))category_id和user_id是外键,删除分类时文章会被约束卡住,所以管理后台删除分类前要先处理该分类下的文章归属。lazy='dynamic'表示category.posts返回查询对象而不是列表,后续可以继续调用.filter_by().order_by()。slug用于文章详情页的 URL,比用数字主键更利于 SEO 和分享,生成逻辑可以依赖uuid或拼音库,不建议把中文标题直接拼进 URL。
2.3 数据库初始化与预置数据:一个脚本解决建表和导入
主键自增、索引、外键这些在 SQLAlchemy 模型里声明完后,初始化数据库只需要两行命令。注意数据库文件不要提交进 git 仓库,但课程设计打包时通常会把带数据的project.db一起发,所以文档里要写明初始账号密码。
flask db init flask db migrate -m "create post tables" flask db upgrade如果项目没接 Flask-Migrate,也可以直接在flask shell里执行:
flask shellfrom app import db from app.models import User, Category, Post db.create_all() admin = User(username='admin') admin.set_password('admin123') db.session.add_all([ admin, Category(name='Python'), Category(name='Flask'), ]) db.session.commit()把这段逻辑放进独立的init_db.py比写在视图里干净。给分类预置两条数据是为了让文章发布页的下拉框不为空,set_password用的是 werkzeug 的generate_password_hash,底层是 pbkdf2,默认带盐,比把明文密码存在数据库里安全一个数量级。数据库字段设计直接决定了后面能不能加评论、加草稿箱、加标签云,所以第一步别偷懒,关联关系和索引位都要预先留好。
3. 把博客的增删改查跑通:蓝图、登录和分页查询
数据表建好后进入业务代码阶段,这部分要做到“能跑、能写、能算”。博客前台至少有文章列表、文章详情、分类归档三个页面;后台至少有发布文章、编辑文章、删除文章三个动作。用蓝图把前台和后台分开,URL 语义才清晰,权限控制也有地方挂。
3.1 用蓝图区分前台和后台,URL 前缀避免路由混乱
前台和后台是两个权限域,不能用同一个视图函数里if 判断当前用户是否管理员来区分,会越写越乱。正确做法是拆成两个蓝图,后台挂在/admin前缀下。
# app/views/__init__.py from flask import Blueprint blog_bp = Blueprint('blog', __name__) auth_bp = Blueprint('auth', __name__)# app/views/auth.py from flask import render_template, redirect, url_for, flash, request from flask_login import login_user, logout_user, login_required from app import db from app.models import User from app.views import auth_bp @auth_bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = User.query.filter_by(username=username).first() if user and user.check_password(password): login_user(user, remember=request.form.get('remember') == 'on') return redirect(request.args.get('next') or url_for('blog.index')) flash('用户名或密码错误') return render_template('login.html')request.args.get('next')是登录后回跳的参数,Flask-Login 在拦截未登录请求时会自动带上?next=原地址,这里如果直接信任next会存在开放重定向风险,生产环境要校验它是以/开头的站内路径。remember参数控制长效 cookie,过期时间可以在 Config 里用REMEMBER_COOKIE_DURATION调整,默认 365 天偏长,个人博客一般改到 7 天。
3.2 文章列表的分页查询与模板渲染参数
博客首页是整站的流量入口,查询要控制每页数量,不能一页加载全部文章。Flask-SQLAlchemy 的paginate方法接收 page 和 per_page 两个参数,error_out=False保证越界页码返回空列表而不是直接抛 404。
from flask import render_template, request from sqlalchemy import or_ from app.models import Post from app.views import blog_bp @blog_bp.route('/') def index(): page = request.args.get('page', 1, type=int) keyword = request.args.get('kw', '').strip() query = Post.query.filter_by(is_published=True) if keyword: like = f'%{keyword}%' query = query.filter( or_(Post.title.like(like), Post.content.like(like)) ) pagination = query.order_by(Post.created_at.desc()).paginate( page=page, per_page=10, error_out=False ) return render_template('index.html', posts=pagination.items, pagination=pagination)这里要注意两点:request.args.get('page', 1, type=int)第三参数直接做类型转换,用户手滑传page=abc时会安全回落成 1,比手动int()包一层 try 更稳妥;or_条件做标题和内容的模糊搜索,个人博客数据量在几千篇以内这是最可靠的中文检索方案。Flask-SQLAlchemy 3.x 中paginate返回的pagination对象自带iter_pages()方法,模板里能直接渲染数字分页条。如果像很多课程设计那样在模板里写死上一页/下一页,注意当pagination.has_prev为 False 时要禁用链接,否则用户会在第一页点着没反应的按钮。
3.3 发布和编辑文章:表单校验与权限控制
后台管理界面要用模板把新建和编辑两个场景合并,判断依据是post对象是否存在。表单页至少包含标题、分类、标签、正文、是否发布五个字段。
# app/views/blog.py from flask import render_template, redirect, url_for, abort from flask_login import current_user from functools import wraps from sqlalchemy.exc import IntegrityError from app import db from app.models import Post, Category, Tag from app.views import blog_bp def admin_required(f): @wraps(f) from flask_login import login_required @login_required def wrapper(*args, **kwargs): if not current_user.is_authenticated: abort(401) return f(*args, **kwargs) return wrapper @blog_bp.route('/admin/post/new', methods=['GET', 'POST']) @admin_required def new_post(): if request.method == 'POST': title = request.form.get('title') content = request.form.get('content') slug = request.form.get('slug') category_id = request.form.get('category_id') is_published = request.form.get('is_published') == 'on' post = Post( title=title, content=content, slug=slug or None, category_id=category_id, is_published=is_published, user_id=current_user.id, ) db.session.add(post) try: db.session.commit() except IntegrityError: db.session.rollback() return 'slug 已存在,请更换后重试', 400 return redirect(url_for('blog.detail', slug=post.slug)) return render_template('admin_post.html', categories=Category.query.all())db.session.commit()可能抛IntegrityError,典型场景是 slug 撞车或外键不存在,必须捕获后rollback(),否则后续所有数据库操作都会在脏 session 上继续执行。权限装饰器里current_user.is_authenticated不能省略,因为login_required只管有没有登录,不管登录的是不是管理员。多用户场景可以给 User 加is_admin字段,但单管理员博客判断 authenticated 就够用了。
中间还缺了初始化时的login_manager.user_loader,这是 Flask-Login 必配项,写在app/__init__.py内:
@login_manager.user_loader def load_user(user_id): from .models import User return User.query.get(int(user_id))没有这段代码,每次请求 session 里的用户 id 都还原不成 User 对象,current_user会一直是匿名用户,表现为“登录后立刻退回登录页”。
4. 让数据库真正活下来:迁移、SQLite 并发与备份同步
数据库文件是这套博客项目的生命线。课程设计的评分点、你部署后的文章数据,全在这一个project.db里。这一章解决三个实际问题:表结构后期怎么改、SQLite 并发读写怎么保护、数据库怎么备份和迁移到别的机器。
4.1 用 Flask-Migrate 管理 schema 变更,别手动 ALTER TABLE
新手最容易犯的错是发现要加一个字段,直接用 GUI 工具打开数据库文件执行ALTER TABLE。这样做在开发环境试没问题,但迁移脚本一旦手动改过就没法在服务器上复现同样结构了。
安装并接入 Flask-Migrate:
pip install flask-migrate# app/__init__.py 中追加 from flask_migrate import Migrate migrate = Migrate(app, db)以后每次模型变更执行三步:
flask db migrate -m "add post.views column" flask db upgradeflask db migrate会对比 models.py 里的模型和当前数据库的差异,自动生成迁移文件;flask db upgrade把迁移应用到数据库。迁移文件生成后先打开看一眼再执行,确认没有误删列。数据库同步到服务器时,直接拷贝project.db文件不够规范,因为 SQLite 会有 WAL 文件未合并的情况,正确做法见 4.2。
4.2 SQLite 生产化配置:WAL 模式与 busy_timeout
SQLite 并不是“玩具数据库”,个人博客的读写量它能扛住,但默认的 rollback journal 模式在并发读取时性能较差。我的常用做法是在建连阶段执行三条 PRAGMA:
from sqlalchemy import event from sqlalchemy.engine import Engine @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA journal_mode=WAL") cursor.execute("PRAGMA foreign_keys=ON") cursor.execute("PRAGMA busy_timeout=10000") cursor.close()journal_mode=WAL允许读操作和写操作并发进行,是 SQLite 在高并发读场景下的核心优化;foreign_keys=ON让外键约束真正生效,SQLAlchemy 模型上的db.ForeignKey写不写这两行在 SQLite 里默认是不检查的,很多人删分类时没报错正是因为没开这个开关;busy_timeout=10000让数据库在锁冲突时等 10 秒而不是立刻抛database is locked。
表结构稳定后,用.backup命令做在线备份,这是 SQLite 官方推荐的备份方式,比直接复制文件安全,能拿到一致快照:
sqlite3 project.db ".backup '/backup/blog_$(date +%F).db'"配一条 crontab 每天凌晨执行即可做到“数据库同步”。注意备份文件要存放在项目目录之外,否则迁移项目时会把备份也拷走,失去异地容灾的意义。
4.3 打标签、搜索和浏览计数:三处低成本的实用改造
标签和文章的多对多关系建好后,文章详情页只需要在模板里遍历post.tags就能显示标签。为每篇文章加浏览量字段后,更新计数时不要整个对象提交,那样会把无关的 updated_at 也改掉:
Post.query.filter_by(id=post_id).update({Post.views: Post.views + 1}) db.session.commit()全文搜索不建议贸然用 SQLite FTS5,原因是它对中文默认没有合适的分词器,MATCH中文关键词经常搜不出结果。几千篇以内的博客用LIKE '%关键词%'就够,量大以后再换 Elasticsearch。这里也顺带解释一个模棱两可的说法:数据库同步工具并不是解决 schema 变更的工具,它解决的是主从复制和多机一致性问题,单机 SQLite 博客用不上主从,做好备份就够了。
5. 部署验证:用 gunicorn 启动服务并检查发布是否真的活了
5.1 用 gunicorn(Linux)或 waitress(Windows)接管生产服务
python run.py启动的是 Flask 自带的开发服务器,它带 reloader 和 debugger,但性能和稳定性都不适合对外提供服务,并发请求稍多就会出现阻塞。Linux 服务器上最常见的做法是 gunicorn,Windows 服务器则用 waitress,两者用法等价且都支持多 worker:
pip install gunicorn gunicorn -w 2 -b 0.0.0.0:8000 --access-logfile - --error-logfile - run:app-w 2表示两个 worker 进程,对 SQLite 博客来说这是比较稳妥的值:太少扛不住并发,太多会导致 SQLite 写入锁竞争明显。run:app指定导入路径,gunicorn 要求必须是“模块:变量”的形式,所以工厂函数要提前在run.py里实例化成全局app。--access-logfile -把访问日志输出到标准输出,systemd 或 docker 部署时方便统一收集。
不要直接在 gunicorn 命令行参数里写--reload,生产环境代码变更要用git pull加重启进程的方式发布,开着自动重载容易被语法错误打挂服务。Nginx 反代127.0.0.1:8000做静态文件和 HTTPS 终结,这才是完整的生产拓扑。
5.2 用 curl 验证“发布是否真的活了”
部署最后一步是验证,不是看着 gunicorn 启动成功就完事。我从三个层面检查:
curl -s -o /dev/null -w "HTTP %{http_code} - %{time_total}s\n" https://你的域名/状态码 200 和返回时间能确认前端可访问。第二步验证登录和管理功能,注意真实项目中开启 CSRF 后直接 POST 表单会 400,要先 GET 登录页提取 token:
CSRF=$(curl -s -c /tmp/cookie.txt http://127.0.0.1:8000/auth/login | grep -oP 'name="csrf_token" value="\K[^"]+') curl -s -b /tmp/cookie.txt -c /tmp/cookie.txt \ -d "csrf_token=$CSRF&username=admin&password=你的密码" \ -e http://127.0.0.1:8000/auth/login \ -o /dev/null -w "login: %{http_code} -> %{redirect_url}\n" \ http://127.0.0.1:8000/auth/login-e指定 Referer 头,有些 CSRF 校验会检查来源;%{redirect_url}能看到登录成功后 302 跳到了哪个地址,如果跳回登录页就说明user_loader或 session 配置有问题。最后验证静态资源路径,打开浏览器开发者工具看 Network 里的 404 和favicon.ico,再查看 gunicorn error log 是否出现Traceback。上线后每周用curl -sI巡检一次,对博客这类低频更改的站点来说,比任何监控告警都实在。
本文还有配套的精品资源,点击获取