
1. 先把Django框架的定位讲透别一上来就背命令1.1 Django框架到底是个什么东西Django框架是一个用Python写Web应用的全栈式框架。所谓全栈不是营销词而是它把绝大多数Web项目绕不开的东西都内置了URL路由分发、模板渲染、数据库访问层ORM、表单校验、用户认证与权限、后台管理界面、缓存、国际化、安全中间件。你新建一个项目不用先纠结选哪个ORM、哪个表单库、哪个后台脚手架它已经替你做好了一套约定。我用生活场景打个类比。Flask、FastAPI这类微框架像毛坯房水电你自己走、墙你自己砌自由度极高但你得对每个细节负责Django框架更像精装交付的房子户型固定、管线齐全你进去主要是摆家具、改局部格局。这个差别直接决定了项目适不适合用它需求稳定、功能偏管理型、需要快速上线、后台要给非技术同事用这类场景Django框架的优势非常明显反过来你要做的是一个极轻量的推理接口或者对请求延迟敏感到毫秒级那它自带的那套东西反而成了负担。很多人对Django框架的第一印象是重。这个印象有一半是对的但要说清楚它的重主要体现在概念数量上而不是运行性能上。你要理解MVT、中间件、App、迁移、QuerySet、Manager这些概念入门曲线确实比微框架陡。可一旦跨过这道坎后面写业务代码的速度会明显快起来因为大量重复劳动已经被框架吸收掉了。浅学Django框架我认为重点不是把所有API背下来而是搞清楚三件事请求是怎么一步步走到你的代码里的数据是怎么从模型变成页面的什么样的需求它顺手、什么样的需求它别扭。这三件事想通了剩下的都是查文档的活。1.2 与Flask、FastAPI的横向取舍选框架这件事没有绝对对错只有匹配度。我列一个实际项目里最常比较的维度你可以对着自己的需求打个分。对比维度Django框架FlaskFastAPI内置能力认证、Admin、ORM、表单、缓存一应俱全核心极简靠扩展拼装核心极简强在类型校验与异步上手门槛概念多前期慢起步快起步快需熟悉异步与类型注解后台管理Admin开箱可用二次定制成本低需自行搭建或引入扩展需自行搭建数据层自带ORM与迁移工具链需自选SQLAlchemy等需自选SQLAlchemy等典型适用内容管理、运营后台、业务中台、内部系统中小型站点、原型验证接口服务、算法服务封装、高并发IO场景学习回报一套体系覆盖大部分业务开发灵活但需自行补齐工程规范接口开发效率高生态相对年轻我的经验是如果这个项目三个月后要交给另一个同事维护并且里面有大量表单、权限、后台运营操作Django框架的胜率很高因为它有一套大家都这么写的约定交接成本低。如果项目本质是一堆对外接口前端完全独立那FastAPI写起来更轻快。这里有个常被忽略的点Django框架并不是不能写接口。它配合DRFDjango REST Framework之后接口层的序列化、认证、分页、限流都有成熟方案只是相比FastAPI多了一层封装。反过来说FastAPI也不是不能做后台页面只是你得自己找模板和Admin方案。框架之争之所以经常吵不出结果就是因为往往比的不是框架而是作者更熟悉哪一套。提示不要为了用新框架而换框架。团队里没有第二个人能维护的技术选型本质上是给自己埋雷。1.3 什么人适合从浅学切入我给浅学下的定义是不追求把官方文档从头翻到尾而是用一个小而完整的需求把框架的主干链路走通一遍。适合这么干的人有三类。第一类是有其他语言Web经验、想快速把Python这门手艺用起来的开发者。你可能写过Java的Spring或者Node的Express那你需要的不是语法教学而是概念映射——知道Django的App对应什么、迁移对应什么、中间件对应什么半天就能上手。第二类是数据分析、算法方向的同学。手上有一堆脚本想包个界面给同事用或者想做个简单的数据录入和查询页面。这类需求用Django框架非常划算模型一写、Admin一开一天能出可用版本。第三类是纯新手。我的建议是先别急着做电商、社交这种大而全的练手项目那类项目会让你把时间全花在业务逻辑上反而学不到框架本身。更好的选择是做一个图书借阅登记或设备报修单这种业务简单、但模型关系、表单校验、权限区分都齐全的小系统。麻雀虽小五脏俱全主干链路一个不少。2. 环境准备与项目骨架让第一个页面真的跑起来2.1 版本选择与虚拟环境版本这件事别纠结太久但有一条底线不要用已经停止维护的旧版本。新项目直接上当前的稳定版即可同时把Python版本控制在框架支持的范围内。命令行的第一步永远是隔离环境这一点我被坑过不止一次——系统级装包过两个月另一个项目要不同版本直接打架。# 建一个独立环境避免污染系统Python python -m venv .venv # 激活Windows .venv\Scripts\activate # 激活macOS / Linux source .venv/bin/activate # 安装框架 pip install django # 确认版本 python -m django --version装完之后一定要把依赖固化到文件里pip freeze requirements.txt。这个动作看着琐碎但它是你未来在任何一台机器上复现环境的唯一凭证。我见过太多我这能跑你那不行的扯皮八成都是依赖版本不一致。注意虚拟环境目录千万不要提交到版本库。在项目根目录放一个.gitignore把.venv/、__pycache__/、*.sqlite3、media/、.env都写进去。2.2 startproject与startapp的目录拆解新建项目的两条命令很多人敲了几十遍也没弄明白它们的分工。django-admin startproject config . python manage.py startapp blog第一条命令里的config是项目配置包的名字末尾那个点非常关键——它表示在当前目录生成而不是再套一层同名文件夹。不加这个点你会得到config/config/settings.py这种套娃结构后期路径处理会很难受。第二条命令创建的App是功能单元一个项目可以有多个App比如blog管内容、accounts管用户、orders管订单。生成之后的结构大致是这样我按你以后会经常打开哪个文件来标注. ├── config/ │ ├── settings.py # 全局配置改得最频繁 │ ├── urls.py # 根路由负责分发到各App │ ├── asgi.py # 异步部署入口 │ └── wsgi.py # 同步部署入口 ├── blog/ │ ├── models.py # 数据模型 │ ├── views.py # 业务逻辑 │ ├── admin.py # 后台注册 │ ├── apps.py # App元信息 │ └── migrations/ # 迁移脚本只增不改 ├── manage.py # 命令行入口 └── requirements.txt关于App的划分我给一条实用建议按业务边界切不要按技术层次切。有人喜欢建models_app、views_app这是典型的错法模型和视图本来就是同一个业务的两面。正确做法是blog、accounts、orders这样切每个App内部自包含。后期某个业务要拆出去做独立服务时你把整个App目录搬走就行改动面很小。2.3 settings.py里必须早点改的几项settings.py是项目的总开关下面这几项如果拖到上线前才改通常要返工。第一是INSTALLED_APPS。新建的App必须注册进去否则模型不会被迁移工具识别你会遇到迁移文件生成了但表没建这类诡异现象。注册时写App的配置类名更稳妥blog.apps.BlogConfig。第二是DATABASES。开发阶段用SQLite没问题零配置、文件即数据库。但只要涉及并发写入、复杂查询或者多人协作尽早换成PostgreSQL。这个切换在项目初期成本极低等到业务跑起来再换光是数据类型差异就够你改一整天。第三是LANGUAGE_CODE和TIME_ZONE。前者设为zh-hans后者设为Asia/Shanghai并且记得把USE_TZ保持为True存储统一用UTC展示时再转本地时区。我见过线上时间差八小时的故障根因就是有人把USE_TZ关了然后服务器时区又是UTC。第四是静态文件与媒体文件的三件套STATIC_URL、STATICFILES_DIRS、MEDIA_ROOT。开发期靠runserver自动托管上线后必须交给Web服务器处理这个认知要提前建立。# config/settings.py 片段 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, blog.apps.BlogConfig, # 自己写的App要注册 ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True STATIC_URL static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL media/ MEDIA_ROOT BASE_DIR / media一处容易踩的坑BASE_DIR是Path对象所以用/拼接而不是字符串的。另外DEBUG True只在本地用上线必须是False同时把ALLOWED_HOSTS配好否则要么报DisallowedHost要么直接把调试信息暴露给外部。3. 核心设计思路拆解一次请求到底经历了什么3.1 从浏览器到视图的完整链路这是我认为浅学Django框架最该先画清楚的一张图——虽然我不画图但你可以用文字在脑子里过一遍。浏览器发起请求先到Web服务器再交给框架的Handler。Handler做的第一件事是匹配中间件栈请求会依次穿过MIDDLEWARE列表里配置的每一层比如安全中间件、会话中间件、CSRF中间件、认证中间件。穿过之后进入URL解析器根urls.py按顺序逐条比对path()规则匹配成功就把请求交给对应的视图函数或视图类。视图里通常会查数据库、组装上下文然后交给模板渲染成HTML或者直接返回JSON。返回的响应再沿着中间件栈反向穿出去每一层都有机会修改响应头或内容最后交给服务器回给浏览器。这个链路里有两个细节值得记住。一是URL匹配是自上而下、先到先得所以路由顺序有意义宽泛规则放后面。二是中间件的顺序不是随便排的请求阶段按列表正序执行响应阶段按倒序执行这意味着排在后面的中间件在响应时先拿到数据。如果你自己写了中间件位置放错了会出现拿不到登录用户这类问题。# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(blog/, include(blog.urls)), # 分发到App内部 ]# blog/urls.py from django.urls import path from . import views app_name blog urlpatterns [ path(, views.ArticleListView.as_view(), namelist), path(int:pk/, views.ArticleDetailView.as_view(), namedetail), ]注意app_name这个变量它是命名空间。有了它模板里就能写{% url blog:detail article.pk %}即使两个App有同名路由也不会冲突。这个习惯建议从第一个项目就养成后期改URL不用全项目搜索替换。3.2 MTV与MVC的对照关系框架文档里说自己是MTV模式很多人一看就懵因为跟熟悉的MVC对不上。其实只是命名差异本质一样传统MVCDjango框架的MTV职责ModelModel数据结构和业务规则ViewTemplate负责展示尽量少写逻辑ControllerView接收请求、调度数据、决定返回什么之所以改叫MTV是因为框架把控制器的活儿放到了View里而把视图这个词让给了模板。理解这个映射之后你会发现它的分层其实是更清晰的模板只管渲染视图只管调度模型只管数据。这种强分工带来的好处是在多人协作时前端同学改模板不会碰到业务逻辑后端同学改视图也不用管HTML长什么样。但这里有个现实问题模板里天生容易渗入业务逻辑。当你在模板里写{% if user.is_authenticated and user.profile.level 3 %}这种判断时就已经在越界了。我的处理原则是模板只做展示层的分支比如有没有数据、显示哪种样式凡是涉及权限和业务规则的判断一律挪到视图里算好传一个布尔值或者状态枚举过去。这样模板保持简单测试也好写。3.3 中间件与信号好用但别滥用中间件是横切关注点的天然落点。什么叫横切就是那些每个请求都要做一遍的事比如请求耗时统计、统一给响应加安全头、按IP做简单限流、给每个请求打上追踪ID。这类逻辑如果写在视图里你就得每个视图都抄一遍。# blog/middleware.py import time import logging logger logging.getLogger(__name__) class TimingMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): start time.perf_counter() response self.get_response(request) cost (time.perf_counter() - start) * 1000 if cost 500: # 只记录慢请求 logger.warning(slow request %s %s %.1fms, request.method, request.path, cost) response[X-Response-Time] f{cost:.1f}ms return response写完之后要在MIDDLEWARE里注册。这个中间件的价值在于你不改任何视图就能拿到全站耗时数据慢请求会自动浮出水面。信号则是另一种机制用于解耦某件事发生后需要做点别的比如用户注册成功后发一封欢迎邮件、订单状态变更后写一条审计日志。用信号的好处是业务代码里不用显式调用坏处是它太隐蔽——排查问题时你看着一段代码怎么也想不通某个字段是谁改的翻半天才发现是某个信号接收器干的。提示信号适合做锦上添花的副作用比如日志、通知、缓存失效。凡是影响主流程结果的操作老老实实写成显式函数调用可读性远比解耦重要。4. 模型与ORM数据结构定错后面全在还债4.1 字段选型与索引设计的实操判断模型是整个项目的地基这一层定错了后面视图、接口、报表全得跟着改。我一般会先问自己三个问题这个实体有哪些属性属性和属性之间是什么关系哪些字段会被频繁作为查询条件举个具体例子做一个内容发布模块。文章的标题、正文、状态、发布时间、作者、分类、标签这些字段的类型选择都有讲究。标题用CharField(max_length200)并加唯一约束正文用TextField状态用choices限定取值别用魔法数字发布时间用DateTimeField(auto_now_addTrue)只在创建时写入作者用外键指向用户模型并且要想清楚删除用户时文章怎么办——是级联删除还是置空这个决定要写进on_delete参数里。# blog/models.py from django.conf import settings from django.db import models from django.urls import reverse class Category(models.Model): name models.CharField(名称, max_length50, uniqueTrue) slug models.SlugField(标识, max_length50, uniqueTrue) class Meta: verbose_name 分类 verbose_name_plural verbose_name def __str__(self): return self.name class Article(models.Model): class Status(models.TextChoices): DRAFT draft, 草稿 PUBLISHED published, 已发布 ARCHIVED archived, 已归档 title models.CharField(标题, max_length200) slug models.SlugField(标识, max_length200, uniqueTrue) author models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namearticles, verbose_name作者, ) category models.ForeignKey( Category, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namearticles, verbose_name分类, ) summary models.CharField(摘要, max_length300, blankTrue) content models.TextField(正文) status models.CharField( 状态, max_length20, choicesStatus.choices, defaultStatus.DRAFT, ) view_count models.PositiveIntegerField(阅读量, default0) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: ordering [-created_at] indexes [ models.Index(fields[status, -created_at]), ] verbose_name 文章 verbose_name_plural verbose_name def __str__(self): return self.title def get_absolute_url(self): return reverse(blog:detail, kwargs{pk: self.pk})这段代码里有几个决定值得单独说明。on_delete必须显式写。作者删除时文章一起删CASCADE还是保留SET_NULL这不是技术问题而是业务问题。内容型系统通常不希望文章消失那就用PROTECT或者给作者加个已注销状态而不是物理删除用户。related_name要写。它决定了反向查询的写法user.articles.all()比默认的article_set可读性好得多而且一旦不写后期想改就得改所有调用点。联合索引(status, -created_at)对应的是最高频的查询——已发布文章按时间倒序分页。索引不是越多越好每个索引都会拖慢写入并占用空间。我的做法是先把查询写出来再看哪些查询出现在列表页、详情页、后台筛选里只给这些建索引。给一个几乎不用的字段建索引纯粹是给自己找麻烦。4.2 迁移机制为什么它只增不改迁移文件本质上是数据库结构变更的版本记录。框架通过比对当前模型定义和迁移文件历史推导出需要执行的SQL。这个机制最大的价值是可追溯任何一次表结构变更都有文件留档多人协作时不会出现你本地加了字段我不知道的情况。常用命令就四条但含义要分清# 根据模型变更生成迁移文件只生成不执行 python manage.py makemigrations # 查看某次迁移将要执行的SQL强烈建议执行前看一眼 python manage.py sqlmigrate blog 0002 # 执行所有未应用的迁移 python manage.py migrate # 查看哪些迁移已应用 python manage.py showmigrations blog头号坑是改了模型但忘记生成迁移。表现是代码里字段存在数据库里没有一查就报no such column。更隐蔽的情况是迁移文件生成了但没执行makemigrations和migrate是两步别当成一步。第二个坑是多人同时改模型。两个分支各自生成了0003_xxx合并时会出现两个同序号文件迁移工具会报冲突。解决办法是手工把它们合并成一个或者用makemigrations --merge让工具帮你生成合并迁移。团队里减少这类问题的办法很土但有效约定每天合并主干前先拉一次代码迁移文件冲突当天解决别攒着。第三个坑是生产环境不可逆操作。删字段、改类型这类迁移一旦执行回滚成本极高。我的习惯是分两步走先加新字段、双写一段时间、确认没依赖后再删旧字段。多花一天省下的是整晚的抢救时间。4.3 QuerySet的惰性与N1排查QuerySet最反直觉的一点是它惰性求值。你写下Article.objects.filter(statuspublished)的时候一条SQL都没发出去只是构造了一个查询描述。真正触发执行的是遍历、切片、len()、list()这些动作。这个设计的好处是链式调用可以无限拼接最后才组装成一条SQL。但惰性也带来了经典陷阱——N1查询。假设列表页要显示每篇文章的作者名# 反例每一次循环都会查一次用户表 articles Article.objects.filter(statuspublished)[:20] for a in articles: print(a.title, a.author.username) # 这里触发N次查询20篇文章就是1次文章查询加20次用户查询共21次。列表页数据量一大响应时间直接起飞。# 正例一次JOIN把作者带出来 articles ( Article.objects .filter(statuspublished) .select_related(author, category) # 外键、一对一用这个 .prefetch_related(tags) # 多对多用这个 [:20] )select_related走的是SQL JOIN适用于外键和一对一prefetch_related是分两次查询再在Python里拼装适用于多对多和反向外键。选错的话效果会打折甚至有反效果。那怎么发现N1三个层次的工具。开发阶段用django-debug-toolbar页面侧边栏直接告诉你这次请求发了几条SQL、每条耗时多少、有没有重复。临时排查可以用from django.db import connection, reset_queries from django.conf import settings settings.DEBUG True reset_queries() list(Article.objects.filter(statuspublished)[:20]) print(len(connection.queries))生产环境则靠日志和连接数监控发现某接口SQL数量与返回条数呈线性关系基本就能定位。4.4 事务与并发写入的边界涉及多表写入的操作必须包在事务里。比如扣库存 生成订单这两步任何一步失败都应该整体回滚否则数据就废了。from django.db import transaction from django.db.models import F transaction.atomic def create_order(user, product_id, quantity): product ( Product.objects .select_for_update() # 行级锁防超卖 .get(pkproduct_id) ) if product.stock quantity: raise ValueError(库存不足) product.stock F(stock) - quantity product.save(update_fields[stock]) return Order.objects.create( useruser, productproduct, quantityquantity )两个关键点。select_for_update()在SQLite上不生效必须用PostgreSQL或者MySQL这类支持行锁的数据库这也是我前面建议尽早换库的原因之一。F()表达式让减法在数据库侧完成避免读出来减一写回去的竞态。还有一个容易忽略的地方transaction.atomic装饰在函数上函数内部的异常如果没有向外抛事务不会回滚。所以别在事务里写try/except把异常吞掉那等于白包了。5. 视图、模板与表单把页面真正做出来5.1 函数视图与类视图怎么选函数视图直观所有逻辑在一屏内看得到适合流程特殊、分支多的页面。类视图CBV把通用流程抽象成了钩子方法列表页、详情页、表单页这类高度模板化的场景用它代码量能少一半。# 类视图列表页 from django.views.generic import ListView from .models import Article class ArticleListView(ListView): model Article template_name blog/list.html context_object_name articles paginate_by 10 def get_queryset(self): qs ( Article.objects .filter(statusArticle.Status.PUBLISHED) .select_related(author, category) ) keyword self.request.GET.get(q, ).strip() if keyword: qs qs.filter(title__icontainskeyword) return qs def get_context_data(self, **kwargs): ctx super().get_context_data(**kwargs) ctx[keyword] self.request.GET.get(q, ) return ctxListView帮你处理了分页、上下文命名、模板查找。代价是你得知道它内部的调用顺序先dispatch再get然后依次调get_queryset、get_context_data、render_to_response。搞不清顺序时调试会很痛苦。我的实际取舍是标准增删改查用类视图逻辑特殊的用函数视图别硬凑。有些团队一刀切全用类视图结果一个流程页面继承了三层读代码要跳五次文件反而更累。5.2 模板继承与静态文件管理模板继承解决的是页面骨架复用问题。做法是抽一个base.html把导航、页脚、样式引入都放进去中间留几个block。子模板只写差异部分。{# templates/base.html #} !DOCTYPE html html langzh-CN head meta charsetutf-8 title{% block title %}站点{% endblock %}/title {% load static %} link relstylesheet href{% static css/app.css %} /head body header{% block header %}导航区{% endblock %}/header main{% block content %}{% endblock %}/main footer页脚/footer {% block extra_js %}{% endblock %} /body /html{# templates/blog/list.html #} {% extends base.html %} {% block title %}文章列表{% endblock %} {% block content %} form methodget input typesearch nameq value{{ keyword }} button typesubmit搜索/button /form {% for article in articles %} article h2 a href{% url blog:detail article.pk %}{{ article.title }}/a /h2 p{{ article.summary|default:article.content|truncatechars:80 }}/p small{{ article.author.username }} · {{ article.created_at|date:Y-m-d }}/small /article {% empty %} p暂时没有文章。/p {% endfor %} {% if is_paginated %} nav {% 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 %} /nav {% endif %} {% endblock %}{% empty %}这个分支很多人不用结果空列表时页面上光秃秃的用户以为加载失败。|default:和|truncatechars:这些过滤器能省掉不少视图里的预处理代码。静态文件有个常见困惑本地runserver一切正常部署后样式全丢。原因是开发期的静态文件托管是框架的开发服务器帮忙做的上线后框架不再管这件事必须执行collectstatic把文件收集到统一目录再交给Web服务器托管。这个流程要提前跑通别等上线当天才发现。5.3 表单与校验把脏数据挡在门口表单层是我不太建议新手绕过的一层。有人图快直接在视图里request.POST.get()取值、手工判断、手工拼对象。代码短是短但校验规则散落各处改一次要改五个地方。# blog/forms.py from django import forms from .models import Article class ArticleForm(forms.ModelForm): class Meta: model Article fields [title, slug, category, summary, content, status] widgets { summary: forms.TextInput(attrs{placeholder: 不超过300字}), content: forms.Textarea(attrs{rows: 12}), } def clean_title(self): title self.cleaned_data[title].strip() if len(title) 5: raise forms.ValidationError(标题至少5个字符) return title def clean(self): data super().clean() status data.get(status) if status Article.Status.PUBLISHED and not data.get(summary): self.add_error(summary, 发布前需要填写摘要) return dataModelForm的好处是字段定义、类型转换、保存逻辑全都自动对齐模型。clean_字段名处理单字段校验clean处理跨字段的联合校验。add_error能把错误挂到具体字段上模板里用{{ form.summary.errors }}就能精确展示比统一弹一个输入有误友好得多。提示clean_xxx里拿到的值已经过基础类型校验但如果你在里面raise了异常后续字段的clean方法仍然会执行。涉及字段依赖的校验放clean里用add_error而不是raise这样能一次性把所有问题都反馈给用户。5.4 Admin后台的二次定制Admin是Django框架最容易被低估的部分。一个上午的配置能换来一个可用的运营后台让非技术同事自己改数据这笔买卖太划算了。# blog/admin.py from django.contrib import admin from .models import Article, Category admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display (name, slug) search_fields (name,) admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display (title, author, category, status, view_count, created_at) list_filter (status, category, created_at) search_fields (title, summary, content) list_select_related (author, category) # 列表页防N1 date_hierarchy created_at readonly_fields (view_count, created_at, updated_at) actions [make_published] admin.action(description批量标记为已发布) def make_published(self, request, queryset): updated queryset.update(statusArticle.Status.PUBLISHED) self.message_user(request, f已更新 {updated} 条)list_select_related这个参数值得单独拎出来说。后台列表默认会为每行去查外键关联对象一页100行就是100次额外查询加上它之后变成一次JOIN页面打开速度差别肉眼可见。date_hierarchy提供一个按年月日钻取的时间筛选条数据量大时非常好用。自定义action适合批量操作场景比让运营一条条点开改强得多。6. 认证权限与接口化从能跑到能交付6.1 用户体系与权限模型框架自带的用户模型够用但现实项目里十有八九要加字段比如手机号、头像、部门。正确姿势是项目一开始就自定义用户模型继承AbstractUser并在settings.py里声明AUTH_USER_MODEL。这件事必须在第一次迁移之前做一旦表建好了再换迁移历史会非常难看。# accounts/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone models.CharField(手机号, max_length20, blankTrue) department models.CharField(部门, max_length50, blankTrue) class Meta: verbose_name 用户 verbose_name_plural verbose_name# config/settings.py AUTH_USER_MODEL accounts.User权限这块有三层粒度别用混。第一层是登录态判断LoginRequiredMixin或者login_required管的是有没有登录。第二层是模型级权限框架自动为每个模型生成增删改查四个权限适合粗粒度控制。第三层是对象级权限判断这个用户能不能改这一篇框架本身不提供需要自己在视图里判断或者引入第三方方案。# 对象级权限的典型写法 class ArticleUpdateView(LoginRequiredMixin, UpdateView): model Article form_class ArticleForm def get_queryset(self): # 只允许编辑自己的文章 return Article.objects.filter(authorself.request.user)用get_queryset过滤而不是在dispatch里判断好处是越权访问会自然返回404不泄露这条数据存在但你无权访问的信息。6.2 接口化的落地思路前端要分离、移动端要复用的时候页面渲染方案就不够用了得输出结构化数据。最省事的做法是基于DRF搭一层。# blog/serializers.py from rest_framework import serializers from .models import Article class ArticleSerializer(serializers.ModelSerializer): author_name serializers.CharField(sourceauthor.username, read_onlyTrue) class Meta: model Article fields [id, title, slug, author_name, summary, status, view_count, created_at] read_only_fields [view_count, created_at]# blog/api.py from rest_framework import viewsets, permissions from .models import Article from .serializers import ArticleSerializer class ArticleViewSet(viewsets.ModelViewSet): serializer_class ArticleSerializer permission_classes [permissions.IsAuthenticatedOrReadOnly] def get_queryset(self): qs Article.objects.select_related(author) if self.request.user.is_staff: return qs return qs.filter(statusArticle.Status.PUBLISHED) def perform_create(self, serializer): serializer.save(authorself.request.user)这里有两个关键判断。get_queryset按身份返回不同数据集是接口层做数据隔离最干净的方式。perform_create里注入当前用户而不是让前端传author字段否则任何人都能冒充别人发文章。接口层的坑主要集中在分页和序列化深度上。默认不分页一次返回几千条会把前端和数据库一起拖垮务必显式配DEFAULT_PAGINATION_CLASS和PAGE_SIZE。序列化嵌套过深则会引发递归查询该用SerializerMethodField或者独立精简序列化器的地方别偷懒。6.3 上线前必须过一遍的安全清单安全这块我不讲大道理直接给一张能对着勾的表。检查项正确做法出错后果DEBUG生产环境设为False报错页泄露源码、配置、路径SECRET_KEY从环境变量读取不进代码库会话可被伪造ALLOWED_HOSTS明确列出域名域名校验失效CSRF表单带{% csrf_token %}中间件开启跨站伪造提交XSS依赖模板自动转义慎用safe过滤器脚本注入SQL注入用ORM参数化查询不拼字符串SQL数据泄露上传文件校验类型、大小存到非代码目录恶意文件执行敏感配置走环境变量.env不提交密钥外泄HTTPS生产强制跳转Cookie加Secure传输被窃听模板自动转义这一条尤其要强调。框架默认会把变量里的HTML标签转义掉这是保护你的。但总有人为了让富文本正常显示随手加个|safe如果这段内容来自用户输入等于自己开了个后门。富文本的正确做法是服务端做白名单清洗只允许有限的标签和属性而不是无脑放行。7. 常见问题与排查技巧实录7.1 高频报错速查表这些问题我几乎每个项目都会遇到一次整理成表方便对照。报错信息常见原因处理办法ModuleNotFoundError: No module named xxx虚拟环境没激活或App没注册确认环境检查INSTALLED_APPSDisallowedHostALLOWED_HOSTS没配当前域名加入域名或本地IPTemplateDoesNotExist模板路径不在TEMPLATES搜索范围检查DIRS与App内templates层级no such table迁移未生成或未执行makemigrations后migrateno such column改了模型但迁移没跑同上并检查是否有未合并的迁移CSRF verification failed表单缺token或跨域提交加{% csrf_token %}静态文件404未collectstatic或Web服务器未配收集并配置静态目录database is lockedSQLite并发写入换PostgreSQL或缩短事务RelatedObjectDoesNotExist一对一反向关系不存在用hasattr判断或get_or_create分页页码非法页码超出范围或非数字用get_page方法做兜底TemplateDoesNotExist这个报错值得多说两句。根因通常是目录层级不对App内的模板必须放在app名/templates/app名/下面多套一层同名目录是为了让{% extends %}和{% include %}能唯一定位。少套这一层两个App有同名模板时就会互相覆盖而且不报错纯粹是页面显示错了排查起来极其折磨。database is locked也是老朋友。SQLite的写锁是库级的一个写事务没结束其他写全得等。本地单人开发几乎遇不到一上测试环境多人同时操作就频繁出现。看到这个报错别急着调超时参数直接换数据库是正解。7.2 我踩过的几个真实坑第一个坑是时区。项目初期为了省事把USE_TZ关掉本地时区是东八区一切正常。部署到UTC的服务器上所有时间戳集体偏移八小时。修复方式是把USE_TZ打开、存储统一UTC、在模板层用date过滤器做展示转换。教训是时间处理从一开始就按规范来别想着以后再改。第二个坑是objects.all()用得太随意。后台有个导出功能直接for obj in Model.objects.all()遍历开发库只有几百条数据跑得飞快。上线三个月后表里有四十万条一次导出把内存吃满。改法是加.iterator()分批取或者干脆改成异步任务、生成文件后给下载链接。# 大数据量遍历的正确姿势 for obj in Article.objects.filter(statuspublished).iterator(chunk_size1000): process(obj)第三个坑是日志配置缺失。项目上线后偶发500错误但错误页只显示一句服务器错误没有任何堆栈。原因是DEBUGFalse之后框架不会把异常详情渲染到页面上而我又没配LOGGING。后来补了一段配置把异常写到文件并按天切分问题才变得可查。LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { class: logging.handlers.TimedRotatingFileHandler, filename: BASE_DIR / logs / app.log, when: midnight, backupCount: 14, encoding: utf-8, }, }, loggers: { django: {handlers: [file], level: WARNING}, blog: {handlers: [file], level: INFO, propagate: False}, }, }第四个坑是文件上传的存储路径。一开始把用户上传的图片直接存到了项目目录下的media/代码发布时整个目录被覆盖用户头像全丢。后来改成MEDIA_ROOT指向代码目录之外的独立路径并且定期备份这类事故才彻底断根。这个经验对任何框架都适用用户产生的数据和代码要物理分离。7.3 部署前的自查清单最后给一份我每次交付前都会过一遍的清单按顺序执行基本不会漏。把DEBUG改成FalseALLOWED_HOSTS填好用python manage.py check --deploy跑一遍它会提示一批安全配置问题。执行python manage.py collectstatic --noinput确认静态文件目录里文件齐全Web服务器指向正确。确认数据库迁移已在目标环境执行showmigrations全部带[X]。启动方式从runserver换成正式的WSGI容器进程数和超时时间按机器配置调。配好日志目录和权限确认应用进程有写权限否则日志静默丢失。打开慢查询日志和错误日志的采集至少保证出问题时能看到堆栈。备份策略跑一次演练恢复流程写进交接文档。我个人在实际操作中的体会是Django框架的学习曲线陡在前两周之后会突然变平。真正让我少走弯路的不是把文档背下来而是每遇到一个问题就顺手记一笔——记下报错原文、根因、解决办法。攒到三四十条之后你会发现大部分新问题都是老问题的变体。前面那张速查表就是这么攒出来的它比任何教程都更贴合你自己的项目。