☰
从零搭建Django全栈博客系统:数据建模到部署的完整实践
2026/10/18 2:57:04 网站建设 项目流程

如果你问我,作为一个刚接触Web开发的同学,第一个拿得出手的Django全栈项目该做什么,我大概率会推荐博客系统。不是因为它简单,而是因为它的覆盖面太合适了——一个完整可用的博客,刚好多达覆盖了Django从数据建模、URL路由、视图处理、模板渲染到后台管理、表单校验、分页搜索的全部主干功能,没有哪一环是凑数的。这篇内容不是复读官方教程,而是把我自己带新手时反复强调的思路和踩过的坑整理一遍,聊聊怎么从零把一个可以发布、可以维护的博客系统搭起来,以及每一步背后的取舍。无论你是刚装好Python,还是已经被各种概念绕晕,按这个节奏走完,你得到的不是一段能跑的Demo,而是一套自己能改、能加功能的代码骨架。

1. 为什么第一个全栈项目要选博客:一次完整的Django能力巡检

1.1 博客系统恰好覆盖Django的三条核心链路

很多教程会让新手直接去做电商、做社交平台,结果数据模型还没设计清楚,人已经崩溃了。博客系统不一样,它的业务规则简单,但它逼着你把三条核心链路全部走通。

第一条是数据链路:文章存哪儿、字段怎么设计、分类和标签是什么关系、怎么从数据库里把数据查出来。第二条是请求链路:用户在浏览器输入一个URL,Django怎么找到对应的视图代码,怎么把数据变成HTML页面返回给浏览器。第三条是管理链路:你总不能每次加文章都在数据库命令行里写SQL,所以要学会用Django自带的后台做内容录入。

这三条链路正好对应了Django官方的核心概念:ORM模型、URLconf、视图函数、模板系统、Admin后台。做一遍博客,这些概念就全都落地了,而不是停留在"看懂了没写过"的状态。

1.2 先看清MVT:一个请求在Django里是怎么跑的

动手写代码之前,我强烈建议你把Django处理一个请求的完整顺序画在脑子里。它的模式叫MVT,Model-View-Template,和传统的MVC很像但又不完全一样。

打个比方,Django的URL路由就像餐厅的菜单,视图函数是服务生,数据库是后厨,模板是摆盘。你点了一份"宫保鸡丁",服务生看到菜单编号,去后厨取菜,厨师从冰箱里拿原料(ORM查数据库),炒好之后装盘(渲染模板),最后端到你面前(返回HTML响应)。整条链路里任何一环出错,你看到的都会是报错页面。

我见过太多新手一上来就忙着写模板,结果页面怎么都显示不出数据,原因就是根本没走通"视图取数据"这一步。建议你先用最笨的方式跑通:写好模型,迁移建表,在视图里用ORM把文章查出来打印到控制台,确认数据能取到,再去做页面渲染。这一步没有捷径。

1.3 动手前的功能边界:第一版博客不要贪多

很多人的第一个项目死在"功能清单太宏大"上。评论区要做登录才能评论吧?那么用户系统要不要做?用户有了,头像、关注、点赞是不是也顺手做了?一个月过去,项目还是半成品。

我的建议是,第一版只做四件事:管理员能在后台写文章并发布、前台能看文章列表、能打开文章详情页、能按分类筛选。等这套东西稳定跑起来,再迭代第二版加入搜索、分页、评论。你会在每一次小迭代里都有完整的成就感,而不是在烂尾的边缘反复横跳。这也是我反复跟身边初学者强调的"垂直切薄"原则——宁可功能少而完整,不要功能多而残缺。

2. 环境准备与项目初始化:把Django骨架跑起来的完整过程

2.1 版本选择与虚拟环境:把依赖关进"隔离病房"

开始敲命令之前,先解决两个基础问题:用哪个Python版本,用哪个Django版本。目前Python 3.10及以上版本都很成熟,Django推荐使用长期支持(LTS)版本,比如4.2或更新的5.x系列,新特性多且社区资料齐全。不建议直接用最新的开发版,有些第三方库还没跟上,容易白踩坑。

然后是虚拟环境,这一步新手最容易偷懒,但偷懒的后果是半年后你分不清自己电脑上哪些依赖属于哪个项目。其实就是一条命令:

python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate pip install django python -c "import django; print(django.get_version())"

