Django全栈开发入门:从零构建博客系统的完整实战教程

发布时间:2026/10/5 2:49:27
Django全栈开发入门:从零构建博客系统的完整实战教程 如果你问我新手学全栈开发第一个项目做什么最划算我的答案从来只有一个博客系统。别看现在短视频教程满天飞Django写博客这个组合至今依然是性价比最高的入门路径。一个博客系统几乎涵盖了你以后做任何Web应用都要用的核心能力——数据建模、增删改查、用户登录、模板渲染、分页列表、权限控制每一项都是硬通货。把这些练熟了你再去碰电商、CRM、内容管理系统会发现底层的套路几乎一样。这篇博文我准备带你从零开始搭一套能真正跑起来的Django博客系统。不是那种只贴代码让你抄的教程我会把每一步“为什么这么做”也讲清楚为什么要用虚拟环境、为什么外键要设计成CASCADE、为什么POST请求忘了加csrf_token就报403、为什么上线前要改ALLOWED_HOSTS。这些东西看着琐碎但正是区分“会照抄”和“真会做”的分水岭。内容会一直走到上线前要避开的坑为止代码我全部手写验证过你可以直接照着敲。1. 项目整体设计与思路拆解1.1 为什么博客系统是新手全栈开发的最佳练手项目很多人一上来就想做电商平台、社交软件我的建议是先冷静。功能越复杂的项目前期挫败感越强一个404都能让你怀疑人生。博客系统的妙处在于“麻雀虽小五脏俱全”它有列表页、详情页、分类、标签、作者登录、后台管理、分页查询还能顺手扩展搜索和评论几乎覆盖了Web开发的全部基础知识点。更重要的是博客系统的数据关系非常直观。一篇文章有一个作者属于一个分类可以打多个标签——这就是典型的一对多和多对多关系恰好能把Django ORM里外键和中间表的用法全部练到。你做完之后脑子里的CRUD思维就固化了任何业务系统本质都是对数据的增删改查再加上一层权限控制。先搞定这个后面学DRF、Celery、Docker这些东西都水到渠成。1.2 技术选型版本、数据库与为什么不选Flask技术栈我推荐Python 3.10以上 Django 4.2 LTS SQLite起步。Django 4.2是当前的长周期支持版本官方提供安全维护到2026年左右学习期间不用担心大版本升级带来的兼容问题。Python选3.10或3.11都行3.10以上的语法特性足够用环境也稳定。为什么选Django而不是Flask不是Flask不好而是Flask太“自由主义”了。你用Flask写博客要自己装数据库ORM、自己接登录组件、自己拼表单处理光是选型就要研究半天这对新手是巨大的隐性成本。Django则是“全家桶”路线ORM、Admin后台、认证系统、表单处理全部内置你只需要专注写业务逻辑。用交通工具类比Flask像自行车自由但所有零件自己配Django像带齐全家桶的房车重量大但开箱就能走。对于“全栈开发入门”这个目标来说开箱即用比轻量更重要。数据库直接用默认的SQLite。别一上来就上PostgreSQL或MySQLSQLite是文件型数据库零配置、零维护、跨平台开发调试效率极高。等你部署上线时因为所有数据库操作都走了ORM层把settings里的数据库配置改成PostgreSQL就行业务代码一行不用动。这就是ORM带来的最大红利。1.3 功能范围与目录结构规划我建议第一版只做核心闭环别把评论、搜索、点赞全部塞进来。功能一多你会陷入无休止的调试而学不到本质。第一版做这五个功能就够文章列表页按发布时间倒序展示带分页文章详情页展示标题、作者、分类、标签和正文分类筛选页按分类过滤文章登录/注销使用Django内置认证系统发布文章仅登录用户可用支持写标题、正文、选分类项目目录结构我习惯这样组织看起来清爽也方便后续扩展appmysite/ # 项目配置目录 settings.py urls.py wsgi.py blog/ # 博客应用 models.py views.py urls.py admin.py templates/ # 全局模板目录 base.html blog/ index.html detail.html post_form.html registration/ login.html static/ # 静态文件目录 css/ js/ manage.py记住一个原则一个项目里可以有多个app比如blog只管文章以后想加个留言板就再新建一个guestbook app。所有与业务无关的公共模板和静态文件放在项目根目录方便全局复用。2. 环境准备与项目初始化2.1 虚拟环境隔离是第一原则我见过太多新手把Django装进系统Python里过两个月装了一堆乱七八糟的包版本冲突到崩溃。虚拟环境的本质就是给你的项目造一个独立的小房间房间里的Python版本和第三方包只归这个项目管互不干扰。创建和激活的命令Linux/macOS和Windows略有区别# 创建虚拟环境会在当前目录生成 venv 文件夹 python3 -m venv venv # Linux/macOS 激活 source venv/bin/activate # Windows 激活 venv\Scripts\activate激活后命令行前面会出现(venv)前缀说明你现在处于虚拟环境中。这时候再装依赖就安全了pip install django4.2.*装完可以验证一下版本python -m django --version输出4.2.x就说明环境OK。新手最容易犯的错误是忘记激活虚拟环境就敲命令导致django-admin找不到、或者pip装到了全局环境。每次打开新终端窗口先看一眼有没有(venv)前缀养成习惯。2.2 创建项目和appstartproject与startapp的职责分工环境准备好后用到两个命令# 创建项目 django-admin startproject mysite # 进入项目目录 cd mysite # 创建博客应用 python manage.py startapp blog这里经常有人犯迷糊startproject和startapp到底啥区别我的理解是项目是整个网站的“总发动机”负责全局配置数据库、中间件、路由入口app是挂在发动机上的“功能模块”比如blog就是专门处理文章业务的模块。一个项目可以挂很多app就像一台服务器可以跑很多服务模块之间职责分明。创建完app后先去blog/models.py里写模型但先别急我们得把app注册到项目里。打开mysite/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, # 注册我们的博客应用 ]不注册的话Django感知不到这个app的存在后面的数据迁移和admin管理全都用不上。接着启动开发服务器验证一下python manage.py runserver浏览器访问http://127.0.0.1:8000/看到火箭起飞页面就说明项目框架搭好了。遇到8000端口被占用可以指定端口比如python manage.py runserver 8001。2.3 基础配置语言、时区和静态文件开发服务器跑起来后第一件事是修改settings里的语言、时区和静态文件配置。默认配置是英文和UTC时间这对中文博客不友好每次显示时间都差8小时。LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ TrueLANGUAGE_CODE zh-hans是为了让Admin后台和Django默认错误信息显示中文亲测体验好很多。TIME_ZONE Asia/Shanghai把时区设到东八区USE_TZ True表示数据库里存的时间统一为UTC展示时按Asia/Shanghai转换。这套配置是Django官方推荐做法新手不要为了省事直接设USE_TZ False否则以后接第三方API时时间会比较混乱。静态文件配置也顺手加上STATIC_URL static/ STATICFILES_DIRS [ BASE_DIR / static, ]然后在项目根目录建一个static文件夹。STATIC_URL是浏览器访问静态文件时的URL前缀STATICFILES_DIRS告诉Django去哪找项目级的静态资源。加上之后模板里才能用{% load static %}加载CSS和图片。3. 核心数据模型设计与ORM实操3.1 模型设计文章、分类、标签与作者的关系这是整个项目的核心。打开blog/models.py我来写博客系统最经典的四个模型分类、标签、文章以及复用Django内置的User作为作者。from django.db import models from django.contrib.auth.models import User from django.utils import timezone from django.urls import reverse class Category(models.Model): name models.CharField(分类名称, max_length50, uniqueTrue) slug models.SlugField(URL标识, uniqueTrue) class Meta: verbose_name 分类 verbose_name_plural 分类 def __str__(self): return self.name class Tag(models.Model): name models.CharField(标签名称, max_length20, uniqueTrue) class Meta: verbose_name 标签 verbose_name_plural 标签 def __str__(self): return self.name class Post(models.Model): title models.CharField(标题, max_length200) content models.TextField(正文) category models.ForeignKey( Category, verbose_name分类, on_deletemodels.CASCADE, related_nameposts ) tags models.ManyToManyField(Tag, verbose_name标签, blankTrue, related_nameposts) author models.ForeignKey( User, verbose_name作者, on_deletemodels.CASCADE, related_nameposts ) created_at models.DateTimeField(创建时间, defaulttimezone.now) updated_at models.DateTimeField(更新时间, auto_nowTrue) is_published models.BooleanField(是否发布, defaultTrue) class Meta: verbose_name 文章 verbose_name_plural 文章 ordering [-created_at] def __str__(self): return self.title def get_absolute_url(self): return reverse(blog:detail, kwargs{pk: self.pk})重点解释几个容易踩坑的设计决策第一外键on_deletemodels.CASCADE的含义是“级联删除”。也就是说删除一个分类该分类下所有文章会被一并删除。这在入门阶段是省事的设计但在真实项目里这个参数选择必须非常谨慎。如果以后做电商系统你绝不想因为删了一个商品分类而把几千条订单记录也删掉。到时候可以换成PROTECT受保护有外键引用时禁止删除或SET_NULL外键置空前提是字段设了nullTrue。第二related_name必须养成制定习惯。没有它默认在分类对象上用category.post_set访问文章列表有了它可以直接写category.posts.all()语义清晰得多。第三defaulttimezone.now和auto_nowTrue的区别一定要记牢。前者是“创建时取当前时间”后者是“每次保存都刷新为当前时间”。用反了就乱套比如把更新时间设成default那每次编辑文章时间都不会变化。第四get_absolute_url是Django老手约定俗成的写法定义它之后在Admin后台和模板里都能通过post.get_absolute_url()直接拿到详情页地址不用到处拼URL。3.2 迁移流程makemigrations与migrate的正确姿势模型写完后它不是马上生效的需要两步操作# 根据模型生成迁移文件 python manage.py makemigrations # 将迁移文件应用到数据库 python manage.py migratemakemigrations相当于把模型变更“拍快照”生成一个迁移文件这个文件就像一个补丁脚本记录了数据库怎么做变更。migrate才真正执行补丁、创建表和字段。很多人第一次接触会搞混我的类比方法是makemigrations是写代码migrate是运行代码。还有一个字符串字段的细节。slug字段我初始定义没有给max_lengthDjango 4.x里SlugField默认是50长度。如果你给文章生成了中文标题slug最好自己生成拼音或英文标识中文直接存进去会变成URL编码体验很差。分类和标签的slug字段会用来做SEO友好的URL比如/category/python/而不是/category/3/。迁移文件相当重要不要随便删。新手常见的翻车现场是模型改来改去数据库迁移乱了干脆把所有迁移文件删了重新makemigrations结果migrate报错说表已存在。正确做法是每次模型变更都正常生成迁移文件提交进版本库数据库和迁移文件保持一一对应。万一真出问题先python manage.py migrate app zero回滚再重新迁移。3.3 ORM增删改查重点说说删除对象这件事模型和数据库都就绪了我们进入Django执行查询的关键实操。打开终端进入Django的shell环境python manage.py shell这个shell是我们调试ORM的最强工具下面是一套完整的增删改查演示。增from blog.models import Post, Category, Tag from django.contrib.auth.models import User user User.objects.first() category Category.objects.create(namePython, slugpython) post Post.objects.create( title第一篇Django博客, content这里是正文内容。, categorycategory, authoruser, ) post.tags.add(Tag.objects.get_or_create(name入门)[0])查# 查全部文章 Post.objects.all() # 过滤只查已发布的文章 Post.objects.filter(is_publishedTrue) # 精确取一条不存在会抛DoesNotExist异常 Post.objects.get(pk1) # 统计数量 Post.objects.filter(category__namePython).count()改post Post.objects.get(pk1) post.is_published False post.save()重点来了删除对象。ORM删除有几种做法每个都有严格的使用场景。最直白的单条删除post Post.objects.get(pk1) post.delete()批量删除# 删除所有未发布的文章 Post.objects.filter(is_publishedFalse).delete()这里必须给你泼一盆冷水delete()是真实物理删除一旦执行不可恢复。而且由于外键是CASCADE删文章没问题但如果删分类对象下面所有文章都会被连带删除# 危险操作删除分类该分类下所有文章同时被删除 category Category.objects.get(slugpython) category.delete()我见过太多新人在生产环境干过这种事删完大呼数据找不回来。所以更稳妥的方式是“软删除”给Post加一个字段比如is_active models.BooleanField(defaultTrue)删除时不执行delete()而是把is_active置为FalsePost.objects.filter(pk1).update(is_activeFalse)查询时默认带上is_activeTrue的过滤条件删除操作变成逻辑隐藏随时可以恢复。电商平台、内容管理系统里几乎都在用这种方案核心原因就是数据太重要物理删除是最后手段。实操心得在执行任何删除之前先查一下会波及多少条数据。方法很简单Post.objects.filter(categorycategory).count()count()确认数量之后再动手。这种“先侦查后执行”的习惯能让你避开90%的误删事故。4. 视图、路由与模板让页面真正跑起来4.1 URL设计与视图函数请求如何到达你的代码项目的URL入口在mysite/urls.py我们需要先注册blog自己的路由from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(blog.urls)), ]然后在blog应用里新建blog/urls.pyfrom django.urls import path from . import views app_name blog urlpatterns [ path(, views.index, nameindex), path(post/int:pk/, views.detail, namedetail), path(category/int:category_id/, views.category_view, namecategory), ]这里的int:pk是Django最常用的路径转换器它会把URL里对应位置的值解析成整数传给视图函数。比如用户访问/post/3/视图函数就会收到pk3。用字符串做的话就是slug:slug这里我们先用pk。视图函数的逻辑我建议入门阶段先全部用函数视图不要一上来就上ListView、DetailView这些类视图。函数视图的请求→响应流程非常直观对你理解Django核心机制帮助极大。等函数视图写熟了再接触类视图的自动分页、自动表单处理就能理解那些“魔法”背后的原理了。写blog/views.pyfrom django.shortcuts import render, get_object_or_404 from django.core.paginator import Paginator from .models import Post def index(request): post_list Post.objects.filter( is_publishedTrue, is_activeTrue, ).select_related(author, category).prefetch_related(tags) paginator Paginator(post_list, 5) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, blog/index.html, {page_obj: page_obj}) def detail(request, pk): post get_object_or_404(Post, pkpk, is_publishedTrue, is_activeTrue) return render(request, blog/detail.html, {post: post}) def category_view(request, category_id): post_list Post.objects.filter( category_idcategory_id, is_publishedTrue, is_activeTrue, ) paginator Paginator(post_list, 5) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, blog/index.html, {page_obj: page_obj, category_id: category_id})两个细节值得展开。第一select_related和prefetch_related是解决N1查询问题的手段。如果你在列表页循环展示文章不预查询外键关系每显示一篇文章都要向数据库发几次请求查作者、查分类10篇文章就是几十次查询。加了这两个方法Django会用JOIN合并成少量查询数据量大时性能差距非常明显。第二get_object_or_404是“查不到就返回404”的简写比你自己try-except写一遍干净得多。它还支持传多个过滤条件比如pkpk, is_publishedTrue意味着即使URL手输一篇文章ID只要它没发布照样404。4.2 模板继承与动态渲染告别重复的HTMLDjango的模板系统跟其他语言不同它不是单纯插值而是有一套自己的语法比如{{ post.title }}是输出变量{% if %}、{% for %}是逻辑控制。最强大的功能是模板继承。先写基础模板templates/base.html!DOCTYPE html html langzh-hans head meta charsetUTF-8 title{% block title %}我的博客{% endblock %}/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css /head body nav classnavbar navbar-expand-lg navbar-dark bg-dark div classcontainer a classnavbar-brand href{% url blog:index %}我的博客/a div classms-auto {% if user.is_authenticated %} span classnavbar-text text-white me-2{{ user.username }}/span a href{% url logout %} classbtn btn-outline-light btn-sm注销/a a href{% url blog:post_create %} classbtn btn-primary btn-sm写文章/a {% else %} a href{% url login %} classbtn btn-outline-light btn-sm登录/a {% endif %} /div /div /nav div classcontainer mt-4 {% block content %}{% endblock %} /div /body /html子模板用{% extends base.html %}继承再{% block content %}填充自己的内容{% extends base.html %} {% block title %}文章列表 | 我的博客{% endblock %} {% block content %} {% for post in page_obj %} article classcard mb-3 div classcard-body h2 classcard-title a href{% url blog:detail post.pk %} classtext-decoration-none{{ post.title }}/a /h2 p classtext-muted small {{ post.created_at|date:Y-m-d }} · {{ post.author.username }} · {{ post.category.name }} /p p classcard-text{{ post.content|truncatechars:120 }}/p /div /article {% empty %} p暂时没有文章。/p {% endfor %} {% if page_obj.has_other_pages %} nav ul classpagination {% if page_obj.has_previous %} li classpage-itema classpage-link href?page{{ page_obj.previous_page_number }}上一页/a/li {% endif %} li classpage-item disabledspan classpage-link第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页/span/li {% if page_obj.has_next %} li classpage-itema classpage-link href?page{{ page_obj.next_page_number }}下一页/a/li {% endif %} /ul /nav {% endif %} {% endblock %}几个关键点{% url blog:index %}是URL反转即通过路由的name定位URL地址。这样做的好处是后端路由地址随便改模板完全不用动。千万不要在模板里硬编码/post/3/这种路径后患无穷。过滤器语法也很实用。{{ post.created_at|date:Y-m-d }}把datetime格式化成年月日{{ post.content|truncatechars:120 }}在正文里截取120个字符超出部分用省略号。冒号后面就是过滤器参数。细节提示如果你发现 extends 后整个页面空白十有八九是base.html里的block名称对不上。block名是连接父模板和子模板的桥梁一个写content一个写contents就静默失败了。4.3 分页查询文章多了也不怕分页用上一节视图里写到的Paginator直接搞定。Paginator(post_list, 5)表示每页显示5篇文章。page_obj是一个Page对象它提供了has_previous、has_next、previous_page_number、next_page_number、number、paginator.num_pages等属性全部在模板里以page_obj.xxx形式访问。分页要处理的另一件事是查询参数。如果你在分类页分页URL里已经有/category/3/了加上?page2之后变成了/category/3/?page2这没问题。但如果列表页还有其他筛选条件比如?keywordpythonpage2上一页下一页的链接需要把其他参数也带上通常用一个自定义模板标签处理。第一版博客不需要搞这么复杂但你要有这个概念分页URL拼接不是把?page简单加到末尾就完了。分页代码写完后去访问首页你会看到文章能正常展示、能翻页了。到这里博客系统的“读”功能已经完整下面我们要做“写”和“权限”。5. 登录认证与文章发布从“游客”到“作者”5.1 Django自带的认证体系别重复造轮子登录功能是博客系统的安全核心。Django的django.contrib.auth内置了完整的用户体系User模型、密码哈希、会话管理、登录状态校验全部开箱即用。你可能觉得“不就是登录吗”但自己从头写一个安全的登录系统非常困难密码加盐哈希、会话固定防护、CSRF这堆问题任何一个做不好都会被攻破。如果你没带过新手你可能不知道很多人为了“快速实现登录”自己写用户名密码存在数据库里明文保存、没有会话管理、没有任何安全防护这在生产环境就是裸奔。所以我的立场也非常明确用Django自带的auth这是标准答案也是唯一推荐答案。现在开始配置路由。在mysite/urls.py里把登录注销的视图接到Django内置视图上from django.contrib.auth import views as auth_views urlpatterns [ path(admin/, admin.site.urls), path(login/, auth_views.LoginView.as_view(), namelogin), path(logout/, auth_views.LogoutView.as_view(), namelogout), path(, include(blog.urls)), ]这两行代码就实现了登录和注销的完整逻辑。LoginView会处理表单提交、密码校验、会话建立等所有动作我们只需要提供一个模板给它。LogoutView更简单点击注销就会清除会话并跳转到登录页。5.2 自定义登录页面与权限控制Django的LoginView默认去registration/login.html这个路径找模板所以我们在templates/registration/目录下创建登录页{% extends base.html %} {% block title %}登录 | 我的博客{% endblock %} {% block content %} div classrow justify-content-center div classcol-md-6 h2 classmb-4登录/h2 form methodpost {% csrf_token %} {{ form.as_p }} button typesubmit classbtn btn-primary w-100登录/button /form p classmt-3a href{% url admin:index %}管理员入口/a/p /div /div {% endblock %}form.as_p是Django表单的快捷渲染方式把每个字段渲染成一个段落。登录成功后Django默认跳转到LOGIN_REDIRECT_URL。我们在settings里配置一下LOGIN_REDIRECT_URL /这样登录完成后回到首页。还有一个细节如果用户在未登录状态访问了需要登录的页面Django会自动跳转到登录页并在URL后面带?next/原页面地址/。登录成功后LoginView会自动读取next参数跳回去。这个机制我们模板里不用额外处理因为form提交时next字段会自动包含在POST数据里。权限控制的核心是login_required装饰器。未登录用户访问被装饰的视图会被重定向到登录页。在blog/views.py里加上from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect login_required def post_create(request): if request.method POST: title request.POST.get(title) content request.POST.get(content) category_id request.POST.get(category) # 这里做最基本的校验 if title and content and category_id: Post.objects.create( titletitle, contentcontent, category_idcategory_id, authorrequest.user, ) return redirect(blog:index) categories Category.objects.all() return render(request, blog/post_form.html, {categories: categories})这里要特别提醒一个新手常犯的安全错误不要把author字段从表单POST数据里直接取值。作者信息应当从会话里取request.user而不是用户自己在表单里提交。如果有人伪造POST请求把自己写成别人就是一个越权漏洞。同理任何涉及权限的操作都要用后端当前登录用户来校验前端传上来的用户ID一律不信任。request.user是当前会话对应的用户对象。在模板里我们也通过user.is_authenticated判断用户是否登录并据此显示“写文章”按钮或“登录”按钮。5.3 文章发布流程与后台管理发布文章的模板post_form.html注意一点Django模板里只有一个{% csrf_token %}是不够的它必须放在form标签内部。每次渲染页面时Django会生成一个随机的CSRF令牌放在隐藏字段里提交时Django会比对请求中的令牌与会话中存的令牌。如果匹配失败直接返回403拒绝请求。{% extends base.html %} {% block title %}写文章 | 我的博客{% endblock %} {% block content %} div classrow justify-content-center div classcol-md-8 h2 classmb-4写文章/h2 form methodpost {% csrf_token %} div classmb-3 label classform-label标题/label input typetext nametitle classform-control required /div div classmb-3 label classform-label分类/label select namecategory classform-select {% for c in categories %} option value{{ c.pk }}{{ c.name }}/option {% endfor %} /select /div div classmb-3 label classform-label正文/label textarea namecontent rows12 classform-control required/textarea /div button typesubmit classbtn btn-primary发布/button /form /div /div {% endblock %}后台管理写起来可能比想象中要简单。打开blog/admin.pyfrom django.contrib import admin from .models import Post, Category, Tag admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display (title, author, category, created_at, is_published) list_filter (is_published, category, created_at) search_fields (title, content) date_hierarchy created_at admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display (name, slug) admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display (name,)然后创建超级管理员账号python manage.py createsuperuser按提示输入用户名、邮箱、密码密码要8位以上且不能过于简单。之后访问/admin/你就能用后台管理文章、分类和标签。Admin后台不只是方便更是调试的好帮手想快速造数据、看模型字段、修改用户状态后台点一点就完成比命令行shell省事得多。实操体会博客系统的“门槛”其实是从登录功能开始的。做这一步你会接触POST请求、CSRF防御、会话、装饰器、内置认证视图这些是Web开发的核心安全点。把它完全理解了比多写100篇CRUD代码都值钱。6. 新手最容易踩的坑CSRF、静态文件与迁移问题速查6.1 CSRF攻击原理与防护为什么忘记加csrf_token就报403新手第一次用Django写表单几乎都会遇到这个问题POST请求提交时报403页面提示“CSRF verification failed”。很多人第一反应是“我代码是不是写错了”其实真正的原因是Django默认开启了CSRF防护而你忘了告诉它这个请求是安全的。CSRF跨站请求伪造的通俗解释你在A网站登录了账号会话还在有效期内然后你打开了B网站的恶意网页B网站向A网站偷偷发了一个转账POST请求浏览器带着你的登录Cookie就发出去了服务器以为是你的合法操作。CSRF防护的核心机制就是服务器给每个合法表单一个唯一的令牌提交时若令牌不匹配就认为请求不合法并拒绝。Django对CSRF令牌的校验可以类比小区取快递取件码就是csrf_token门卫就是服务器只有取件码对上号的快递才放行。所以在Django里每个使用POST提交的表格都必须包含{% csrf_token %}DTL渲染它时会在表单里生成类似下面这样的隐藏字段input typehidden namecsrfmiddlewaretoken value随机字符然后Django把value与用户会话中存的令牌比对一致才放行。别试图绕过它也别因为嫌麻烦关掉CSRF中间件。生产环境关闭CSRF防护等于把自己家的门锁卸掉没有任何一个合格的Web框架会推荐你这么做。6.2 静态文件与部署配置清单本地开发时Django在DEBUGTrue的情况下会自动帮你服务静态文件。但当你把DEBUG设为False准备上线时Django的静态文件服务会停掉这是所有新手第一次部署时都会遇到的迷之404页面有HTML没有CSS。解决办法是先运行python manage.py collectstatic这个命令会把所有app以及STATICFILES_DIRS里的静态文件收集到STATIC_ROOT指定的目录下然后由Nginx这类Web服务器专门负责托管。在settings里加配置STATIC_ROOT BASE_DIR / staticfiles部署前还有三个必须处理的配置项。第一ALLOWED_HOSTS这个列表指定允许访问你的域名。开发时设置成ALLOWED_HOSTS [localhost, 127.0.0.1]上线时要加上你的真实域名。如果留空或漏配访问时Django会直接报DisallowedHost这是安全机制防止有人拿别的域名指向你的服务器。第二SECRET_KEY不要写在代码仓库里。这个key是Django用于加密会话、签名令牌的核心密钥泄露出去的后果是攻击者可以伪造会话和签名。正确的做法是通过环境变量注入import os SECRET_KEY os.environ.get(DJANGO_SECRET_KEY, dev-only-insecure-key)开发环境用默认值生产环境在服务器上设置真实的环境变量。第三数据库配置。小流量个人博客用SQLite完全扛得住但如果你预期有较大并发建议部署时切换到PostgreSQL。配置方法就是在settings里把DATABASES字典换成PostgreSQL的参数ORM的代码一行都不用动。6.3 常见疑难杂症排查速查表做这个项目过程中我总结了最常出现的几类问题给你做个速查表遇到问题对照着查症状根本原因解决办法POST表单提交报403 CSRF模板里忘了{% csrf_token %}在form标签内加上csrf_token页面有HTML但没有CSS样式DEBUGFalse后Django不再服务静态文件运行collectstatic由Nginx托管修改模型后数据库没变化运行makemigrations/migrate两步都要执行缺一不可删除分类后文章全没了外键on_delete用了CASCADE改用PROTECT或SET_NULL或软删除文章时间显示比本地晚8小时USE_TZTrue但TIME_ZONE没设置设置TIME_ZONEAsia/Shanghai未登录访问写文章页登录后没跳回原页登录模板没保留next参数确认form里包含next字段默认POST会带上点击生成的网址变了但页面没反应runserver没重启重启开发服务器Django不会热更新某些配置migrate报“表已存在”错误删除了迁移文件但数据库没回滚不要删迁移文件用migrate app zero回滚分类页URL访问404路由里没注册category路径检查blog/urls.py里是否有path(category/int:category_id/)最后一个常见问题值得单独说模板里判断用户权限时注意user.is_authenticated是属性不是方法不要写成user.is_authenticated()。这是从旧版Django沿用下来的历史包袱新版虽然兼容但新手照抄旧博客代码时经常在这里翻车。排查问题的方法论也很重要。遇到报错别慌先看终端里的完整报错堆栈Django会把出错的代码行、最近的请求信息、具体异常原因全部打印出来。理解一下是哪一层出的问题是路由层URL匹配不上、视图层Python代码报错、还是模板层模板语法错误。80%的新手问题都能靠读报错信息解决。剩下的再配合python manage.py check做系统检查或是在视图里加一行print()看变量的值定位很快。关于日志我再多一句嘴生产环境项目最好配置日志文件不然出问题只能对着控制台干瞪眼。简单办法是在settings里加LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: WARNING, class: logging.FileHandler, filename: logs/django.log, }, }, root: { handlers: [file], level: WARNING, }, }这在入门阶段可能用不上但提前有这个意识等你要上线的时候就知道它的价值了。最后再分享一个我个人的习惯我做这类练手项目一定会写一个seed脚本来造测试数据。手动在admin页面一篇篇点文章太痛苦了写一个命令几分钟就能填充几十条数据方便调试分页、分类、标签这些功能。比如可以在blog/management/commands/seed.py里用Django的管理命令框架写一套造数据的逻辑运行python manage.py seed就能一键生成。这个习惯我保留至今不管项目大小造测试数据的速度直接影响调试效率。这个博客系统做完你手上就有了一个完整的项目骨架。后面想加什么功能评论、搜索、RSS、文章置顶都是往已有框架里填东西而已。关键不是功能多华丽而是你真正理解了请求怎么进来、数据怎么流转、权限怎么控制、安全怎么保障。把这套逻辑吃透Django全栈开发这条路你就站上起跑线了。