资讯详情 Django入门项目:手把手带你用博客系统掌握全栈开发
📅 2026/10/8 10:23:42
1. 为什么博客系统是Django入门的第一站1.1 全栈开发在Django语境下到底指什么很多初学者对全栈开发这四个字有误解以为一定要先啃完Vue、React再加一套Spring Boot才算数。其实在Django的世界里全栈的含义非常朴素一个框架同时管好数据库、后端逻辑和前端页面渲染你不需要在项目初期引入第二个技术栈就能做出来一个具备完整功能的Web应用。我自己的经历是这样的Python基础学完之后跟着各种教程写爬虫、写脚本感觉挺顺手但一打开浏览器就蒙了——数据怎么变成页面用户提交的表单怎么进数据库删除一条内容为什么还要写SQL这些问题堆在一起Django是当时成本最低的解法。它的核心思路叫全家桶ORM负责和数据库打交道URL路由负责把请求分发给对应代码模板系统负责生成HTML自带的Admin后台直接帮你管理数据甚至连用户登录、表单校验都有现成方案。这意味着你可以用这一套工具完整走一遍Web开发的主流程。我用博客系统当入门项目还有一个现实原因它足够小小到周末两天能跑通又足够全全到几乎覆盖了Django日常开发80%的操作。你不需要纠结这个功能是不是多余因为博客天然就需要这些能力一个项目把所有环节串起来学的知识才真正落在自己手里。1.2 博客系统恰好覆盖了完整开发链路博客系统看似简单但仔细拆一下功能点你会发现它正好踩在Django最核心的几块能力上博客功能对应的Django知识点文章列表与详情模型定义、QuerySet查询、视图函数、模板渲染后台录入文章Admin站点、模型注册新建、编辑、删除文章ModelForm、表单校验、POST请求处理、重定向按分类/标签浏览ForeignKey、ManyToManyField、filter查询分页展示Paginator分页组件部署上线settings配置、静态文件、ALLOWED_HOSTS这些东西不是零散的知识点而是层层递进的你要先建数据模型才能谈查询和删除要先配好路由才能写视图要先写视图才能做模板。博客系统把这些环节天然地串联成一条线每学会一个环节上一个环节的知识又会被加深一遍。所以如果你是刚学完Python基础、想用Django做第一个真实项目的同学博客系统几乎是量身定做的练手项目。接下来我会按我自己实操的顺序从环境搭建一路写到增删改查闭环中途会把我踩过的坑和不方便写进文档的细节都放出来照着敲就能跑通。2. 环境搭建从Python到项目骨架的完整步骤2.1 虚拟环境先给自己圈一块干净地盘先说一个很多人第一次装Django都会踩的坑直接用系统Python的pip装包。用不了多久你就会发现某个项目要Django 3.2另一个项目要Django 5.0再加一个要旧版requestspip一升级版本就冲突最后只能重装Python。虚拟环境解决的就是这个问题——每个项目一套独立的解释器和第三方包互不干扰。操作非常简单。先确认Python版本Django 4.x和5.x都要求至少Python 3.10建议直接用3.10以上版本太低的话后面会碰到生态兼容问题。然后# 创建虚拟环境目录 venv python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate看到命令行前面出现(venv)标记就说明环境激活成功了。这时候再用pip安装任何包都只会装进这个项目自己的环境里。我习惯在每个Django项目目录下建一个venv文件夹不进版本控制别人clone下来也要各自重新建虚拟环境。2.2 安装Django与创建项目虚拟环境激活后安装Djangopip install django这里我不建议上来就指定版本装最新的而是先装当前稳定版。你可以执行pip show django查看版本号如果是最新的5.x系列直接用就行。等跑通项目之后再在项目里生成requirements.txt锁住版本pip freeze requirements.txt这样别人一条pip install -r requirements.txt就能还原你的环境。创建项目有两种方式差别在一个点# 方式一在当前目录生成manage.py django-admin startproject blog_project . # 方式二生成一个blog_project子目录 django-admin startproject blog_project我推荐方式一。注意最后的那个点它的含义是以当前目录作为项目根目录生成的目录结构更干净后面部署和整理都方便。执行完后项目里会多出一个manage.py和blog_project配置目录blog_project/ ├── manage.py ├── blog_project/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.pymanage.py是项目的遥控器后续的启动、建数据库、建App全都靠它。settings.py是全局配置urls.py是根路由入口。接着创建应用。博客本身作为业务模块单独放到一个App里。python manage.py startapp blog执行后会生成blog/目录里面有models.py、views.py、admin.py、migrations/等文件。注意这时候项目还不知道你已经创建了这个App不手动配置的话Django不会加载它的任何代码。2.3 App与settings配置最容易漏掉的那行代码打开blog_project/settings.py找到INSTALLED_APPS列表把blog加进去INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, blog, # 注册自己的App ]这里有两个配置我建议一上来就改掉不然后面看后台全是英文、时间总是差8小时LANGUAGE_CODE zh-hans TIME_ZONE Asia/ShanghaiUSE_TZ保持默认的True就好不要因为看不懂就去关掉Django的时区处理是经过设计的后续项目复杂了你会感激这个默认值。还有一个困扰新手的配置ALLOWED_HOSTS。本地开发时它默认是空的runserver也能跑一旦你部署到服务器上用IP或域名访问就会报DisallowedHost。这是Django的安全机制只允许访问指定的域名。本地开发可以先加一个星号图省事ALLOWED_HOSTS [*]但上生产环境之前一定要改成具体的域名或IP这是个安全习惯问题。配置改完先跑一下自带的迁移为Django内置的用户、会话等功能建好数据表python manage.py migrate python manage.py runserver浏览器打开http://127.0.0.1:8000/看到火箭页面说明项目跑起来了。我的习惯是在这个阶段先不急着写代码花五分钟把目录结构和每个文件的用途搞明白后面每写一行代码都知道它在哪层工作。3. 数据模型与迁移把地基打牢再盖楼3.1 Post模型字段设计思路博客系统最核心的数据就是文章。在blog/models.py里定义Post模型我建议直接从最少但完整的字段集开始from django.db import models from django.contrib.auth.models import User from django.utils import timezone class Post(models.Model): title models.CharField(标题, max_length200) content models.TextField(正文) created_at models.DateTimeField(创建时间, defaulttimezone.now) updated_at models.DateTimeField(更新时间, auto_nowTrue) author models.ForeignKey(User, verbose_name作者, on_deletemodels.CASCADE, related_nameposts) class Meta: ordering [-created_at] verbose_name 文章 verbose_name_plural verbose_name def __str__(self): return self.title逐字段解释我的设计理由title用CharField必须写max_length这个参数不仅是约束还会影响数据库字段类型和后台校验。content用TextField文章正文长度不可控用大文本字段。以后如果要做富文本或Markdown存的内容结构会变但字段类型不变所以这一步不亏。created_at用defaulttimezone.now而不是auto_now_addTrue。两个都会自动填当前时间但auto_now_add只在创建时写入且之后不可改在后台里你无法手动修正错误时间defaulttimezone.now更灵活。updated_at用auto_nowTrue每次保存自动刷新。author用ForeignKey关联到User这是一个一对多关系一个用户可以写多篇文章。related_nameposts让反向查询变直观比如user.posts.all()能取到这个用户的所有文章。ordering [-created_at]让所有查询默认按创建时间倒序省得每次都要手动order_by。__str__方法一定要写。它决定文章对象在后台列表和调试时显示成什么不写的话你会看到一堆Post object (1)根本分不清是哪篇。如果想让博客更像样可以再加Category和Tag模型用外键和多对多字段关联。但我强烈建议入门阶段先不要加每多一个外键和多对多字段后台录入、查询逻辑、模板展示都会复杂一层。第一版跑通了再回来加难度低很多。3.2 makemigrations与migrate到底做了什么模型定义好之后执行两条命令python manage.py makemigrations python manage.py migrate很多新手把这两个命令当成同步数据库的固定动作但理解它们各自的角色很重要。makemigrations是生成迁移文件。它对比模型定义和数据库中已有的表结构把差异写成Python文件存放在App的migrations/目录下。你可以打开blog/migrations/0001_initial.py看看里面全是CreateModel操作这就是Django对数据库的操作说明书。migrate是执行迁移文件真正把变更应用到数据库。它读取所有尚未执行的迁移文件逐个执行并记录已执行状态。如果只改了模型但不执行这两条命令你会在查询时报OperationalError: no such table: blog_post或者字段变了但数据库还是老结构。我踩过的一次教训是为了图快跳过makemigrations结果数据库里根本没有blog_post这张表查了半天才意识到迁移没执行。还有一条经验迁移文件是项目的版本历史不要手动删。你可以在团队协作时随时makemigrations生成新的增量迁移但删除旧迁移文件很容易导致线上数据库和代码状态不一致。migrate完成后可以进入数据库看一眼用命令python manage.py shell在shell里创建一个测试对象from django.contrib.auth.models import User from blog.models import Post user User.objects.first() post Post.objects.create(title第一篇测试文章, content内容占位, authoruser) print(post.id, post.title, post.created_at)这一步能立刻验证模型、数据库、ORM三层是否打通比反复刷新页面排查问题快得多。3.3 Admin后台与本地数据录入数据模型通了下一个需求是往里面填数据。虽然有shell但真实场景里写文章不可能用命令行。Django自带的Admin后台就是为了解决这个问题。在blog/admin.py里注册模型from django.contrib import admin from .models import Post admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display (title, author, created_at, updated_at) search_fields (title, content) list_filter (created_at, author) ordering (-created_at,)然后创建超级管理员账号python manage.py createsuperuser按提示输入用户名、邮箱、密码。启动服务访问http://127.0.0.1:8000/admin/用超管账号登录就能看到文章管理界面。这里我要多讲两句list_display的用法。默认注册模型后后台列表只显示__str__返回的内容很单调。加上list_display后文章列表直接展示标题、作者、时间一眼扫过去就能管理。search_fields让标题和正文支持模糊搜索list_filter提供时间、作者筛选。这三个配置是后台体验的关键也是我日常开发的标准配置。打下了数据层的基础后面所有页面功能都建立在能通过ORM拿到数据这个前提上。4. 视图、路由与模板让页面真正跑起来4.1 URL路由设计入口长什么样数据库里有文章了但浏览器还看不到。Django的工作流是请求进来 → URL路由匹配 → 交给视图函数 → 视图操作数据并渲染模板 → 返回HTML。路由配置分两层。项目根路由blog_project/urls.py里把blog的地址分发到App自己的路由文件from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(blog.urls)), ]然后新建blog/urls.py定义博客自身的路由from django.urls import path from . import views urlpatterns [ path(, views.post_list, namepost_list), path(post/int:pk/, views.post_detail, namepost_detail), ]这里两个知识点很关键int:pk是路径转换器int:限定参数必须是整数pk是传来的文章主键。访问/post/3/时Django会把3作为pk参数传给视图函数。namepost_list是路由的名字。模板里用{% url post_list %}生成链接时靠的是这个名字而不是路径字符串。这样后期改路径模板里的链接不用跟着改。新手容易忽略的一点模板、视图、redirect里引用URL一律用name不要硬编码路径。我见过太多人写死/post/3/这种路径改一次路由全站404。4.2 视图函数从数据库到页面的中间人在blog/views.py里写两个最基础的视图from django.shortcuts import render, get_object_or_404 from .models import Post def post_list(request): posts Post.objects.all() return render(request, blog/post_list.html, {posts: posts}) def post_detail(request, pk): post get_object_or_404(Post, pkpk) return render(request, blog/post_detail.html, {post: post})第一个视图的逻辑取全部文章渲染列表模板把数据传进去。第二个视图用get_object_or_404——文章不存在时直接返回404页面而不是报DoesNotExist异常。我强烈推荐用它替代手动写try...except代码干净行为也符合预期访问一篇不存在的文章就该返回404而不是500错误。这里先讲函数视图是因为对新手来说它最直观一个请求进来一个函数处理返回一段响应。等对数据流熟悉了再切到类视图通用视图ListView、DetailView你会发现它们其实就是把函数视图的重复逻辑封装了一遍。渲染模板时用到的render(request, 路径, {...})是快捷函数等效于loader加载模板再HttpResponse返回。日常开发都用它。4.3 模板继承与页面渲染模板在blog/templates/blog/目录下创建。Django约定以App名建一层目录防止多个App有同名模板时互相覆盖。先建base.html作为所有页面的骨架!DOCTYPE html html langzh-hans head meta charsetUTF-8 title{% block title %}我的博客{% endblock %}/title /head body header h1a href{% url post_list %}我的博客/a/h1 /header main {% block content %}{% endblock %} /main /body /html{% block %}是模板继承的关键子模板填充同名block即可。base.html负责全站统一的布局子模板只写差异部分。再写post_list.html{% extends base.html %} {% block title %}文章列表{% endblock %} {% block content %} {% for post in posts %} article h2a href{% url post_detail post.pk %}{{ post.title }}/a/h2 p classmeta作者{{ post.author }} | {{ post.created_at|date:Y-m-d H:i }}/p p{{ post.content|truncatewords:30 }}/p /article {% empty %} p还没有文章。/p {% endfor %} {% endblock %}post_detail.html类似把整个正文显示出来{% extends base.html %} {% block title %}{{ post.title }}{% endblock %} {% block content %} article h2{{ post.title }}/h2 p classmeta作者{{ post.author }} | 发布时间{{ post.created_at|date:Y-m-d H:i }}/p div{{ post.content|linebreaks }}/div pa href{% url post_list %}返回列表/a/p /article {% endblock %}到这里页面已经能正常显示文章列表和详情了。模板里用到的{% url post_detail post.pk %}会按pk值生成真实链接|date:和|truncatewords:是模板过滤器前者格式化时间后者截断正文——这些语法不复杂但都是日常写模板的高频操作。启动服务访问首页看到自己写的文章出现在页面上这个成就感是hello world给不了的。5. ORM查询与对象删除日常开发最高频的操作5.1 QuerySet查询从取全部到精准过滤查数据是Django开发里最常用的操作理解了QuerySet的机制很多诡异问题就迎刃而解。Post.objects.all()返回的不是列表而是一个惰性的QuerySet——它不会立刻去数据库查询只有在你遍历、取值、判断时才会真正执行SQL。惰性带来的好处是你可以不断链式调用# 不会立刻执行 posts Post.objects.all() # 加上排序和过滤仍然没有真正查询 posts posts.filter(author__usernamezhang).order_by(-created_at) # 直到遍历时才执行数据库查询 for post in posts: print(post.title)常用查询写法我整理成了一张表需求写法取所有文章Post.objects.all()按条件过滤Post.objects.filter(authoruser)排除条件Post.objects.exclude(statusdraft)取单个对象Post.objects.get(pk1)字段模糊匹配Post.objects.filter(title__icontainsDjango)按年份过滤Post.objects.filter(created_at__year2025)排序Post.objects.order_by(-created_at)链式组合Post.objects.filter(...).exclude(...).order_by(...)只要某个字段的值Post.objects.values_list(title, flatTrue)特别注意filter和get的区别。filter返回QuerySet允许0个、1个或多个结果get只允许恰好一个结果否则抛异常。你在视图里用get_object_or_404时其实也是这个逻辑没用就404多于一个还是异常。还有一个常见需求文章详情页里的上一篇/下一篇。可以用时间比较来做previous_post Post.objects.filter(created_at__ltpost.created_at).order_by(-created_at).first() next_post Post.objects.filter(created_at__gtpost.created_at).order_by(created_at).first()created_at__lt是小于__gt是大于再配合first()把QuerySet截断成单条。这种写法比先把所有文章取出来在内存里找要优雅得多也让数据操作尽量下沉到数据库层面。5.2 删除对象两种方式别用错删除操作在Django里有两种触发方式我专门讲一下是因为它们的行为和返回值不一样。第一种对单个模型实例删除。post Post.objects.get(pk1) post.delete()这种方式适合删除一个明确的对象比如详情页里的删除这篇文章。返回的是(1, {blog.Post: 1})这样的元组第一个数是总共删了多少条记录第二个数是按模型分类的删除数量明细。第二种对QuerySet批量删除。Post.objects.filter(author__usernamezhang).delete()这条命令会删除所有符合条件的文章同样返回删除数量明细。批量删除是直接构造SQL执行不会逐个调用模型上的delete()方法所以如果你重写了模型的delete()做额外清理批量删除时这部分逻辑不会触发。删除还有一个必须理解的连锁反应外键的on_delete参数决定了关联数据的命运。我们在Post.author上用了CASCADE含义是删除用户时该用户的所有文章会被一起删除。如果不想这样可以用PROTECT有文章则拒绝删用户或SET_NULL把作者字段置空前提是字段允许为空。选择哪种取决于业务规则里删掉一个作者他的文章怎么办。下面这段代码演示了一个完整的删除场景from django.contrib.auth.models import User from blog.models import Post # 单条删除 post Post.objects.get(pk3) result post.delete() print(result) # (1, {blog.Post: 1}) # 批量删除删除标题中包含草稿的文章 deleted Post.objects.filter(title__contains草稿).delete() print(deleted) # (2, {blog.Post: 2})5.3 查询删除中的经典坑这一节是我自己踩坑经验的合集每条都是真实发生过的坑一get()返回多个对象。如果你没有给数据加唯一性约束查询条件又能匹配多条记录get()会抛MultipleObjectsReturned。我遇到过给文章标题加get(title某某)的情况撞上两篇同标题文章直接500。解决办法是业务上确保唯一或者改用filter().first()。坑二DoesNotExist异常位置很隐蔽。Post.DoesNotExist挂在模型类上不是ORM模块里很多人写成from django.core.exceptions import DoesNotExist其实也通用但更地道的写法是try: post Post.objects.get(...) except Post.DoesNotExist:。坑三批量删除误伤。手滑写了Post.objects.all().delete()博客文章全部清零这种事故我和身边朋友都经历过。Django没有撤销机制所以执行高危删除前建议先执行同条件的count()确认数量或者把查询条件打印出来核对一遍。坑四删除后忘记处理关联引用。文章被删除后如果其他地方还存着它的ID或者生成过缓存就会出现引用不到数据的页面。最简单的防御是使用get_object_or_404它天然兜底这种场景。坑五修改数据不生效。新手常犯的错是只改字段不调用save()post.title 新标题 # 忘了 post.save()页面刷新没变化save()才是真正执行UPDATE语句的地方。同理只调save()但不makemigrations导致字段结构不匹配也会报错。6. ModelForm与完整增删改查从展示到全栈闭环6.1 ModelForm快速生成表单列表和详情都跑通了下一步是让博客能写——要有新建、编辑、删除文章的入口。手写HTML表单并手工校验数据可以做但Django提供了一套更省力的方案ModelForm。在blog/下新建forms.pyfrom django import forms from .models import Post class PostForm(forms.ModelForm): class Meta: model Post fields [title, content] widgets { title: forms.TextInput(attrs{class: form-control}), content: forms.Textarea(attrs{rows: 10, class: form-control}), }ModelForm会根据Post模型自动生成表单字段fields指定包含哪些字段widgets控制渲染样式。这里没有含author因为作者不应该由用户在表单里随便填而是在视图里从登录状态写入。表单的价值不只是省去手写HTML它把校验一起封装了CharField的max_length会触发长度校验必填字段会自动提示更重要的是is_valid()结果可以直接决定视图逻辑的走向。6.2 新建、编辑、删除的视图处理流程以新建文章为例视图的标准写法是from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from .forms import PostForm from .models import Post def post_create(request): if request.method POST: form PostForm(request.POST) if form.is_valid(): post form.save(commitFalse) post.author request.user post.save() messages.success(request, 文章发布成功) return redirect(post_detail, pkpost.pk) else: form PostForm() return render(request, blog/post_form.html, {form: form})流程拆开是三步GET请求时渲染空表单POST请求时用提交数据构造表单实例并校验校验通过则保存并重定向到详情页。这里form.save(commitFalse)是关键——它先把数据构建成Post对象但不写库让我们有机会补上author字段再真正save()。很多教程直接form.save()完事作者字段就空了报错都不知道错在哪。编辑视图几乎一样区别是初始值来自已有对象def post_edit(request, pk): post get_object_or_404(Post, pkpk) if request.method POST: form PostForm(request.POST, instancepost) if form.is_valid(): form.save() messages.success(request, 文章已更新) return redirect(post_detail, pkpost.pk) else: form PostForm(instancepost) return render(request, blog/post_form.html, {form: form})第二个参数instancepost让表单回填已有内容保存时更新而不是新建。删除用一个小表单提交来触发比直接在页面放URL链接安全避免爬虫或误点击GET请求就删数据def post_delete(request, pk): post get_object_or_404(Post, pkpk) if request.method POST: post.delete() messages.success(request, 文章已删除。) return redirect(post_list) return render(request, blog/post_confirm_delete.html, {post: post})对应的URL路由也要注册urlpatterns [ path(, views.post_list, namepost_list), path(post/int:pk/, views.post_detail, namepost_detail), path(post/new/, views.post_create, namepost_create), path(post/int:pk/edit/, views.post_edit, namepost_edit), path(post/int:pk/delete/, views.post_delete, namepost_delete), ]这里我用了login_required装饰器拦截未登录用户因为写文章天然需要登录态。你需要在settings.py里配一行LOGIN_URL /admin/login/让未登录用户被重定向到登录页。模板里post_form.html需要处理表单渲染和错误展示{% extends base.html %} {% block content %} h2{% if form.instance.pk %}编辑文章{% else %}新建文章{% endif %}/h2 form methodpost {% csrf_token %} {{ form.as_p }} button typesubmit保存/button /form {% endblock %}{% csrf_token %}是Django的安全要求所有POST表单都必须带上否则报CSRF verification failed。form.as_p是快捷渲染方式自动把每个字段包在p标签里并显示校验错误。到这一步博客系统的增删改查已经完整了列表看文章、详情看正文、页面里能新增和编辑、误操作能删除。一个全栈闭环雏形已经成型。6.3 分页、搜索与页面体验功能闭环之后有两个体验问题立刻会暴露文章多了以后列表页一次加载所有数据又慢又长没有搜索想找旧文章只能翻页。分页用Django自带的Paginator改一下post_list视图from django.core.paginator import Paginator def post_list(request): posts Post.objects.all() paginator Paginator(posts, 5) # 每页5篇 page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, blog/post_list.html, {page_obj: page_obj})模板里循环对象从posts换成page_obj底部加分页导航{% for post in page_obj %} !-- 文章内容 -- {% 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 %}搜索可以继续沿用列表视图加一个q参数def post_list(request): posts Post.objects.all() q request.GET.get(q) if q: posts posts.filter(title__icontainsq) posts posts.order_by(-created_at) # 后面照常分页模板里放一个搜索表单即可form methodget action{% url post_list %} input typetext nameq value{{ request.GET.q }} placeholder搜索文章标题 button typesubmit搜索/button /form到这一步博客系统已经不是能用而是好用了一点。分页和搜索看着不起眼却是真实项目里躲不开的标配功能它们用到的Paginator和filter(__icontains)以后在任何Django项目里都会反复出现。7. 入门阶段的踩坑清单与后续进阶方向7.1 新手最常遇到的五个报错及其根因我把带过的学生和自己在博客项目里高频遇到的报错整理成一张表每个都附上根因和解法报错信息根因处理方法OperationalError: no such table: blog_post模型建了但没迁移执行makemigrations和migrateTemplateDoesNotExist模板路径不对或未放在正确目录检查templates目录层级确认文件名拼写CSRF verification failed表单缺{% csrf_token %}所有POST表单的form里加上该标签DisallowedHost访问域名不在ALLOWED_HOSTS修改settings.py中的ALLOWED_HOSTSAttributeError: Post object has no attribute ...视图/模板里字段名拼错对照models.py确认字段名注意别用auth等内置名前三个我在正文里都讲过了这里重点说第四个和第五个。DisallowedHost是Django部署时必踩的坑因为它只在非本地访问时报错本地127.0.0.1永远正常导致刚上服务器时一头雾水。第五个字段不存在则多半是大小写或下划线写错自己的代码让新人排查起来反而最费时间——我的经验是先print(post.__dict__)看对象到底有哪些属性定位最快。还有一个隐藏很深的坑改了模型字段、也迁移了但代码里还在用旧字段查询。Django不会立即报错而是等你真正执行查询时抛FieldError这种延迟爆炸很折磨人。预防方法只有一条每次改模型后顺手全局搜一下旧字段名的引用。7.2 规范与性能上的两个提醒项目做完第一版后我建议花半小时做两件事它们直接影响后续开发体验。第一件事是检查查询性能。博客规模小感觉不出来但文章多了以后列表页会越来越慢罪魁祸首往往是外键关联的N1查询。比如列表页要显示作者名如果每次都通过post.author查询用户表10篇文章就多查10次数据库。解决办法是在视图里加一行posts Post.objects.select_related(author).all()select_related通过SQL的JOIN一次把关联对象查出来省掉N次查询。这个知识点现在理解可能有点早但记住这条规则不亏列表页需要展示外键关联字段时优先加select_related。第二件事是整理依赖和静态文件配置。项目根目录里维护好requirements.txt部署时把DEBUG设为False并在settings.py里配置好静态文件收集。这一步等真正部署时用得上但习惯从早期养成后面不会被本地能跑、线上崩折磨。7.3 下一步该往哪走从入门到进阶的路线参考博客系统跑通之后很多人会陷入接下来学什么的迷茫。我给一条亲身验证过的路线第一个方向把现有项目变复杂。给文章加分类和标签实现按分类浏览给用户加注册登录和编辑个人资料给文章加评论功能——评论涉及多层级的外键设计和权限控制难度刚好够挑战。这些功能都是博客系统的自然延伸但每个都能让你把Django的某一块能力练扎实。第二个方向切换更高级的写法。把函数视图改成ListView、DetailView、CreateView这类通用类视图。我自己是从函数视图切过来的体会非常深类视图确实封装了大量重复逻辑两三行代码顶原来一个函数但前提是你已经理解它背后做了什么所以要现在这个阶段再做。第三个方向做前后端分离。用Django REST Framework把博客改造成API前端用Vue或React对接。这时候Django全栈就变成了Django后端是另一种职业路线的起点。不过建议先把传统渲染的博客做扎实因为ORM、建模、认证这些底层能力是共通的。博客项目最宝贵的地方在于它是你自己的、完整的东西你能解释每一个环节为什么这么写知道哪里可能出错也清楚下一步改哪条功能。带着这种状态去学新东西效率远高于盲目刷教程。最后分享一个我坚持到现在的习惯每跑通一个功能就在项目里写一小段注释记下为什么这样做每踩一个坑记录下来以后不再犯。博客系统只是个开始这套方法能让你的下一个项目走得稳得多。