虚拟环境相当于给每个项目开了个独立的小房间,你在房间里装的Django和其他第三方库不会污染全局环境,换电脑或换项目时也不会冲突。这个习惯一旦养成了,后面做任何Python项目都受用。

2.2 startproject与startapp的分工:别把项目和应用混为一谈

初始化项目时用两条命令:

django-admin startproject myblog . python manage.py startapp blog

注意第一条命令末尾的英文句点,它表示就在当前目录下创建项目配置文件,而不是再多套一层目录,这样结构更清爽。startproject创建的是整个站点的"外壳":settings.py是全局配置,urls.py是根路由,manage.py是管理入口。而startapp创建的才是具体业务模块。

很多新手把"项目"和"应用"当成一回事,我举个例子你就明白了:一个"博客项目"下可以同时有"文章应用""评论应用""用户应用",每个应用负责一块独立业务。我们现在只做文章和评论,所以一个blog应用就够了,但先在根路由里给这个应用留好挂载点,逻辑就清晰了。创建完应用后,立刻去settings.py的INSTALLED_APPS里加上'blog',不加的话Django不会认识它,后面写模型、迁移都会报错。

2.3 settings.py里第一件事就该改的三处配置

新项目生成后,settings.py里有几个地方几乎是必改的,改完你的开发体验立刻不一样。

第一是语言和时区,改成中文环境会舒服很多:

LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai'

注意不要动USE_TZ,保留它等于True就好。它的作用是让Django以UTC标准时间存储数据,展示时再换算成本地时间,这是跨时区部署的正确姿势。如果你把它改成False,短期没问题,将来一旦部署到海外服务器,时间就会变得一团糟。

第二是INSTALLED_APPS里添加上一步创建的blog应用,我已经说过,但值得再强调一遍——漏掉它你会遇到各种"找不到表""No module named"的诡异报错。

第三是确认数据库配置。默认的DATABASES配置用的是SQLite,一个本地文件数据库,对博客这种量级的项目完全够用,零配置就能跑。我建议第一版就一直用SQLite,等真的要把项目发布到公网再考虑换MySQL或PostgreSQL,不要一开始就掉进数据库运维的坑里。

2.4 数据库迁移:ORM是怎么把代码变成真实表结构的

配置改完后,先执行一次迁移:

python manage.py migrate

这条命令会把Django自带的用户、会话、后台管理等内置应用的数据表建到SQLite里。迁移机制是Django最值得花十分钟理解的设计之一:你用Python类定义数据结构,然后生成迁移文件,最后执行迁移文件去数据库建表。

这里有个新手容易迷糊的知识点:Django并不会在你定义完模型后自动同步数据库。你得先执行makemigrations让Django根据模型变化生成一个"迁移计划",再执行migrate把这个计划应用到数据库。这一步是为了保证你在团队开发或上线时,数据库结构的变化有迹可循,而不是直接手动去数据库里删表加字段。

跑完migrate后用下面的命令看一下,确认生成了db.sqlite3文件,你的项目骨架就算真正立起来了:

python manage.py runserver

浏览器打开http://127.0.0.1:8000,看到火箭图标页面,说明环境没问题。

3. 数据模型:文章、分类、标签怎么设计才不返工

3.1 三个模型的完整代码与字段解读

博客的核心模型其实就三个:文章Post、分类Category、标签Tag。我在blog/models.py里是这么写的:

