如果你跟我一样是刚把 Python 基础过了一遍打算上手第一个正经 Web 项目那你大概率绕不开 Django。“django” 这个框架我用了很多年也带过不少新手一起做项目实战说实话Django 最吸引人的地方不在于它“啥都有”而在于它把 Web 开发里最容易跑偏的部分——数据库操作、后台管理、路由分发、模板渲染——都给你安排得明明白白。这篇博文我会以“教室管理系统”为例完整走一遍 Django 项目的设计、建模、权限、增删改查、模板静态资源配置以及最后在 Windows 10 上用 waitress nginx 做部署。你关心的 django 基础、django MTV 模式、django RBAC、django 创建 app、django 执行查询删除对象、vscode 里 static 文件显示不出来之类的坑我都会逐个拆开讲。内容偏实战建议你跟着我的步骤把项目跑起来很多问题自己动手做一遍比看十遍文档都管用。1. Django与MTV模式为什么Django适合第一个Web项目1.1 教室管理系统的整体拆解很多新手拿到一个项目题目第一反应是“我要先写页面”这个顺序其实反了。我在做教室管理系统这类项目时第一步永远是先拆业务把系统里涉及的角色、数据和动作列清楚。一个典型的教室管理系统大致可以拆成这么几块教学楼管理维护哪几栋楼每栋楼有哪些楼层。教室信息管理每个教室的编号、座位数、是否有多媒体设备、是否可用。课程与排课课程名称、上课教师、上课时间、上课教室。预约与审批教师或管理员申请使用空闲教室批准后生成占用记录。用户与权限管理员、教师、学生三类角色各自能看到的页面和能执行的操作不同。你看把业务拆成这个粒度之后再去思考技术方案会很自然地落到 Django 的 Model、View、Template 三层结构上数据模型负责教室、课程、预约这些实体视图层负责“查询空闲教室”“提交预约”“取消预约”这些动作模板层负责把结果渲染成 HTML 页面。这就是 MTV 模式在真实项目中的价值——它不是为了好看而是让项目从第一天起就有一条清晰的划分线多人协作时不至于互相踩脚。1.2 模板层、视图层、模型层各自管什么Django 的 MTV拆开是 Model、Template、View。很多人拿它跟经典的 MVC 对比说 Template 对应 ViewView 对应 Controller。这个类比有一定道理但容易产生误解。我习惯这样跟新人解释Model 就是“数据账本”。一张教室表一个预约记录都对应一个 Model。它负责跟数据库打交道保证数据怎么存、怎么读、怎么关联都由它统一管理。View 是“业务调度员”。用户在浏览器里点了“查询空闲教室”View 收到请求去 Model 里查数据再把数据交给 Template。它本身不关心 HTML 长什么样它只关心“这次请求要做什么业务”。Template 是“页面模板工”。它拿到 View 传来的数据把变量填进 HTML 模板里生成最终用户看到的页面。页面上的文字、表格、样式都由它负责。举个例子教室管理里最常用的一个功能“按座位数筛选可用教室”。用户提交了一个表单传过来参数min_seats50。这时候 View 接收参数调用Room.objects.filter(capacity__gte50, is_availableTrue)去 Model 层查询拿到结果后把它丢给模板渲染成一个表格页面。所以 MTV 这三个字母正好对应了一个请求从浏览器到服务器再到浏览器的完整路径。理解了这个路径后面所有的路由配置、ORM 查询、模板继承都是在这个路径上打补丁。2. 初始化项目从拉起Django到第一个App上线2.1 虚拟环境与Django版本选择我见过不少新手直接在系统 Python 环境里pip install django然后项目一多依赖互相打架。这个坏习惯早改早轻松。哪怕你只做一个小项目也建议从第一行命令就开虚拟环境。Windows 10 下的操作非常简单python -m venv venv venv\Scripts\activate执行完看到命令提示符前面出现(venv)标记就说明虚拟环境已经激活。然后安装 Djangopip install django4.2.*这里我专门强调一下版本选择。Django 5.x 已经发布了但如果你是想做项目实战、跟着教程走选择 4.2 LTS 版本会更稳。LTS 意味着长期支持社区里的资料、第三方库的兼容性都更成熟。新版本的功能虽然多但教学中用到的核心 API 几乎没差别没必要为了追新给自己添坑。2.2 创建项目与AppDjango 的项目和 App 是两个概念新手最容易混淆。一个项目是“整个站点”App 是项目里的“功能模块”。教室管理系统里用户认证、教室管理、预约管理都可以拆成独立的 App但为了教学方便我通常只建一个核心 App。创建项目的命令django-admin startproject course_manager .注意结尾的那个点表示在当前目录下创建项目文件而不是再嵌套一层course_manager目录。落盘后项目结构大致是这样course_manager/ __init__.py settings.py urls.py wsgi.py asgi.py manage.py接着创建 Apppython manage.py startapp classroom然后最重要的一步把 App 注册到项目里。打开settings.py在INSTALLED_APPS列表中加入classroom。很多新手这一步忘了做结果后面python manage.py makemigrations提示没有变化或者模板里的 App 资源加载不出来排查半天其实根因就是这个。2.3 settings配置三件套每个 Django 项目的settings.py都有三个地方我觉得最值得关注配置好了后面能省很多麻烦。第一个是语言与时区。把默认配置改成中文环境LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True第二个是静态文件配置。Django 开发环境下要能正确加载 CSS、JS 和图片settings.py里要有STATIC_URL static/ STATICFILES_DIRS [ BASE_DIR / static, ]第三个是数据库配置。新手直接用默认的 SQLite 完全够用复杂项目再切换到 MySQL 或 PostgreSQL。Django 的一大优势就是数据库切换时业务代码几乎不用改只需要动settings.py里的DATABASES配置。这三件套配好跑一次python manage.py runserver浏览器里看到那个火箭页面项目基础设施就算通了。3. 数据模型与RBAC权限把权限做成索引而非装饰3.1 实体模型的建模思路教室管理系统的核心模型我通常这么设计from django.db import models from django.contrib.auth.models import User class Building(models.Model): name models.CharField(教学楼名称, max_length50) floors models.IntegerField(楼层数, default1) def __str__(self): return self.name class Room(models.Model): building models.ForeignKey(Building, on_deletemodels.CASCADE, verbose_name所属教学楼) number models.CharField(教室编号, max_length20) capacity models.IntegerField(座位数) has_multimedia models.BooleanField(是否有多媒体, defaultTrue) is_available models.BooleanField(是否可预约, defaultTrue) def __str__(self): return f{self.building.name}-{self.number} class Reservation(models.Model): room models.ForeignKey(Room, on_deletemodels.CASCADE, verbose_name教室) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name申请人) date models.DateField(使用日期) start_time models.TimeField(开始时间) end_time models.TimeField(结束时间) purpose models.CharField(用途, max_length200) status models.CharField(状态, max_length20, defaultpending) def __str__(self): return f{self.room} {self.date} {self.start_time}-{self.end_time}这里有几个关键点要说明。ForeignKey字段用来表达模型之间的关联关系。教室属于哪栋楼用外键关联Building预约记录关联到申请人和教室也用外键。on_deletemodels.CASCADE表示被关联的记录删除时关联它的记录也一并删除。比如拆掉某栋楼那这栋楼下的教室和预约记录会一起清掉避免数据库里出现“指向不存在数据”的孤儿记录。实际项目里删除教学楼是高风险操作我会把级联删除和逻辑删除结合起来用。更稳妥的做法是增加一个is_active字段删除时只标记为不可用不真正删数据。这个经验在做真实系统时非常有用。3.2 用Django内置auth实现RBAC热词里有“django rbac”展开说一下。RBAC 是 Role-Based Access Control基于角色的权限控制。它的核心思想是不直接给用户配置权限而是给角色配置权限再把用户映射到角色上。Django 自带的django.contrib.auth已经内置了 User、Group、Permission 三张表。User 是用户Group 是角色Permission 是具体操作权限。这三张表天然就是一套 RBAC 模型。教室管理系统里可以创建“教师”和“管理员”两个 Group管理员组拥有所有权限可以查看所有预约、审批预约、管理教室。教师组可以创建预约、查看自己的预约、取消未审批的预约。在代码层面我可以在views.py里用装饰器做控制from django.contrib.auth.decorators import login_required, permission_required login_required def my_reservations(request): ... login_required permission_required(classroom.can_approve_reservation) def approve_reservation(request, reservation_id): ...权限控制的本质是“先判断能不能做再决定怎么做”。把它放在视图入口统一处理比在每个函数内部写一堆 if-else 要清晰得多。Django 的auth框架把这个流程已经封装得足够好用新手没必要自己再造一套权限轮子。3.3 迁移与admin后台模型定义完之后要生成数据库表。两条命令python manage.py makemigrations python manage.py migratemakemigrations的作用是对比模型定义和当前数据库结构生成迁移文件migrate才是真正把迁移文件应用到数据库。新手经常只跑migrate不跑makemigrations结果数据库结构一点变化没有这个顺序一定要记牢。Django 的 admin 后台是这个框架最亮眼的卖点之一。把模型注册进后台立刻就有了一套可视化的增删改查界面from django.contrib import admin from .models import Building, Room, Reservation admin.site.register(Building) admin.site.register(Room) admin.site.register(Reservation) admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): list_display (room, user, date, start_time, end_time, status) list_filter (status, date)这套后台在项目中期的价值极大。天气冷不想手敲数据直接在 admin 里录入想快速确认某天某教室有没有被占拉一个列表筛选就行。很多小型管理系统甚至可以直接让客户用 admin 做日常工作省去写一堆页面的工作量。4. 视图与ORM实操查询、新增、删除对象不靠猜4.1 常用ORM操作的完整示例ORM对象关系映射让你用 Python 对象操作数据库但新手最大的问题是写 ORM 全靠背遇到复杂查询就懵。其实核心的套路就那么几类我列举几个教室系统里一定会用到的场景。查所有可用教室available_rooms Room.objects.filter(is_availableTrue)查可容纳不少于50人的多媒体教室rooms Room.objects.filter(capacity__gte50, has_multimediaTrue)__gte是 Django ORM 的字段查找语法意思是“大于等于”。类似的还有__lt小于、__contains包含、__startswith以某字符开头、__in在指定列表中。查某个用户在某个日期之后的预约记录reservations Reservation.objects.filter(userrequest.user, date__gtedate.today()).order_by(date, start_time)order_by用来排序参数是模型里的字段名。默认是升序如果要降序就在字段名前面加一个负号order_by(-date)。新增一条预约记录reservation Reservation.objects.create( roomroom, userrequest.user, datedate, start_timestart_time, end_timeend_time, purpose期末考试备考, )查询单个对象用get但要小心查不到或查到多条都会抛异常。业务上更稳妥的写法是filter(...).first()查不到返回None不影响程序继续跑。4.2 删除对象时的级联行为与外键保护“django执行查询-删除对象”是热搜里的一条这里重点讲。Django 删除对象有三种常见形式。第一种是删单个对象reservation Reservation.objects.get(idreservation_id) reservation.delete()第二种是批量删除Reservation.objects.filter(title__contains临时调整).delete()第三种是级联删除。如果删除一个 Room 对象所有关联它的 Reservation 对象也会被一并删除因为定义模型时on_deletemodels.CASCADE。这里有个非常容易踩的坑删除教室前你需要先确认“这个教室是否有未结束的预约”。如果直接room.delete()所有历史预约记录都会被删掉这在管理员给领导汇报数据时是灾难。我通常会在业务层先做一次检查pending Reservation.objects.filter(roomroom, statuspending) if pending.exists(): # 有未审批的预约不允许删除教室 return JsonResponse({success: False, message: 该教室存在未审批的预约不能删除})如果你希望数据库层面不级联删除而是“限制删除”可以把on_delete改成models.PROTECT。一旦有预约关联这个教室删除操作就会被数据库拒绝保护数据不被误删。选CASCADE还是PROTECT取决于业务上“删除”到底意味着什么这是设计模型时就要想清楚的。4.3 事务与批量操作的性能教室管理系统在线预约功能里有一个典型的并发风险两个教师同时申请同一个教室的同一个时间段都看到教室是空闲的结果都提交成功教室就冲突了。解决思路是数据库锁。Django 里可以用事务加行锁from django.db import transaction from django.db.models import F with transaction.atomic(): room Room.objects.select_for_update().get(idroom_id) # 在事务里再次检查该时间段是否已被占用 conflict Reservation.objects.filter(roomroom, datedate, start_time__ltend_time, end_time__gtstart_time).exists() if conflict: raise ValueError(该时间段教室已被占用) Reservation.objects.create( roomroom, userrequest.user, datedate, start_timestart_time, end_timeend_time, purposepurpose, )select_for_update会锁住这条教室记录直到事务结束其他人再来做同样的操作只能排队等锁释放。这样冲突问题就在源头解决了。批量插入数据时的性能问题也很常见。如果要在系统初始化时生成 50 个教室用create写在一个循环里会发出 50 条 SQL 插入语句效率很低。用bulk_create一条语句搞定rooms [ Room(buildingbuilding, numberfA-{i}, capacity30 i) for i in range(1, 51) ] Room.objects.bulk_create(rooms)同理批量更新可以用update()方法Room.objects.filter(has_multimediaFalse).update(is_availableFalse)ORM 的终极法则很简单能够用一句数据库操作完成的就不要在 Python 里写循环完成。5. 模板渲染与静态资源vscode里的static困局5.1 模板继承与静态文件加载前面讲 MTV 时说过Template 负责渲染 HTML。Django 模板有一个特别强大的功能是模板继承它能够把全站共用的导航栏、侧边栏、页脚抽到一个base.html里子页面只需要写自己独有的内容。templates/base.html大致长这样{% load static %} !DOCTYPE html html langzh-hans head meta charsetUTF-8 title{% block title %}教室管理系统{% endblock %}/title link relstylesheet href{% static css/base.css %} /head body header a href/首页/a a href/rooms/教室列表/a a href/reservations/我的预约/a /header main {% block content %} {% endblock %} /main /body /html子页面templates/rooms.html里只需要写{% extends base.html %} {% block content %} h1可用教室列表/h1 ul {% for room in rooms %} li{{ room.building.name }} - {{ room.number }}可容纳{{ room.capacity }}人/li {% endfor %} /ul {% endblock %}这套继承机制极大减少了重复代码。我见过不少新手在每个页面里复制同样的导航栏代码到后面改一次菜单要改十几个文件非常低效。静 态文件的加载要记住一个核心规则模板里使用static标签之前必须先{% load static %}。即使你在base.html里已经 load 过子页面如果要用静态文件也仍然需要重新 load。这跟 Python 里每个模块都要 import 是一样的逻辑。5.2 静态文件加载失败的五个排查方向热词里“vscode写img标签 在django的static文件中显示不了”是一个很具体的问题也是我在带新手项目时被问得最多的问题之一。通常有五个排查方向。第一检查目录结构。图片要在classroom/static/classroom/img/下或者其他已注册的静态文件目录里。Django 默认会在每个 App 的static/子目录中查找文件。第二检查模板里有没有写对加载方式。很多人直接写img srcstatic/img/room.png这样是不行的。Django 模板里应该写{% load static %} img src{% static classroom/img/room.png %}第三检查STATIC_URL是否正确。默认是static/如果改成了assets/模板里的static标签会自动跟着变但手写的srcstatic/...就会失效。第四检查开发服务器有没有重启。新增静态文件后runserver 有时候不会自动感知手动刷新页面仍然看不到。重启一下python manage.py runserver能解决很多莫名其妙的问题。第五检查资源是否真的在磁盘对应位置。vscode 的文件树里看着有但可能文件扩展名被隐藏了或者实际目录是img而模板里写的是image。Linux 上路径大小写敏感Windows 上不敏感这个差异换环境部署时也要注意。5.3 开发环境与生产环境的静态资源切换开发环境下Django 自带静态文件服务所以DEBUGTrue时图片和 CSS 正常加载。但一旦到了部署阶段DEBUGFalseDjango 就不再帮你提供静态文件了需要用collectstatic把项目所有静态文件收集到一个统一目录再由 nginx 之类的 Web 服务器直接处理。命令是python manage.py collectstatic它会按照STATICFILES_DIRS和各个 App 的static/目录把文件收集到STATIC_ROOT指定的目录里。部署时 nginx 的静态文件路径就指到这个目录。这里有一个很实用的技巧在settings.py里把STATIC_ROOT配置好采集完后用python manage.py runserver --insecure可以简单验证一下静态文件是否正常。这个参数允许开发服务器在DEBUGFalse时照样提供静态文件仅用于验证不需要长时间开着。6. 上线部署waitress nginx把服务跑在Windows10上6.1 waitress纯Python的WSGI服务开发环境下用runserver调试没问题但它本质是一个轻量开发服务器性能和稳定性都不适合生产环境。Windows 10 上部署 Django我最常用的组合是 waitress nginx。waitress 是一个纯 Python 实现的 WSGI 服务器不需要额外编译 C 扩展Windows 上装起来零障碍和 Linux 上常用的 gunicorn 相比它跨平台的表现更稳定。安装pip install waitress在项目根目录建一个serve.pyfrom waitress import serve from course_manager.wsgi import application if __name__ __main__: serve( application, host0.0.0.0, port8000, threads8, url_schemehttp, )threads参数控制并发处理线程数。一般小项目管理后台并发量不大4 到 8 个线程足够如果访问量高可以适当调大但不建议盲目开几十个线程线程切换也是有开销的。启动命令python serve.py此时 Django 项目就跑在8000端口上了。6.2 nginx反向代理与静态资源分离waitress 能处理 Django 动态请求但静态文件的读取效率并不是它的强项。更合理的架构是nginx 监听 80 端口遇到静态文件请求直接返回磁盘上的文件遇到动态请求再转发给本地 8000 端口的 waitress。Windows 上的 nginx 配置核心部分是这样的server { listen 80; server_name 127.0.0.1; location /static/ { alias C:/path/to/course_manager/static_cdn/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }其中static_cdn就是collectstatic收集后的目录。配好之后浏览器访问/static/css/base.css时nginx 直接从磁盘返回文件访问页面时nginx 把请求转给 8000 端口的 waitress。为什么要做这一层拆分我给你打个比方Django 是个业务能力强但“手速”一般的厨师适合专心做菜nginx 是个跑腿极快的外卖员适合专门送文件。你把两者分工性能会明显提升。6.3 部署调试中的几个细节第一DEBUG必须设置为False。不然一旦程序报错页面会输出完整的错误堆栈和服务器路径这是严重的安全隐患。同时还需要配置ALLOWED_HOSTS [127.0.0.1, localhost, your-domain.com]第二静态文件路径不能错位。我遇到过最常见的问题是collectstatic收集后文件确实在static_cdn目录里但 nginx 配置的alias路径最后一个斜杠没写对导致访问时找不到文件。建议收集后先在浏览器直接访问一个静态文件地址确认能返回再继续下一步。第三数据库从 SQLite 切换到 MySQL 或 PostgreSQL 时注意备份数据并提前在服务器上建好数据库。Django 的迁移机制会帮你建表但不会帮你建数据库本身。7. 新手自查清单常见问题与排查速查表7.1 高频报错与对策我把带新手做 Django 项目时最常遇见的报错整理成了一张表你可以直接收藏起来遇到类似问题时对着排查。现象可能原因排查方向页面 404URL 未配置或 App 未注册检查应用根urls.py与 App 的urls.py静态文件加载不出来模板未load static路径写错服务器未重启按第5节五个方向排查数据库表未生成没有执行迁移或模型改动后未 makemigrations依次执行makemigrations和migrateAdmin后台没有模型未 admin.site.register在admin.py注册模型中文乱码HTML 未声明 charset或数据库字符集不对模板 head 中加meta charsetUTF-8表单提交 CSRF 报错模板表单未加{% csrf_token %}确认表单内包含该标签get()返回多条数据查询条件不够精确改用filter().first()或调整条件migrate提示无变化模型未 import 或 App 未注册检查INSTALLED_APPSwaitress 启动后访问不到防火墙拦截端口或 host 绑定了 127.0.0.1用0.0.0.0绑定开放对应端口7.2 我从这个项目里沉淀的3条经验第一先把 MTV 的请求路径理解透再开始写代码。很多报错之所以解决不了是因为你根本不知道这个请求走过了哪些地方。你在浏览器按一下 F12看 Network 请求状态码再对照请求从 URL 到 View 到 Template 的链路绝大部分问题都能自己定位。第二模型设计阶段多花一点时间后面省十倍的时间。教室管理系统的表结构如果不预先规划好到后面加外键、加联动字段会牵一发动全身。建议在纸上把实体、字段、关系画一遍再动手写 Model。第三日志必须从第一天就留着。哪怕只是简单地在settings.py里配置一个文件日志把错误堆栈记录下来部署之后排查问题会轻松非常多。Django 这个框架入门不难但真正熟练需要靠项目喂。教室管理系统麻雀虽小五脏俱全把 MTV、ORM、RBAC、模板继承、静态资源、部署这些环节都过一遍你对整个 Web 开发的认知会上一个台阶。后续你可以沿着这条线继续扩展加一个课表提醒功能或者把设备借用也纳进来都可以。
