简介:这是一份基于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里如果漏掉blog,Django 不会为博客应用创建任何数据表,这是初学者常踩的坑之一。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, name='home'), # 博客首页,展示文章列表 path('article/<int:pk>/', views.article_detail, name='article_detail'), path('register/', views.register, name='register'), path('login/', views.user_login, name='login'), path('logout/', views.user_logout, name='logout'), path('publish/', views.publish_article, name='publish_article'), path('admin_manage/', views.admin_manage, name='admin_manage'), ]<int:pk>是 Django 的路由参数语法,尖括号里前半部分是类型转换器,后半部分是参数名。int限定这个位置只能接收数字,访问/article/abc/会直接返回 404。name参数给路由起了名字,模板里用{% url 'article_detail' pk=article.id %}动态生成链接,这样改 URL 规则时不需要改模板,反向解析会自动更新。这种设计在博客系统里特别实用,因为文章链接的 ID 是变化的,不可能写死。
3. myblog.sql 与 ORM 建模:把数据库设计落到表结构
3.1 设计博客系统最少需要几张表
虽然压缩包自带了一份myblog.sql,但拿到手不要急着导入。正确顺序是先读它的建表语句,理解表之间的关系,再考虑导入。这套博客系统的数据库设计,按最常见的课程设计要求,至少包含用户表、文章表、评论表三张核心表。如果系统支持点赞、分类、标签,则再扩展,但核心业务三张表足够跑通。
| 表名 | 主要字段 | 说明 |
|---|---|---|
user | id, username, password, email, create_time | 存储注册用户信息,密码字段存的是哈希值,不得存明文 |
article | id, title, content, author_id, create_time, update_time, views | author_id 外键关联 user 表,views 是阅读量计数 |
comment | id, article_id, user_id, content, create_time | article_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_length=200, verbose_name='标题') content = models.TextField(verbose_name='正文') author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='作者') create_time = models.DateTimeField(auto_now_add=True, verbose_name='发布时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') views = models.IntegerField(default=0, verbose_name='阅读量') class Meta: ordering = ['-create_time'] # 按发布时间倒序 def __str__(self): return self.title class Comment(models.Model): article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments') user = models.ForeignKey(User, on_delete=models.CASCADE) content = models.TextField(verbose_name='评论内容') create_time = models.DateTimeField(auto_now_add=True) def __str__(self): return f'{self.user.username}: {self.content[:20]}'on_delete=models.CASCADE这个参数经常被忽视,但它决定了一个重要行为:删除文章时,该文章下的所有评论会同步删除。如果改成SET_NULL,则删除文章后评论保留,但article_id变成空值。对博客系统来说,评论依附于文章内容,文章没了评论留着的意义也不大,所以用 CASCADE 是合理选择。auto_now_add和auto_now都是自动填充时间的,区别在于前者只在创建时写入一次,后者在每次修改时更新。related_name='comments'给反向查询起了名字,模板和视图中可以通过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_count=Count('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,明天要换成达梦或 PostgreSQL,ORM 代码不需要改,但原生 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(username=username).exists(): return render(request, 'register.html', {'error': '用户名已被占用'}) # create_user 会自动对密码做哈希,不要用 create() user = User.objects.create_user(username=username, password=password) 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, username=username, password=password) 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(): # commit=False 先不落库,把当前登录用户填充到 author 字段 article = form.save(commit=False) article.author = request.user article.save() return redirect('article_detail', pk=article.id) else: form = ArticleForm() return render(request, 'publish.html', {'form': form})@login_required是 Django 提供的装饰器,作用是检查用户是否登录,未登录则跳转到登录页。它的跳转地址默认是/accounts/login/,如果要改成项目自己的登录页,需要在settings.py中添加LOGIN_URL = '/login/'。commit=False是 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,被称作 N+1 查询问题。而select_related会在查询文章时通过 JOIN 把作者信息一并查出来。分页参数的含义也值得记一下:Paginator(article_list, 5)里的 5 是每页数量,设置得越小数据库单次查询压力越小,但用户体验越差;PageNotAnInteger捕获非数字页码,EmptyPage捕获超过最大页数的页码,对应?page=99的情况不会报 500,而是直接返回到最后一页。
在index.html模板里,分页控件的写法基本固定,核心是articles.has_previous、articles.has_next、articles.paginator.num_pages三个方法加当前页码循环。
<div class="pagination"> {% 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 class="current">{{ 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 = False,Django 会检查请求的 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 分别做镜像再编排,那是另一种玩法了。
本文还有配套的精品资源,点击获取