from django.db import models from django.utils import timezone class Category(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(max_length=100, unique=True) class Meta: verbose_name = '分类' verbose_name_plural = '分类' def __str__(self): return self.name class Tag(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(max_length=100, unique=True) class Meta: verbose_name = '标签' verbose_name_plural = '标签' def __str__(self): return self.name class Post(models.Model): STATUS_CHOICES = [ ('draft', '草稿'), ('published', '已发布'), ] title = models.CharField(max_length=200, verbose_name='标题') slug = models.SlugField(max_length=200, unique=True, verbose_name='URL别名') category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='posts', verbose_name='分类') tags = models.ManyToManyField(Tag, blank=True, related_name='posts', verbose_name='标签') content = models.TextField(verbose_name='正文') excerpt = models.CharField(max_length=300, blank=True, verbose_name='摘要') status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft', verbose_name='状态') created_at = models.DateTimeField(default=timezone.now, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: ordering = ['-created_at'] verbose_name = '文章' verbose_name_plural = '文章' def __str__(self): return self.title

逐个字段说几个关键点。title用CharField和max_length=200,这是给文章标题设个合理上限,既不浪费性能也防止用户输入失控。content用TextField,因为正文是长文本,MySQL里对应的是longtext。excerpt是摘要字段,用来在列表页做预览用,设为blank=True就表示可以留空。

created_at我用了default=timezone.now,含义是"创建时自动取当前时间"。updated_at用了auto_now=True,含义是"每次保存记录时自动刷新为当前时间"。这两个看起来相似但语义不同:default是给初始值,auto_now是每次更新都会触发。

3.2 外键和多对多:为什么分类用ForeignKey,标签用ManyToManyField

分类和标签最大的区别在关系模型上。一篇文章只能属于一个分类,但一个分类下可以有很多篇文章,这就是一对多关系,所以分类用ForeignKey外键。而标签是多对多:一篇文章可以打多个标签,一个标签也可以被多篇文章使用,所以用ManyToManyField。

写外键的时候必须指定on_delete参数,它决定分类被删除时文章怎么办。Django提供了几个选项,CASCADE是级联删除,分类删了文章跟着全删;SET_NULL是把外键置空,但需要字段允许为空;PROTECT是阻止删除——只要还有文章挂在分类下,分类就删不掉。我在这里用PROTECT,因为博客内容有沉淀价值,我不希望误删分类时把整个文章列表清空。这个选择背后是对数据安全性的考虑,建议你也认真想一下自己项目的业务场景。

再说related_name,它定义了从分类反向查找文章时用的属性名。比如category.posts.all()就能查出这个分类下所有文章。这个命名不是无所谓的小细节,命名冲突时会让ORM查询变得很拧巴。

3.3 Meta选项、__str__与后台注册:让代码更可读

每个模型里的Meta类是个小宝藏。ordering = ['-created_at']表示所有查询默认按创建时间倒序,最新文章排最前。verbose_name和verbose_name_plural用来指定中文名称,不然后台界面里显示的会是Post、Category这样英文单词。

写完后,在blog/admin.py里注册模型,顺便配置常用选项:

from django.contrib import admin from .models import Post, Category, Tag @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ('name', 'slug') prepopulated_fields = {'slug': ('name',)} @admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display = ('name', 'slug') prepopulated_fields = {'slug': ('name',)} @admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'status', 'created_at') list_filter = ('status', 'category') search_fields = ('title', 'content') prepopulated_fields = {'slug': ('title',)}

list_display决定后台文章列表里显示哪些列,list_filter提供右侧筛选,search_fields让后台自带搜索框。prepopulated_fields是懒人神器:你在后台输入标题,slug会自动按拼音或英文规则填充,省得手敲URL别名。

做完这一步,先执行两个命令把模型同步到数据库:

python manage.py makemigrations blog python manage.py migrate

再创建一个管理员账号登录后台试试:

python manage.py createsuperuser

如果你已经在前台数据结构上绕晕了,我的建议是先在后台手动录入一两篇文章,把分类、标签都挂上,这些数据是下一章调试视图用的"弹药"。

4. 视图、URL与数据流:从数据库到页面

4.1 函数视图还是类视图:新手阶段的取舍

Django的视图有两种写法,函数视图和类视图。类视图很强大,自带分页、增删改查的通用实现,代码看起来很简洁。但我带新手时仍然坚持先写函数视图,原因很简单:函数视图的数据流是显式的,每一行代码在做什么清清楚楚。

比如你写def post_list(request):,然后从POST表里查数据,把数据塞进模板上下文,传给render函数返回。这个"请求进来、数据取出、页面返回"的过程没有任何魔法,理解起来很直接。等你把函数视图写熟了,再去看ListView、DetailView这些类视图,你会一眼看穿它们只是在帮你隐藏重复代码,底层的逻辑完全一样。反过来,一上来就堆类视图,报错时你会连调试入口都找不到。

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.filter(status='published') return render(request, 'blog/post_list.html', {'posts': posts}) def post_detail(request, slug): post = get_object_or_404(Post, slug=slug, status='published') return render(request, 'blog/post_detail.html', {'post': post})

post_list的逻辑很直白:从数据库里把所有已发布的文章取出来,连同查询结果一起传给模板。注意filter(status='published')这个细节,只展示已发布文章,草稿不会出现在前台,这比在模板里做if判断要高效且安全得多。

