用Django全栈开发博客系统:从模型设计到部署实战
发布时间:2026/9/9 3:03:42 锦皓数字建站

做了几年Python后端接过的需求里有一半以上都带“先弄个网站”这个前提。想快速把想法变成能跑的系统我第一反应还是Django。这篇就用“构建一个博客系统”这个经典项目把Django全栈开发的完整路径走一遍从环境搭建、数据模型设计到视图路由、模板渲染再到用户认证、评论互动、分页搜索最后落地点部署前的安全检查和常见故障排查。整个过程基于Django 5.x版本强调实操和背后的逻辑适合刚学完Python基础、想做一个完整项目的同学也适合后端转全栈的开发者快速建立技术地图。1. 为什么用Django做博客选型逻辑与现实考量1.1 Django的“全家桶”理念到底香在哪第一次在真实项目里体会到Django的“省心”是有一次凌晨还在折腾Flask和SQLAlchemy手动搭配登录、表单、分页的中间件组合。你当然可以把Flask搭成任何形状但每次都要自己去拼积木时间成本非常高。Django则是“官方帮你配好了一整套”ORM、Admin后台、表单框架、认证系统、模板语言、中间件机制全部内置。做一个博客系统Django从“登录注册”到“文章发布”几乎每块能力都是现成的你只需要去控制“我要怎么组织它”。用Django做博客还有一个隐藏优势它的Admin后台简直是为内容管理而生的。你不需要前端做一堆管理页面直接给编辑开一个/admin入口就能在上面写文章、传封面、管理评论。这在项目很早期、前端还没就绪的时候能直接把内容生产跑起来特别适合个人博客、团队知识库这类小团队系统。1.2 全栈开发在这个项目里指什么这里说的“全栈”不是要求你前端三件套数据库运维全精通而是借助Django的MVT架构把“数据模型→业务逻辑→页面渲染”这条完整链路打通。对比传统的前后端分离架构Vue/React RESTful APIDjango全栈开发的核心优势是一个Python项目里你能同时控制数据库表结构、HTTP处理逻辑和HTML渲染结果。做这种整体式应用时你不用考虑跨域、Token鉴权、接口文档同步这些前后端分离带来的额外负担浏览器直接请求Django渲染好的页面数据直接嵌进HTML返回。对个人项目、企业内部系统、运营后台这种场景这种方式开发效率最高维护成本也低。这个博客系统就是一个标准范例用户看到的文章列表页、详情页、评论表单都由Django直接渲染链路短、排查容易。2. 从零搭建工程环境、项目骨架与应用拆分2.1 版本选择与虚拟环境隔离当前Django主分支已进入5.x新项目建议直接用5.x版本。Django 5.x对Python 3.10支持良好并且移除了大量旧版兼容层代码更干净。实际操作中我的选择是mkdir django_blog cd django_blog python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install django django-admin --version虚拟环境是必须做的不要图省事直接装在全局环境里。我踩过最痛的一次坑A项目的Django版本还是2.2B项目要用5.0如果装在同一个环境里依赖冲突能让人疯掉。虚拟环境隔离能让每个项目拥有自己独立的第三方包目录后续部署到服务器也干净。Django项目安装完成后立刻创建项目骨架django-admin startproject blog_project python manage.py startapp blog这里的名字值得说明blog_project是存放全局配置settings.py、urls.py的“项目容器”blog是真正承载业务模型的“应用”。一个项目中可以有多个应用比如以后想加“相册”模块可以再建一个gallery应用这样各模块边界清晰代码不容易纠缠成一团。2.2 项目和应用该拆多细很多新手纠结“要不要现在就建一堆app”我的建议是开始阶段只建一个blog应用就够了等业务真正成长之后再做拆分。过度设计是个人项目最常见的浪费——你花了半天把所有模块分得干干净净结果半年后才发现当初预想的用户体系根本没有独立价值。不过有一条底线要守住业务逻辑必须留在应用里而不是堆在views.py里变成一坨无法阅读的代码。后续可以按功能拆成services.py、utils.py或者直接把查询逻辑封装到模型管理器这都属于代码组织层面的优化不是架构层面的拆分。创建好应用后还需要把blog注册到INSTALLED_APPS否则Django不会识别应用内的模型和模板。这一步是新手最容易忘记的。3. 数据模型设计博客的灵魂在数据库结构3.1 核心模型用户、分类、文章、评论博客系统最核心的四个实体用户、分类、文章、评论。Django内置了User模型所以用户这块不用从零写。但有一个点需要认真设计你的用户表到底够不够用如果只是登录、发帖内置User就足够如果以后要存手机号、生日、头像等额外信息那就需要定制。代码层面有两种做法一种是用AbstractUser继承后扩展字段from django.contrib.auth.models import AbstractUser class User(AbstractUser): nickname models.CharField(max_length50, blankTrue) avatar models.ImageField(upload_toavatar/, blankTrue)另一种是在内置User基础上新建一对一关系的Profile表。哪个更合理看项目规模。博客这种轻量场景直接继承AbstractUser更省事查用户信息一次就能拿全Profile方式适合用户体系非常复杂的场景因为可以按需加载。需要提前设置AUTH_USER_MODEL blog.User注意一定要在第一次migrate之前设置否则之后改会被Django警告甚至报错。分类和文章的代码模型class Category(models.Model): name models.CharField(max_length100) slug models.SlugField(uniqueTrue) class Meta: verbose_name_plural categories def __str__(self): return self.name class Post(models.Model): title models.CharField(max_length200) slug models.SlugField(uniqueTrue) author models.ForeignKey(User, on_deletemodels.CASCADE, related_nameposts) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, related_nameposts) tags models.ManyToManyField(Tag, blankTrue, related_nameposts) content models.TextField() created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) status models.CharField(max_length10, choices[(draft, 草稿), (published, 发布)], defaultdraft)设计这里面每一个字段时我都在想“未来读代码的人会不会困惑”。on_delete参数就是最容易困惑的地方。写评论和文章的关系时CASCADE表示评论跟着作者一起删但分类被删时SET_NULL则希望文章保留只是分类置空。这两种策略代表了两种业务决策一定要想清楚再写。评论模型相对简单class Comment(models.Model): post models.ForeignKey(Post, on_deletemodels.CASCADE, related_namecomments) author models.ForeignKey(User, on_deletemodels.CASCADE) content models.TextField() created_at models.DateTimeField(auto_now_addTrue)这里用related_namecomments后面在模板里就能用post.comments.all()拿到该文章下的所有评论这个语法糖能省掉手写一堆查询的麻烦。3.2 外键、多对多与迁移中的细节模型设计里最值得展开的是“文章和标签”的多对多关系。一篇文章可以打“Python”“Web开发”多个标签一个标签也能挂在多篇文章上。Django用ManyToManyField自动生成中间表你不需要手动建关联表这是它方便的地方。但要注意如果你需要在关联关系上存储额外信息比如“A标签是这篇文章的主标签”就要手动建中间模型指定throughPostTag否则默认中间表没有扩展余地。模型字段写完后真正数据库变更靠迁移完成python manage.py makemigrations python manage.py migrate第一次看到migrate执行日志时应该留意它到底建了哪些表。Django默认会创建认证相关的表auth_user等以及session、admin等内置模块的表。这些表是一个可用系统的基础设施不要为“太多无关表”而惊讶。迁移机制背后的逻辑是版本控制数据库结构。你写的每个models.py改动都被记录成一次迁移文件以后团队协作时伙伴拉到代码只要执行migrate就能把本地数据库升级到最新不用手动在数据库里敲SQL。这个设计在真实团队里帮过大忙但前提是不要去手动删迁移文件。我见过有人为了“清理”直接删掉整个迁移目录结果数据库状态和版本对不上最后只能重建痛不欲生。4. Admin后台与文章发布零成本获得管理界面4.1 注册模型与定制列表Django的Admin让我对“框架红利”有了直观感知。写完模型后注册到Admin几乎不需要额外开发# blog/admin.py from django.contrib import admin from .models import Post, Category, Comment, Tag admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display (title, author, category, status, created_at) list_filter (status, category, tags) search_fields (title, content) prepopulated_fields {slug: (title,)} date_hierarchy created_at这段配置每一行都值得解释一下。list_display决定后台列表页展示哪些列你可以在这里放模型字段也可以放自定义方法list_filter是在侧边栏生成按状态、分类、标签的筛选器search_fields通过title和content两个字段提供搜索prepopulated_fields则是在管理界面输入标题后自动生成slug这个细节能避免手动敲URL地址的麻烦。Admin自定义的可能性非常多比如用list_editable直接在列表页改状态、用inline在文章编辑页内联操作评论。但有一个原则Admin只给内部人员用不要拿它做面向用户的复杂界面。它的价值在于让你用一个下午搭建一套内容管理工具后续把精力留给面向用户的部分。4.2 超级用户与后台发布的完整流程启动后台前必须先创建超级用户python manage.py createsuperuser创建过程会提示输入用户名、邮箱、密码密码必须符合强度要求。之后启动开发服务器python manage.py runserver访问http://127.0.0.1:8000/admin/用刚创建的超级用户登录就能看到后台管理界面。此时你可以正式发布第一篇文章在Post里点击“增加”填写标题、正文、作者选分类和标签设置状态为“发布”保存。后台操作背后其实完成了三步工作向数据库插入文章记录、设置作者外键指向当前用户、把多对多标签关系写入中间表。作为一个全栈开发者看后台不只是点按钮要能想象它对应的数据变化。这也是为什么我强烈建议新手在Admin走一遍全部流程因为它能帮你直观理解模型字段和实际数据之间的对应关系。提示Admin后台的样式依赖Django自带的静态文件。开发环境一切正常一旦部署到生产环境设置DEBUGFalse后后台CSS可能全部丢失需要在项目里配置好静态文件收集逻辑这个问题第九章会专门讲。5. 视图与URL路由把MVT串成一条链路5.1 FBV还是CBV视图是Django处理请求、返回响应的核心枢纽。写视图有两种风格函数视图FBV和类视图CBV。函数视图直观一个函数接一个请求适合复杂、个性强、无法套通用模板的页面类视图优雅Django内置了ListView、DetailView等通用视图适合列表页、详情页这种高度模板化的场景。对一个博客系统我建议两者混用理解。文章列表页用ListView可以省掉大量样板代码# blog/views.py from django.views.generic import ListView, DetailView from .models import Post class PostListView(ListView): model Post template_name blog/post_list.html context_object_name posts paginate_by 10 def get_queryset(self): return Post.objects.filter(statuspublished).select_related(author, category)这里get_queryset中做的filter是把草稿过滤掉select_related则是一条优化技巧后面性能部分会细讲。类视图的好处在于你关注的是“数据怎么查”和“模板叫什么”而不是写一整套HTTP状态码、模板上下文处理逻辑。但类视图也有个缺点:当你需要处理特别复杂的逻辑时类视图的重写链条会让人头晕。此时FBV反而是更清晰的选择。我的判断标准是如果页面只是标准列表/详情用CBV如果页面有复杂表单、多阶段流程用FBV或者直接写一个专门的Service层。5.2 路由设计、命名空间与参数传递在blog/urls.py里配置应用级路由然后在项目主路由中include进来# blog/urls.py from django.urls import path from . import views app_name blog urlpatterns [ path(, views.PostListView.as_view(), namepost_list), path(post/int:pk/, views.post_detail, namepost_detail), path(category/slug:slug/, views.category_detail, namecategory_detail), ]# 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)), ]路由里最需要掉入注意的是动态参数的匹配规则。int:pk只匹配整数slug:slug匹配文本slug这种强类型约束能避免把非法请求分发给视图。比如你输入/post/abc/Django会直接返回404而不是进入视图发现参数类型不对然后报错。app_name blog定义命名空间是为了在多个应用同时存在时避免路由名冲突。模板里用{% url blog:post_detail pkpost.pk %}生成链接这样即使后面改了URL前缀模板代码也不用改全栈开发的维护性大幅提升。6. 模板与页面渲染让数据在浏览器里“长”出来6.1 模板继承与页面骨架Django模板系统最重要的思想是“继承”。一个博客统一风格后所有页面应该有相同的头部导航、底部版权、侧边栏不需要每个页面各写一份HTML。传统做法是把重复代码复制粘贴后面改一处要改十个文件。模板继承则是先定义一个base.html作为骨架!-- templates/base.html -- !DOCTYPE html html langzh-cn head meta charsetUTF-8 title{% block title %}我的博客{% endblock %}/title link relstylesheet href... /head body nav a href{% url blog:post_list %}首页/a a href{% url blog:category_list %}分类/a {% if user.is_authenticated %} span{{ user.username }}/span a href{% url logout %}退出/a {% else %} a href{% url login %}登录/a a href{% url register %}注册/a {% endif %} /nav main {% block content %} {% endblock %} /main /body /html子模板只需要重写自己关心的块!-- templates/blog/post_list.html -- {% extends base.html %} {% block title %}最新文章{% endblock %} {% block content %} ul {% for post in posts %} li a href{% url blog:post_detail pkpost.pk %}{{ post.title }}/a span{{ post.created_at|date:Y-m-d }}/span span{{ post.category.name }}/span /li {% empty %} li暂无文章/li {% endfor %} /ul {% endblock %}{% extends %}是核心语法子模板会自动继承父模板所有结构然后用block覆盖对应内容块。理解模板继承后你就可以把导航、页脚、统计代码统一管理起来以后想加一个备案号只需要改一处。6.2 上下文字典、过滤器与模板标签视图返回给模板的数据统一封装在“上下文”里。对于FBV上下文就是一个字典{posts: queryset}对于CBVDjango会自动帮你组装这个字典你通过context_object_name控制上下文中的键名。模板中通过{{ posts }}读取。过滤器是模板格式化数据的利器。我常用的几个过滤器示例作用典型场景{{ post.created_at|date:Y-m-d }}日期格式化文章发布时间显示{{ post.content|truncatechars:200 }}截断文本列表页摘要{{ post.content|linebreaks }}把换行转成段落评论区展示富文本注意管道符|两边不能有奇怪的字符语法非常严格。过滤器和模板标签的区别一定要搞清楚过滤器是处理变量输出的标签{% if %}、{% for %}、{% url %}是编程逻辑控制。这是一个很容易搞混的界限也是面试常客。模板还有一个隐蔽但重要的特性自动转义。插入到HTML中的变量默认会被转义比如用户提交的内容里包含scripttag/scriptDjango会把它转成不执行的字符这是防止XSS攻击的基本保障。所以除非你明确知道自己在干什么否则不要使用|safe过滤器那是关闭转义。真实项目里XSS漏洞有不少都是因为开发者图省事滥用safe造成的。7. 用户注册登录与评论互动从单向展示到双向交互7.1 用户认证注册、登录、退出Django内置了完整的认证系统包含login、logout、authenticate函数。登录视图的逻辑是接收表单数据、校验用户名密码、登录用户、跳转。一个常见的注册视图如下from django.contrib.auth.models import User from django.contrib.auth import login, authenticate from django.shortcuts import render, redirect def register(request): if request.method POST: username request.POST[username] password request.POST[password] if User.objects.filter(usernameusername).exists(): return render(request, registration/register.html, {error: 用户名已存在}) user User.objects.create_user(usernameusername, passwordpassword) login(request, user) return redirect(blog:post_list) return render(request, registration/register.html)create_user和create的区别是新手必考点前者会把密码进行哈希加密再存入数据库后者则直接存明文。你如果在项目里用了User.objects.create(username..., password...)那你的用户表就是一座随时会被攻破的危楼。永远不要直接写明文密码这是不可触碰的红线。Django默认的密码哈希算法是PBKDF2还支持Argon2、bcrypt等更现代的内存硬性算法安全强度足够不需要自己造轮子。真正要在意的是不要在业务代码里手写密码加密逻辑框架已经给了最优解。登录视图直接用内置的LoginView能省事不少重定向到/accounts/profile/的行为可以通过LOGIN_REDIRECT_URL配置去修改LOGIN_URL login LOGIN_REDIRECT_URL blog:post_list LOGOUT_REDIRECT_URL blog:post_list7.2 评论表单与CSRF防护评论功能是博客互动的第一步也是安全风险高发点。Django处理POST请求时必须通过CSRF中间件验证否则会返回403。模板里的表单一定要加{% csrf_token %}form methodpost action{% url blog:post_detail pkpost.pk %} {% csrf_token %} {{ form.as_p }} button typesubmit提交评论/button /form这个{% csrf_token %}的作用是在表单里生成一个隐藏的CSRF token服务端提交时会比对请求中的token和session中存的值以此确认请求来自本站页面而不是第三方伪造站点。不要图省事加csrf_exempt装饰器那是关掉一道合法的保险。评论表单可以定义成一个Django Formfrom django import forms class CommentForm(forms.Form): content forms.CharField(widgetforms.Textarea, label评论内容) def post_detail(request, pk): post get_object_or_404(Post, pkpk, statuspublished) if request.method POST: form CommentForm(request.POST) if form.is_valid(): Comment.objects.create( postpost, authorrequest.user, contentform.cleaned_data[content] ) return redirect(blog:post_detail, pkpost.pk) else: form CommentForm() comments post.comments.select_related(author).all() return render(request, blog/post_detail.html, { post: post, comments: comments, form: form, })这里强制要求用户先登录才能评论但是request.user可能是匿名用户需要处理。最简单的方案是在视图里判断request.user.is_authenticated未登录则跳转登录页。更好的实践是评论表里允许匿名用户但在页面上通过验证码来防爬具体取舍取决于你的产品策略。8. 分页、搜索与性能优化让博客真正可用8.1 分页与简单搜索一个博客的文章数量会持续增长不可能一页渲染从头到尾。Django的ListView自带分页功能模板里渲染翻页控件{% if is_paginated %} div classpagination {% 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 %} /div {% endif %}对于FBV手动写分页逻辑也不复杂from django.core.paginator import Paginator def post_list(request): posts Post.objects.filter(statuspublished) paginator Paginator(posts, 10) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, blog/post_list.html, {page_obj: page_obj})get_page比get_page_or_404更宽容越界时会自动返回最后一页不会直接报错这个容错在用户体验层面很关键。搜索功能也是博客需求里的标配。简单场景下不需要引入Elasticsearch直接用ORM的contains查询即可def search(request): q request.GET.get(q, ) results Post.objects.filter(statuspublished, title__icontainsq) return render(request, blog/search_results.html, {results: results, query: q})icontains是大小写不敏感的模糊匹配底层对应SQL里的LIKE %keyword%。这种方式在几千篇文章的规模下体验尚可但一旦数据量到几万、几十万LIKE %keyword%会全表扫描。到这种阶段再考虑引入全文搜索引擎前期不必过度设计。8.2 查询优化select_related与prefetch_related写博客系统时最容忽略的是ORM查询优化。一个列表页要显示10篇文章的作者名和分类名初学者最容易这样写先查文章列表然后在模板里通过post.author.username访问作者名。这个写法背后隐藏着一个N1查询问题。查文章列表本身是1次SQL但渲染模板时每篇文章都要再查一次作者表10篇文章就是10次额外SQL全部加起来11次。如果每页50篇文章就是51次查询。真实项目里数据库压力就是这么被放大的。解决方法是在查询时告诉Django“连外层关联一起查出来”posts Post.objects.filter(statuspublished).select_related(author, category)select_related会把关联的单个对象外键、一对一用SQL的JOIN一次查出。多对多关系则要用prefetch_related它是先关联表查出一批数据再统一做第二次查询posts Post.objects.filter(statuspublished).prefetch_related(tags)怎么验证是否生效在开发环境开一个SQL日志中间件或者用Django Debug Toolbar一目了然能看见每次请求执行了多少条查询。我第一次发现自己列表页要打40多条SQL时震惊之余也深刻体会到全栈开发不只是“页面能显示数据”更要关心到查询效率这一层。9. 部署前的安全检查与常见故障排查9.1 必须做掉的安全项项目能跑通不等于可以上线。部署前我习惯把下面这几项逐一过一遍每一行都是踩坑换来的经验。SECRET_KEY是Django签名用的密钥它被用来加密session、生成Hash等绝对不能提交到Git仓库。正确做法是从环境变量读取import os SECRET_KEY os.environ.get(DJANGO_SECRET_KEY)DEBUG在生产环境必须设成False否则报错页面会展示完整的堆栈、环境变量、文件路径等于把系统安全白送给了攻击者。ALLOWED_HOSTS需要配置允许访问的域名列表DEBUG False ALLOWED_HOSTS [www.example.com, example.com]数据库账户也别用root高权限博客系统这种业务单独建一个只有该库读写权限的账号就够了。密码加密、CSRF、XSS转义这些前面已经提过这里再强调一次启用Django默认的安全中间件MIDDLEWARE [ # ... django.middleware.security.SecurityMiddleware, django.middleware.clickjacking.XFrameOptionsMiddleware, ]简单说XSS和CSRF是博客这类用户生成内容系统的两条最大安全红线Django已经在框架层面给足了保护你要做的不是绕过它们而是确保自己一直站在框架的保护伞下。9.2 高频报错与排查实录我把做博客系统过程中遇到的高频问题整理成一张速查表这些都是社区里反复出现的问题。现象原因解决方案管理员后台CSS消失DEBUGFalse后静态文件服务未配置运行collectstatic收集静态文件由Nginx/Whitenoise托管表单提交提示CSRF验证失败模板缺少{% csrf_token %}或Session过期确认模板已加上标签清理浏览器Cookie重试NoReverseMatch报错路由名错误或缺少参数检查{% url %}里的路由名是否和urls.py一致缺少的pk、slug参数是否传入中文内容乱码数据库连接编码不是UTF-8检查数据库字符集设置为utf8mb4迁移后模型字段变了但报错迁移文件混乱或有人手动改库生成新的迁移慎用migrate --fake页面访问404URL匹配失败确认path表达式是否准确动态参数类型是否匹配登录后重定向到/accounts/profile/默认登录重定向地址未配置在settings.py设置LOGIN_REDIRECT_URL还有一个我踩过值得写出来的坑时区问题。Django默认时区是UTC如果不在settings.py里配置就会导致auto_now_addTrue的创建时间比北京时间少8小时文章明明下午发布页面上却显示凌晨。正确配置TIME_ZONE Asia/Shanghai USE_TZ TrueUSE_TZ True表示以UTC标准时间存入数据库展示时再转换成TIME_ZONE。如果你改了时区后发现既有数据的时间不对需要确认旧数据是否按UTC存储再决定是否批量转换。结尾这个博客项目做下来我最深的感受是Django的全栈能力并不在于帮你把代码写完而在于它把开发中真正繁琐的部分——数据库迁移、认证、后台、安全中间件——都收敛成了规范化的解决方案让你能专注于业务本身。真正碰出经验值的地方恰恰是那些框架没有替你决定的决策点模型外键关系怎么定、查询怎么做优化、安全边界画在哪里、错误日志如何排查。把这些地方亲手踩一遍比看十遍文档都管用。最后送上一个做完整项目的小建议不要急着追求把所有功能“一步到位”先跑通“发文章、看列表、看详情、登录后才能评论”这条最小闭环后续再逐步补充标签、搜索、分页、部署。每完成一个环节就提交一次代码并写下当时的决策原因等整个项目结束往回翻你会看到自己从一个只会写CRUD的初学者慢慢变成能对系统整体负责的全栈开发者。这个过程正是做博客系统最珍贵的回报。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。