ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Django开发博客系统:全栈入门到上线的完整实践

2026/9/28 5:15:36 拓冰建站 浏览量
用Django开发博客系统:全栈入门到上线的完整实践 最近好几个朋友找我聊同一个话题想学后端不知道从哪下手。我通常给出的答案是同一个——拿Django做一个博客系统而且是认真的那种不是照着教程敲一遍就算完而是真刀真枪把一个能跑、能写、能部署的博客从头到尾做出来。用全栈开发这个词来描述这个项目最贴切因为你会在里面经历数据库设计、后台逻辑、页面渲染、权限控制、消息推送和部署上线的完整链条。这篇文章就照着我实际做项目的过程把每个环节里那些网上教程不肯明说的细节和坑一次性说透。先说结论Django全栈开发入门用博客系统练手教材级的合适。博客功能不复杂但麻雀虽小五脏俱全用户、文章、分类、标签、评论该有的数据关系全都有同时它又是你身边真实在用的产品写起来有动力做完了也真的有东西拿得出手。下面从设计思路、模型、视图、进阶功能到疑难杂症排查一条线讲完。1. 项目整体设计与思路拆解1.1 为什么是Django为什么是博客先说个现实情况市面上的后端框架多得很Python圈子里Flask轻量、FastAPI新潮但全栈入门这件事上Django的意见几乎是压倒性的。原因很简单Django自带的admin后台、ORM、表单系统、模板引擎、认证体系这些别的框架要靠各种第三方库拼出来的东西Django默认就全套配齐。一个没写过几行后端代码的新手用Django创建项目后立刻就能看到后台管理界面这种即时反馈对建立信心非常重要。你拿博客系统练手选型时还要考虑一点Django的“batteries included”自带电池特性跟博客系统的需求是天然匹配的。文章要存数据库Django的ORM帮你处理所有SQL要区分作者和管理员自带的auth就能用写页面需要把数据填充进HTML模板引擎直接手到擒来用户提交评论要校验数据和安全验证Django的Form类一步到位。说句实在话第一次用Django做完一个完整功能的时候你会明显感到那些“最小可行产品”阶段最折磨人的琐碎事都被框架兜住了。至于“全栈”这两个字很多刚入门的朋友容易理解偏。全栈不是指你一个人维护所有代码级别的东西就够了而是你对整个请求链路有感知浏览器发送请求路由分发给视图视图调模型层读写数据库再交给模板渲染最后通过HTTP返回到前端页面期间穿插表单验证、会话管理、权限判断。做完博客你这条链路会完整过一遍以后再学任何框架都不会再是盲人摸象。1.2 先把需求边界卡死别让项目烂尾我给新手的第一个劝告是做项目前先把手和脑子按住把需求写清楚。什么叫博客系统的MVP最小可行产品我建议你第一版只做这些用户注册、登录、退出文章的新增、编辑、删除、列表和详情页展示文章分类与标签评论功能未登录可看登录后可评分页后台管理Django自带admin即可不用自己写列表页这些还不够先留住。像Markdown编辑器、全文搜索、文章浏览量统计、RSS订阅、实时消息通知、多用户协作这些全部放到第二个版本再做。我自己第一次做博客的时候就是因为在第一版里塞了太多需求导致核心功能迟迟不完整最后项目停在“文章可以发布了但搜索还没写完”的尴尬状态那确实是个让人很泄气的经历。这里还可以把产品规划的思路拉出来聊一下MVP的价值不在于“功能少”而在于“闭环完整”。你把用户的注册和文章的发布这条核心路径跑通之后加任何功能都是往这个稳定的底盘上添砖心里是踏实的。反过来如果第一版就试图搞十几个功能堆叠任何一个环节报错都会让你怀疑整个设计出了问题。博客系统的功能边界定了之后接着考虑技术侧的选型。数据库选SQLite起步就好因为Django零配置就能跑但如果这个博客准备放线上长期用建议还是把数据库换成PostgreSQL后面写搜索功能时也能用上PG自带的全文检索性能和质量都有保障。这一步不用纠结太久因为Django的ORM屏蔽了数据库差异切库很多时候只是改个配置加个依赖的问题。2. 环境准备与项目骨架搭建2.1 Python虚拟环境和Django安装动手第一步不是pip install django而是先把Python环境隔离好。很多人图省事直接在系统环境装包几个月后你会发现不同项目之间的依赖版本互相踩踏一个项目要Django 3.2另一个要Django 5.0系统里装谁都会让另一个炸。这个场景我见过太多次了所以把这一步写在了最前面。用venv做虚拟环境是标准做法。具体步骤你直接照下面这个来# 创建虚拟环境注意是venv不是virtualenv——Python 3自带的就好 python3 -m venv venv # 激活虚拟环境注意Windows跟macOS/Linux的命令不一样 # macOS / Linux: source venv/bin/activate # Windows: # venv\Scripts\activate # 激活后命令行前面会出现(venv)字样这时再装依赖 pip install --upgrade pip pip install django装完以后可以用python -c import django; print(django.get_version())确认一下版本。我建议装最新稳定版而不是dev版如果是新手安装4.2 LTS或5.0这种LTS版本更稳——长期支持版本有官方持续维护查资料时也最不容易遇到“我这个版本怎么跟教程不一样”的问题。注意虚拟环境激活是一次性的每次打开新的终端窗口都需要重新激活否则你敲的django-admin命令根本找不到。这一步是新手中招率最高的没有之一。2.2 创建项目与第一个App别学网上那些混乱目录Django项目的组织方式有两种流派一种是把所有代码扔进一个项目目录里另一种是创建多个App按功能模块拆分。博客这种规模的小项目你可能会看到很多教程用一个叫blog的App塞下所有代码其实这样做也能跑但后续功能一旦变多代码就会变成一个巨大的包非常难维护。推荐的做法是先创建项目再按业务边界创建App。让我演示一下标准的创建过程# 创建项目注意最后的点号表示在当前目录下生成 django-admin startproject blog_project . # 这样项目配置和manage.py会在当前目录而不是多套一层目录 # 创建两个App一个管博客内容一个管用户配置 python manage.py startapp blog python manage.py startapp users创建完后的目录结构大概是这样的blog_project/ ├── manage.py ├── blog_project/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── asgi.py │ └── wsgi.py ├── blog/ │ ├── models.py │ ├── views.py │ ├── admin.py │ └── ... └── users/ ├── models.py ├── views.py └── ...“blog”这个App只管文章、分类、标签、评论这些内容模块“users”这个App只管用户相关的扩展。两者的划分天然贴合业务边界不会让你迷茫。创建好App之后要去blog_project/settings.py里把这两个App注册到INSTALLED_APPS列表中不然Django压根不知道有这个App存在。顺带还可以把语言和时区改成中文时区# settings.py INSTALLED_APPS [ # Django 自带的 App django.contrib.admin, django.contrib.auth, # 认证 django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 自定义 App blog, users, ] # 中文和国内时区 LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai # 注意USE_TZ建议不要改成False后面做日期相关统计时更容易踩坑 USE_TZ True完成之后执行数据库迁移并启动开发服务器验证项目能不能跑python manage.py migrate python manage.py runserver浏览器打开http://127.0.0.1:8000如果看到Django默认的火箭页面说明整个项目骨架已经通了。每次创建项目后做一次完整的migrate是好的习惯因为Django内置的auth和session等模块需要对应的数据表才能工作。3. 模型设计和ORM核心操作3.1 从需求到模型先把关系图画清楚博客系统的数据模型说难不难但它恰好覆盖了编程中最典型的三种数据关系。我在设计时先不看代码而是先画关系图。作者与文章一对多一个作者写多篇文章文章归属一个作者分类与文章一对多一个分类下多篇文章一篇文章只属于一个分类文章与标签多对多一篇文章可以有多个标签一个标签也能挂到多篇文章上文章与评论一对多一篇文章有多个评论一条评论只属于一篇文章用户与评论一对多一个用户可以发多条评论关系画清楚之后models.py的编写就是水到渠成的事。我特别建议从项目一开始就自定义User模型哪怕你现在什么都不改。原因很简单Django默认的User模型有些字段是写死的你项目上线以后再想加一个“作者简介”或者“头像”字段就得用Profile表去绕非常别扭。提前自定义一个用户模型后面扩展起来随时随地加字段。我这里的做法是在users这个App里自定义一个继承AbstractUser的用户模型from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): 自定义用户模型后面加字段很方便 # 示例bio models.TextField(blankTrue, verbose_name个人简介) pass然后在settings.py中指定使用它AUTH_USER_MODEL users.User之后在blog这个App里写核心的模型from django.conf import settings from django.db import models from django.utils import timezone class Category(models.Model): name models.CharField(分类名, max_length100) class Meta: verbose_name 分类 verbose_name_plural verbose_name def __str__(self): return self.name class Tag(models.Model): name models.CharField(标签名, max_length100) class Meta: verbose_name 标签 verbose_name_plural verbose_name def __str__(self): return self.name class Article(models.Model): title models.CharField(标题, max_length200) content models.TextField(正文) author models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, verbose_name作者 ) category models.ForeignKey( Category, on_deletemodels.PROTECT, # 注意这里用的是PROTECT verbose_name分类 ) tags models.ManyToManyField(Tag, blankTrue, verbose_name标签) created_at models.DateTimeField(创建时间, defaulttimezone.now) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: verbose_name 文章 verbose_name_plural verbose_name ordering [-created_at] def __str__(self): return self.title class Comment(models.Model): article models.ForeignKey(Article, on_deletemodels.CASCADE, related_namecomments) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE) content models.TextField(评论内容) created_at models.DateTimeField(评论时间, defaulttimezone.now) class Meta: verbose_name 评论 verbose_name_plural verbose_name ordering [created_at] def __str__(self): return f{self.user.username} 在 {self.article.title} 下的评论这段代码里有几个设计决策我逐个解释为什么第一Category的on_deletemodels.PROTECT。这跟很多教程用的CASCADE不一样。如果用CASCADE你在后台删掉一个分类这个分类下所有文章会被连带删除。对一个博客来说这是灾难级的操作代价。PROTECT的保护机制就是只要还有文章关联到这个分类数据库就拒绝删除这个分类让你先处理文章。这个设计也许会让你在进行删除操作时多一步考虑但能把误删风险按在摇篮里。第二Comment里用了related_namecomments。这个参数给外键反向查询起了个名字。有了它在模板或者视图里可以用article.comments.all()取到一篇文章的全部评论而不是默认的article.comment_set.all()。代码读起来自然得多。第三created_at用defaulttimezone.nowupdated_at用auto_nowTrue。前者是新增记录时写入当前时间后者是每次修改时自动更新时间。这个“创建时间/更新时间”双字段的配置非常常见养成习惯写上是好的。3.2 ORM查询与删除操作解谜filter().delete()不是你想的那样网上有一个高频搜索词是“django执行查询-删除对象”可见这个点让很多人卡过。Django的ORM删除操作看起来就是个.delete()方法实际上背后有好几个版本理解错了会出大事。先看两种最常见写法# 方式一查一个对象再删除 article Article.objects.get(pk1) article.delete() # 方式二批量删除 Article.objects.filter(category__id3).delete()这两种方式的触发链路完全不一样。单个对象的.delete()会走模型实例级的逻辑Django会依次调每个关联对象处理外键级联等操作。而filter().delete()是QuerySet的批量删除Django会优先在SQL层面直接执行DELETE语句效率高很多但也有一个很多人不知道的副作用——批量删除不会触发表的delete()方法的自定义逻辑。举例来说我在博客里重写过Article.delete()用来在删除文章时清理它关联的本地图片文件和缓存。用单个对象删除时这段逻辑能正常执行但如果在后台管理里勾选多篇文章执行“删除所选”走的是QuerySet批量删除的路子自定义的delete()方法压根不会被执行。图片文件就会变成残留垃圾。那么如果想在批量删除里也触发自定义逻辑怎么办可以重写ModelAdmin的delete_queryset或者干脆不要用批量删除一个对象一个对象地删for article in Article.objects.filter(category__id3): article.delete()这样效率会差一些但能确保每条自定义逻辑都被执行。业务上删除操作本来就该谨慎这点性能开销是值得的。另一个更安全的选择是软删除。我现在维护的项目里Article模特加了一个is_active字段is_active models.BooleanField(是否展示, defaultTrue)软删除的做法是“从不真正删除数据”只是把is_active置为False查询时默认过滤掉不展示的文章。这样做有三个好处误删恢复容易改一个字段就能回来数据库里的数据完整统计时不丢历史不会触发级联删除的连带损失查询时配合自定义管理器把软删除的过滤逻辑统一收口日常使用的代码也不会更复杂。这是我在真实项目里会向团队推荐的做法简单可靠。ORM查询还有一个值得认真对待的点是“查询的条数”。博客列表页要展示文章标题、作者名、分类名如果不用select_related你每取一篇文章都要多查一次作者表和分类表50篇文章会变成“1条文章查询 50次作者查询 50次分类查询”整整101条SQL这在本地开发感受不到线上迁移后数据库负载会明显升高。正确的姿势是一开始就预取关联字段# 文章列表预取作者和分类 articles Article.objects.select_related(author, category).all() # 详情页同时预取评论的用户避免评论循环中逐条查用户表 article Article.objects.select_related(author, category) \ .prefetch_related(comments__user) \ .get(pkpk)这里的逻辑很容易类比select_related是在SQL里用JOIN的方式一次性把关联表的数据查出来适用于一对一、多对一这种“单个对象关联另外单个对象”的场景prefetch_related则是先查主表、再查关联表用Python内存中的对应来完成“预加载”适用于一对多、多对多这种“一个对象关联多个对象”的场景。这两个方法在列表页和详情页里配合使用查询效率天差地别。4. 视图、模板与静态文件4.1 千万不要一上来就沉迷类视图Django的视图写法有函数视图FBV, Function-Based View和类视图CBV, Class-Based View两种。很多教程直接教ListView、DetailView这些花哨的类视图美其名曰“更省代码”。但我会强烈建议新手先踏踏实实写函数视图。原因很简单类视图是Django给你做好的“填空游戏”很多逻辑都被封装在get_queryset、get_context_data这些方法里。你填对了看起来很厉害一旦要自定义某个边角行为比如“列表页某些文章不展示某些文章要单独标记”你很可能卡在不知道重写那个方法的困境里。函数视图的逻辑是一行行写出来的你看到的就是执行的顺序出了问题排查起来非常直接。我用函数视图写一个文章列表页给你感受一下from django.core.paginator import Paginator from django.shortcuts import render from .models import Article def article_list(request): # 只展示is_active为True的文章按时间倒序 articles Article.objects.filter(is_activeTrue) \ .select_related(author, category) paginator Paginator(articles, 10) # 从GET参数里拿页码默认第1页 page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, blog/article_list.html, {page_obj: page_obj})这一段把“过滤条件”“预加载”“分页”三件事写在明面上谁看了都能懂。如果换成ListView代码是变短了但分页逻辑藏在Django内部的MultipleObjectMixin里一旦结果不对你还要去翻框架源码。所谓“可读性就是可维护性”对一个练手项目是这样对一个生产项目更是这样。再写一个简单的详情页视图加上阅读计数的逻辑from django.shortcuts import get_object_or_404, render from .models import Article def article_detail(request, pk): article get_object_or_404( Article.objects.select_related(author, category), pkpk, is_activeTrue, ) # 阅读计数进程外缓存后面再用真实计数的更重方案替代 article.views_count 1 article.save(update_fields[views_count]) comments article.comments.select_related(user).order_by(created_at) return render(request, blog/article_detail.html, { article: article, comments: comments, })注意这里用了update_fields[views_count]这个小技巧。它让save()只更新指定的字段不会覆盖掉其他字段也不会因为updated_at是auto_now而每次都变更数据库开销小很多。这个经验是真的踩坑之后才学会的——一开始直接article.save()导致每次浏览页面都触发整行更新多敲几个接口就发现数据库连接满了。模板方面基础的渲染语法用起来不难关键是别在模板里写太重的逻辑。比如“文章列表页如果用户没登录显示请先登录如果是管理员显示置顶按钮”这类需求不要在模板里搞一堆{% if %}嵌套Django的模板不是用来承载业务逻辑的地方干干净净的模板永远是首选。4.2 静态文件配置那个让人崩溃的坑标题里提到的热词“vscode写img标签在django的static文件中显示不了”简直说到了历代Django新手的痛处。刚跑起来项目时你兴冲冲在模板里写了个img src/static/img/logo.png刷新浏览器一片空白控制台一片红。这个问题的根源其实跟VSCode没什么关系是Django静态文件的机制没搞明白。先理清两个配置项这两个是Django静态文件的基石。以blog_project/settings.py为例STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ]STATIC_URL是URL前缀浏览器请求的地址是/static/xxxSTATICFILES_DIRS是开发模式下Django在磁盘上查找静态文件的目录列表。你建立了static目录后模板里要这样写才规范{% load static %} img src{% static img/logo.png %} altlogo不要硬编码/static/img/logo.png因为当你调整部署时的STATIC_URL硬编码路径会全部失效而{% static %}标签会跟随配置自动变化隐私顾虑和可迁性都解决了。如果你的文件放在static目录下却仍然显示不出来按下表一步步排查排查点检查内容目录结构是否正确确认文件位置在项目根目录/static/img/logo.png不是static/static/img/logo.png是否加载static模板标签模板文件顶部有没有写{% load static %}是否重启开发服务器新增静态文件后开发服务器不一定自动刷新手按 CtrlC 重启一次浏览器缓存强制刷新快捷键 CtrlCmdR / CtrlF5排除缓存干扰开发服务器是否在运行静态文件404先去看终端有没有请求到/static/img/logo.png那STATIC_ROOT又是做什么的这个配置在开发阶段用不到它是留给生产环境的聚合目录。执行python manage.py collectstatic时Django会把所有App和系统自带的静态资源统一复制到这个目录交给Nginx或WhiteNoise服务。很多新手把STATIC_ROOT和STATICFILES_DIRS搞混两个都配成同一个目录执行collectstatic时直接报错。记住一句话STATICFILES_DIRS是“源码位置”STATIC_ROOT是“产物位置”两者不能相同。如果涉及用户上传的图片比如文章封面、用户头像那是另一套叫media的系统MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media开发模式下还需要在项目级urls.py里手动挂载媒体文件from django.conf import settings from django.conf.urls.static import static urlpatterns [ # 你的路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这个配置在生产环境里不是这么用的生产环境的媒体文件要交给对象存储比如OSS、S3或专门的静态服务器来服务否则服务器一重启或扩容上传的图片就丢得到处都是。这块等到做部署时再展开也不迟但开发阶段先把 media 挂载好不然上传功能一测就报404。5. 让博客更“全栈”实时推送与权限设计基础的博客跑通以后你可能会想让博客更“像一个全栈项目”。这里我把两个热点方向拆开讲一个是后台数据实时推送到前端WebSocket另一个是权限控制RBAC。这两个是搜索热度非常高的话题同时也是区分“能跑”和“专业”的分水岭。5.1 Django Channels后台数据实时推送到前端普通的Http请求走的是“请求-响应”模式前端要拿新数据就得刷新页面或者用AJAX定时轮询。定时轮询笨但简单WebSocket才是做实时体验的正解。Django本身不含WebSocket需要引入Django Channels这个扩展。我以“新文章发布后所有在线浏览用户实时收到提示”这个场景为例演示一下基本思路。首先安装依赖pip install channels channels-redis然后把项目由ASGI模式托管。把blog_project/asgi.py改成Channels的协议栈import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack import blog.routing os.environ.setdefault(DJANGO_SETTINGS_MODULE, blog_project.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack( URLRouter(blog.routing.websocket_urlpatterns) ), })接着在settings.py里配置channel layer这里需要Redis作为消息队列的载体CHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: { hosts: [(127.0.0.1, 6379)], }, }, }说白了channel layer就是一个跨进程的消息总线。视图进程比如你通过admin发布文章的那个进程和WebSocket连接的常驻进程不是同一个进程它们靠Redis中转数据。这正是“后台有数据前端能推送”的桥梁。然后写一个consumerimport json from channels.generic.websocket import AsyncWebsocketConsumer class BlogConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name blog_updates await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def article_created(self, event): # 收到group里的消息后发送给前端 await self.send(text_datajson.dumps({ type: new_article, title: event[title], url: event[url], }))发布文章的视图里在保存文章后给group发一条消息from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( blog_updates, { type: article_created, title: article.title, url: article.get_absolute_url(), } )这个功能做完的效果是用户开着博客页面你在后台发布新文章他的页面不刷新就弹出一条“有更新”。这种站在全栈角度的体验远比“刷新页面才看到文章”有趣得多也是体现全栈能力的加分项。注意Channels和Redis这套组合是为了解决高并发下的实时推送服务的。如果你只是做个人博客流量不大轮询其实完全够用。选型的原则永远是“别拿重锤敲核桃”但把这个方案学会以后做站内信、聊天室、消息通知模块方法论可以平移过去。5.2 权限与RBAC从登录装饰器到角色控制博客系统至少有两类人普通用户和管理员。普通用户只能写自己的文章、评论文章管理员可以删除任何用户的文章、审查评论。Django自带的auth系统已经提供了基础的登录、登出、权限判断新手最常用的是装饰器from django.contrib.auth.decorators import login_required login_required def article_create(request): # 只有登录用户可以创建文章 passlogin_required会重定向到登录页这个机制本身没什么问题但“能登录”不等于“有权限”。一个普通登录用户能不能删除别人的文章login_required回答不了这个问题。于是文章的所有权校验通常会长这样from django.core.exceptions import PermissionDenied def article_delete(request, pk): article get_object_or_404(Article, pkpk) if article.author ! request.user and not request.user.is_staff: raise PermissionDenied(你没有权限删除这篇文章) article.delete()这段逻辑能覆盖小项目的需求但它有一个明显的天花板当权限规则变多比如“运营专员只能编辑分类不能删除文章”“编辑只能改自己写的文章管理员能改所有人的”散落在每个视图里的 if 判断会变得难以维护。这就是需要RBACRole-Based Access Control基于角色的访问控制的时机。RBAC的核心抽象是三张表用户表、角色表、权限表再加用户-角色、角色-权限两张关联表。用户不直接跟权限挂钩而是通过角色间接获得。用博客系统举例角色可以设计为游客只能看已发布文章注册用户可以写文章、改自己文章、发表评论编辑能改所有文章、审核评论但不能删分类管理员拥有所有权限Django自带的auth模型其实已经提供了基础角色is_staff、is_superuser和permissions表的概念。如果你想要更精细的角色权限体系可以在模型层扩展出UserRole和RolePermission两张表。但在中小型项目中我建议先借助Django自带的django.contrib.auth的权限框架Django在Meta里可以声明模型权限视图里用user.has_perm(blog.delete_article)判断。这样比强行造RBAC轮子可靠得多因为Django自带权限表已经跟admin后台深度集成了。真要动手扩展RBAC我的经验是先从“2个角色”起步——普通用户、管理员——把is_staff和所有权的判断用好、写清楚你会发现这个小系统的权限边界已经覆盖了绝大部分场景。等到需要第三个角色时再去设计角色表让前期的权限代码平滑迁移过去。这里的核心思想是“权限这种东西建模越晚越好”因为你永远预估不准业务会怎么变化。6. 常见问题与排查技巧实录6.1 高频报错速查表开发阶段最容易踩的问题我把它们整理成一份速查表保存下来能省不少查资料的功夫。症状根因解决方法静态文件全是404忘记配STATICFILES_DIRS或路径写错在settings中确认STATICFILES_DIRS指向真实的static目录上传图片显示不了media目录未挂载或路径错误检查MEDIA_ROOT、MEDIA_URL确保开发模式urls.py中mount了mediaCommandError: No installed app with label blogApp没有加入INSTALLED_APPS到settings里确认App名拼写正确数据库表不存在no such table模型改了但没迁移执行python manage.py makemigrations再python manage.py migrateCSRF验证失败表单中没有{% csrf_token %}在模板form中加上{% csrf_token %}标签迁移时报字段不存在有旧的迁移文件残留看生成迁移记录必要时用--fake处理但操作前务必备份数据库404与500页面样式丢失生产环境没有collectstatic在部署阶段执行python manage.py collectstatic外键关联数据被连带删除on_delete设置了CASCADE关键业务数据换PROTECT或SET_NULL删除前先想被关联的数据是否要保留明明保存了文章列表没有新内容模板里过滤了没有匹配的条件或缓存先直接在数据库里确认数据再检查QuerySet的过滤条件这张表之外还有一个特别容易忽略的问题Django项目的日志。开发时你看到的报错都是带有堆栈信息的页面但线上环境为了安全会把DEBUG设为False此时报错变成一片空白。所以我建议项目一开始就配置日志把blog_project/settings.py里加一段基础日志记录LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: ERROR, class: logging.FileHandler, filename: BASE_DIR / logs / error.log, }, }, loggers: { django: { handlers: [file], level: ERROR, propagate: True, }, }, }日志比报错页面更可靠。很多排查工作如果没有日志你连从哪查起都不知道。6.2 我踩过的几个坑你最好别踩第一个坑项目写了一半才想起自定义User模型。那时注册登录已经跑通几十号测试用户的数据都躺在表里。要在中途换自定义User模型Django会警告跟auth_user表冲突迁移几乎没法平滑处理最后只能删库重建。这件事让我长记性AUTH_USER_MODEL一定要在第一次migrate之前就配置好宁可先用Pass继承也不要让项目置于尴尬境地。第二个坑把所有外键全设成了CASCADE。我管理文章时随手把某个分类误删了后来发现这个分类下几十篇文章全部人间蒸发。当时文章的正文用的不是富文本备份没有历史快照恢复成本极其痛苦。从那以后我给自己立了条规矩业务关键数据的关联字段只要被关联的数据删不得一律用PROTECT或SET_NULL。分类、标签这类基础数据宁可让删除操作先失败也不能让内容被无声地连带清掉。第三个坑DEBUGFalse之后静态文件四百全暴毙。我把博客部署到服务器上设置了DEBUG False瞬间所有页面纯文本裸奔。Django在非DEBUG模式下默认不再服务静态文件这不是“bug”是安全设计。生产环境要么用Nginx直接serve静态文件要么用WhiteNoise中间件托管。我对新手推荐WhiteNoise因为它零配置就接入Django顺手到不行pip install whitenoise然后settings里把WhiteNoise中间件插到SecurityMiddleware后面MIDDLEWARE [ django.middleware.security.SecurityMiddleware, whitenoise.middleware.WhiteNoiseMiddleware, # 其他中间件... ]最后执行collectstatic静态文件就由应用自己托管不需要单独配置Nginx了。这里还提醒一点collectstatic是在部署机上执行的反复部署记得清空过时的静态文件否则会残留旧资源。写到这里我自己的体会是做博客系统这个项目技术难度从来不在任何一个单独的知识点上而在“把这些知识点按正确顺序串起来”这个过程本身。你会在做数据模型时重新审视表的关系在配静态文件时搞明白开发与生产环境的差异在加WebSocket时打破对“Django只能做同步请求”的固有认知——这种既有知识广度又有深度的练习真的很难找到一个比博客系统更好的入口。如果你想在第一个博客版本之外继续折腾我建议按这个顺序扩展先加个RSS订阅几行代码的事但很显专业再把文章正文改成支持Markdown渲染搜索python-markdown库接入成本很低然后尝试把系统部署上线给朋友看看你写的文章。最后如果兴趣还在给这套系统写几个接口给一个移动端或小程序用全站链路就跑通一大半了。我的建议很朴素别急着追求高级技术把一个简单博客系统做得完整、稳定、代码整洁你的Django全栈之路就真正站稳了。