post_detail接收一个slug参数,slug是文章在URL里的英文别名,比如http://你的域名/post/my-first-post/,这里的my-first-post就是slug。用get_object_or_404而不是普通的Post.objects.filter,是因为这个函数在查不到对应文章时,会自动返回一个404页面,省去了你手动判断对象是否存在再渲染错误页的代码。

4.3 URL路由与命名:模板里写链接的正确姿势

接下来在blog/urls.py里定义路由。如果还没有这个文件,自己创建,然后到项目根目录的myblog/urls.py里include进来:

from django.urls import path from . import views app_name = 'blog' urlpatterns = [ path('', views.post_list, name='post_list'), path('post/<slug:slug>/', views.post_detail, name='post_detail'), ]

根路由里挂载:

from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('blog.urls')), ]

这里有两个值得记住的知识点。一是路径转换器的用法, slug:slug 表示URL中的这段期望是一个slug格式的字符串,Django会自动校验格式并把值传给视图函数作为参数。二是name参数和app_name的作用,它们给URL起了个"别名",模板里就可以用{% url 'blog:post_detail' post.slug %}来生成链接,而不是写死路径。这样做的好处是,你将来改了URL的结构,模板不用挨个改。

连起来跑通一次的节奏是按顺序检查:登录Admin后台发布一篇文章,在浏览器访问首页,看列表是否出现;点击文章链接,看详情页是否正常。如果报错了,先把错误信息截图,看是路由错误、视图错误还是模板错误,这一步排查能力的价值不亚于功能本身。

5. 模板系统:页面长什么样由模板决定

5.1 用模板继承省掉一半重复代码

Django的模板系统有一个很实用的机制叫模板继承,它解决的是"每个页面都有网站头部和底部"这种重复劳动。先在blog/templates/blog/下创建base.html:

{% load static %} <!DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="UTF-8"> <title>{% block title %}我的博客{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/style.css' %}"> </head> <body> <header> <nav> <a href="/">首页</a> </nav> </header> <main> {% block content %}{% endblock %} </main> <footer> <p>© 我的博客</p> </footer> </body> </html>

模板继承的核心是block块,它可以理解成"占位符"。base.html定义好页面的公共框架,子页面只需要声明{% extends "blog/base.html" %},然后在对应的block里填自己的内容。这是Django模板设计里最值得养成的习惯,比起每个页面复制粘贴一套头尾,维护成本低了一个数量级。

5.2 文章列表页的渲染写法

post_list.html这么写:

{% extends "blog/base.html" %} {% block title %}文章列表 | 我的博客{% endblock %} {% block content %} <h1>最新文章</h1> {% for post in posts %} <article> <h2><a href="{% url 'blog:post_detail' post.slug %}">{{ post.title }}</a></h2> <p class="meta">{{ post.created_at|date:"Y年m月d日" }} · {{ post.category.name }}</p> <p>{{ post.excerpt|default:post.content|truncatewords:40 }}</p> </article> {% empty %} <p>还没有发布文章,去后台写一篇吧。</p> {% endfor %} {% endblock %}

这里有几个很实用的小技巧。{% empty %}是for循环自带的空分支,列表为空时显示提示信息,避免页面上出现一片空白让用户困惑。模板过滤器管道的用法值得多说一句,|default:post.content表示摘要为空时降级用正文,|truncatewords:40表示截断到40个单词,两个过滤器串起来用,就是一套"选显示文本"的小逻辑。article标签里包一个链接,用{% url %}去解析视图名而不是硬编码路径,前面说过,这是维护友好的关键。

5.3 详情页与内容安全底线

post_detail.html的核心部分:

{% extends "blog/base.html" %} {% block title %}{{ post.title }}{% endblock %} {% block content %} <h1>{{ post.title }}</h1> <p class="meta">{{ post.created_at|date:"Y年m月d日" }} · 分类:{{ post.category.name }}</p> <p>标签: {% for tag in post.tags.all %} <a href="#">{{ tag.name }}</a> {% empty %} 无 {% endfor %} </p> <div class="content"> {{ post.content|linebreaks }} </div> {% endblock %}

注意我故意没在正文上加|safe过滤器。linebreaks这个过滤器会把换行转成HTML的p标签,同时保留自动转义,也就是说正文里的尖括号、脚本标签都会被转成安全字符。|safe的意思是"信任内容,原样输出",它一旦和用户输入内容结合起来,就是XSS漏洞的温床。所以我的底线是:不是自己后台录入且完全可信的内容,绝不上|safe。

5.4 静态文件与CSS引入的正确姿势

前面base.html里用了{% static 'css/style.css' %},这要求你正确配置静态文件目录。最省事的做法是在项目的settings.py里设置:

STATIC_URL = '/static/'

然后在blog应用目录下建一个static/css文件夹,把style.css放进去。Django的AppDirectoriesFinder机制会自动收集各个应用下static目录里的文件。开发阶段用runserver就能直接访问,生产环境需要collectstatic把散落在各处的静态文件统一收集到一个目录,这个坑我在第7章细说。

样式方面,新手阶段直接用简单的手写CSS或引入Bootstrap的CDN都行。我的观点是,博客是你练手的第一套系统,前端框架可以不急着上,先把HTML结构和CSS定位、布局、字体这些基本功过一遍,后面再学Bootstrap会快很多。

6. 进阶功能:分页、搜索、评论的落地思路

6.1 分页:不要让一页文章列表越滚越长

文章一多,列表页就没法一屏放完,这时分页就该上了。函数视图里用Django自带的分页器:

from django.core.paginator import Paginator def post_list(request): post_list = Post.objects.filter(status='published') paginator = Paginator(post_list, 5) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'blog/post_list.html', {'posts': page_obj})

