Django+MySQL博客系统开发实战:从环境搭建到部署运维

Django+MySQL博客系统开发实战:从环境搭建到部署运维 简介这是一份基于 Python、Django 与 MySQL 开发的博客系统完整源码定位为高校毕业设计及课程设计项目适合计算机、通信、人工智能、自动化等专业的学生、教师或从业者用于学习、二次开发与答辩演示。项目代码经过调试测试运行可靠曾获评98分的答辩成绩具备较好的工程完整性与学习借鉴价值。资源包共包含631个文件压缩后约19.8MB其中82个Python文件覆盖核心业务逻辑150个JavaScript与52个CSS文件支撑前端交互与样式103个HTML模板构成页面结构另含图片、数据库及字体图标等素材目录清晰、便于按模块查阅。目前已有159人学习下载适合零基础入门及希望提升Django实战能力的开发者参考。下载后可直接运行体验并可根据需要修改模型、视图与模板实现文章管理、用户登录、分类标签等博客常用功能对毕业设计选题和期末大作业均有较强参考意义。1. 毕业设计题目是 PythonDjangoMySQL 博客系统说明你选对了方向博客系统在毕业设计里出现频率极高不是因为它简单而是它恰好覆盖了一个信息管理系统的完整闭环前台展示、后台管理、数据库建模、用户交互、权限控制。很多同学答辩被问的第一句往往是「为什么选 Django 而不是 Flask」能说出 Admin 后台、ORM 迁移、内置用户认证这三件事就已经赢了一半。用 Python Django MySQL 做这套系统是在功能完整度和工作量可控之间最稳的组合。这篇文章从环境装配开始按模型设计、视图编写、模板渲染、后台接管的顺序走一遍给出可直接落到自己项目里的源码。适合课程设计、毕业论文系统实现也适合刚学完 Python 想拿完整 Django 项目练手的人。2. 环境搭建与项目初始化从 Python 安装到 Django 连接 MySQL2.1 先锁定版本组合能省掉一半报错很多项目卡在第一步问题不在代码而在 Python、Django、MySQL 三方的版本兼容。Django 对 Python 版本有硬性要求MySQL 8.x 默认的 caching_sha2_password 认证插件又会让老版本驱动连不上。做毕业设计不要追新我一般建议 Django 4.2 LTS 配 Python 3.10 或 3.11MySQL 选 8.0 系列。这个组合的踩坑贴最多在搜索引擎里搜 python安装教程、mysql安装教程、django install mysqlclient 都能直接找到对应的解决方案比用最新版本摸着石头过河效率高得多。组件推荐版本说明Python3.10 / 3.11与 Django 4.2 LTS 完全兼容第三方库支持成熟Django4.2 LTS官方长期维护版本安全更新周期长MySQL8.0.x默认 utf8mb4避免字符集坑mysqlclient2.2.xDjango 官方推荐的 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 django4.2.* mysqlclient2.2.* Pillow第一条创建名为 venv 的隔离目录第二、三条按操作系统激活。激活后命令行提示符前出现 (venv) 前缀说明当前 shell 的 python 和 pip 都指向虚拟环境。第三条装三个包django 是框架本体mysqlclient 是连接 MySQL 的驱动Pillow 是 Django 图片上传字段 ImageField 的依赖文章封面图功能一定会用到。版本号里4.2.*是浮动符号表示装到 4.2 系列的最新补丁版不会跳到大版本 5.x 造成 API 差异。提示pip 下载包慢或超时时可以在命令末尾加国内镜像源地址只改变下载通道不改变包本身的行为。2.3 创建项目和 Appdjango创建app 的标准两步创建项目两条命令就够了。第一步生成项目骨架第二步创建业务 Appdjango-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_APPSINSTALLED_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 保持 TrueDjango 在数据库里统一存 UTC展示时按 TIME_ZONE 转本地时间这是最不容易出错的时区姿势。2.5 mysqlclient 装不上时PyMySQL 是保底方案在 Windows 上 pip install mysqlclient 经常编译失败报 Microsoft Visual C 14.0 is required原因是这个驱动需要本地编译 C 扩展。如果不想装 Visual Studio Build Tools直接切换到 PyMySQLpip 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_userlocalhost IDENTIFIED BY your_password; GRANT ALL PRIVILEGES ON blog_db.* TO blog_userlocalhost; FLUSH PRIVILEGES;库、账号、密码要和 settings.py 里的配置逐字一致特别是 host 限定blog_userlocalhost 只允许本机连接服务器部署时还要加一个 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.pyPost、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_length50, uniqueTrue) slug models.SlugField(URL标识, max_length60, uniqueTrue) class Meta: verbose_name 分类 ordering (name,) def __str__(self): return self.name class Tag(models.Model): name models.CharField(标签名, max_length30, uniqueTrue) class Meta: verbose_name 标签 ordering (name,) def __str__(self): return self.name class Post(models.Model): STATUS_CHOICES ( (draft, 草稿), (published, 已发布), ) title models.CharField(标题, max_length200) slug models.SlugField(URL标识, max_length200, uniqueTrue) author models.ForeignKey(User, verbose_name作者, on_deletemodels.CASCADE, related_nameposts) category models.ForeignKey(Category, verbose_name分类, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameposts) tags models.ManyToManyField(Tag, verbose_name标签, blankTrue, related_nameposts) body models.TextField(正文) status models.CharField(状态, max_length10, choicesSTATUS_CHOICES, defaultdraft) views models.PositiveIntegerField(阅读数, default0) created_at models.DateTimeField(创建时间, defaulttimezone.now) updated_at models.DateTimeField(更新时间, auto_nowTrue) 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 用 defaulttimezone.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_deletemodels.CASCADE, related_namecomments) user models.ForeignKey(User, verbose_name评论用户, on_deletemodels.CASCADE, related_namecomments) body models.TextField(评论内容) is_visible models.BooleanField(是否显示, defaultTrue) created_at models.DateTimeField(评论时间, auto_now_addTrue) 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_visibleTrue) 直接取某篇文章的可见评论代码读起来和自然语言一样。重写 delete 方法是软删除的经典实现默认的 ORM delete() 会发出 DELETE FROM 语句把记录物理删掉这里把动作替换成置 is_visibleFalse 的 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 doesnt 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, namepost_list), path(category/slug:category_slug/, views.category_posts, namecategory_posts), path(tag/slug:tag_slug/, views.tag_posts, nametag_posts), path(post/slug:slug/, views.post_detail, namepost_detail), path(post/slug:slug/comment/, views.add_comment, nameadd_comment), ]URL 里用 slug 替换int:pk是博客系统的常见做法。地址栏显示 post/django-blog 而不是 post/12语义更清晰搜索引擎也更友好。注意路由参数名必须和视图函数形参一致post_detail 的形参是 slug路由里就得写slug:slugcategory_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 与 N1 查询优化列表视图承担首页和分类页的大部分逻辑下面的代码是首页文章列表的完整实现from django.core.paginator import Paginator from django.shortcuts import render from .models import Post def post_list(request): posts Post.objects.filter( statuspublished ).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 条左右不用的话每篇文章渲染作者、分类、标签都要各自查一次库总查询数呈 N1 增长。答辩时被问网站性能优化这个点是拿分项。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), slugslug, statuspublished ) Post.objects.filter(pkpost.pk).update(viewsF(views) 1) post.refresh_from_db(fields[views]) comments post.comments.filter(is_visibleTrue).select_related(user) return render(request, blog/post_detail.html, { post: post, comments: comments, })阅读数自增必须在数据库端做。如果写成 post.views 1 再 post.save()两个请求同时读到 views10各自加一写回结果是 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 classpost-item h2a href{% url blog:post_detail post.slug %}{{ post.title }}/a/h2 p classmeta{{ post.author.username }} · {{ post.created_at|date:Y-m-d H:i }} · 阅读 {{ post.views }}/p div classexcerpt{{ 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(statuspublished) 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_visibleFalse)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, slugslug, statuspublished) if request.method POST: body request.POST.get(body, ).strip() if not body: messages.error(request, 评论内容不能为空) return redirect(blog:post_detail, slugslug) if len(body) 500: messages.error(request, 评论不能超过 500 字) return redirect(blog:post_detail, slugslug) Comment.objects.create( postpost, userrequest.user, bodybody, ) messages.success(request, 评论成功) return redirect(blog:post_detail, slugslug)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 里不适合生产环境的配置逐条列出来DEBUGTrue、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 消除 N1 查询、F() 表达式保证阅读数并发安全这两个点从代码里能直接指出来比背概念更有说服力。最后留一个检查小动作把 SECRET_KEY 从 settings.py 里移出去之前先用python manage.py check跑一遍确认配置无误。这个检查工具会在缺少环境变量时直接报错能提前暴露 .env 读取的问题避免答辩现场启动失败。本文还有配套的精品资源点击获取