最近一个朋友问我,新人学 Django 到底该怎么下手,我直接把他拉到了我写了大半年的博客系统代码仓库前。这个项目既是我的练手作品,也是我接触全栈开发的起点:博客系统刚好覆盖了前端页面、后端接口、数据库建模、用户登录、权限控制这些核心环节,规模又不至于大到劝退。如果你也想找一条“能跑通、能分享、能积累实战经验”的入门路径,对着这个项目一步步敲,会比刷十遍教程都管用。
我在这里就把整个博客系统从项目设计到最终上线前的关键节点完整拆一遍,包括技术选型、数据库模型、ORM 的增删改查、登录功能实现,还有新手最容易栽进去的坑。所有代码细节都是我在实际操作里验证过的,你照着做就能复现一套能用的博客。
1. 项目设计与搭建准备
1.1 为什么选 Django 做博客系统
选择 Django 不是因为它是最新潮的框架,而是因为它把全栈开发里最琐碎的部分替你料理好了。对比过 Flask、Express 这类轻量框架后你会发现,博客系统需要的用户认证、后台管理、数据库迁移、表单处理,几乎每一项都得自己搭配第三方库,而 Django 自带全套。用一句话概括:Django 是一个“自带电池”的框架,它默认给你组装好了脚手架,你只需要专注业务逻辑本身。
具体到博客系统,Django 自带的 Admin 后台可以让你在几个小时之内拥有一个可管理文章、用户、分类的可视化界面,这在项目早期价值巨大。而且它的 ORM 屏蔽了不同数据库之间的差异,无论是开发阶段的 SQLite 还是部署后的 PostgreSQL,改一行配置就能切换。很多人说 Django 笨重,但对新手来说,这种“约束”反而是保护,你不需要纠结发送邮件选哪个库、做分页要不要引入第三方包,框架已经帮你选好了最常见的方案。
1.2 本地环境与项目骨架搭建
我用的环境是 Python 3.10 和 Django 4.2 LTS,选 LTS 版本是因为它维护周期长,社区资料也最丰富。建议你先用虚拟环境隔离依赖,别图省事直接装到全局:
python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install Django==4.2.* django-admin startproject myblog cd myblog python manage.py startapp blog python manage.py runserver执行完startproject后,项目根目录会生成manage.py、myblog/settings.py、myblog/urls.py等文件,startapp blog则创建了业务逻辑所在的 app。我习惯把项目配置和应用模块分开放,这样以后如果博客要加评论、加订阅号功能,直接新建 app 扩展就行,不用把代码都堆在同一个文件里。
起步阶段有一个非常容易被忽视的配置:settings.py里的ALLOWED_HOSTS。本地调试可以留空,但如果后面你想用手机在同一局域网下访问,或者在部署后通过域名访问,必须把宿主地址加进去,否则 Django 会直接拒绝请求并报DisallowedHost。我第一次部署到服务器时在这上面卡了快一个小时,其实就是少加了一个域名。
数据库方面,开发阶段直接使用默认的 SQLite 就可以。它不需要额外安装服务,数据也存在一个本地文件里,非常适合学习和单机部署。生产环境我会换到 PostgreSQL,但这不影响业务代码,Django 的 ORM 把这个差异完全抹平了。真正要留心的是,在跑任何数据库操作前先执行:
python manage.py makemigrations python manage.py migrate新手最容易急着写模型,却忘了生成并执行迁移文件,结果后端一运行就报no such table。顺带一提,migrate会自动创建 Django 内置的用户表、会话表等基础数据表,这也是为什么我总说这个框架“自带电池”。
2. 核心模型设计与用户体系
2.1 博客文章模型设计
一个博客系统的数据模型不必复杂,但每个字段都要想清楚用途。我设计的Post模型如下:
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_length=50, unique=True) def __str__(self): return self.name class Tag(models.Model): name = models.CharField(max_length=30, unique=True) def __str__(self): return self.name class Post(models.Model): STATUS_CHOICES = [ ("draft", "草稿"), ("published", "已发布"), ("trash", "回收站"), ] title = models.CharField("标题", max_length=200) slug = models.SlugField("URL别名", max_length=250, unique=True) author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者") category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="分类") tags = models.ManyToManyField(Tag, blank=True, verbose_name="标签") summary = models.TextField("摘要", max_length=500, blank=True) content = models.TextField("正文") status = models.CharField("状态", max_length=10, choices=STATUS_CHOICES, default="draft") views = models.PositiveIntegerField("浏览量", default=0) created_at = models.DateTimeField("创建时间", default=timezone.now) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: ordering = ["-created_at"] def __str__(self): return self.title def get_absolute_url(self): return reverse("blog:post_detail", args=[self.slug])重点说几个设计细节。slug字段用于生成文章详情页的 URL,比如https://example.com/post/django-intro/,它比?id=1更利于 SEO,也让分享链接更可读。你可能发现我把slug设成unique=True,这意味着标题一旦确定后再改 URL 会造成链接失效,所以我在后台表单里让用户手动维护这个字段而不是自动生成,内容编辑多了你就会知道这个“麻烦”换来了多大的可控性。
status字段是博客系统内容管理的关键。我的热词里有“小修系统博客”这类词,其实说的就是后台维护场景:文章不必一写完就发布,可以先存草稿,等配图、校对完成后再改成“已发布”。回收站状态则避免了误删后无法恢复的尴尬。
views字段用来统计浏览量。很多初学者会在这里踩坑:每次刷新页面就在视图里执行post.views += 1然后save(),这种做法不仅会有并发覆盖问题,还会在每次请求时多写一次数据库。推荐的做法是用 Django 的F()表达式:
from django.db.models import F Post.objects.filter(pk=post.pk).update(views=F("views") + 1)这行 SQL 是在数据库层面原子完成的,不会因为两个用户同时访问而丢失更新。
2.2 用户模型与认证体系
用户模块我直接使用了 Django 自带的auth.User,没有自己重写UserModel。原因很简单:博客需要的登录、密码加密、权限分组,Django 全都已经实现了。对新手来说,自定义用户模型的坑很多——比如改到一半迁移历史乱七八糟、外键指向出错,不如先用官方默认方案。
文章和用户的关联用的是models.ForeignKey(User, on_delete=models.CASCADE)。CASCADE意味着如果删除了某个作者,他写的文章也会一起删除。实际运营博客时这可能不是你想要的行为,你更希望保留文章,只是作者显示成“已注销用户”。我第二次重构时改成了on_delete=models.SET_NULL并配合null=True,这样即便作者账号删除,文章记录依然存在。这个取舍建议你在写之外都考虑清楚,因为一旦上线,修改关联策略涉及的迁移和数据维护成本不小。
登录页面我用的是类视图LoginView配合自定义模板。Django 默认的认证视图已经把“验证账号密码、写入 session、跳转下一页面”整套流程封装好了,你只需要在urls.py里指一下模板位置:
from django.contrib.auth import views as auth_views urlpatterns = [ path("login/", auth_views.LoginView.as_view(template_name="registration/login.html"), name="login"), path("logout/", auth_views.LogoutView.as_view(), name="logout"), ]之所以用内置视图,是因为它内部处理了next参数——用户未登录访问受限页面时会自动跳转登录页,登录成功后再回到原本想看的页面。换成自己手写登录逻辑时,这个细节非常容易漏掉。
2.3 用 Django Unfold 提升后台管理水平
默认的 Django Admin 长得很“极简”,对新人来说谈不上好用但绝对够用。后来我在打磨时加了django-unfold这个主题插件,配合pip install django-unfold和INSTALLED_APPS配置,几行代码就能把后台变成现代一点的界面风格:
INSTALLED_APPS = [ "unfold", "unfold.contrib.filters", "unfold.contrib.forms", "django.contrib.admin", # ... ]这里要特别强调unfold必须放在django.contrib.admin之前,否则主题无法覆盖默认模板。很多人照着文档配完发现没变化,十有八九是安装顺序错了。Unfold 带来的不只是外观改善,它还把筛选、搜索、导出等操作集成得更好。博客后台每天要处理几十篇文章时,这种操作效率的提升是非常直观的。
3. 博客系统的查询、删除与 ORM 细节
3.1 文章列表与查询集优化
博客首页通常要展示已发布的文章列表,并按时间倒序排列。ORM 里的对应写法是:
posts = Post.objects.filter(status="published").select_related("author", "category").prefetch_related("tags")select_related和prefetch_related是 Django ORM 里最值得学习的两个方法。理解它们的重点在于“减少数据库查询次数”。如果不加这两句,每循环一篇文章取作者时都要执行一次 SQL,首页 10 篇文章就要多出 10 次查询,这就是典型的 N+1 问题。加了之后,Django 会用一次 JOIN 把作者和分类查出来,通常能省掉大半查询时间。
如果文章数量大,分页也是必须的。我用的不是自己手写的页码逻辑,而是内置Paginator:
from django.core.paginator import Paginator paginator = Paginator(post_list, 10) page_obj = paginator.get_page(request.GET.get("page"))get_page的好处是页码越界时它不会抛异常,而是自动给出最后一页或第一页,对用户友好得多。
3.2 创建、更新与删除对象的正确姿势
热词里反复出现了“django 执行查询-删除对象”,我也把这一块单独拿出来讲,因为这是新手面向数据库操作最容易出问题的地方。
创建对象可以用create(),也可以用objects.create():
post = Post.objects.create( title="我的第一篇博客", slug="my-first-blog", author=request.user, content="Hello world", status="published", )更新则建议用update而不是先查出对象再save()。区别在于queryset.update()只执行一条 UPDATE 语句,不会触发每个实例的save()方法,性能更好;但要注意,它也不会自动更新auto_now字段和信号,所以在更新内容时我会用save(),在批量更新浏览量时用update()。
删除操作最值得警惕:
post = Post.objects.get(slug="my-first-blog") post.delete()上面这段代码会把文章从数据库里永久移除。Django 的delete()是级联删除,也就是说,如果文章下面挂着评论、图片,它们也会一并被删除,而且这个过程不可逆。我最初做博客时不小心把一篇文章删掉,才发现关联的评论全没了,当时就意识到“物理删除”对内容型系统来说有多危险。
更稳妥的方案是“逻辑删除”,也就是把status改成"trash",保留数据但不在前台渲染。实现起来就是在列表页的查询条件里加一个exclude(status="trash")。回收站功能不是“多此一举”,而是内容管理系统的底线保障。
3.3 关联对象删除策略与回收站的实现
回到外键on_delete参数,Django 支持多个策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
CASCADE | 父对象删除时子对象一起删 | 帖子与评论、订单与明细 |
SET_NULL | 父对象删除时子对象外键置空 | 文章作者注销后保留文章 |
PROTECT | 有子对象引用时禁止删除父对象 | 分类下有文章时阻止删除分类 |
SET_DEFAULT | 删除后设为默认值 | 有默认外键的情况 |
博客的文章和用户之间,我最终选择了SET_NULL,避免了一删作者文章全没的惨剧。分类用SET_NULL是因为一篇文章没有分类依然可以正常展示,而评论和文章之间则保留CASCADE,因为评论失去所属文章后毫无意义。
回收站的查询实现很简单:
Post.objects.filter(status="trash")恢复时改回published即可。这套逻辑可以在 Admin 后台操作,也可以写一个管理命令定期清空超过 30 天的回收站,看你的具体需求。我在实际运营中发现,保留“恢复”能力远比定期清空重要,因为编辑常常是删完过两天又后悔。
4. 登录功能与全栈闭环实现
4.1 登录、登出与 Session 机制
博客系统的登录功能看起来简单,但背后牵扯的是 Django 的 Session 机制。用户提交账号密码后,LoginView会校验用户信息,校验通过就把用户 ID 加密写入服务端的 Session 表,同时给浏览器下发一个sessionidCookie。之后的每次请求,Django 都会用这个 Cookie 找到对应的 Session,从而恢复出request.user。
理解这个流程对排查登录失效问题很有帮助。比如很多人会遇到“登录后刷新又变回未登录”,多半是浏览器禁用了 Cookie,或者SESSION_COOKIE_SECURE配合非 HTTPS 环境导致 Cookie 没能写入。本地开发时别急着设SESSION_COOKIE_SECURE=True,否则http://127.0.0.1:8000上登录永远无法保持。
登出同样简单,LogoutView会清掉服务端的 Session 数据。不过在实现时我故意没有用 “POST 跳转”而是设置了LOGIN_REDIRECT_URL和LOGOUT_REDIRECT_URL:
LOGIN_URL = "login" LOGIN_REDIRECT_URL = "blog:post_list" LOGOUT_REDIRECT_URL = "blog:post_list"这里有个小细节,Django 从某个版本开始将登出接口严格限制为 POST 方法,如果你用某个教程里的老代码直接发 GET 请求,会报 405。倒不是越改越难用,只是应了那句老话——框架升级后,你最少要扫一眼官方 release notes,不然很多“旧写法”会莫名其妙失效。
4.2 保护视图与登录要求
博客的“创建文章”和“后台管理”页面不应该让未登录用户访问。靠模板里藏掉链接不够安全,后端必须做权限校验。最简单的做法是加装饰器:
from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST @login_required def post_create(request): ... @login_required @require_POST def post_delete(request, slug): ...如果你要限定只有管理员(is_staff)能操作,可以用@staff_member_required。权限这块的原则是“后端兜底,前端美化”,前端隐藏操作按钮只为了用户体验,真正的安全边界始终在后端视图和模型层。
文章详情的浏览量统计也要考虑登录状态吗?不需要,views是公开数据,登录与否只影响你能不能进入编辑页。我在模板里用了{% if user.is_authenticated %}判断是否显示“编辑”按钮,但即便有人绕过前端直接构造 URL 访问编辑页,后端没有@login_required的视图照样会被拦截。
4.3 模板继承与全栈页面串联
博客的前端页面由模板、静态文件和 URL 路由共同构成。Django 的模板系统支持继承,这是我搭建页面时最享受的一步:先写一个base.html,把导航栏、页脚、CSS 引用都放进去,然后每个页面只需继承它并覆盖 content 块。
<!DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="UTF-8"> <title>{% block title %}{% endblock %} - 我的博客</title> </head> <body> <nav> {% if user.is_authenticated %} <p>你好,{{ user.username }}</p> <a href="{% url 'logout' %}">退出</a> {% else %} <a href="{% url 'login' %}">登录</a> {% endif %} </nav> <main> {% block content %}{% endblock %} </main> </body> </html>文章详情页里展示正文和作者:
{% extends "base.html" %} {% block title %}{{ post.title }}{% endblock %} {% block content %} <article> <h1>{{ post.title }}</h1> <p>作者:{{ post.author.username }} | 分类:{{ post.category.name }} | 浏览 {{ post.views }}</p> <div>{{ post.content|linebreaks }}</div> </article> {% endblock %}{% url %}标签是我反复强调要用的。它根据路由 name 动态生成 URL,而不是直接硬编码/post/my-first-blog/。这样即使你调整了路由路径,所有引用它的模板都会自动跟随,不需要逐个改文件。
静态文件处理方面,开发阶段把 CSS、JS、图片放在全局static/目录,并在配置里加入:
STATIC_URL = "static/" STATICFILES_DIRS = [BASE_DIR / "static"]然后用{% load static %}在模板里引用。等到部署时再执行collectstatic收集所有静态文件交给 Nginx 处理。这个流程在全栈开发里属于“最后一公里”,很多新手在本地一切正常,一上线样式全丢,就是因为忘了配置静态文件的对外服务路径。
5. 常见问题排查与干货速查
5.1 Django 全栈新手最容易踩的坑
把项目从开发带到运行的过程中,我统计过自己踩过的坑,发现有几种是反复出现的。
第一个是迁移混乱。我最初偷懒直接删了 SQLite 数据库文件来重置数据,但后来发现migrations目录里的历史记录还在,导致再执行migrate时出现“表已存在”之类的错。正确处理方式是用 Django 提供的方式创建迁移并重置,而不是暴力删库。特别是多人协作场景下,随意改迁移文件会让队友直接崩溃。
第二个是没有给模型写__str__。Admin 后台列表默认展示Post object (1),根本看不出是哪篇文章。加上__str__之后,后台立即变得人性化,这个习惯要尽早养成。
第三个是模板中跨过预取关系。比如在循环里写{{ post.author.blog_profile.bio }},每一行都可能触发额外的 SQL。虽然博客规模不大时看不出问题,但一旦数据量上来,页面响应会变得奇慢。排查这类问题最快的方式是安装 Django Debug Toolbar,它能在页面底部显示所有 SQL 执行情况和耗时。
第四个是DEBUG=False后静态文件和服务突然“失效”。生产环境里 Django 不会自动帮你托管静态文件,这部分要交给 Nginx。如果你还没有部署 Nginx,也可以临时用python manage.py runserver --insecure客串一下,但仅供测试,不要拿来长时间对外服务,性能和安全都靠不住。
5.2 登录和权限相关报错速查
结合我做登录和权限时踩过的坑,整理一个速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 登录页面显示 403 | 缺少 CSRF 令牌 | 表单里加{% csrf_token %} |
| 登录成功但一直跳回登录页 | LOGIN_REDIRECT_URL未配置或 next 参数失效 | 设置跳转地址,检查视图是否被装饰器保护 |
| 退出登录时报 Method Not Allowed | 用了 GET 请求登出 | 改成 POST 表单提交 |
| 未登录访问发布页报 404 | LOGIN_URL配置不对 | 检查 settings 里LOGIN_URL与 urls 中 name 是否一致 |
| 后台能登录但看不到文章管理 | 用户不是is_staff | 分配后台权限或改用 admin 账号 |
| 删除分类时系统报错 | 外键设置了PROTECT | 查看哪些文章还在引用该分类,先处理旧数据 |
权限这块,绝大多数实际问题不是“用户能看不该看的”,而是“用户连该看的都看不了”。原因多半出在装饰器用错位置,或者模板里变量名写错。我用这几个细节对照检查后,基本能把 90% 的登录问题解决掉。
5.3 对博客功能的进一步扩展建议
博客系统做到这里,已经能完成“创作、发布、管理、登录、删除、回收站”的完整闭环了。后续扩展方向有很多,但我不建议一上来就全做。根据我自己的经验,优先级应该是:
第一个是评论功能。评论涉及用户、文章、审核三层,比博客本身更能锻炼业务设计能力。可以先做“登录用户才能评论、博主可删除评论”,再考虑嵌套回复和邮件通知。
第二个是 RSS 订阅。Django 内置的syndication框架可以快速输出 RSS 2.0 订阅源,技术难度低但很实用。
第三个是 REST API,为以后小程序或手机 App 做数据接口。用django-rest-framework,要注意接口的权限控制和序列化设计。
第四个才是部署上线。用gunicorn加 Nginx 再配 HTTPS,其实也是一套自己的知识点。把这个流程跑通,你才真正从一个“写代码的人”变成一个“上线产品的人”。
我个人在实际操作中最深的一点体会是:博客系统这类全栈项目,最大的价值不在于用了多新的技术,而在于它让你把 Django 最核心的 ORM、认证、模板、路由、Admin 全部串起来了。每一次踩坑都是对框架运行机制更深一层的理解。你做完这一个项目,再去学 DRF、Celery、Docker 这些周边工具,会更加有的放矢,因为你已经知道它们在为哪一环服务。
我在跑这个博客系统的过程中,逐渐养成了一种习惯:每加一个新功能,我都会先问自己一句“这个功能解决了谁的问题,有没有更简单的替代方案”。比如我一度想给文章加浏览量排行榜,后来发现首页列表按时间排序其实已经满足了绝大多数读者的需求,就把更多精力放在了编辑器体验上。对新手来说,少即是多,先让一个完整的闭环稳定跑起来,再谈优化和加花。