get_page和page的区别在于:page遇到页码越界会直接抛异常,get_page则会自动纠正,比如访问超出范围的页码会返回最后一页,稳健得多。模板里的分页导航:

{% if posts.has_previous %} <a href="?page={{ posts.previous_page_number }}">上一页</a> {% endif %} <span>第 {{ posts.number }} / {{ posts.paginator.num_pages }} 页</span> {% if posts.has_next %} <a href="?page={{ posts.next_page_number }}">下一页</a> {% endif %}

注意这里分页器生成的所有文章对象都在posts对象上,之前的for post in posts写法几乎不用改,Paginator返回的对象也支持迭代。

6.2 搜索:用Q对象一次性搞定标题和正文的OR查询

搜索功能本质上就是ORM查询。用Q对象可以实现"在标题里搜,或者也在正文里搜"的OR逻辑:

from django.db.models import Q def post_list(request): posts = Post.objects.filter(status='published') query = request.GET.get('q', '').strip() if query: posts = posts.filter( Q(title__icontains=query) | Q(content__icontains=query) ) return render(request, 'blog/post_list.html', {'posts': posts, 'query': query})

icontains是大小写不敏感的模糊匹配,__双下划线是Django ORM关联字段的语法。Q后面不加任何条件时是AND,多个Q用|分隔就是OR。有一点值得单独说:直接把输入框内容拼进SQL字符串是极其危险的做法,而Django的ORM参数化查询天然做了防注入处理,所以用ORM不只是方便,更是安全底线。

模板里记得在搜索框加value="{{ query }}"回显搜索词,这样搜索失败时输入框里还留着用户刚才输入的内容,体验会好很多。

6.3 评论:主从表关系与POST数据处理的完整闭环

评论功能是博客从"展示型"走向"交互型"的第一大步。评论相对文章来说是"主从"关系,用外键把评论挂到文章上:

