做后端开发这些年,Django 一直是我最顺手的 Web 框架之一。框架本身不难上手,但真正的痛点却在日常细节里:环境怎么搭、迁移命令执行顺序是什么、settings.py 里哪个配置不写会导致上线直接 400、ORM 查询怎么避免 N+1……这些问题单看文档都能找到答案,可它们分散在各处,等你踩坑后才想起来去查。这篇 Django 后端命令与代码总结,就是我把实际项目中反复用到的东西整理出来的一份“直接抄作业”手册,覆盖初始化、配置、模型迁移、视图路由、模板静态文件、自定义命令和问题排查。刚接触 Django 的人可以照着走一遍流程,中途接手老项目的开发者也能用来查漏补缺,哪怕只是面试前想把知识体系过一遍,这份笔记同样合适。
1. 环境准备与项目初始化
1.1 虚拟环境与依赖管理
我强烈建议所有 Django 项目都用虚拟环境。你可能会觉得“我电脑上就装一个 Django 不行吗”?真不行。我见过太多次因为全局环境里 Django 版本不一致,导致项目在 A 电脑能跑、到 B 电脑直接语法报错的场景。Python 的依赖冲突本来就很折磨人,虚拟环境就是把每个项目的依赖隔离到独立目录里,互不干扰。
创建虚拟环境的命令很简单:
python -m venv venvmacOS 和 Linux 下激活:
source venv/bin/activateWindows 下激活:
venv\Scripts\activate激活成功后,命令行前面会多一个(venv)提示符,这时候你再用pip装东西就只会进到当前项目的虚拟环境里。我第一次用的时候经常忘了激活,直接在全局环境里pip install,结果项目里怎么都 import 不到,排查半天才发现根本没激活环境。这个细节看起来蠢,但极其常见。
装 Django 也一样:
pip install django如果你希望版本可控,建议直接指定大版本,例如pip install Django==5.0.*。当前 Django 5.x 已经很稳定,LTS 版本 4.2 也仍在维护期内,具体用哪个取决于项目需求和老代码兼容性。我在老项目里一般保留 4.2,新项目会直接用 5.0 以上版本,避免一开始就背历史包袱。
依赖管理方面,项目根目录下维护requirements.txt是基本功:
pip freeze > requirements.txt但要注意pip freeze会把所有间接依赖也列进去,时间久了文件里会有一堆你根本不知道干嘛的包。更优雅的方案是用pip-compile维护requirements.in,再生成锁定版本的requirements.txt。不过对大多数中小项目来说,一个干净的requirements.txt就够了,关键是团队协作时大家用同一个文件装依赖,避免“你本地能跑,我拉下来跑不起来”的尴尬。
1.2 创建项目与应用
Django 里的“项目”和“应用”是两个概念,新手经常混为一谈。项目是整个站点的配置和入口,应用则是实现具体业务功能的模块。一个项目可以包含多个应用,一个应用理论上也可以被多个项目复用。
创建项目的标准命令是:
django-admin startproject config .这里的.非常重要,它表示在当前目录生成manage.py和config配置包,而不是再套一层目录。很多教程会让你直接写django-admin startproject config,结果生成的是config/config这种嵌套结构,后续所有路径都别扭。我习惯把配置包命名为config而不是项目同名,这样以后项目改名也不会影响内部引用。
创建应用:
python manage.py startapp users python manage.py startapp orders有人问为什么不直接用django-admin startapp,因为通过manage.py创建应用会自动关联当前项目的配置环境,避免某些路径问题。创建完应用后,第一件事就是去settings.py的INSTALLED_APPS里注册,否则 Django 根本不会加载这个应用的模型和迁移文件。
这里有个提升体验的小习惯:在应用的apps.py里自定义AppConfig,把应用显示名称改成中文,后台管理界面看起来就舒服很多:
# apps/users/apps.py from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' verbose_name = '用户管理'然后在INSTALLED_APPS里注册'users.apps.UsersConfig',而不是简单的'users'。这个配置不会影响功能,但管理后台更清晰,属于“做了不亏、不做也能跑”的润色项。
1.3 启动开发服务器
初始化完成后,跑起来看效果的命令:
python manage.py runserver默认监听127.0.0.1:8000,只能本机访问。如果你想用手机在同一个局域网里调试,或者项目跑在远端开发机上,就要指定地址:
python manage.py runserver 0.0.0.0:8000这样开发服务器会监听所有网卡,你可以用http://开发机IP:8000访问。但我要强调,这个runserver是开发服务器,性能和安全性都不适合生产环境。我见过有人图省事,直接把runserver挂到公网当正式服务用,结果并发一高就卡死,这是大忌。
runserver自带自动重载,你改代码后它会自动重启。但有个注意点:新增文件、修改INSTALLED_APPS这种操作有时候不会触发自动重载,遇到“改了半天没生效”的情况,手动重启一下最省时间。
启动后如果提示有未应用的迁移,控制台会直接显示警告。这时候我一般顺手执行python manage.py migrate,把 Django 内置的表建好。刚创建的项目还没建任何业务表,数据库里只要完成初始迁移就能正常进后台。
2. settings.py 配置:后端开发的第一个大坑
2.1 基础配置项:DEBUG、SECRET_KEY、ALLOWED_HOSTS
每个 Django 项目的配置差异,最后都落在 settings.py 里。这个文件里的每一项几乎都能讲出一堆故事,但最重要的三个基础项是 DEBUG、SECRET_KEY 和 ALLOWED_HOSTS。
DEBUG 表示调试模式。调试模式下出错页面会显示完整堆栈信息,方便你本地排查问题,但这个特性绝对不能在生产环境开启。想象一下,你的线上站点突然报错,用户直接看到代码路径、数据库连接参数、甚至环境变量,那和把后台密码贴在门口没区别。所以上线前必须把 DEBUG 设为 False,并且配置好 ALLOWED_HOSTS。
ALLOWED_HOSTS 是 Django 的安全校验机制,只有在这个列表里的域名或 IP 才能访问。DEBUG=True 时默认允许 localhost,但 DEBUG=False 且 ALLOWED_HOSTS 为空时,任何访问都会返回 400 Bad Request。很多人第一次部署时忘了配置,怎么访问都是 400,其实就是这里的问题。配置示例:
ALLOWED_HOSTS = ['example.com', 'www.example.com', 'localhost', '127.0.0.1']如果服务器有内网 IP 或临时域名,也可以先加上测试,但不要为了省事直接设成['*'],除非你在完全隔离的内网环境。
SECRET_KEY 是 Django 做加密签名的核心,比如 session、cookie、密码重置令牌都依赖它。这个值一旦泄露,攻击者就能伪造签名数据,严重性不亚于数据库密码泄露。开发环境里 settings.py 里写死还好,生产环境我强烈建议用环境变量读取:
import os SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')这样密钥就不会出现在代码仓库里。如果你用 git 管理项目,一定要确保 settings.py 里的真实密钥没提交上去,或者提交的是一个占位值。这类事故我见过不止一次。
2.2 数据库连接配置
Django 默认用的是 SQLite,配置简单,文件型数据库,适合本地开发和演示。但真实业务项目一般都会换成 MySQL 或 PostgreSQL。以 MySQL 为例,基本配置长这样:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'myapp', 'USER': 'root', 'PASSWORD': os.environ.get('DB_PASSWORD'), 'HOST': '127.0.0.1', 'PORT': '3306', 'CONN_MAX_AGE': 60, 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }, } }这里的ENGINE是数据库驱动,MySQL 需要额外安装mysqlclient。在 Windows 上装mysqlclient可能遇到编译问题,一个临时替代方案是用pymysql,并在项目__init__.py里调用:
import pymysql pymysql.install_as_MySQLdb()但这个方案我一般只在本地开发用,生产环境还是尽量用 mysqlclient,性能和稳定性更好。
CONN_MAX_AGE是数据库长连接时间,默认 0 表示每个请求都重新建连。改成 60 表示连接可以在 60 秒内复用,能明显减少握手开销。要注意的是,MySQL 服务端默认wait_timeout是 8 小时,如果CONN_MAX_AGE设置得比服务端超时时间还长,就可能出现“连接已被关闭”的报错,到时候重启服务或加个连接池清理逻辑即可。
还有一个高频坑就是中文乱码。Django 连接 MySQL 时,数据库表、连接的字符集都要统一成 utf8mb4,否则 emoji 或者生僻字在存储时会出问题。建库时直接用CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,然后在上面OPTIONS里指定charset,两边对上了才稳。
2.3 时区与静态资源配置
时区配置看起来不起眼,但在做数据统计、定时任务、日志分析时,时间相差八小时会让你崩溃。Django 默认配置:
LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_TZ = True这里最重要也最容易忽略的是USE_TZ = True。开启后,Django 在数据库里存储时区时间都使用 UTC,显示时才按TIME_ZONE转成本地时间。这样做的优点是不同地区用户,时间都能正确转换。但在编写代码时,千万不要用datetime.datetime.now()获取当前时间,因为拿到的可能是不带时区的 naive 时间,与数据库里的 UTC 时间比较会出问题。正确写法是:
from django.utils import timezone now = timezone.now()静态文件配置也在这附近。开发环境最简单的套路是用 Django 自带的静态文件服务:
STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static'] MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'STATIC_URL是浏览器访问静态资源的 URL 前缀,STATICFILES_DIRS是开发时你自己放置静态文件的目录列表,MEDIA_ROOT则是用户上传文件的磁盘目录。我习惯把所有上传文件统一放media目录,和代码目录分开管理,这样备份数据时只要额外备份media就行。生产环境下这些目录交给 Nginx 托管,后面说到模板与静态文件时再展开。
3. 模型层与数据库迁移:ORM 命令全解
3.1 模型字段与关联关系
Django 的模型(Model)就是数据库表结构的 Python 描述形式。写模型时不需要先建表,而是靠迁移命令把模型变成真实表。基础模型示例:
from django.db import models class Category(models.Model): name = models.CharField(max_length=50, unique=True) class Meta: db_table = 'category' verbose_name = '分类' def __str__(self): return self.name class Article(models.Model): title = models.CharField(max_length=200, verbose_name='标题') content = models.TextField(verbose_name='内容') category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='articles') views = models.IntegerField(default=0, verbose_name='浏览量') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: db_table = 'article' ordering = ['-created_at'] indexes = [ models.Index(fields=['created_at']), ]几个字段设计的核心细节:
null=True表示数据库该字段允许为空,blank=True表示表单校验时允许不填。很多人分不清这两个参数,一句话总结:null 是数据库层面的,blank 是校验层面的。字符串字段多用blank=True加默认空值,不要轻易设null=True,否则查询会出现None和空字符串两种“空”的形态,给业务逻辑添乱。
on_delete参数在外键关系里必填,它决定了关联对象被删除时,当前对象怎么处理。CASCADE是最常用的级联删除,但要注意,删除一条分类会把所有文章也删掉,如果这不是期望行为,可以用PROTECT阻止删除,或者SET_NULL把外键置空(前提是该字段设了null=True)。我一般在需要保护历史数据的场景用PROTECT,避免手一抖把关联数据全删了。
auto_now_add和auto_now有区别,前者是第一次插入时自动设置时间,且之后不再变化;后者是每次修改记录时自动更新。这两个参数不要去手动赋值,由 Django 维护。如果你需要自定义保存时间,就别用这两个参数,改成在save()方法里手动写。
verbose_name会影响后台管理界面和错误提示,建议所有字段都写上。它和中文说明没有必然关系,但配合LANGUAGE_CODE = 'zh-hans'后,后台界面看起来会友好很多。
3.2 迁移命令的执行流程
模型定义好后,并不会立刻影响数据库,必须要执行迁移命令。这套流程我记成三步:改模型、生成迁移、应用迁移。
改完模型后,先执行:
python manage.py makemigrations这个命令会对比当前模型和历史迁移记录,生成新的迁移文件到应用的migrations目录。如果你有多个应用,可以指定应用名:
python manage.py makemigrations articles接着执行:
python manage.py migrate这个命令才真正把迁移文件里的变化同步到数据库。注意,makemigrations只是生成“计划”,migrate才是执行,两个命令经常被搞混。新手最常见的错误是只执行了makemigrations,然后查数据库发现表没变,以为出 bug 了。
迁移之后,我习惯用下面几条命令确认状态:
python manage.py showmigrations python manage.py showmigrations articlesshowmigrations会列出所有迁移文件,前面有[X]标记的表示已应用,没有的就是还没执行。这个命令是排查“表为什么没建出来”的第一利器。
还有一条很实用的命令:
python manage.py sqlmigrate articles 0001_initial它会打印出某个迁移文件对应的真实 SQL,而不是直接执行。我在排查索引、外键约束这类问题时,都会先看 SQL 确认是否符合预期。比如你担心某次迁移会不会把表 DROP 掉,先用sqlmigrate看一遍,心里就有底了。
在项目上线后,迁移文件就是数据库的“版本管理工具”。团队协作时不要手动去数据库里改表结构,任何改表操作都要通过新增迁移文件完成。否则就会出现“你本地模型是新结构,线上数据库还是旧结构”的分裂状态,迁移校验一报错,整条链路都堵住。
当一个字段被删除,但迁移历史里还引用着它时,可能会出现迁移冲突。常见处理办法是找出冲突的迁移文件,按依赖关系重新排序,或者创建新的空迁移来“吸收”差异。不建议直接编辑已经应用过的迁移文件,尤其是线上环境。
除了结构迁移,Django 还支持数据迁移。比如给新字段填充默认值,或者把旧数据从一个表搬到另一个表,都可以用RunPython实现:
# 在迁移文件里 from django.db import migrations def fill_default_category(apps, schema_editor): Category = apps.get_model('articles', 'Category') for item in Category.objects.all(): item.level = 1 item.save() class Migration(migrations.Migration): dependencies = [] operations = [ migrations.RunPython(fill_default_category), ]要注意,数据迁移里建议用apps.get_model获取模型,而不是直接 import model 文件,因为迁移执行时模型结构可能还没到最新状态,apps.get_model能拿到的是迁移历史节点对应的模型版本。
3.3 常用 ORM 查询与性能优化
ORM 查询是后端编码里频率最高的操作之一。日常写业务时最常用的查询套路有这些:
# 获取所有 Article.objects.all() # 过滤 Article.objects.filter(category_id=1, title__icontains='Django') # 排除 Article.objects.exclude(status='draft') # 取单条,不存在会报 DoesNotExist Article.objects.get(id=1) # 拿第一条/最后一条 Article.objects.first() Article.objects.last() # 只取某些字段,返回字典列表 Article.objects.values('id', 'title') # 只取某些字段,返回元组列表 Article.objects.values_list('id', 'title') # 统计 Article.objects.filter(category_id=1).count()filter里支持很多字段查询条件,比如__contains是模糊匹配,__icontains是不区分大小写模糊匹配,__in是范围查询,__range是区间查询,__isnull判断是否为空。这些符号刚开始容易记混,其实规律很统一:双下划线后面跟的是查询类型。
再复杂一点的动态查询常用Q对象和F表达式:
from django.db.models import Q, F # 多条件 OR Article.objects.filter(Q(title__icontains='python') | Q(content__icontains='python')) # 字段间比较,比如浏览量大于点赞数 Article.objects.filter(views__gt=F('likes'))F表达式的作用是在数据库层面直接引用某字段的值,而不是先把数据取到 Python 里再比较,既快又避免并发下读到旧值。比如文章浏览量 +1,写成Article.objects.filter(id=1).update(views=F('views') + 1)是原子操作,不会因为两个人同时点击把增量覆盖掉。
性能优化里最经典的问题是 N+1 查询。下面这段代码很容易写出:
articles = Article.objects.all() for article in articles: print(article.category.name)每次取article.category都会发一条 SQL,100 篇文章就是 101 条查询。解决方式是用select_related,它在查询时直接通过 JOIN 把外键关联对象一起取出来:
articles = Article.objects.select_related('category').all()对于反向关联、多对多这类无法用 JOIN 简单拿下的场景,用prefetch_related:
categories = Category.objects.prefetch_related('articles').all()判断该用哪个有一个简单的经验:select_related适合“我通过当前对象马上拿单个外键对象”的情况;prefetch_related适合“我要拿到某个对象的多个关联对象”的情况。prefetch_related本质上是先查主表,再根据主表 id 批量查关联表,然后在内存里完成映射,避免一条条查询。
3.4 数据导入导出命令
Django 提供了内置的序列化工具,可以把数据导出成 JSON 或 YAML,再用命令导回来。这在环境迁移、数据备份、测试数据准备时很有用。
# 导出指定应用的全部数据 python manage.py dumpdata articles > articles.json # 只导出部分数据 python manage.py dumpdata articles.Article --pks 1,2,3 > articles_123.json # 导入数据 python manage.py loaddata articles.jsondumpdata导出的是模型对应的序列化数据,默认包含所有字段。loaddata导入时会按主键去重,如果主键冲突,可以选择--ignorenonexistent忽略不存在的字段。我在迁移环境时最常用的套路是:先把旧环境dumpdata导出,再到新环境loaddata,然后清理掉 session、log 这类没用的数据。要注意,如果两家数据库的字符集、表结构不一致,数据导出导入后可能对不上,所以导出前先确认好模型版本一致。
4. 视图与路由:业务逻辑的出口
4.1 URL 路由配置
Django 的 URL 路由本质是一张映射表,把用户请求的 URL 匹配到对应的视图函数。新版用path和re_path,老版本里常见的url已经不太推荐了。我在新项目里基本只用path,除非遇到特别复杂的正则需求。
根路由文件是项目配置包下的urls.py,一般长这样:
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('articles.urls')), ]应用内再建一个urls.py:
from django.urls import path from . import views urlpatterns = [ path('', views.article_list, name='article_list'), path('<int:pk>/', views.article_detail, name='article_detail'), ]<int:pk>这种写法是路径转换器,Django 会自动把这一段 URL 参数解析成整数,然后作为pk关键字参数传给视图。如果只想捕获任意字符串,用<str:name>;如果你想捕获路径分隔符,用<path:url_path>。这些转换器既让路由清晰,又省了在视图里手动正则匹配的麻烦。
命名 URL 是个好习惯,我给每个路由都加name参数,然后在视图或模板里通过reverse反向解析:
from django.urls import reverse url = reverse('article_detail', args=[1])这样做的好处是,URL 结构改动时,只要路由里的 name 不变,代码里的引用就不用跟着改。我在项目后期重构 URL 时经常靠这个节省大量时间。
路由顺序也有讲究。Django 按照urlpatterns里从上到下的顺序匹配,第一个命中就停止。所以具体路由要放在模糊路由前面。比如path('<int:pk>/', ...)如果在path('article/new/', ...)前面,article/new会被前一个规则匹配成 pk 为字符串的请求,直接 404。
4.2 函数视图与装饰器
函数视图是最直观的写法,用一个函数接收request,返回HttpResponse或JsonResponse。基础例子:
from django.shortcuts import render, get_object_or_404 from django.http import JsonResponse from django.views.decorators.http import require_http_methods from django.contrib.auth.decorators import login_required @login_required @require_http_methods(['GET', 'POST']) def article_detail(request, pk): if request.method == 'POST': # 处理提交 return JsonResponse({'status': 'ok'}) article = get_object_or_404(Article, pk=pk) return render(request, 'articles/detail.html', {'article': article})这里有几个常用装饰器,每个都解决一个具体问题:
@login_required要求用户必须登录,未登录时会跳转到登录页或返回 302。这在内部管理系统里几乎是必备的。@require_http_methods(['GET', 'POST'])限制请求方法,比如某个接口只允许 POST,其他请求直接返回 405 Method Not Allowed,比你在视图里手动判断request.method更规范。
使用render时,Django 会自动去 templates 目录下找模板文件。get_object_or_404等价于get加DoesNotExist异常处理,查不到就返回 404,比手动 try-except 简洁得多。
开发 API 时,我习惯统一返回JsonResponse。它默认会把字典转成 JSON 字符串,并且设置正确的 Content-Type。如果你要返回列表,JsonResponse(data, safe=False)才能传非字典对象。这个safe=False是新手的常见报错点。
CSRF 验证是 Django 给表单类 POST 请求提供的防护机制。普通后端页面里的表单需要带{% csrf_token %}模板标签,否则提交会报 403。如果你写的接口设计为纯 API,又不想麻烦每个客户端都处理 CSRF Token,可以在视图上加@csrf_exempt。但我强烈建议仅对确实需要的外部回调接口使用,内部接口尽量保留 CSRF 防护,多一层安全就多一分安心。
4.3 类视图
Django 的函数视图能解决大部分问题,但 CRUD 场景的代码会写得很重复:列表查询、分页、详情查询、表单校验、重定向……几乎都是复制粘贴。类视图(Class-Based View,CBV)把常见的模式抽象出来,写出来的代码更少。
最常用的是ListView和DetailView:
from django.views.generic import ListView, DetailView from .models import Article class ArticleListView(ListView): model = Article template_name = 'articles/list.html' context_object_name = 'articles' paginate_by = 10 ordering = ['-created_at'] class ArticleDetailView(DetailView): model = Article template_name = 'articles/detail.html' context_object_name = 'article'ListView默认会把当前模型的所有对象传给模板。如果你不想用默认的object_list变量名,就设置context_object_name,模板里直接用你想用的名字,这样更符合阅读直觉。paginate_by开启分页后,模板里会自动获得page_obj,通过page_obj.has_next、page_obj.has_previous可以渲染上一页下一页。
CreateView和UpdateView则用来处理表单提交:
from django.views.generic.edit import CreateView, UpdateView from django.urls import reverse_lazy from .models import Article class ArticleCreateView(CreateView): model = Article fields = ['title', 'content', 'category'] template_name = 'articles/form.html' success_url = reverse_lazy('article_list')我强调一点,使用 CBV 时success_url最好用reverse_lazy而不是reverse。因为reverse在模块加载时就会执行路由解析,如果此时 URL 配置还没加载完,可能抛出异常;reverse_lazy会延迟到真正需要时才解析,更稳妥。
CBV 和 FBV 怎么选?我的经验是:简单接口、逻辑复杂的接口,用 FBV 更灵活,调试也直观;标准的列表、详情、创建、更新场景,用 CBV 能省一半代码。项目里两种方式混用完全没问题,别为了统一而把简单的接口硬写成 CBV。
4.4 请求生命周期与中间件
理解 Django 的请求生命周期,能帮你快速定位很多奇怪的报错。
一次请求大致经过这几个阶段:客户端发请求 → Web 服务器(Nginx 或 Django 自带服务器)接收 → 经过中间件链 → URL 路由匹配 → 视图函数处理 → 返回响应 → 再经过中间件链返回给客户端。
中间件是 Django 非常强大的机制,它相当于在请求前后插入的钩子。settings.py里的MIDDLEWARE列表就是中间件链,顺序不能随便调。比如CommonMiddleware负责 URL 规范化,CsrfViewMiddleware负责 CSRF 校验,AuthenticationMiddleware负责把用户信息绑定到 request 上。
调试时,如果某个中间件抛了异常,后续的中间件就不会执行。我看到很多 500 报错,其实是中间件里自定义逻辑写得有问题,而不是视图的问题。排查方法是看堆栈显示异常发生在哪个中间件类,一般在process_view或process_request方法里。
如果你需要统计每个接口的耗时,自己写一个中间件也很简单:
import time class RequestTimingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): start = time.time() response = self.get_response(request) duration = time.time() - start response['X-Request-Time'] = f'{duration:.3f}s' return response注册到MIDDLEWARE后,每个响应头都会带上耗时,排查慢接口非常有用。
5. 模板与静态文件
5.1 模板语法与模板继承
Django 的模板系统语法简单,上手很快。核心语法有:{% %}是逻辑标签,{{ }}是变量输出,{# #}是注释。
变量输出示例:
<h1>{{ article.title }}</h1> <p>分类:{{ article.category.name }}</p> <p>浏览量:{{ article.views }}</p>模板里的变量自动支持属性访问,article.category.name会先尝试取属性,再尝试字典取值,这个查找顺序和 Python 不太一样,新手可能觉得灵活,但也会掩盖数据结构的类型问题。
模板循环和判断:
{% for article in articles %} {% if article.views > 1000 %} <span class="hot">热门</span> {% else %} <span>普通</span> {% endif %} {% empty %} <p>列表为空</p> {% endfor %}模板继承是后台系统里最有价值的功能。我做一个管理后台时,把公共的侧边栏、顶栏、底部脚本放在base.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>{% block title %}默认标题{% endblock %}</title> </head> <body> <header>公共顶栏</header> <main> {% block content %}{% endblock %} </main> <footer>公共底部</footer> </body> </html>子模板只需要写:
{% extends 'base.html' %} {% block title %}文章列表{% endblock %} {% block content %} <ul> {% for article in articles %} <li>{{ article.title }}</li> {% endfor %} </ul> {% endblock %}这样每个页面只维护自己差异化的部分,整体结构永远保持一致。多人协作时,公共部分改一处,全站生效,比复制粘贴强壮太多。
模板过滤器常用|date、|length、|default、|safe。比如:
<p>发布时间:{{ article.created_at|date:"Y-m-d H:i" }}</p> <p>标题长度:{{ article.title|length }}</p> <p>摘要:{{ article.summary|default:"暂无摘要" }}</p>|safe会告诉 Django 这个变量是可信的 HTML,不做转义直接输出。使用它时要格外小心,如果变量里包含用户输入的内容,相当于给 XSS 攻击开了门。比如用户提交了一段<script>代码,你用|safe输出,浏览器就会执行。我的原则是:除非明确知道内容是安全的,否则永远不要给用户输入的数据加|safe。
5.2 静态文件收集与部署
开发环境里,浏览器访问STATIC_URL下的文件时,Django 会自动去STATICFILES_DIRS目录找。但生产环境通常不会让 Django 直接服务静态文件,而是用 Nginx 或 CDN 处理。
部署时有一个必做命令:
python manage.py collectstatic这个命令会把项目里所有应用下的 static 目录、STATICFILES_DIRS里的文件,全部复制到STATIC_ROOT指定的目录。Nginx 只需要把/static/请求映射到这个目录就行了。
如果项目不想单独配 Nginx,或者机器上不方便部署独立静态服务,可以用whitenoise中间件。它让 Django 在生产模式也能直接服务静态文件,简单项目部署很省事。配置方式是在MIDDLEWARE里加一行,再把STATICFILES_STORAGE改成它的存储类。但并发量较高的项目,我还是建议静态文件走 Nginx 或 CDN,让 Django 专注处理动态业务。
上传文件相关的MEDIA_ROOT在部署时也要配套处理。用户上传的图片存在项目目录外的media文件夹,Nginx 单独配置/media/的 alias 指向它。我踩过的坑是:本地开发能上传,部署后上传失败,一查发现是MEDIA_ROOT的路径权限不对,进程没有写权限。解决方式就是确保运行项目的用户对该目录有读写权限,或者把目录挂载到专门的存储上。
6. 自定义命令与调试测试
6.1 自定义 management command
Django 项目跑久了会发现,很多重复工作其实可以交给命令完成:清理过期数据、给用户发提醒邮件、同步第三方平台数据、定时生成报表……这些任务如果靠手工执行,不仅累,还容易漏。Django 的自定义命令机制可以让你通过manage.py 命令名直接调用自己的脚本。
创建自定义命令的第一步是在应用目录下建好固定结构:
your_app/ management/ __init__.py commands/ __init__.py clean_expired.py注意management和commands目录下都要有__init__.py文件,否则 Django 识别不到。命令文件名就是命令名,这里对应的是python manage.py clean_expired。
命令文件的核心代码:
from django.core.management.base import BaseCommand, CommandError from django.utils import timezone from myapp.models import Token class Command(BaseCommand): help = '清理过期 token' def add_arguments(self, parser): parser.add_argument('--days', type=int, default=7, help='保留天数') def handle(self, *args, **options): days = options['days'] cutoff = timezone.now() - timezone.timedelta(days=days) deleted_count, _ = Token.objects.filter(created_at__lt=cutoff).delete() self.stdout.write(self.style.SUCCESS(f'清理完成,共删除 {deleted_count} 条记录'))add_arguments用 argparse 的方式定义参数,handle是命令真正执行的入口。self.stdout.write和self.style.SUCCESS是标准输出封装,支持颜色提示,比动辄print更像命令行工具。
自定义命令还可以和系统定时任务结合。比如 Linux 上用 crontab 每天凌晨跑一次清理:
0 3 * * * cd /path/to/project && source venv/bin/activate && python manage.py clean_expired --days 1这样重复工作就不需要人肉惦记了。调试命令时,我经常在handle里先打印参数,确认传参是否符合预期,再执行实际逻辑。
6.2 调试工具与日志
Django 开发时的调试利器是django-debug-toolbar。安装后在settings.py里添加配置,即可在页面右侧显示一个调试面板,能看到当前请求执行了多少条 SQL、每一条耗时多久、模板渲染时间、缓存命中情况等。
配置要点:
INSTALLED_APPS = [ # ... 'debug_toolbar', ] MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', # ... ] INTERNAL_IPS = ['127.0.0.1']注意INTERNAL_IPS要包含你本机访问的 IP,否则调试面板不显示。如果用了远程开发环境,可以把开发机 IP 加进去,或者临时设置SHOW_TOOLBAR_CALLBACK为恒真。但生产环境绝对要关掉,调试工具栏会暴露接口性能和 SQL 语句,不该出现在公网。
日志配置是后端排查问题的另一只眼睛。settings.py里可以这样配一个基础的日志输出:
import os LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, 'file': { 'class': 'logging.FileHandler', 'filename': BASE_DIR / 'logs/app.log', 'encoding': 'utf-8', }, }, 'root': { 'handlers': ['console', 'file'], 'level': 'INFO', }, 'loggers': { 'django.db.backends': { 'handlers': ['console'], 'level': 'DEBUG', }, }, }把django.db.backends的日志级别设为 DEBUG,就会在控制台打印每条 SQL 语句。这个开关在排查慢查询时非常有用,能直接看到是哪条 SQL 耗时最长。日志文件要注意按天切割,不然跑几个月文件会奇大无比,我一般用logging.handlers.TimedRotatingFileHandler当 handler。
6.3 自动化测试基础
后端项目如果完全没有测试,改一个字段都可能让另外十个接口悄悄挂掉。Django 自带的测试框架基于unittest,不需要额外安装库。最基础的测试长这样:
from django.test import TestCase from django.urls import reverse from myapp.models import Article class ArticleViewTest(TestCase): def setUp(self): self.article = Article.objects.create(title='测试文章', content='内容') def test_article_list_status(self): resp = self.client.get(reverse('article_list')) self.assertEqual(resp.status_code, 200) self.assertContains(resp, '测试文章')self.client是 Django 提供的测试客户端,可以模拟 GET、POST 请求。跑测试的命令:
python manage.py test只跑某个应用的测试:
python manage.py test myapp写测试时不用覆盖所有场景,优先覆盖核心业务逻辑和容易回归的边界情况,比如用户没有权限访问某个接口时返回 403、创建文章时没有传必填字段时提示错误等。测试的目的不是为了追求覆盖率数字,而是让你在改代码时敢继续往下写,跑一遍测试就知道有没有破坏旧行为。
7. 常见问题排查速查表
7.1 高频报错对照表
我在项目维护过程中整理了一些出现频率最高的报错,下面这张表基本能覆盖大部分日常问题。
| 报错现象 | 常见原因 | 解决思路 |
|---|---|---|
| 400 Bad Request | ALLOWED_HOSTS 没配置或配错 | 检查 DEBUG=False 后是否填了域名/IP |
| 403 Forbidden | CSRF 校验失败 | 表单加上{% csrf_token %},或对纯 API 接口谨慎使用@csrf_exempt |
| 404 Not Found | 路由写错或视图不存在 | 用resolve或手动访问确认 URL,检查路由匹配顺序 |
| 405 Method Not Allowed | @require_http_methods限制之外的方法请求 | 确认接口支持的请求方法 |
| 500 Internal Server Error | 视图代码异常、模型字段不存在 | 看服务日志和堆栈,重点区分调试模式和生产模式 |
Table 'xxx' doesn't exist | 迁移未执行 | 执行migrate,用showmigrations确认状态 |
Column 'xxx' doesn't exist | 模型新加字段但未迁移 | 执行makemigrations和migrate |
Field 'id' expected a number | URL 参数被解析成字符串或类型不匹配 | 确认路由转换器用的是<int:pk> |
Connection refused | 数据库服务没启动或 HOST/PORT 配错 | 检查数据库连接配置,用客户端工具先连通试试 |
| 中文乱码 | MySQL 字符集与连接字符集不一致 | 统一建库和连接为 utf8mb4 |
| 静态文件 404 | collectstatic 未执行或 Nginx 路径没配对 | 执行collectstatic,检查STATIC_ROOT和 Nginx alias |
如果报错是后台管理的OperationalError: no such table: django_session,基本就是初始迁移没跑,python manage.py migrate一下就好。这个错我每次新建环境都会见到,属于“新手的第一课”。
7.2 避坑经验清单
除了对照表,还有一些我自己踩过多次的坑,值得单独列出来。
第一,不要在数据库层面手动改表。很多人图快,直接用 SQL 改了线上表结构,然后 Django 迁移一执行就报“字段已存在”或“字段不存在”的冲突。正确做法是永远通过makemigrations和migrate来改表,数据库结构以迁移历史为准。
第二,User模型要早做扩展。Django 自带用户模型能满足基础登录,但如果业务需要手机号登录、昵称、头像等字段,项目早期就应该通过AbstractUser自定义用户模型,配置到AUTH_USER_MODEL。一旦项目上线再换用户模型,迁移会非常痛苦,涉及大量外键引用变动。我吃过一次亏,之后每个新项目第一件事就是扩展用户模型。
第三,上传文件路径不要写死。用户上传的文件名如果直接用用户输入,既容易产生路径冲突,也可能埋下安全问题。建议用工具库生成随机文件名,并把文件按日期分目录存储。具体到 Django 后端,upload_to参数可以传一个函数,自己控制路径:
def upload_path(instance, filename): ext = filename.split('.')[-1] return f'uploads/{timezone.now().strftime("%Y/%m")}/{uuid4().hex}.{ext}'第四,多个环境之间,requirements.txt要定期对齐。我在维护旧项目时经常遇到本地装的依赖版本很新,线上还是老版本,导致某些新语法在线上报错。建议部署时用虚拟环境全量重装,而不是增量升级。
第五,runserver不是万能调试器。有时修改了settings.py里的配置,但服务没有冷重启,新配置没生效。比如改了MEDIA_ROOT后,上传位置还是老目录,就是因为开发服务器没有完全重启。遇到诡异问题时,先重启服务再排查,这是成本最低的一步。
写在最后的一点体会
这份 Django 后端命令和代码总结,最初只是我自己存储在项目仓库里的一个维护文档,每次排查完一个奇怪问题就往里补充一条,后来发现它比大部分现成教程都好用,因为里面的每一条记录都对应一次真实的踩坑过程。尤其是迁移命令、CSRF、静态文件 404、数据库长连接这几类问题,几乎是每个项目都会遇到的“老朋友”。我个人最大的体会是:Django 的上手门槛并不高,但能让你真正高效起来的,是你对自己项目沉淀下来的那份“命令记忆”。遇到报错别急着改代码,先想想是配置问题还是迁移问题,用showmigrations、sqlmigrate、日志面板把现场摸清楚,再动手,往往几分钟就能定位。希望这份整理能帮你少走一些弯路,把时间花在真正的业务逻辑上。