ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django全栈实战:从ORM模型到WebSocket实时推送与部署

Django全栈实战:从ORM模型到WebSocket实时推送与部署 第一次真正让 Django 进入我的工作流是接一个在线预约管理系统。需求不复杂后台录入服务项目、前台展示排期、用户登录后查看自己的预约记录。但看到要求里写着同时有几千条数据要能快速筛选管理和管理后台要做得像个正经系统我就知道 Flask 那种自由发挥的风格要吃亏了。Django 自带 Admin、自带头认证、自带 ORM这些自带的东西在初期看不出优势等需求堆上来就知道省了多少事。这篇文章不是 Django 语法手册而是一条从无到有把 Django 项目跑起来的实战路径。我会按我自己做项目的顺序把一个完整的 Django 应用从初始化、模型设计、查询与删除、视图联动、认证与 token到 WebSocket 实时推送、最终部署的关键点串起来。无论你是刚接触 Django 的新手还是想补全某个环节比如 cookies 设置 token、channels 实时推送的老手都可以直接参考对应章节。1. 为什么说 Django 是一个人撑起全栈的最佳选择1.1 Django 到底给了你什么Django 常被称作全家桶框架这不只是营销话术。它把 Web 开发中高频重复的模块全部内置了ORM不需要写原生 SQL 就能完成表结构设计、增删改查还能在 MySQL 和 SQLite 之间无缝切换。Admin 后台Django 的杀手锏功能。只要你定义了模型后台会自动生成一套可登录、可筛选、可排序的管理界面甚至在数据量不大时完全能当业务后台用。认证系统内置 User 模型、Session、Permission 和 Group。做登录、权限控制几乎开箱即用。URL 路由与模板引擎自带模板继承与过滤机制配合 Bootstrap 或 Vue 做前端渲染都行。中间件与信号从 CSRF 防护到请求钩子Django 为你铺好了扩展点。1.2 和 Flask、FastAPI 放在一起怎么选很多新手会在 Django、Flask、FastAPI 之间纠结。我的判断标准很简单框架适合场景短板Flask小型 API、微服务、完全掌控一切大量业务逻辑要自己搭容易失控FastAPI高并发 API、异步接口、实时数据服务没有内置模板和 Admin纯 API 取向Django完整业务系统、后台管理、数据密集型、快速迭代重同步模型处理高并发需额外调优如果项目需要一个后台管数据 一个前台展示业务 一个用户体系Django 是最优先选择。毕竟 Admin 和 ORM 是经过十几年沉淀的稳定性和开发效率都在那摆着。1.3 版本选择别追新求稳目前 Django 官方主打 4.x 与 5.x。个人建议是选择 LTS 长期支持版本比如 Django 4.2 LTS 或 5.x 的 LTS按你建项目时的稳定版本来定。不要因为想用新特性就用 dev 版本生产环境出问题没人替你兜底。2. 从零创建 Django 项目与 App骨架搭建实操2.1 环境准备与项目初始化先在项目目录里建一个虚拟环境避免系统 Python 的包污染mkdir django_project cd django_project python3 -m venv venv source venv/bin/activate # Windows 是 venv\Scripts\activate pip install django然后创建项目。注意django-admin startproject和python manage.py startapp是两回事——startproject生成的是整个站点配置startapp生成的是一个业务模块。一个项目里可以有多个 App每个 App 负责独立的一块业务。django-admin startproject config . python manage.py startapp appointments我把项目名写成config而不是默认的django_project是因为一个项目中配置文件、业务 App、静态文件夹混在一起很长起一个简短的配置目录名后面改起来顺手。执行完后的目录结构大概是这样config/ settings.py urls.py wsgi.py / asgi.py appointments/ models.py views.py admin.py migrations/ manage.py2.2 settings.py 里必须调整的几个点新手初始化项目后第一件事就是python manage.py runserver结果看到英文界面的欢迎页。但真正开始写业务前先打开settings.py做三件事第一注册 App。在INSTALLED_APPS列表里加入你刚才创建的 App 名称。很多人忘记这一步导致后面python manage.py makemigrations时提示没有检测到模型变化排查半天才发现 App 没注册。第二调整时区和语言。LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai注意TIME_ZONE要配合数据库里的时间字段使用。如果建表以后才改时区旧数据里的时间一般不会自动转换需要额外处理。第三配置数据库。如果只是开发默认的 SQLite 完全够用DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }但生产我建议换 MySQL 或 PostgreSQL并且一定要把密码和主机地址通过环境变量读取不要硬编码在文件里。后面部署时你就知道这个习惯多救命。2.3 快速让 Admin 界面好看一点django-unfoldDjango 自带的 Admin 功能没问题但 2024 年了还在用那套老界面客户看了总觉得是上个时代的产物。想在不重写后台的前提下提升观感可以直接装django-unfoldpip install django-unfold在INSTALLED_APPS中把unfold放在django.contrib.admin之前INSTALLED_APPS [ unfold, django.contrib.admin, # ... ]然后给管理后台的 models 加admin.register装饰器时样式会自动生效。我这套小型管理后台用django-unfold后侧边栏、卡片、表单整体观感直接翻了一倍几乎零成本。2.4 修改默认后台 URLconfig/urls.py里默认是/admin如果你不想让后台路径暴露得太明显可以改成一个难猜的路径from django.contrib import admin from django.urls import path urlpatterns [ path(manage-panel/, admin.site.urls), ]顺便把 Django 默认的注册视图也开发出来。第一步跑通后先别急着写代码先在 Admin 里创建一个超管账号python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000访问http://127.0.0.1:8000/manage-panel/如果能登录说明整个骨架已经活了。3. ORM 模型设计与增删改查的心法3.1 从建表到迁移理解 makemigrations 与 migrateORM 最大的优势是你用 Python 类描述数据Django 替你翻译成数据库表。models.py里定义一个预约模型from django.db import models class ServiceItem(models.Model): name models.CharField(服务名称, max_length100) price models.DecimalField(价格, max_digits10, decimal_places2) duration models.PositiveIntegerField(时长(分钟), default30) is_active models.BooleanField(是否上架, defaultTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: ordering [-created_at] def __str__(self): return self.name定义完只是第一步真正让数据库有这张表必须执行两条命令python manage.py makemigrations python manage.py migratemakemigrations生成迁移文件migrate把迁移文件翻译成 SQL 执行到数据库。这个过程新手经常懵我把它们类比成修改方案和施工队进场makemigrations只是把所有改动记录在案migrate才真正动数据库。所以每次都先跑前者确认迁移文件内容无误后再跑后者。3.2 查询的艺术all/filter/get/exclude 不只是语法Django ORM 的查询是惰性执行的链式调用的每一步只是构建 QuerySet只有真正需要数据时才执行 SQL。这个特性在排查性能问题时很关键。基础查询示例# 获取所有上架的服务 services ServiceItem.objects.filter(is_activeTrue) # 按价格排序取前3 top3 ServiceItem.objects.filter(is_activeTrue).order_by(-price)[:3] # 排除某些类型 available ServiceItem.objects.exclude(name__contains测试) # 精准获取单条 item ServiceItem.objects.get(id1)get有个脾气结果必须唯一查不到或查到多条直接抛 DoesNotExist 和 MultipleObjectsReturned。如果你只是取一条没有也无所谓建议用.first()返回None不会触发异常。使用filter时字段名后加__contains、__gte、__lte、__in是 Django ORM 的核心细节# 价格大于100且时长小于60分钟 items ServiceItem.objects.filter(price__gt100, duration__lt60) # 名称包含的关键词 items ServiceItem.objects.filter(name__icontains洗) # id 在某个列表中 items ServiceItem.objects.filter(id__in[1, 2, 3])字段名__后面接的是查询表达式这比写原生 SQL 直观多了。真要排查数据问题我都是直接在python manage.py shell里逐行敲比轮询看日志快得多。3.3 删除对象单条删除、批量删除与级联陷阱删除在 Django ORM 里是个看着简单、坑也不少的操作。单个实例直接调用.delete()item ServiceItem.objects.get(id1) item.delete()返回值是一个(total_deleted, detail_dict)元组。如果你只是在控制台里操作看到1代表删除成功如果看到大于 1说明级联删除了关联数据。这点特别重要——Django 默认的 FK外键行为是on_deletemodels.CASCADE即删除主表数据会连带删除所有关联的外键数据。class Appointment(models.Model): service_item models.ForeignKey(ServiceItem, on_deletemodels.CASCADE, related_nameappointments)如果我删掉一个ServiceItem所有关联的Appointment也会一并消失。这通常不是业务上想要的效果。保险的做法是改成service_item models.ForeignKey(ServiceItem, on_deletemodels.PROTECT)PROTECT会让 Django 在你尝试删除仍有关联数据的ServiceItem时主动抛出 ProtectedError提醒你先处理子表数据。只有确定子数据必须随主数据一起消失时才用 CASCADE。批量删除则是直接对 QuerySet 调用.delete()ServiceItem.objects.filter(is_activeFalse).delete()注意批量删除不会触发模型里的delete()重写方法。如果你有pre_save/post_save信号或自定义清理逻辑批量删除会绕过它们。要精确控制删除逻辑时最好还是遍历实例逐个删或手动执行清理脚本。3.4 查询性能select_related 与 prefetch_related 的差别项目中如果模型有关联关系查询很容易出现 N1 问题。第一次查主表 10 条记录每条记录访问外键字段时又产生一次数据库查询总共 11 次 SQL。本人第一次做列表页时没注意这个本地测试毫无感觉数据量到三五千条时页面明显变慢后台直接显示几百条 Query 日志。解决办法是对外键和一对一关系使用select_related对多对多和反向外键关系使用prefetch_related# 查询预约列表时顺便把关联的服务项目一起查出来 appointments Appointment.objects.select_related(service_item).all()这条语句会 JOIN 查询一次性拿回主表和关联表数据后续访问appointment.service_item.name不再触发额外 SQL。数据量大、关联层级深时这两种方法带来的性能提升是数量级的。4. 视图、路由与模板把数据变成页面4.1 路由设计不只是path()这么简单Django 的 URL 通路由urls.py统一指派。写大型项目时主urls.py应该只做入口分发具体的 App 路由在各自目录下定义再通过include引入from django.contrib import admin from django.urls import path, include urlpatterns [ path(manage-panel/, admin.site.urls), path(api/, include(appointments.urls)), path(, include(appointments.urls)), ]App 内的urls.py可以按业务把 URL 分到不同模块from django.urls import path from . import views urlpatterns [ path(services/, views.service_list, nameservice_list), path(services/int:pk/, views.service_detail, nameservice_detail), path(appointments/create/, views.create_appointment, namecreate_appointment), ]这里int:pk是 Django 的路径转换器会把 URL 中的123自动转成 int 类型传给视图函数。类似的还有str:slug和uuid:uuid。路径转换器在 Django 2.0 之后就全面替代了旧的url()正则写法看着清爽也省心。4.2 视图函数视图与类视图怎么选视图层可以使用函数视图FBV或类视图CBV。新手项目我建议先用函数视图因为它逻辑直观想在哪里加 if 加 for 都不会被类方法挡路。from django.shortcuts import render, get_object_or_404 from .models import ServiceItem def service_list(request): services ServiceItem.objects.filter(is_activeTrue) return render(request, appointments/service_list.html, {services: services}) def service_detail(request, pk): service get_object_or_404(ServiceItem, pkpk, is_activeTrue) return render(request, appointments/service_detail.html, {service: service})当你熟悉之后再换取 CBV 会体会到它处理泛型逻辑的便利。比如列表页、详情页、创建页这些几乎全是重复代码直接用ListView、DetailView、CreateView几行代码搞定from django.views.generic import ListView from .models import ServiceItem class ServiceListView(ListView): model ServiceItem template_name appointments/service_list.html context_object_name services def get_queryset(self): return ServiceItem.objects.filter(is_activeTrue)一个原则页面只有读 展示逻辑时用 CBV 最爽页面有复杂的自定义业务动作时回到 FBV 更自由。两者混用在同一个项目里完全没问题。4.3 模板继承一步到位避免改 50 个页面Django 的模板系统最核心的能力是继承。我通常建一个base.html把导航栏、页头、页脚、CSS/JS 引入全部放进去然后用{% block %}留出变化区!DOCTYPE html html langzh-hans head meta charsetUTF-8 title{% block title %}默认标题{% endblock %}/title /head body nav{% include appointments/navbar.html %}/nav main {% block content %}{% endblock %} /main /body /html子模板只需要写{% extends base.html %} {% block title %}服务列表{% endblock %} {% block content %} div classrow {% for service in services %} div classcard h3{{ service.name }}/h3 p价格{{ service.price }}/p /div {% empty %} p暂无可预约服务/p {% endfor %} /div {% endblock %}{% empty %}是 Django 模板的隐藏福利当列表为空时显示指定内容比在视图里先判断再传递上下文要省事。4.4 Cookie 设置 Token登录后如何正确种下凭证Django 默认用 Session 认证但 API 端或前后端分离项目里更常用 JWT 或自定义 Token。很多新手问cookie 到底什么时候该设置怎么设置令牌。这里结合 Django 内置的set_cookie做一套完整实践前端发起登录请求后后端验证通过生成 Token可以用 Django 内置的加密签名也可以引入djangorestframework-simplejwt生成 JWT。然后把 Token 写入 HttpResponse 的 cookiefrom django.http import JsonResponse def login_view(request): # 假设这里验证了账号密码生成了 token_str token_str generate_token_for_user(user) response JsonResponse({code: 200, message: 登录成功}) response.set_cookie( auth_token, token_str, max_age60 * 60 * 24 * 7, # 7天有效期 httponlyTrue, # 前端 JS 无法读取防 XSS samesiteLax, # CSRF 保护策略 secureFalse, # 生产环境改 True启用 HTTPS 传输 path/ ) return response参数细节值得讲透max_age控制有效期单位是秒。不设置的话 cookie 会被浏览器标记为会话 cookie关闭浏览器就失效。httponlyTrue非常关键。它禁止 JavaScript 读取 Document.cookie是防止恶意脚本偷取 Token 的第一道防线。samesiteLax控制跨站请求携带 cookie 的策略。同站正常请求不受影响跨站 POST 时不携带 cookie这对 CSRF 有防护作用。secureTrue生产环境必须开它保证 cookie 只在 HTTPS 连接下传输。登录页面同时也要设计退出登录逻辑。要让 cookie 失效最好的方式是覆盖同名 cookie 并把max_age设为 0response.delete_cookie(auth_token)注意delete_cookie需要和当初设置 cookie 时的path参数一致否则可能删不掉。如果是纯 API 模式推荐直接上djangorestframework-simplejwt。它的优势在于 Token 默认有过期时间不需要服务端存储会话状态适合做无状态 APIfrom rest_framework_simplejwt.tokens import RefreshToken def get_tokens_for_user(user): refresh RefreshToken.for_user(user) return { refresh: str(refresh), access: str(refresh.access_token), }前端拿到 access token 后放在Authorization: Bearer token头里调用 API。若用 Swagger 调试这个头就是标准操作。4.5 从 Django 到前后端分离什么时候用 DRF如果页面需要给小程序、App 或多端提供数据不能在模板里渲染。有两个选择一是写普通 Django JSONResponse 视图二是引入 Django REST FrameworkDRF。强烈建议数据接口上 DRF哪怕只是返回三个字段因为你很快就会发现需要序列化外键、嵌套关系、限流和权限控制。DRF 的ModelViewSet和ModelSerializer几乎能零代码生成标准 CRUD API。5. WebSocket 实时推送后台有数据前端立即知道5.1 为什么 Django 传统同步机制做不到实时Django 默认用 WSGI 协议它的模型是请求-响应客户端发起 HTTP 请求服务端处理完后响应连接就断开了。如果后台新增了一条数据想让处于同一个页面的所有用户看到轮询是笨办法WebSocket 才是正解客户端与服务端保持长连接服务端可以随时主动往客户端的连接里推数据。Django 的解决方案是引入channels它让 Django 从 WSGI 扩展为 ASGI 模式既支持传统同步视图也支持异步 WebSocket。5.2 channels 的安装与配置首先安装必要的包pip install channels channels-redis然后在settings.py中注册channels并指定 ASGI 应用INSTALLED_APPS [ daphne, # ASGI 服务器替换 runserver 默认服务器 channels, # ...其他应用 ] ASGI_APPLICATION config.asgi.application同时配置 channel layers这是 WebSocket 消息转发的中间枢纽。生产环境用 RedisCHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: { hosts: [(127.0.0.1, 6379)], }, }, }开发环境如果没有 Redis也可以使用 InMemory 层CHANNEL_LAYERS { default: { BACKEND: channels.layers.InMemoryChannelLayer } }但重启服务后通道信息会丢失只建议调试时用。5.3 编写 Consumer处理连接、收消息、推送消息在 App 里新增consumers.py。下面是一个预约状态更新后实时通知前端的例子import json from channels.generic.websocket import AsyncWebsocketConsumer class AppointmentConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name appointment_notify # 加入消息组 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 receive(self, text_data): data json.loads(text_data) # 可以在这里处理前端传来的指令 await self.send(text_datajson.dumps({ message: 服务器已收到消息 })) # 后台通过 group_send 会触发这个方法把数据推给前端 async def appointment_updated(self, event): await self.send(text_datajson.dumps({ type: appointment_update, data: event[data] }))这里的关键是消息组的概念。多个 WebSocket 连接可以加入同一个 group后台只要往 group 里发一条消息组内所有活跃连接都能收到。非常适合做所有用户同步看到后台数据变更。5.4 同步视图里如何推送async_to_sync 是关键WebSocket Consumer 是异步的但普通 Django 视图是同步的。在视图里如何触发推送用async_to_sync包装from asgiref.sync import async_to_sync from channels.layers import get_channel_layer def update_appointment(request, pk): # ...业务逻辑比如修改状态为已完成 appointment.status completed appointment.save() channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( appointment_notify, { type: appointment_updated, data: { id: appointment.id, status: appointment.status, } } ) return JsonResponse({code: 200})注意type字段的值是appointment_updated它会自动映射到 Consumer 里的appointment_updated方法。这是 channels 消息路由的命名约定形如xx.updated就会调用名为xx_updated的 async 方法。5.5 前端 JavaScript 如何接收前端不需要复杂操作原生 WebSocket 就够了const ws new WebSocket(ws://yourdomain.com/ws/appointments/); ws.onmessage function(e) { const data JSON.parse(e.data); if (data.type appointment_update) { // 例如更新页面上的状态标签 document.querySelector(#appointment-${data.data.id}).innerText data.data.status; } }; ws.onclose function() { // 断线后重连策略 setTimeout(() { connectWebSocket(); }, 3000); };要在 Django 路由里注册 WebSocket 的 URL需要建立routing.pyfrom django.urls import re_path from . import consumers websocket_urlpatterns [ re_path(rws/appointments/$, consumers.AppointmentConsumer.as_asgi()), ]然后在项目主asgi.py中用ProtocolTypeRouter分类路由import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack import appointments.routing os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack( URLRouter( appointments.routing.websocket_urlpatterns ) ), })整体链路是后台保存数据 → 视图调用 group_send → Redis channel layer 转发 → Consumer 的 appointment_updated 方法执行 → 前端 onmessage 收到数据 → 页面局部更新。5.6 channels 实际踩过的坑channels 配置正常后本地跑起来很容易遇到几个诡异现象连不上 WebSocket报 404。多半是routing.py的路径没有匹配上。re_path里的正则必须以^开头、$结尾否则会匹配到不该匹配的路径。group 消息所有人收到多份。这通常是 Redis 复用旧连接导致的。检查ASGI_APPLICATION是否配置正确重启 Redis 和 Django 进程往往是解决办法。开发服务器行为异常。runserver默认走 WSGI不处理 WebSocket。要么用daphne启动daphne config.asgi:application要么在settings.py中把daphne放入INSTALLED_APPS顶部Django 就会用 Daphne 替换内置 runserver 的服务器让本地开发同时支持 HTTP 和 WebSocket。数据量大时如何加速常见 ORM 优化清单除了前面提到的select_related和prefetch_related还有几个高频性能提升手段值得记录。用.values()或.values_list()减少内存占用。如果你只需要查出一批字段做只读展示不要再加载完整的模型实例。例如只取 ID 和名称pairs ServiceItem.objects.values_list(id, name)构建一个[ (1, 清洗), (2, 保养), ... ]的元组列表。比all()加载完整对象占用的内存少得多在后台导出等场景特别明显。对经常参与查询的字段加数据库索引class Appointment(models.Model): status models.CharField(max_length20, db_indexTrue)加了索引后filter(statuscompleted)在数据量大时会快很多。代价是写入时要更新索引但业务场景下读多写少值得。用annotate在数据库中完成聚合而不是拿到 Python 层循环from django.db.models import Count statistics Appointment.objects.values(status).annotate( countCount(id) )得到的每条数据形如{status: completed, count: 12}。整个统计过程只产生一条 SQL如果自己循环统计数据性能会差一个数量级。在查询中加only(id, name)指定需要的字段配合.defer(description)延迟加载大字段对列表页和导出场景优化也很有用。记住一点ORM 不是银弹凡是能下推给数据库的操作就不要放在 Python 中做。6. 部署上线从 DEBUGTrue 到生产环境的踩坑清单6.1 环境变量与敏感信息隔离上线第一步是把settings.py里的密钥抽成环境变量。用一个.env文件存储变量并用python-decouple读取pip install python-decouplefrom decouple import config SECRET_KEY config(SECRET_KEY) DEBUG config(DEBUG, defaultFalse, castbool) ALLOWED_HOSTS config(ALLOWED_HOSTS, default).split(,) DATABASE_URL config(DATABASE_URL, defaultsqlite:///db.sqlite3)把.env加进.gitignore防止泄露。哪怕只是个人项目我也建议一开始就保持这个习惯等真要多人协作时你会发现这个决定太正确了。6.2 DEBUGFalse 与静态文件DEBUGTrue时 Django 会自动从静态文件目录读取资源DEBUGFalse时不会你必须手动收集静态文件并交给 Nginx 等静态服务器处理。先执行python manage.py collectstatic然后把STATIC_ROOT指向收集目录STATIC_ROOT BASE_DIR / staticfilesNginx 的配置里把/static/路径指向staticfiles目录即可location /static/ { alias /path/to/django_project/staticfiles/; }这步部署时最容易出的问题是后台页面样式全没了原因基本是没跑collectstatic或 Nginx 的 alias 路径不对。6.3 用 Gunicorn Daphne 混合服务Django 项目同时包含 HTTP API 与 WebSocket部署时常采用 Gunicorn 服务 HTTP、Daphne 或 Uvicorn 服务 ASGI/WebSocket。很多云平台现在支持多端口可以在容器里跑两个进程。最简单稳定的组合# HTTP 请求用 Gunicorn 跑 WSGI gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 # WebSocket 流量用 Daphne 跑 ASGI daphne config.asgi:application --bind 0.0.0.0:8001然后在 Nginx 里做分发upstream django_http { server 127.0.0.1:8000; } upstream django_ws { server 127.0.0.1:8001; } server { listen 443 ssl; server_name yourdomain.com; location / { proxy_pass http://django_http; } location /ws/ { proxy_pass http://django_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }WebSocket 转发时Upgrade和Connection头是缺一不可的。少了这两个头WebSocket 连接握手就一直失败。6.4 集中式日志与错误告警生产环境DEBUGFalse后任何 500 错误只会显示通用页面日志默认输出到服务器标准输出。我习惯配一个简单的LOGGINGLOGGING { version: 1, disable_existing_loggers: False, handlers: { console: { class: logging.StreamHandler, }, }, root: { handlers: [console], level: WARNING, }, }然后把 stdout 接入到 ELK 或云日志服务。别等出问题时才想起来居然没有日志可查。6.5 上线前的安全检查清单本人每次部署前会对着下面这个清单自查DEBUG必须是False。SECRET_KEY使用环境变量注入不包含在任何代码仓库中。ALLOWED_HOSTS只包含域名不用*。数据库连接使用强密码且只允许内网访问。/admin路径尽量改掉并启用两因素认证。HTTPS 强制跳转HTTP 端口 80 全部 301 到 443。上传图片的媒体目录不能放在项目源码里单独挂载。每一条都是踩过别人或自己踩过的坑总结出来的。尤其是ALLOWED_HOSTS设成*一旦有恶意请求传入日志会被刷爆CSRF 防护也会变得脆弱。结尾几句实在话Django 是一个越深入越省心的框架。你用原生 SQL 可能半小时写出来的查询它在迁移、序列化、后台管理三个层面替你省下的时间绝对远超学习成本。但也正因为自带太多新手往往会在框架规则上栽跟头——最常见的坑无非三个忘记了makemigrations、漏掉了INSTALLED_APPS、把 DELETE 的级联行为想得太简单。这篇文章里的章节顺序就是我当时踩坑、看源码、查文档后沉淀下来的完整路径。我个人的实际操作体会是项目一开始就要把 ORM 模型的related_name和on_delete定义清楚想清楚删除主数据时关联数据到底该怎样再开始写视图。别急着追求上了多少新技术栈先把 Django 自身的 URL、ORM、Admin、认证榨干等到确实需要面向实时场景时再来引入 channels 和 WebSocket。如果你正卡在哪一步照着对应章节跑一遍大概率能直接过关。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进