资讯动态

Django博客系统开发实战:从MVT架构到Linux部署

发布时间:2026/9/16 19:02:42 来源:尧图企业网站定制
简介这是一份基于Python的个人博客系统毕业设计项目包适合正在学习Python Web开发、需要完成课程设计或毕业设计的开发者参考。项目围绕博客核心功能展开覆盖文章发布、用户注册登录、评论互动等常见模块并结合数据库完成数据持久化能够帮助理解从需求分析、系统设计到编码实现与部署的完整流程。压缩包共7387个文件大小约25.83MB以Python源码、模板文件、静态资源及数据库脚本为主其中包含大量.py业务逻辑文件、HTML页面、JS/CSS前端资源以及.sql数据库文件并附带软件说明书docx文档便于查阅。目前已有766人学习使用对于想系统掌握Python博客项目构建、ORM操作及Django/Flask框架应用的学习者来说是一份内容完整、结构清晰的实践资料。1. 这套个人博客系统适合拿来拆着学而不是照着抄解压这套项目的压缩包里面是mysite工程目录、一个myblog.sql数据库文件、一份软件说明书.docx。mysite这个名字基本说明它是 Django 项目的标准结构myblog.sql则表明数据持久化走的是 MySQL。这是一份典型的课程设计/毕业设计资源功能覆盖用户注册登录、文章发布、评论管理、后台数据维护技术栈不算新但胜在完整前后端、ORM、数据库、部署文档全部齐活。这套项目对两类人最有用。一类是准备做 Python Web 或数据库课程设计的在校生拿它当功能清单和代码结构参考另一类是刚接触 Django 但只写过增删改查例子的自学者用它理解一个真实博客系统里模型、路由、视图、模板怎么配合工作。如果你期望的是一个开箱即用的现代化博客那它并不合适但如果你是想要一个能把 Django 和数据库串起来、能跑通全流程的范本这个压缩包比网上的碎片教程值钱得多。2. Django MVT 与路由这套个人博客系统的分层骨架2.1 为什么是 Django 而不是 Flask这套项目大概率基于 Django判断依据有两个。第一工程文件夹叫mysite这是django-admin startproject的默认命名方式Flask 项目通常不会有这个目录结构。第二Django 自带 Admin 后台和 ORM对毕业设计来说后台可以直接拿来管理文章数据不用额外造轮子能省下大量开发时间。Django 的核心是 MVT 模式M 是 Model模型、V 是 View视图、T 是 Template模板。要注意这里的 View 和大多数后端工程师理解的接口函数不太一样Django 的 View 负责接收请求、调用模型、把数据交给模板再返回 HttpResponse。真正处理业务逻辑的部分分散在 View、Form、Serializer 里这个分层思路需要先立住后面读代码才不会绕晕。我刚拿到这类资源的第一反应是先把目录结构看清楚而不是直接去读代码文件。它决定了整个项目的边界在哪。2.2 打开工程先看这几个文件mysite/ ├── manage.py # Django 命令行入口迁移、启动服务器都靠它 ├── mysite/ │ ├── settings.py # 项目配置数据库连接、应用注册、模板路径 │ ├── urls.py # 根路由表所有 URL 入口 │ └── wsgi.py # Web 服务器与 Django 应用的桥梁 └── blog/ # 业务应用通常叫 blog 或 article ├── models.py # 定义博客文章、用户、评论的数据结构 ├── views.py # 业务逻辑处理 HTTP 请求 ├── urls.py # 应用下的子路由表 └── templates/ # HTML 模板目录manage.py是整个项目的操作入口。日常开发里用得最多的是python manage.py runserver启动本地服务、python manage.py makemigrations生成迁移文件、python manage.py migrate同步数据库结构。运行项目前先确保本机 Python 版本在 3.8 以上MySQL 服务已经启动并且建好了一个空数据库比如myblog字符集选utf8mb4。如果你还没有装好 Python 环境常见做法是用 Anaconda 或官方安装包装完再在命令行里执行python --version确认环境变量生效。这一步卡住的人不少大部分是安装时没勾选 Add Python to PATH。2.2.1 settings.py 里要改什么settings.py是这个项目里最值得逐行看的文件。你需要关注三类配置数据库连接、应用注册、模板路径。下面是一份典型的配置片段和我实际部署这套博客时改动过的内容一致。# mysite/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, blog, # 注册业务应用否则 migrate 时找不到表 ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: myblog, # 对应 myblog.sql 里的数据库名 USER: root, # 改成你自己 MySQL 账号 PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, # 保证中文和 emoji 不乱码 }, } } TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], # 全局模板目录业务模板也建议放这 APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.request, django.contrib.auth.context_processors.auth, ], }, }, ]这段配置说明了三个关键点。INSTALLED_APPS里如果漏掉blogDjango 不会为博客应用创建任何数据表这是初学者常踩的坑之一。DATABASES的ENGINE决定 Django 用哪个数据库驱动连接 MySQL如果系统提示找不到 MySQLdb说明缺少依赖包在 Python 3 环境下我一般会安装mysqlclient它的安装依赖系统里的 MySQL 客户端库Windows 上容易装失败换成pymysql并把它安装进项目入口会更省事。TEMPLATES里的DIRS告诉 Django 去哪里找模板文件如果模板渲染 404通常就是路径没写对。2.3 路由层URL 到 View 的映射Django 的路由机制和 Flask 的app.route装饰器完全不同它拆成两层项目根路由mysite/urls.py和应用路由blog/urls.py。根路由只做分发具体 URL 规则写在业务应用里。这样做的优势是应用可以独立复用换一个项目直接把blog应用拷过去注册路由就能跑。# mysite/urls.py项目根路由 from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), # Django 自带后台管理用户和文章用 path(, include(blog.urls)), # 根路径交给 blog 应用处理 ] # blog/urls.py应用路由 from django.urls import path from . import views urlpatterns [ path(, views.index, namehome), # 博客首页展示文章列表 path(article/int:pk/, views.article_detail, namearticle_detail), path(register/, views.register, nameregister), path(login/, views.user_login, namelogin), path(logout/, views.user_logout, namelogout), path(publish/, views.publish_article, namepublish_article), path(admin_manage/, views.admin_manage, nameadmin_manage), ]int:pk是 Django 的路由参数语法尖括号里前半部分是类型转换器后半部分是参数名。int限定这个位置只能接收数字访问/article/abc/会直接返回 404。name参数给路由起了名字模板里用{% url article_detail pkarticle.id %}动态生成链接这样改 URL 规则时不需要改模板反向解析会自动更新。这种设计在博客系统里特别实用因为文章链接的 ID 是变化的不可能写死。3. myblog.sql 与 ORM 建模把数据库设计落到表结构3.1 设计博客系统最少需要几张表虽然压缩包自带了一份myblog.sql但拿到手不要急着导入。正确顺序是先读它的建表语句理解表之间的关系再考虑导入。这套博客系统的数据库设计按最常见的课程设计要求至少包含用户表、文章表、评论表三张核心表。如果系统支持点赞、分类、标签则再扩展但核心业务三张表足够跑通。表名主要字段说明userid, username, password, email, create_time存储注册用户信息密码字段存的是哈希值不得存明文articleid, title, content, author_id, create_time, update_time, viewsauthor_id 外键关联 user 表views 是阅读量计数commentid, article_id, user_id, content, create_timearticle_id 与 user_id 都是外键构成多表关联查询设计表结构时要特别注意两个点。第一文章和评论之间是典型的一对多关系一篇文章对应多条评论所以comment表里要放article_id外键用 ForeignKey 表示。第二用户和文章之间也是一对多author_id字段存放的是user.id而不是用户名字符串。如果建表时把作者直接写成username一旦用户改名所有历史文章的作者信息就全部错乱了这就是外键存在的意义。3.2 从 models.py 定义到 migrate 迁移Django 的 ORM 让开发者用 Python 类定义表结构再通过迁移工具生成并执行 SQL。这种做法在开发阶段比手写 SQL 高效得多因为改字段类型、加列、删表都不需要手动维护 SQL 脚本。# blog/models.py from django.db import models from django.contrib.auth.models import User class Article(models.Model): title models.CharField(max_length200, verbose_name标题) content models.TextField(verbose_name正文) author models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name作者) create_time models.DateTimeField(auto_now_addTrue, verbose_name发布时间) update_time models.DateTimeField(auto_nowTrue, verbose_name更新时间) views models.IntegerField(default0, verbose_name阅读量) class Meta: ordering [-create_time] # 按发布时间倒序 def __str__(self): return self.title class Comment(models.Model): article models.ForeignKey(Article, on_deletemodels.CASCADE, related_namecomments) user models.ForeignKey(User, on_deletemodels.CASCADE) content models.TextField(verbose_name评论内容) create_time models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.user.username}: {self.content[:20]}on_deletemodels.CASCADE这个参数经常被忽视但它决定了一个重要行为删除文章时该文章下的所有评论会同步删除。如果改成SET_NULL则删除文章后评论保留但article_id变成空值。对博客系统来说评论依附于文章内容文章没了评论留着的意义也不大所以用 CASCADE 是合理选择。auto_now_add和auto_now都是自动填充时间的区别在于前者只在创建时写入一次后者在每次修改时更新。related_namecomments给反向查询起了名字模板和视图中可以通过article.comments.all()获取全部评论。3.2.1 迁移流程与 myblog.sql 的关系# 先检查模型定义是否有语法错误 python manage.py makemigrations blog # 生成迁移文件后同步到数据库 python manage.py migrate # 如果已有 myblog.sql也可以跳过迁移直接导入 mysql -u root -p myblog myblog.sql这里有一个容易混淆的地方。migrate是 Django 根据models.py自动生成表结构适用于功能开发阶段。而导入myblog.sql是直接把别人的数据库镜像导入到本地适用于拿现成数据的情况。如果两份表结构不一致导入后 Django 查询会报字段不存在所以二选一即可。我在复现这类毕业设计时一般是先执行migrate建出空表再用python manage.py createsuperuser创建管理员账号然后通过 Django Admin 手动录入几篇测试文章数据量不大完全没必要导入 SQL。3.3 ORM 与原生 SQL 怎么选初学 Django 时容易走两个极端要么全程用 ORM 不写一行原生 SQL要么觉得 ORM 不够灵活全部手写 SQL。这套博客系统典型的场景是文章列表页需要显示作者名和评论数如果用 ORM可以写成一段简洁的查询如果换写原生 SQL则要拼很长一段 JOIN 语句。# ORM 写法查询文章标题、作者名、评论数 from django.db.models import Count articles Article.objects.values(id, title, author__username) \ .annotate(comment_countCount(comments)) # 等价的原生 SQL 写法 SELECT a.id, a.title, u.username, COUNT(c.id) AS comment_count FROM article a LEFT JOIN user u ON a.author_id u.id LEFT JOIN comment c ON c.article_id a.id GROUP BY a.id, a.title, u.username;ORM 的核心优势是跨数据库兼容今天用 MySQL明天要换成达梦或 PostgreSQLORM 代码不需要改但原生 SQL 基本要重写。author__username这种双下划线语法表示跨越外键关系取字段对应 SQL 里的JOIN逻辑上是一一映射的。需要注意的是如果只想统计文章列表并带上作者信息ORM 会自动生成 LEFT JOIN如果查询的字段全部来自单表它不会做多余的表连接。判断标准很简单单表操作无脑用 ORM复杂报表或性能瓶颈明显时再考虑手写 SQL。4. 从注册登录到文章发布核心业务逻辑实现4.1 用户认证用 Django 内置还是自己写这套博客系统的用户认证最大的可能仍然是复用 Django 内置的django.contrib.auth。自己做 Session 认证不是不行但没有必要内置方案已经处理好了密码哈希、会话保持、登录状态校验这些细节自己实现很容易出现密码明文存储或用错哈希算法的问题。# blog/views.py from django.contrib.auth.models import User from django.contrib.auth import login, logout, authenticate from django.shortcuts import render, redirect def register(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) confirm request.POST.get(confirm_password) if password ! confirm: return render(request, register.html, {error: 两次密码输入不一致}) if User.objects.filter(usernameusername).exists(): return render(request, register.html, {error: 用户名已被占用}) # create_user 会自动对密码做哈希不要用 create() user User.objects.create_user(usernameusername, passwordpassword) login(request, user) # 注册成功后自动登录 return redirect(home) return render(request, register.html) def user_login(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) return redirect(home) else: return render(request, login.html, {error: 用户名或密码错误}) return render(request, login.html)create_user和create的区别是这段代码里最值得讲清楚的地方。create_user是 Django 提供的默认用户创建方法内部调用set_password对密码进行哈希处理而create会把密码原样存进数据库一旦数据库泄露用户在其他平台的密码也会遭殃。authenticate负责校验账号密码是否匹配返回None则表示认证失败它和login是两件事前者是验证身份后者是写入 Session 状态。login(request, user)执行后后续视图里通过request.user就能拿到当前登录用户对象。4.2 文章发布的表单与视图表单处理是 Django 开发里最繁琐的部分涉及到数据校验、错误提示、回填数据。不少课程设计直接在视图里用request.POST.get(title)取值写法简单但代码臃肿。更规范的做法是用 Django Form 管理输入验证。# blog/forms.py from django import forms from .models import Article class ArticleForm(forms.ModelForm): class Meta: model Article fields [title, content] widgets { title: forms.TextInput(attrs{class: form-control}), content: forms.Textarea(attrs{class: form-control, rows: 10}), } # blog/views.py from django.contrib.auth.decorators import login_required from .forms import ArticleForm login_required def publish_article(request): if request.method POST: form ArticleForm(request.POST) if form.is_valid(): # commitFalse 先不落库把当前登录用户填充到 author 字段 article form.save(commitFalse) article.author request.user article.save() return redirect(article_detail, pkarticle.id) else: form ArticleForm() return render(request, publish.html, {form: form})login_required是 Django 提供的装饰器作用是检查用户是否登录未登录则跳转到登录页。它的跳转地址默认是/accounts/login/如果要改成项目自己的登录页需要在settings.py中添加LOGIN_URL /login/。commitFalse是 ModelForm 的一个关键参数意思是先不执行保存操作给开发者机会在保存前补充字段。这里的author字段在表单中没有暴露给用户而是在视图里从request.user中取出再赋值这样用户无法通过伪造 POST 请求把文章挂到别人名下。4.3 列表页模板渲染与分页文章数量达到几十篇以后列表页必须做分页。Django 内置的有Paginator类用法简单但实际项目里要注意它和查询集的交互方式。# blog/views.py from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger def index(request): # 一页显示 5 篇按时间倒序查最新 article_list Article.objects.all().select_related(author) paginator Paginator(article_list, 5) page request.GET.get(page) try: articles paginator.page(page) except PageNotAnInteger: articles paginator.page(1) except EmptyPage: articles paginator.page(paginator.num_pages) return render(request, index.html, {articles: articles})select_related(author)和Paginator搭配使用可以避免几类常见的性能问题。如果不加这个查询模板里每次访问article.author.username都会触发一次新的数据库查询一页 5 篇文章就是 5 次额外的 SQL被称作 N1 查询问题。而select_related会在查询文章时通过 JOIN 把作者信息一并查出来。分页参数的含义也值得记一下Paginator(article_list, 5)里的 5 是每页数量设置得越小数据库单次查询压力越小但用户体验越差PageNotAnInteger捕获非数字页码EmptyPage捕获超过最大页数的页码对应?page99的情况不会报 500而是直接返回到最后一页。在index.html模板里分页控件的写法基本固定核心是articles.has_previous、articles.has_next、articles.paginator.num_pages三个方法加当前页码循环。div classpagination {% if articles.has_previous %} a href?page{{ articles.previous_page_number }}上一页/a {% endif %} {% for num in articles.paginator.page_range %} {% if num articles.number %} span classcurrent{{ num }}/span {% else %} a href?page{{ num }}{{ num }}/a {% endif %} {% endfor %} {% if articles.has_next %} a href?page{{ articles.next_page_number }}下一页/a {% endif %} /div模板逻辑不复杂但有一个细节值得注意articles.number是当前页的页码如果通过{% url home %}生成的分页链接不带任何查询参数那么page取不到值时Paginator.page()会收到一个空值None此时上面的PageNotAnInteger分支并不会捕获它page(None)会直接抛异常。处理方式是给page设置一个默认值比如request.GET.get(page, 1)确保它总能拿到字符串形式的页码。5. 部署在 Linux 上容易踩的坑从本机跑到服务器的提醒5.1 先关调试再谈上线本地runserver能跑通不代表部署没问题。最典型的一个问题是把DEBUG True直接带到生产环境一旦程序出错Django 会把完整的堆栈信息、本地文件路径、甚至数据库配置暴露在浏览器页面上。安全风险极高。部署前至少要把下面这段配置改掉。# settings.py 生产环境片段 DEBUG False # 必须设为 False否则出错时会输出敏感信息 ALLOWED_HOSTS [your-domain.com, www.your-domain.com] # 必须包含服务器 IP 或域名 STATIC_ROOT /var/www/myblog/static/ # 收集所有静态文件到这一个目录ALLOWED_HOSTS是部署时最容易报错的一项。本地跑的时候它是空的但一旦DEBUG FalseDjango 会检查请求的 Host 是否在这个列表里不在就返回 400 错误。如果你的服务器有多个域名或 IP 要访问这个站点把它们全部加进去如果你只通过 IP 访问也可以直接写[*]但不建议这样做等于是放弃了 Host 头校验。STATIC_ROOT是python manage.py collectstatic的输出目录它会把所有应用下的 static 文件集中到一个地方方便 Nginx 统一托管。5.2 用 Gunicorn 把 Django 跑起来再用 Nginx 挡在前面Django 自带的开发服务器不能用于生产这是必须记住的一条红线。常见做法是 Gunicorn 作为应用服务器Nginx 作为反向代理。Gunicorn 接收来自 Nginx 转发的请求Nginx 再负责静态文件、负载均衡、访问日志。# 安装并启动 Gunicorn绑定到本机 8001 端口 pip install gunicorn gunicorn mysite.wsgi:application --bind 127.0.0.1:8001 --workers 3mysite.wsgi:application中的mysite是工程目录名对应mysite/wsgi.py文件里的application对象这个对象是 Django 与 WSGI 服务器之间的接口。--workers 3是并行处理请求的进程数一般设置为 CPU 核心数的 2 倍加 1比如 2 核机器就设 5。设置过高会导致进程频繁切换反而降低吞吐量。设置过低则一个进程卡住时其他用户也会响应变慢。server { listen 80; server_name your-domain.com; # 用户上传的图片、CSS、JS 文件由 Nginx 直接返回不经过 Django location /static/ { alias /var/www/myblog/static/; } location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }Nginx 的配置把请求分成了两类/static/路径直接查磁盘上的静态文件浏览器加载样式和图片不走 Django其余请求全部转发到 Gunicorn。这样做减轻了 Python 进程的压力。如果部署后发现页面有样式但图片加载不出九成是STATIC_ROOT和 Nginx 的alias路径没有对上。5.3 遇到 500 错误先看日志部署后访问任何页面都报 500排除方法有固定套路先打开 Gunicorn 的输出看有没有完整的 Traceback 日志。常见的错误无非三种数据库连接不上、ALLOWED_HOSTS配置错误、静态文件路径不对。数据库连不上时检查 MySQL 是否允许远程连接以及settings.py里的 HOST 是127.0.0.1还是服务器的实际 IP。如果是权限问题在 MySQL 里执行GRANT ALL PRIVILEGES ON myblog.* TO root%;再刷新权限即可。迁移到服务器上的开发环境后别忘了执行python manage.py migrate否则有新增模型时生产库会因缺表而报错。最后还有一个容易被忽略的小技巧导出和导入myblog.sql时如果文件较大建议用mysql命令行直接导入而不是用 Navicat 等图形工具的导入按钮前者不会因为超时中断后续排错也更容易从日志中定位。如果你拿到的这台服务器 Linux 版本较老Gunicorn 装不上可以考虑换成 uWSGI配置方式类似。至于 Docker 部署需要把 MySQL 和 Django 分别做镜像再编排那是另一种玩法了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价