
每个做宿舍管理系统的开发者应该都见识过宿管办公室桌上那几本翻得起了毛边的台账。新生入住登记本、报修记录本、来访登记表、调宿申请表一摞摞堆着。有人问3栋还有没有空床位管理员得翻小半天才能答上来。我做的这个django基于Python的学生宿舍管理系统就是把这些纸面流程全部搬到线上——学生档案、楼栋宿舍床位管理、入住退宿调宿、报修工单、访客登记、公告通知、统计报表一个项目全包。系统采用Django 4.2 LTS和Python 3.11开发已经用真实数据跑过了几个学期。无论你是刚学完Django基础、想找个完整实战项目练手的新手还是要给学校或园区落地一套宿舍管理工具这篇文章从需求拆解讲到模型设计从核心功能实现讲到部署踩坑都是这个项目里实际发生过的事。1. 从翻台账到点鼠标宿舍管理系统的需求到底长什么样1.1 宿管办公室的真实痛点很多初学者拿到学生宿舍管理系统题目后第一反应是打开Django文档开始建models结果做出来一个学生成绩管理系统换皮——只有增删改查没有业务流程。我动手之前先花了两天坐在宿管办公室旁边把她们的工作流完整捋了一遍。宿舍管理的日常大概有这几条线新生入学时要登记个人信息、安排宿舍床位、发钥匙平时学生因为换专业、换学院或者室友矛盾申请调宿宿管审批后床位要及时释放和重新分配报修流程覆盖了灯坏、门锁坏、空调不制冷这些高频问题访客、维修工、家长进出楼栋要登记时间和事由辅导员查寝需要快速看到谁不在、谁晚归月底还要统计入住率、空床位数、维修完成率报给上级部门。这一堆需求落到系统里就是五个模块基础数据楼栋/宿舍/床位、学生管理档案与入住关系、日常业务报修/来访/调宿、公告通知、统计报表外加系统管理用户/角色/权限做支撑。我把每个场景的线下做法和线上方案整理成一张对照表这个动作看着笨但对后面的数据建模帮助极大。业务场景线下做法系统需求新生入住手写台账登记批量导入名单、自动分配床位、打印入住单调宿申请纸质申请单人工审批在线申请、多级审批、床位状态联动报修口头或电话通知在线提交、状态跟踪、超时提醒来访人员手写登记本电子登记、按楼栋按时间检索查寝晚归点名加手工表入住名单一键导出、未归标注统计报表人工挨个数楼栋入住率、床位使用率自动统计这张表还有另一个用处它是后面写测试用例的清单。每个需求点都能对应到一个可验证的行为而不是开发完才知道漏了哪个环节。1.2 角色与权限矩阵权限是系统的骨架宿舍管理系统有个容易被忽视的点不同角色看到的界面和数据完全不同。我最后把角色收敛成四类权限矩阵如下角色可访问模块典型操作超级管理员所有模块用户管理、角色配置、数据备份宿管员基础数据、学生、宿舍、日常业务、统计分配床位、审批调宿、处理报修、登记来访辅导员学生、查寝、统计查看本学院学生、导出名单、标记晚归学生个人中心、报修、调宿申请、公告提交报修、发起调宿、查看公告这个矩阵决定了后面要不要引入第三方权限框架。我当时的判断是系统规模不大Django自带的auth加permission已经够用不需要引入django-guardian这种对象级权限库。如果你想做学生只能看到自己那条报修单这种精细控制guardian确实有帮助但学习成本也不低。我在这个项目里采用的是URL级权限加视图内对象过滤的折中方案第4部分会细说。这里特别强调一句权限矩阵一定要写在需求文档里不要写代码的时候才想。我见过太多管理类项目开发到一半发现学生能访问宿管员的接口再回头补权限代码改得面目全非工期也搭进去了。2. 选Django不选Flask技术选型这笔账怎么算2.1 Django的全家桶对管理类项目有多划算如果是做纯API服务或者微信公众号后端Flask、FastAPI都很顺手轻量灵活。但宿舍管理系统是典型的页面多、表单多、权限多、后台管理需求重的项目Django的电池batteries included优势非常明显。最直观的是Admin后台。管理类系统几乎有一半需求是让管理员能录入和修改数据Django的admin只要把模型注册进去自动生成列表页、编辑页、筛选、搜索、分页。我在做这个项目时宿管员那边大部分日常录入工作都是直接通过Admin后台完成的省掉了为纯数据维护单独写页面的时间。# dormitory/admin.py from django.contrib import admin from .models import Building, Dormitory, Bed admin.register(Building) class BuildingAdmin(admin.ModelAdmin): list_display (name, gender, floor_count, order) list_filter (gender,) admin.register(Dormitory) class DormitoryAdmin(admin.ModelAdmin): list_display (building, room_number, capacity) list_filter (building,) search_fields (room_number,)有人嫌Admin后台丑这问题确实存在默认界面停留在十年前。两条出路一是自己写前端页面做好展示交互数据维护走admin二是给admin套现代主题。社区里目前比较活跃的是django-unfold界面风格接近现代中后台设计装完配置一下就能用。我是在二期迭代里接入了unfold宿管员那边接受度高了很多培训成本肉眼可见地降下来了。2.2 ORM、迁移和Form三位一体Django的ORM帮你把SQL变成Python对象这不只是少写几行SQL的事。对管理类项目来说最大的价值在于模型定义本身就是数据字典团队沟通成本低。业务方问床位的当前状态存在哪里代码里一个is_occupied字段一目了然。光有ORM不够Django还提供了内置的迁移系统。改模型后执行makemigrations和migrate数据库结构跟着版本走不会出现开发环境改了字段、生产环境忘记同步的尴尬。我在开发时每次调整模型都会先生成一个迁移再配一条对应的数据迁移RunPython。比如给已有宿舍批量初始化床位这一步就是用数据迁移完成的# dormitory/migrations/0003_init_beds.py from django.db import migrations def create_beds(apps, schema_editor): Dormitory apps.get_model(dormitory, Dormitory) Bed apps.get_model(dormitory, Bed) for dorm in Dormitory.objects.all(): for i in range(1, dorm.capacity 1): Bed.objects.create(dormitorydorm, bed_numberf{dorm.room_number}-{i}) class Migration(migrations.Migration): dependencies [(dormitory, 0002_alter_dormitory_capacity)] operations [migrations.RunPython(create_beds)]Form组件也一样。Django的ModelForm可以根据模型自动生成表单和校验逻辑注册入住时学号重复、手机号格式不对这类问题在表单层就被拦住了不用在视图里堆一堆重复的字段校验。2.3 项目初始化和应用拆分技术选型定了之后实际开工第一步是搭工程。我用的Python版本是3.11Django选4.2 LTS长期支持版本维护时间持续到2026年比追最新的5.x更稳。创建项目的命令如下python -m venv venv source venv/bin/activate pip install django4.2.* django-admin startproject dormitory_project . python manage.py startapp accounts python manage.py startapp student python manage.py startapp dormitory python manage.py startapp repair python manage.py startapp notice应用拆分的原则我总结过一句话按业务域拆不按功能类型拆。不要建一个views.py塞下几百个函数而是把学生相关的一切放进student应用楼栋床位相关的一切放进dormitory应用报修放repair公告放notice。这里有个Django新手特别容易踩的坑如果打算自定义User模型比如用学号登录一定要在第一次migrate之前设置好AUTH_USER_MODEL否则中途换User模型会非常痛苦。我的做法是第一行代码就先写自定义User哪怕初期只是给自带User加一个role字段后续扩展就自由了。3. 楼栋、宿舍、床位、学生数据模型的一次到位设计3.1 为什么床位必须单独建模宿舍管理系统的核心资源不是宿舍是床位。我见过不少把床位设计成宿舍一个字段比如bed_count、empty_count的项目那样的系统一旦发生调宿统计就只能靠手写逻辑反复算非常容易出错。床位单独建模之后住没住人就是一个可以直接查询的状态而且每张床位都能追溯历史入住记录。这是整个系统业务逻辑的地基我建议无论如何都不要省掉这个模型# dormitory/models.py from django.db import models class Building(models.Model): GENDER_CHOICES ((M, 男), (F, 女)) name models.CharField(楼栋名称, max_length50, uniqueTrue) gender models.CharField(适用性别, max_length2, choicesGENDER_CHOICES) floor_count models.PositiveSmallIntegerField(楼层数, default6) order models.PositiveSmallIntegerField(排序, default0) class Meta: ordering [order] verbose_name 楼栋 class Dormitory(models.Model): building models.ForeignKey(Building, on_deletemodels.CASCADE, related_namedormitories, verbose_name所属楼栋) room_number models.CharField(房间号, max_length20) capacity models.PositiveSmallIntegerField(床位容量, default8) is_active models.BooleanField(是否启用, defaultTrue) class Meta: unique_together (building, room_number) verbose_name 宿舍 class Bed(models.Model): dormitory models.ForeignKey(Dormitory, on_deletemodels.CASCADE, related_namebeds, verbose_name所属宿舍) bed_number models.CharField(床位编号, max_length20) is_occupied models.BooleanField(是否占用, defaultFalse) current_student models.OneToOneField(student.Student, nullTrue, blankTrue, on_deletemodels.SET_NULL, related_namecurrent_bed, verbose_name当前学生) class Meta: unique_together (dormitory, bed_number) verbose_name 床位几个细节值得注意。unique_together保证了同一栋楼里房间号不会重复、同一宿舍里床位编号不会重复current_student用OneToOneField因为一张床同一时间只能住一个人on_delete用SET_NULL而不是CASCADE原因是我们保留学生档案而不是整个删掉这一点后面第5部分会展开讲。3.2 学生档案和入住历史用记录表而不是字段学生信息放在Student模型里并且用OneToOneField关联Django自带的User。为什么不直接在User上写学号、学院、专业因为User更偏向账号概念Student更偏向档案概念。拆开之后未来如果接一卡通、门禁系统改的是Student表而不是认证体系影响面小很多。# student/models.py from django.conf import settings from django.db import models class Student(models.Model): GENDER_CHOICES ((M, 男), (F, 女)) user models.OneToOneField(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namestudent_profile, verbose_name账号) student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length30) gender models.CharField(性别, max_length2, choicesGENDER_CHOICES) college models.CharField(学院, max_length100) major models.CharField(专业, max_length100, blankTrue) phone models.CharField(手机号, max_length20, blankTrue) id_card models.CharField(身份证号, max_length18, blankTrue) enrolled_at models.DateField(入学时间, nullTrue, blankTrue) def __str__(self): return f{self.student_no} {self.name}学生和床位的关系不是一个学生恒久占着一张床而是会变化的。所以当前床位其实可以从Bed.current_student反向推出来Student表里不要重复存字段。真正要建模的是入住记录CheckInRecordclass CheckInRecord(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, related_namecheckin_records, verbose_name学生) bed models.ForeignKey(dormitory.Bed, on_deletemodels.PROTECT, related_namecheckin_records, verbose_name床位) check_in_date models.DateField(入住日期, nullTrue, blankTrue) check_out_date models.DateField(退宿日期, nullTrue, blankTrue) class Meta: verbose_name 入住记录每次入住生成一条记录退宿时更新check_out_date床位是否占用用Bed.is_occupied快速标记但历史入住记录永远保留。这套设计能回答大量运营问题3栋302这张床这学期住过几个人某个学生之前住在哪。如果只用一个当前关系字段这类历史问题全部查不了。3.3 报修、来访、调宿业务表的关联与状态机日常业务表我按流程类和非流程类分开设计。来访记录是纯登记类的一个模型加时间、事由、经办人字段就够了。报修和调宿需要状态流转必须把状态设计成显式字段# repair/models.py from django.db import models class RepairOrder(models.Model): STATUS_CHOICES ( (pending, 待处理), (processing, 处理中), (done, 已完成), (rejected, 已驳回), ) student models.ForeignKey(student.Student, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name报修人) dormitory models.ForeignKey(dormitory.Dormitory, on_deletemodels.PROTECT, verbose_name宿舍) description models.TextField(问题描述) image models.ImageField(现场照片, upload_torepair/%Y/%m/, blankTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultpending) assignee models.ForeignKey(accounts.User, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namerepair_orders, verbose_name接单人) created_at models.DateTimeField(报修时间, auto_now_addTrue) finished_at models.DateTimeField(完成时间, nullTrue, blankTrue) class Meta: ordering [-created_at] verbose_name 报修工单调宿单类似不过多了一组新旧床位的关联class RoomChangeOrder(models.Model): STATUS_CHOICES ( (pending, 待审批), (approved, 已通过), (rejected, 已驳回), (canceled, 已撤销), ) student models.ForeignKey(student.Student, on_deletemodels.CASCADE, verbose_name申请人) old_bed models.ForeignKey(dormitory.Bed, on_deletemodels.PROTECT, related_nameold_orders, verbose_name原床位) new_bed models.ForeignKey(dormitory.Bed, on_deletemodels.PROTECT, related_namenew_orders, verbose_name目标床位) reason models.TextField(申请原因) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(申请时间, auto_now_addTrue)状态用choices定义的好处是数据层直接约束取值范围前端下拉选项、列表筛选、权限判断全部复用这一处定义不会出现有的单子是审核中有的单子是处理中这种口径不统一的问题。外键的on_delete我特意做了两种不同设置。报修单里的dormitory用PROTECT因为一条报修单是历史事实宿舍被删会引起一连串问题不如直接禁止删除有业务记录的宿舍student用SET_NULL报修人毕业后档案可能被清理但工单还要保留把外键置空即可。4. 权限、分配、工单、实时推送四个核心功能的实现要点4.1 认证权限三层防守权限方案我分了三层登录验证login_required、权限验证permission_required、对象级过滤视图内按角色限制查询集。前两层是Django内置能力实现成本极低# dormitory/views.py from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import render from .models import Dormitory login_required permission_required(dormitory.view_dormitory, raise_exceptionTrue) def dormitory_list(request): dormitories Dormitory.objects.select_related(building).all() return render(request, dormitory/list.html, {dormitories: dormitories})第三层对象过滤得自己写。学生登录后访问报修列表不能返回全部工单。我的做法是先确认request.user是否是学生再过滤出当前学生相关的记录from django.core.exceptions import PermissionDenied from .models import RepairOrder login_required def my_repair_list(request): profile getattr(request.user, student_profile, None) if profile is None: raise PermissionDenied orders RepairOrder.objects.filter(studentprofile).select_related(dormitory) return render(request, repair/my_list.html, {orders: orders})关于登录态Django默认用session把sessionid写入cookie纯Web项目完全够用。如果你以后要给小程序或者移动端提供接口再考虑token方案DRF的TokenAuthentication或者JWT网页端老老实实用默认session反而简单可靠。函数视图用装饰器最直观Class-Based Views则可以用UserPassesTestMixin重写test_func达到同样效果。4.2 空床位查询与并发控制宿舍分配是整个系统最核心的业务动作学生入住、调宿、退宿都涉及床位的占用和释放。查空床看起来简单写不好会埋雷。最基本的空床查询是这样# dormitory/services.py def get_available_beds(gender): from dormitory.models import Bed return Bed.objects.filter( dormitory__building__gendergender, dormitory__is_activeTrue, is_occupiedFalse, current_student__isnullTrue, ).select_related(dormitory, dormitory__building)这里通过跨关联过滤一次把楼栋性别、宿舍启用状态、床位占用状态全部过滤掉。select_related则是在查询时把关联的宿舍和楼栋一起取出来避免后面循环里产生大量SQL。统计模块还会用到聚合查询比如计算每栋楼的入住率from django.db.models import Count, Q stats Building.objects.annotate( total_bedsCount(dormitories__beds), occupied_bedsCount( dormitories__beds, filterQ(dormitories__beds__is_occupiedTrue), ), )真正的坑在并发。新生入住那几天两个宿管可能同时给同一个床位办入住。如果代码是先查空床再设置占用两个人查到同一张床的概率不低。Django默认事务隔离级别下不加锁的查-改操作会有竞态条件。我的处理是分配床位时使用select_for_update行锁并包在原子事务里from django.db import transaction transaction.atomic def assign_bed(student, bed_id): bed Bed.objects.select_for_update().filter( idbed_id, is_occupiedFalse, current_student__isnullTrue ).first() if bed is None: raise ValueError(该床位已被占用请刷新后重试) CheckInRecord.objects.create(studentstudent, bedbed) bed.is_occupied True bed.current_student student bed.save() return bedselect_for_update会锁定命中的行直到事务结束。第二个宿管再来抢同一张床时会被阻塞到前一个事务完成然后看到is_occupied已经是True走已被占用的分支。这个坑补上之前我一度怀疑是宿管员操作失误后来才发现是并发问题。4.3 报修工单的状态流转报修模块的业务规则其实只有一条状态只能按合法路径流转。我把状态机的规则显式写在服务层# repair/services.py ALLOWED_TRANSITIONS { pending: [processing, rejected], processing: [done, pending], done: [], rejected: [pending], } def transition_repair_order(order_id, to_status, user): order RepairOrder.objects.select_for_update().get(pkorder_id) if order.status not in ALLOWED_TRANSITIONS: raise ValueError(f工单当前状态为{order.get_status_display()}无法执行该操作) if to_status not in ALLOWED_TRANSITIONS.get(order.status, []): raise ValueError(非法的状态流转) # 此处叠加权限校验... order.status to_status if to_status done: order.finished_at timezone.now() order.save() return order所有改状态的入口都走这一个函数非法流转在源头就被拦住了。选择写在服务层而不是散落在各视图里的原因是报修的触发点不止一个页面学生端提交、宿管端派单、师傅端完工、超时自动提醒都可能改状态集中在服务层才能保证规则一致。状态流转的权限矩阵也值得列出来操作允许角色状态变化提交报修学生无 → pending驳回宿管员pending → rejected接单处理宿管员/维修人员pending → processing重新派单宿管员processing → pending标记完成宿管员/维修人员processing → done4.4 WebSocket实时推送宿舍大屏的数据是怎么实时刷新的项目里有一个宿舍数据大屏要看实时入住率、最新报修工单。最初我打算写一个3秒轮询接口后来发现前端大屏不止一个轮询请求在高峰期挺浪费实时性也不理想。于是上了Django Channels做WebSocket推送。Channels的安装路径是安装channels、channels-redis在settings里把ASGI_APPLICATION指到新的asgi.py把协议改成http和websocket双支持。核心推送逻辑是一个AsyncWebsocketConsumer# notice/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name dormitory_notifications 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.channel_layer.group_send( self.group_name, {type: notify_message, message: data.get(message, )} ) async def notify_message(self, event): await self.send(text_datajson.dumps({message: event[message]}))业务端有新报修单时通过Django的signal触发推送# repair/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from .models import RepairOrder receiver(post_save, senderRepairOrder) def push_repair_order(sender, instance, created, **kwargs): channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( dormitory_notifications, { type: notify_message, message: f新增报修工单{instance.dormitory.room_number} - {instance.description[:20]} } )这套方案在几百个并发连接规模下很稳。不过要实话实说如果只是给学生发个通知、消息列表加个红点普通轮询就够了WebSocket反而增加复杂度。只有当你确实需要后台数据一变前端立刻更新的场景大屏、工单看板、消息中心才值得上Channels。开发环境还得记得装daphnerunserver默认不支持WebSocket要用 daphne -b 0.0.0.0 -p 8000 dormitory_project.asgi:application 启动。5. 这个项目里我踩过的三个坑权限绕过、N1查询、误删数据5.1 前端隐藏按钮不等于权限隔离第一期上线闹过笑话。我以为前端模板里用{% if perms.xxx %}把删除按钮藏掉就算权限控制了结果测试同学直接在浏览器地址栏输入删除URL照样把数据删了。原因很简单——模板渲染只控制看不看得见按钮视图函数从头到尾没校验过调用者有没有权限。教训是任何敏感操作权限校验必须发生在后端前端隐藏只是用户体验优化。Django的permission_required配raise_exceptionTrue天生就是干这个的别自己发明前端隐藏方案。后来我给每个涉及写操作的视图又加了一层对象归属校验防止学生A尝试修改学生B的报修单这种越权操作def get_owned_repair_order(request, pk): order get_object_or_404(RepairOrder, pkpk) profile getattr(request.user, student_profile, None) if profile and order.student_id ! profile.id: raise PermissionDenied return order5.2 列表页卡顿的元凶是N1查询宿舍列表、报修列表这类页面数据量一大就卡。一开始我没在意后来看数据库日志才发现一个列表请求触发了上百条SQL。问题出在模板循环里访问关联字段# 反面教材循环里访问关联字段每次都发一次SQL students Student.objects.all() for s in students: print(s.current_bed.dormitory.building.name)假设有50个学生这段代码会执行1次主查询加50次关联查询这就是经典N1问题。Django给的修复方案是select_related和prefetch_related。select_related用于一对一、多对一这种外键跳转会在SQL里用JOIN直接带出来students Student.objects.select_related(current_bed__dormitory__building).all()prefetch_related用于一对多、多对多比如查所有宿舍时把每个宿舍的床位一次取出来dormitories Dormitory.objects.prefetch_related(beds).all()这个优化做完列表页SQL从一百多条降到两条响应时间从秒级降到毫秒级。我现在的习惯是写完列表视图就用django-debug-toolbar看一眼SQL数量超过阈值立刻优化不拖。5.3 on_delete设置不当导致数据链断裂Django的ForeignKey必须显式指定on_delete这是新手最容易含糊的地方。一期开发时我图省事把报修记录里的student外键设成CASCADE想着学生删了他的报修记录跟着删挺干净。后来复盘发现这是错的报修工单是业务凭证学生毕业清理档案账号时工单要是被连带删除宿管那边的维修记录就断了出了问题没法追溯。合理的做法是区分两类外键一类是从属关系比如宿舍被删、同楼栋的床位可以一并删除用CASCADE或PROTECT另一类是业务引用比如报修单引用宿舍引用对象没了但单子还在用PROTECT或SET_NULL。这个项目最终的配置是Bed的dormitory外键CASCADE宿舍没了床位没有独立意义CheckInRecord的bed外键PROTECT有入住历史的床位禁止删除RepairOrder的dormitory外键PROTECT防止误删有工单的宿舍RepairOrder的student外键SET_NULL工单保留报修人置空这套配置在Admin后台会直接体现误删一个被引用的宿舍时Django会抛ProtectedError而不是静默删除一堆关联数据。宁可报错不可静默丢数据。6. 部署到一台普通服务器的完整过程6.1 锁定依赖与迁移环境开发阶段依赖管理我用的是最传统的requirements.txt。别小看这一步项目上服务器时Python版本、Django版本差一点都可能跑不起来。先导依赖pip freeze requirements.txt服务器上如果没有Python 3.11先通过包管理器安装装完用python3 --version确认版本然后创建干净的虚拟环境再安装python3.11 -m venv venv source venv/bin/activate pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput python manage.py createsuperusersettings.py里与环境相关的配置比如SECRET_KEY、DEBUG、数据库地址我习惯放到环境变量里代码用os.getenv读取服务器上用.env文件管理。DEBUG在生产必须设为False同时配置ALLOWED_HOSTS为域名或IP列表。这两项配错是最常见的部署翻车点页面会出现离谱的报错或者干脆拒掉请求。6.2 Nginx Gunicorn常规但可靠的组合Django自带的runserver只能用于开发。生产环境我用Gunicorn跑WSGI进程四个worker对宿舍系统这种低并发场景绰绰有余gunicorn dormitory_project.wsgi:application -w 4 -b 127.0.0.1:8001然后让Nginx做反向代理、托管静态文件和媒体文件server { listen 80; server_name dorm.example.com; client_max_body_size 20m; location /static/ { alias /data/dorm/static/; } location /media/ { alias /data/dorm/media/; } location / { proxy_pass http://127.0.0.1:8001; 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和media必须分开。执行collectstatic后Django会把项目的静态文件收集到STATIC_ROOT指定目录用户上传的图片走MEDIA_ROOT两边磁盘目录和Nginx配置对不上页面样式和图片就全挂。如果用了Channels的WebSocketNginx还需要在location /下面加Upgrade相关请求头转发并把连接timeout调长proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 86400;6.3 备份策略数据库和media文件都不能漏管理类系统最怕数据丢失尤其是入住记录这种历史数据。我备份做了两级数据库每天全量备份media文件每周同步一次。数据量不大时Django自带的dumpdata够用但它导出JSON格式恢复时要先清空再loaddata对某些字段类型支持也不够理想。我用的是PostgreSQL所以直接用原生pg_dump备份和恢复都快得多pg_dump -U dorm_user dorm_db /backup/dorm_$(date %Y%m%d).sql配合crontab每天早上跑一次保留最近30天再往另一台机器同步一份基本就能覆盖灾难场景。media目录里的报修照片和头像同样重要别只备份数据库漏了文件。到这一步一套能稳定运行的宿舍管理系统就算完整交付了。7. 如果再让我做一遍这三件事我会放在前面第一件事把数据字典和字段枚举一次性定义清楚不要边写边加。中途因为宿舍是否启用这个字段一开始没设计数据量大了才补迁移前端筛选逻辑跟着改了一大圈纯属浪费工期。第二件事从一开始就写接口文档。哪怕是前后端都自己写接口文档也能逼着我把入参、出参、错误码想清楚更不用说后面接小程序端时文档就是契约。用django-rest-framework的自动文档或者给写死的视图加docstring都好过没有。第三件事给核心业务加测试。手工测试根本覆盖不了所有状态流转路径。我把三个应用里最重要的服务层函数都配了单元测试用Django test client模拟登录、提交报修、审批流转一次跑通能省下数不清的回归成本。最后说一个我常用的土办法开发环境里永远保留一套造假数据脚本用Django fixture或者management command生成几十个学生、几栋楼、若干报修单。每次改完模型或视图刷新页面就能看到真实效果排查问题也比对着空表猜快得多。宿舍管理系统这种业务数据一多很多设计问题自己就暴露出来了空表状态下看着完美的代码很可能只是纸老虎。