class Comment(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments') name = models.CharField(max_length=50, verbose_name='昵称') body = models.TextField(verbose_name='评论内容') created_at = models.DateTimeField(default=timezone.now) class Meta: ordering = ['-created_at']

注意我在这里的on_delete用了CASCADE,因为评论属于文章的附属内容,文章删了评论跟着删是符合直觉的清理逻辑,不会像分类那样牵扯内容价值问题。

视图里处理POST提交的标准流程是:

from django.http import HttpResponseRedirect from django.urls import reverse from django.shortcuts import get_object_or_404 from .models import Post, Comment def post_comment(request, slug): post = get_object_or_404(Post, slug=slug, status='published') if request.method == 'POST': name = request.POST.get('name', '').strip() body = request.POST.get('body', '').strip() if name and body: Comment.objects.create(post=post, name=name, body=body) return HttpResponseRedirect(reverse('blog:post_detail', args=[slug])) return HttpResponseRedirect(reverse('blog:post_detail', args=[slug]))

这里有个业界标准做法叫PRG模式,POST- Redirect- GET。也就是说,提交评论成功后不要直接渲染页面,而是重定向回详情页。这样浏览器地址栏保持干净,用户刷新页面时不会重复提交评论。这是Web开发里的一个高频知识点,在博客项目里第一次实践它再合适不过。

模板里评论表单务必加{% csrf_token %},这是Django的CSRF防护机制,不加的话POST请求会被拦截并报403错误。表单提交的input里加上name="name"和name="body",serverside才能通过POST.get取到值。

7. 上线前必须处理的五个坑

7.1 ALLOWED_HOSTS与DEBUG=False:别让报错信息泄露底牌

如果你只是在自己电脑上运行,DEBUG保持True没问题。但一旦要让别人通过局域网或公网访问,第一件事就是改配置。DEBUG=True时,任何页面出现异常,Django都会展示携带环境变量、文件路径等敏感信息的调试页面,这是安全漏洞。

把settings.py里的DEBUG改成False,并配置ALLOWED_HOSTS,它用来限制哪些域名可以访问你的站点:

DEBUG = False ALLOWED_HOSTS = ['your-domain.com', '203.0.113.5']

填不进去的域名会直接返回DisallowedHost错误,这也是一种防域名劫持的手段。如果只是临时给朋友看,可以填运行服务器的IP地址,但域名能填一个是一个,别图省事填'*'。

7.2 静态文件收集:样式有时全丢的经典教训

DEBUG=False之后,runserver不再自动托管静态文件,这是新手最容易蒙圈的瞬间——页面结构还在,CSS全没了。解决方法是收集静态文件到一个统一目录:

STATIC_ROOT = BASE_DIR / 'staticfiles'

然后执行:

python manage.py collectstatic

这条命令会把所有应用下static目录里的文件复制到staticfiles目录,后续由Nginx之类的Web服务器直接托管。开发阶段用runserver玩,记得加上static serving,但正式部署请在Wen服务器层面解决,而不是继续依赖Django。

7.3 N+1查询:文章列表页根本没查几次数据库

这个坑属于性能层面的经典问题。文章列表页要显示文章和它所属的分类名称,如果这样查:

posts = Post.objects.filter(status='published')

然后在模板里每篇文章都执行一次post.category.name,就会产生"N+1"次查询:1次查文章列表+N次查分类。文章一多,性能立竿见影地崩。

解决办法是用select_related提前把关联的外键对象一并查出来:

posts = Post.objects.filter(status='published').select_related('category')

select_related适用于ForeignKey和一对一关系,它通过JOIN把关联数据一次性查好。我的习惯是:只要列表页会用到关联字段,就顺手加上,这个成本几乎可以忽略,收益却很大。

7.4 安全底线:CSRF、XSS与评论管理一个都不能少

安全方面有几个点必须心里有数。CSRF的问题我在评论表单那里已经说了,凡是POST表单一律加{% csrf_token %}。XSS的问题在模板那块也提过,核心就是不信任用户内容,不做无谓的|safe。

评论功能上线后,还要留意垃圾评论。最简单的方案是给评论模型加一个status字段,默认pending待审核,前台只展示审核通过的评论,管理员在后台定期处理。不要觉得这是过度设计,公开可访问的站点一旦被爬虫盯上,垃圾评论可以用分钟计算增长。

7.5 数据库备份与版本管理:越早养成习惯越好

SQLite数据库就是一个文件,备份它最简单粗暴的方式是定期复制文件到别处。但更值得强调的习惯是Git——从项目一开始就初始化仓库,每个功能完成就提交一次。这样你写坏代码时可以随时回退,出错范围被限制在一个小改动里。

git init echo "venv/" > .gitignore echo "db.sqlite3" >> .gitignore git add . git commit -m "博客第一版:文章列表、详情、后台发布"

db.sqlite3和venv目录都不该进版本库,数据库是运行时数据,虚拟环境是环境配置,别人克隆你的仓库后执行pip install -r requirements.txt就能重建环境。

落到我自己带过的项目里,博客系统的意义不在于"做出来了",而在于你把数据、请求、模板这三条线都走了不止一遍。评论区、搜索、分页这些功能不是给博客添花,是给下一套系统铺路——等你要写一个带订单管理的后台时,你现在写博客时养成的习惯全都会回来:先想数据结构,再定义URL,最后才动模板。最后分享一个小建议:把第一版控制在"能发布文章、能看文章"就好,剩下的每一个新功能都当成一次独立的小迭代去做,你会明显感觉到自己从"跟着教程抄"变成了"自己设计着写",那个节点,才是你真正跨过入门这道坎的时刻。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询