简介:这是一份基于 Python、Django 与 MySQL 开发的博客系统完整源码,定位为高校毕业设计及课程设计项目,适合计算机、通信、人工智能、自动化等专业的学生、教师或从业者用于学习、二次开发与答辩演示。项目代码经过调试测试,运行可靠,曾获评98分的答辩成绩,具备较好的工程完整性与学习借鉴价值。资源包共包含631个文件,压缩后约19.8MB,其中82个Python文件覆盖核心业务逻辑,150个JavaScript与52个CSS文件支撑前端交互与样式,103个HTML模板构成页面结构,另含图片、数据库及字体图标等素材,目录清晰、便于按模块查阅。目前已有159人学习下载,适合零基础入门及希望提升Django实战能力的开发者参考。下载后可直接运行体验,并可根据需要修改模型、视图与模板,实现文章管理、用户登录、分类标签等博客常用功能,对毕业设计选题和期末大作业均有较强参考意义。
1. 毕业设计题目是 Python+Django+MySQL 博客系统,说明你选对了方向
博客系统在毕业设计里出现频率极高,不是因为它简单,而是它恰好覆盖了一个信息管理系统的完整闭环:前台展示、后台管理、数据库建模、用户交互、权限控制。很多同学答辩被问的第一句往往是「为什么选 Django 而不是 Flask」,能说出 Admin 后台、ORM 迁移、内置用户认证这三件事,就已经赢了一半。用 Python + Django + MySQL 做这套系统,是在功能完整度和工作量可控之间最稳的组合。这篇文章从环境装配开始,按模型设计、视图编写、模板渲染、后台接管的顺序走一遍,给出可直接落到自己项目里的源码。适合课程设计、毕业论文系统实现,也适合刚学完 Python 想拿完整 Django 项目练手的人。
2. 环境搭建与项目初始化:从 Python 安装到 Django 连接 MySQL
2.1 先锁定版本组合,能省掉一半报错
很多项目卡在第一步,问题不在代码,而在 Python、Django、MySQL 三方的版本兼容。Django 对 Python 版本有硬性要求,MySQL 8.x 默认的 caching_sha2_password 认证插件又会让老版本驱动连不上。做毕业设计不要追新,我一般建议 Django 4.2 LTS 配 Python 3.10 或 3.11,MySQL 选 8.0 系列。这个组合的踩坑贴最多,在搜索引擎里搜 python安装教程、mysql安装教程、django install mysqlclient 都能直接找到对应的解决方案,比用最新版本摸着石头过河效率高得多。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.10 / 3.11 | 与 Django 4.2 LTS 完全兼容,第三方库支持成熟 |
| Django | 4.2 LTS | 官方长期维护版本,安全更新周期长 |
| MySQL | 8.0.x | 默认 utf8mb4,避免字符集坑 |
| mysqlclient | 2.2.x | Django 官方推荐的 MySQL 驱动 |
Python 安装时勾选 Add Python to PATH,否则后续执行 pip 和 python 都要写全路径,排查环境问题时会很痛苦。装完在命令行分别敲 python --version 和 pip --version,两个都能输出版本号再继续下一步。MySQL 下载官方 Installer 一路装完,把 root 密码记在本地文件里,编码选 utf8mb4。Windows 上建议顺手装 MySQL Workbench,建库、导数据、看表结构都比命令行直观,后面备份恢复也用得上。
2.2 用 venv 隔离项目环境并安装依赖
为什么要用虚拟环境:机器上经常同时存在多个 Python 版本,Django 装在全局会让不同项目互相污染版本。venv 是 Python 内置的方案,不需要额外安装其他工具,初始化与安装依赖的命令如下:
python -m venv venv # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate pip install django==4.2.* mysqlclient==2.2.* Pillow第一条创建名为 venv 的隔离目录,第二、三条按操作系统激活。激活后命令行提示符前出现 (venv) 前缀,说明当前 shell 的 python 和 pip 都指向虚拟环境。第三条装三个包:django 是框架本体,mysqlclient 是连接 MySQL 的驱动,Pillow 是 Django 图片上传字段 ImageField 的依赖,文章封面图功能一定会用到。版本号里4.2.*是浮动符号,表示装到 4.2 系列的最新补丁版,不会跳到大版本 5.x 造成 API 差异。
提示:pip 下载包慢或超时时,可以在命令末尾加国内镜像源地址,只改变下载通道,不改变包本身的行为。
2.3 创建项目和 App:django创建app 的标准两步
创建项目两条命令就够了。第一步生成项目骨架,第二步创建业务 App:
django-admin startproject blog_project . python manage.py startapp blog第二条命令最后那个点表示在当前目录生成 manage.py,不额外套一层项目目录。生成后 blog_project 是配置目录,blog 是业务 App。Django 的约定是「项目管配置、App 管业务」,同一个项目可以挂多个 App,博客的模型、视图、模板都放在 blog 里。这个边界要从第一天建立,很多同学把所有逻辑塞进一个 views.py,到后面加搜索、加评论时文件越拉越长,答辩翻代码时自己也理不清。
然后把 blog 注册进 settings.py 的 INSTALLED_APPS:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', ]不注册的话,后面 makemigrations 扫不到 blog 下的 models.py,业务表根本不会生成,这是 django创建app 之后最常见的卡点。
2.4 修改 settings.py 连接 MySQL 数据库
Django 默认数据库是 SQLite,本地调试确实方便,但题目里写了 MySQL,就要从第一天切过去,否则最后交付时模型行为和 SQL 特性和答辩预期不一致。修改 settings.py 里的 DATABASES 配置块:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'blog_db', 'USER': 'blog_user', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } } LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = TrueENGINE 指定 Django 使用哪套数据库后端,这个字符串不能写错。NAME 是 MySQL 里的库名,USER 和 PASSWORD 是连接账号,HOST 和 PORT 本地开发固定 127.0.0.1:3306,写成显式配置方便以后部署到云服务器时只改这里。OPTIONS 里的 charset 建议固定 utf8mb4,否则正文里出现 emoji 时会报 Incorrect string value,还要回头改库的默认字符集。LANGUAGE_CODE 和 TIME_ZONE 改成 zh-hans 和 Asia/Shanghai 后,Admin 是中文界面,时间显示也不差 8 小时。USE_TZ 保持 True,Django 在数据库里统一存 UTC,展示时按 TIME_ZONE 转本地时间,这是最不容易出错的时区姿势。
2.5 mysqlclient 装不上时:PyMySQL 是保底方案
在 Windows 上 pip install mysqlclient 经常编译失败,报 Microsoft Visual C++ 14.0 is required,原因是这个驱动需要本地编译 C 扩展。如果不想装 Visual Studio Build Tools,直接切换到 PyMySQL:
pip install pymysql然后在项目同名目录 blog_project/init.py 里写两行:
import pymysql pymysql.install_as_MySQLdb()PyMySQL 用纯 Python 实现 MySQL 协议,不用编译,装完即用。install_as_MySQLdb 是把 PyMySQL 注册成 MySQLdb 的替身,这样 Django 的 django.db.backends.mysql 后端在 import MySQLdb 时会拿到 PyMySQL 的实现,settings.py 一行都不需要改。教学项目里两者性能没有可感知的差别,唯一要注意的是 pymysql 版本至少要 1.1.0,旧版本和 Django 4.2 的某些查询接口不兼容。
2.6 建库建账号,跑通首次迁移
连接 MySQL 之前,先建库和应用账号。这一步可以在命令行执行,也可以在 MySQL Workbench 的查询编辑器里跑,效果一样。复用 root 虽然省事,但生产习惯是给应用单独建一个最小权限账号:
CREATE DATABASE IF NOT EXISTS blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'blog_user'@'localhost' IDENTIFIED BY 'your_password'; GRANT ALL PRIVILEGES ON blog_db.* TO 'blog_user'@'localhost'; FLUSH PRIVILEGES;库、账号、密码要和 settings.py 里的配置逐字一致,特别是 host 限定:'blog_user'@'localhost' 只允许本机连接,服务器部署时还要加一个 'blog_user'@'%' 或用内网 IP。执行完返回项目根目录,先迁移再启动开发服务器:
python manage.py migrate python manage.py runserver 0.0.0.0:8000migrate 会把 Django 内置的 auth、session、admin 等十几张系统表建到 MySQL 里,这一步成功说明 ORM 到 MySQL 的链路已经通了。浏览器打开 http://127.0.0.1:8000 看到默认欢迎页后,再用 python manage.py createsuperuser 建一个管理员账号,登录 /admin/ 验证后台可用。如果 runserver 时报 Access denied for user,先核对账号密码和 host;报 Lost connection to MySQL server,先确认 MySQL 服务是否启动、端口是否被占用。
3. 数据库设计:博客系统的核心表结构与建模要点
3.1 先画关系再写模型:一对多、多对多、自关联怎么落
博客系统功能再多,核心表就四张:文章表、分类表、标签表、评论表,加上 Django 内置的用户表。设计阶段先把关系确认好,再动手写代码。一篇文章属于一个分类,一个分类下有多个文章,是一对多,外键放在文章表;一篇文章可以有多个标签,一个标签也能属于多篇文章,是多对多,Django 会自动生成中间关联表;一篇评论属于一篇文章,也属于一个用户,文章和用户对评论都是一对多。这套关系对应「数据库设计 - 博客系统」题目里的概念结构设计,ER 图是给文档用的,模型里的 ForeignKey 和 ManyToManyField 才是真正落地的结构,两者必须保持一致。
有一个容易被忽略的点:文章的作者直接用 Django 内置的 User 表,不需要自己建用户扩展表。auth_user 已经带了密码哈希、权限分组、会话管理等能力,自己实现一遍反而容易在密码存储上出安全问题。如果论文里需要体现用户模块的额外信息,比如昵称、头像,再建一张 Profile 表用 OneToOne 关联到 User,而不是去改 auth 表的结构。
3.2 完整 models.py:Post、Category、Tag 的可直接落地版本
把关系理清之后,直接看可落地的 models.py。Category 和 Tag 放在 Post 前面定义,因为 Post 的外键和多对多关系要引用它们:
from django.db import models from django.contrib.auth.models import User from django.utils import timezone class Category(models.Model): name = models.CharField('分类名', max_length=50, unique=True) slug = models.SlugField('URL标识', max_length=60, unique=True) class Meta: verbose_name = '分类' ordering = ('name',) def __str__(self): return self.name class Tag(models.Model): name = models.CharField('标签名', max_length=30, unique=True) class Meta: verbose_name = '标签' ordering = ('name',) def __str__(self): return self.name class Post(models.Model): STATUS_CHOICES = ( ('draft', '草稿'), ('published', '已发布'), ) title = models.CharField('标题', max_length=200) slug = models.SlugField('URL标识', max_length=200, unique=True) author = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE, related_name='posts') category = models.ForeignKey(Category, verbose_name='分类', on_delete=models.SET_NULL, null=True, blank=True, related_name='posts') tags = models.ManyToManyField(Tag, verbose_name='标签', blank=True, related_name='posts') body = models.TextField('正文') status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default='draft') views = models.PositiveIntegerField('阅读数', default=0) created_at = models.DateTimeField('创建时间', default=timezone.now) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ('-created_at',) verbose_name = '文章' verbose_name_plural = '文章' indexes = [ models.Index(fields=['created_at']), ] def __str__(self): return self.title逐个说明关键字段。slug 是 URL 里用的短标识,由标题转换而成,比如「Django 博客实战」转成 django-blog,比用数字主键的 URL 更可读,列表路由和详情路由都要用到它。author 外键指向 User,删除用户时文章跟着删,符合内容归属逻辑。category 用 SET_NULL 而不是 CASCADE:分类被误删时文章要保留,这个字段允许为空正是为了配合 SET_NULL。body 用 TextField 而不是 CharField,因为正文字数不确定,CharField 的 max_length 在 MySQL 里对应 VARCHAR 长度上限是 65535 字节,塞进一篇带格式的长文就会报错。created_at 用 default=timezone.now 而不是 auto_now_add,两者区别在于 auto_now_add 新增时写入一次且之后不再改变,而 default 可以在管理后台手工修正发布时间,对需要补录旧文章的博客场景更灵活。
Meta 里的 indexes 给 created_at 建索引,配合 ordering 按时间倒序,列表页查询直接走索引排序。数据量到几万条时,这个索引能让首页响应时间从几百毫秒降到几十毫秒。答辩被问到数据库优化时,这是一个可以直接讲的点。
3.3 评论表的软删除设计:django执行查询-删除对象的一个变体
评论表是博客系统里用户生成内容最多的地方,设计上要考虑审核和删除的语义,完整定义如下:
class Comment(models.Model): post = models.ForeignKey(Post, verbose_name='文章', on_delete=models.CASCADE, related_name='comments') user = models.ForeignKey(User, verbose_name='评论用户', on_delete=models.CASCADE, related_name='comments') body = models.TextField('评论内容') is_visible = models.BooleanField('是否显示', default=True) created_at = models.DateTimeField('评论时间', auto_now_add=True) class Meta: ordering = ('created_at',) verbose_name = '评论' verbose_name_plural = '评论' def __str__(self): return f'{self.user.username} 评论了 {self.post.title}' def delete(self, *args, **kwargs): self.is_visible = False self.save(update_fields=['is_visible'])related_name 决定了反向查询的名字。不写它时默认是 comment_set,写成 comments 之后,视图里可以用 post.comments.filter(is_visible=True) 直接取某篇文章的可见评论,代码读起来和自然语言一样。重写 delete 方法是软删除的经典实现:默认的 ORM delete() 会发出 DELETE FROM 语句把记录物理删掉,这里把动作替换成置 is_visible=False 的 UPDATE,数据仍然留在库里,统计、审核、恢复都有依据。这个做法对应 django执行查询-删除对象时要考虑的「真删还是假删」问题,评论区这种用户生成内容,生产环境几乎都是软删除。
3.4 迁移执行与三种排错命令
数据模型定义完,接下来的工作是把结构同步到 MySQL 并验证每一条迁移的状态。三个命令的执行顺序是固定的:
python manage.py makemigrations blog python manage.py migrate python manage.py showmigrations blogmakemigrations 只生成迁移脚本不写库,migrate 才真正把表建进 MySQL。showmigrations 列出迁移文件的应用状态,[X] 表示已应用。改完字段想确认影响范围,可以先执行python manage.py sqlmigrate blog 0001看 Django 生成的原生 SQL,确认 CREATE TABLE 或 ALTER TABLE 符合预期再 migrate。
| 排查命令 | 作用 | 典型使用场景 |
|---|---|---|
| makemigrations --check --dry-run | 只检测不生成,模型与迁移不一致时返回非零码 | 提交代码前检查是否漏了迁移文件 |
| sqlmigrate blog <编号> | 打印某次迁移对应的原生 SQL | 确认字段类型、索引、外键的最终形态 |
| showmigrations blog | 列出迁移应用状态 | migrate 报错时判断哪些迁移残留 |
常见报错里,Table 'blog.blog_post' doesn't exist发生在查询模型但迁移没应用时,检查 showmigrations 输出即可定位;(1062, "Duplicate entry ...")通常是 slug 或 name 的唯一约束冲突,说明库里已有重复值,要先去重再迁移。如果是中途改模型字段类型导致 ALTER 失败,最省事的办法是备份数据后删除相关表重新 migrate,毕业设计阶段数据量小,不要在这种地方耗太久。
提示:
--fake参数可以把迁移标记为已应用而不真正执行 SQL,只在恢复数据库、表结构已经存在时使用,业务开发阶段不要碰它。
4. 核心功能落地:路由、视图、模板与 Admin 后台接管
4.1 URL 路由:slug 参数与参数名一致性问题
路由是整个站点访问的入口,先把 blog/urls.py 的内容定下来。每条 path 的第一个参数是 URL 模式,第二个参数是视图函数,第三个 name 用于模板反向解析:
# blog/urls.py from django.urls import path from . import views app_name = 'blog' urlpatterns = [ path('', views.post_list, name='post_list'), path('category/<slug:category_slug>/', views.category_posts, name='category_posts'), path('tag/<slug:tag_slug>/', views.tag_posts, name='tag_posts'), path('post/<slug:slug>/', views.post_detail, name='post_detail'), path('post/<slug:slug>/comment/', views.add_comment, name='add_comment'), ]URL 里用 slug 替换<int:pk>,是博客系统的常见做法。地址栏显示 post/django-blog 而不是 post/12,语义更清晰,搜索引擎也更友好。注意路由参数名必须和视图函数形参一致:post_detail 的形参是 slug,路由里就得写<slug:slug>;category_posts 的形参是 category_slug,路由里就得写<slug:category_slug>,对不上时会直接报 TypeError,而且这个错误在 URL 反向解析阶段和请求阶段都会出现。总路由文件里把 blog 的 urls 挂进来,注意 include 的位置别放进 admin 前面造成路径冲突:
# blog_project/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('blog.urls')), ]4.2 文章列表视图:Paginator 与 N+1 查询优化
列表视图承担首页和分类页的大部分逻辑,下面的代码是首页文章列表的完整实现:
from django.core.paginator import Paginator from django.shortcuts import render from .models import Post def post_list(request): posts = Post.objects.filter( status='published' ).select_related( 'author', 'category' ).prefetch_related( 'tags' ) paginator = Paginator(posts, 10) page_obj = paginator.get_page(request.GET.get('page')) return render(request, 'blog/post_list.html', {'page_obj': page_obj})Paginator 第一个参数是查询集,第二个是每页条数。get_page 比 Page 方法更宽容:页码越界时自动返回第一页或最后一页,不会抛 404,用户体验好也少写异常处理。这里有两个查询优化点值得展开。select_related 针对外键,通过 JOIN 把 author 和 category 一次查出来;prefetch_related 针对多对多,先查文章主体,再按外键批量查 tags,最后在 Python 层做关联。列表页渲染 20 篇文章,用这两个方法 SQL 查询数稳定在 3 条左右;不用的话,每篇文章渲染作者、分类、标签都要各自查一次库,总查询数呈 N+1 增长。答辩时被问网站性能优化,这个点是拿分项。
4.3 详情页与阅读数自增:F() 表达式的并发安全
详情页除了渲染文章正文,还要处理阅读数自增和评论列表。完整的视图函数如下:
from django.shortcuts import render, get_object_or_404 from django.db.models import F def post_detail(request, slug): post = get_object_or_404( Post.objects.select_related('author', 'category'), slug=slug, status='published' ) Post.objects.filter(pk=post.pk).update(views=F('views') + 1) post.refresh_from_db(fields=['views']) comments = post.comments.filter(is_visible=True).select_related('user') return render(request, 'blog/post_detail.html', { 'post': post, 'comments': comments, })阅读数自增必须在数据库端做。如果写成 post.views += 1 再 post.save(),两个请求同时读到 views=10,各自加一写回,结果是 11 而不是 12,这就是典型的丢失更新。F('views') + 1 生成的原生 SQL 是UPDATE ... SET views = views + 1,自增操作由数据库行锁保证原子性。refresh_from_db 把数据库端更新后的值同步回当前内存实例,模板里显示的阅读数才是准确的。get_object_or_404 的第二个参数是过滤条件,slug 和 status 一起传,草稿文章即使 URL 对了也返回 404,不会泄露未发布内容。评论查询也带了 select_related('user'),模板里显示评论者昵称时不会每条评论再查一次用户表。
4.4 模板渲染:分页、空列表与日期格式化
模板这部分直接给可用的 post_list.html,分页导航按 Django 模板语法写:
<!-- blog/templates/blog/post_list.html --> {% for post in page_obj %} <article class="post-item"> <h2><a href="{% url 'blog:post_detail' post.slug %}">{{ post.title }}</a></h2> <p class="meta">{{ post.author.username }} · {{ post.created_at|date:"Y-m-d H:i" }} · 阅读 {{ post.views }}</p> <div class="excerpt">{{ post.body|truncatechars:120 }}</div> </article> {% empty %} <p>还没有发布任何文章,去后台写第一篇吧。</p> {% endfor %} {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}">上一页</a> {% endif %} <span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span> {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">下一页</a> {% endif %}模板里 URL 用 {% url %} 走路由 name 反向解析,以后改路径模板不用动。truncatechars 按字符截断,中文英文都算一个字符;如果正文是富文本带 HTML 标签,要改用 truncatewords_html,否则可能把标签截断导致页面样式崩掉。date 过滤器用的是 Django 模板语法,Y 表示四位年份,m 是两位月,d 是两位日,H:i 是时和分,注意和 Python strftime 的 %Y-%m-%d 写法不同。分页对象的关键属性和模板用法对比如下:
| 属性 | 含义 | 模板用法 |
|---|---|---|
| page_obj.has_previous / has_next | 是否有上一页/下一页 | 控制翻页按钮显隐 |
| page_obj.previous_page_number | 上一页页码 | 生成上一页链接 |
| page_obj.number | 当前页码 | 高亮当前页 |
| page_obj.paginator.num_pages | 总页数 | 显示页码总量 |
| page_obj.object_list | 当前页数据列表 | 循环渲染文章卡片 |
4.5 Admin 后台接管:list_display 到批量操作,一步到位
Admin 的注册配置可以直接照抄。以下是 Post 和 Comment 的 ModelAdmin 定义:
from django.contrib import admin from .models import Post, Category, Tag, Comment @admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ('title', 'author', 'category', 'status', 'views', 'created_at') list_filter = ('status', 'category', 'tags') search_fields = ('title', 'body') prepopulated_fields = {'slug': ('title',)} list_editable = ('status',) date_hierarchy = 'created_at' list_per_page = 20 actions = ['make_published'] @admin.action(description='批量发布所选文章') def make_published(self, request, queryset): queryset.update(status='published') @admin.register(Comment) class CommentAdmin(admin.ModelAdmin): list_display = ('post', 'user', 'is_visible', 'created_at') list_filter = ('is_visible',) actions = ['hide_comments'] @admin.action(description='隐藏所选评论') def hide_comments(self, request, queryset): queryset.update(is_visible=False)list_display 决定列表页展示的列,字段、方法、属性都能放进去。list_editable 让 status 字段在列表页直接下拉切换,不进详情页就完成草稿到发布的流转。prepopulated_fields 是 Django 很有价值的特性:填标题时自动生成 slug 并做 URL 编码,省去手工输入。date_hierarchy 在列表页顶部生成一个按日期钻取的时间轴,很适合博客这种按时间组织的业务。django admin 界面美化如果不想引第三方皮肤,最稳的做法就是把上述配置用到位,再调整 list_per_page 控制每页行数。默认界面确实朴素,但毕业设计的重点是业务闭环,Admin 的职责是让「发布文章—管理评论—切换状态」这些动作高效完成,配置到这一步已经足够,不推荐自己改 admin 模板,维护成本高且升级时容易失效。
4.6 评论提交视图:登录校验、长度校验与 PRG 模式
评论提交是典型的表单 POST 场景,视图里要同时处理登录、校验和防重复提交:
from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, get_object_or_404 from django.contrib import messages from .models import Post, Comment @login_required def add_comment(request, slug): post = get_object_or_404(Post, slug=slug, status='published') if request.method == 'POST': body = request.POST.get('body', '').strip() if not body: messages.error(request, '评论内容不能为空') return redirect('blog:post_detail', slug=slug) if len(body) > 500: messages.error(request, '评论不能超过 500 字') return redirect('blog:post_detail', slug=slug) Comment.objects.create( post=post, user=request.user, body=body, ) messages.success(request, '评论成功') return redirect('blog:post_detail', slug=slug)@login_required 保证未登录用户被重定向到登录页而不是收到 500 错误。body 先 strip 再判空,排除纯空格提交。长度上限设在 500 字,虽然 MySQL 的 TEXT 类型能存更多,但业务上没有理由让一条评论超长,Python 层校验比数据库层校验的错误提示更友好。校验失败和成功都走 redirect 而不是 render,这是 POST/Redirect/GET 模式,防止用户刷新页面时浏览器弹「确认重新提交表单」并产生重复评论。需要注意,Django 默认开启 CSRF 中间件,评论表单的 HTML 里必须包含 {% csrf_token %},否则 POST 请求会返回 403。
提示:messages 提示依赖 Django 的 messages 中间件,模板里要写 {% if messages %} 循环输出,否则用户提交后看不到任何反馈。
5. 最后一公里:部署自检、数据备份与答辩演示的三件事
5.1 上线前跑一遍 Django 部署自检
python manage.py check --deploy python manage.py collectstatic --noinputcheck --deploy 会把 settings.py 里不适合生产环境的配置逐条列出来:DEBUG=True、ALLOWED_HOSTS 为空、SECRET_KEY 硬编码、安全相关中间件缺失等。每一条都值得改掉,因为它们正好是答辩时评审老师会盯的安全点。SECRET_KEY 不要留在 settings.py 里,抽到 .env 文件用 os.environ 读取,代码仓库只提交 .env.example。collectstatic 把所有 App 的静态文件合并到 STATIC_ROOT 指向的目录,交给 Nginx 托管,跳过这一步,生产模式打开页面会发现完全没有样式。
5.2 mysqldump 备份与恢复,答辩前至少跑三遍
mysqldump -u blog_user -p blog_db > blog_backup_$(date +%Y%m%d).sql # 恢复 mysql -u blog_user -p blog_db < blog_backup_20250101.sqlmysqldump 导出的文件包含建表和 INSERT 语句,一条命令即可完整恢复。Linux 下可以写进 crontab 每天凌晨执行,毕业设计阶段手动备份也够。恢复后务必打开首页和文章详情页验证数据完整,再登录 Admin 看评论是否存在,文件字节数不能说明问题。备份文件名带上日期是为了保留历史版本,万一某次演示前数据改坏了,可以回滚到前一天。
5.3 答辩演示的三个固定动作
第一,准备 15 到 20 篇真实感的文章数据,覆盖至少三个分类和六七个标签,让列表页分页、分类筛选、标签筛选都有内容可演示,不要拿 lorem ipsum 填充,评审翻到空列表会留下完成度不高的印象。第二,演示后台操作链路:登录 Admin、创建一篇带标签的文章、在列表页切换草稿与发布状态、隐藏一条违规评论,这一套动作展示的是完整的后台闭环。第三,主动讲数据库层面的两个优化:select_related 消除 N+1 查询、F() 表达式保证阅读数并发安全,这两个点从代码里能直接指出来,比背概念更有说服力。
最后留一个检查小动作:把 SECRET_KEY 从 settings.py 里移出去之前,先用python manage.py check跑一遍确认配置无误。这个检查工具会在缺少环境变量时直接报错,能提前暴露 .env 读取的问题,避免答辩现场启动失败。
本文还有配套的精品资源,点击获取