宿舍管理系统算是我接触过的Python Web项目里非常典型的一类CRUD应用。说是“典型”,是因为它的业务逻辑不复杂,核心就是对学生信息、宿舍床位、入住退宿、报修记录这些数据进行增删改查,但真正把一个宿舍管理系统从零写明白,需要处理的需求细节却不少。我最早用Django做这个系统时,光是一个“宿舍满了怎么办”就折腾了好几个版本,后来才把数据模型和流程理顺。这篇博文就把我完整的实现思路、数据库设计、核心代码和踩坑记录整理出来,不管是期末课程设计、毕业设计,还是想快速搭建一个内部管理后台,都可以直接参考。
1. 项目定位与需求拆解
1.1 宿舍管理到底在管什么
做任何系统之前,先把业务对象想清楚。宿舍管理系统的核心参与者有三类:学生、宿管员、系统管理员。围绕这三类角色,日常业务可以拆成这么几块:
- 学生信息管理:学号、姓名、性别、院系、专业、联系方式,以及这个学生当前住在哪个宿舍。这是最基础的数据。
- 宿舍资源管理:楼栋、房间号、床位容量、当前已住人数,以及房间是男生宿舍还是女生宿舍。
- 入住与退宿流程:学生入住时要分配宿舍,退宿时要释放床位,换宿时要记录历史。
- 报修处理:学生提交水电、门锁、家具等报修申请,宿管员更新处理状态。
- 公告通知:管理员发布停水停电、安全检查等通知,学生登录后能看到。
这些需求看起来简单,但真正做起来会发现,很多细节会影响数据库设计。比如宿舍分配时,如果房间已经住满了,系统必须拦截;退宿之后,该学生不能再占着床位;调宿舍时还要保留历史记录,方便以后查账或者排查问题。这些流程性需求必须在数据模型层面就想清楚,而不是等写代码的时候再临时补。
1.2 为什么选 Python + Django,而不是其他方案
我见过不少人用PHP、或者用Node.js写前后端分离来折腾宿舍管理,但说实话,对这种偏内部管理的系统,Python加Django是开发效率最高的组合之一。理由很实在:
- Django自带ORM,可以直接用Python类定义表结构,迁移命令一键建表,不用手写SQL。
- Django自带Admin后台,默认就有数据管理页面,开发阶段几乎不用写前端代码,就能完成大部分管理操作。
- 自带用户认证体系,登录、权限、session这些直接复用,省掉一大块工作量。
- 模板引擎够用,列表页、详情页直接用Django Template渲染,不需要维护前后端两套工程。
如果用前后端分离方案,等于把简单问题复杂化:要单独做接口、做Token认证、做跨域配置,还得再写一套Vue页面。宿舍管理系统本身没有复杂的交互,Django的“服务端渲染 + Admin后台”路线,一个人短时间内就能把完整系统跑起来,这是它最大的优势。
当然,选择Django也有需要注意的地方。它的“约定优于配置”风格意味着目录结构、命名规范最好按官方习惯来,否则后面维护会别扭。还有Django自带的ORM在简单查询上非常舒服,但遇到极复杂的统计SQL时,还是得用extra或原生SQL兜底。针对宿舍管理这个体量,这些都不是问题。
2. 整体设计与数据库建模
2.1 功能模块划分
我在设计时,没有把功能拆得特别碎,而是按照业务域划分成四个APP,每个APP职责单一,方便维护:
- accounts:用户登录、学生账号与User模型关联、宿管员权限分组。
- dormitory:宿舍楼栋与房间管理、学生基本信息管理、入住和退宿记录。
- repair:报修工单的创建、处理、状态流转。
- notice:公告的发布与展示。
这种拆分的好处是,后续如果只想给某个模块加字段,改对应APP即可,不会动到其他模块。比如报修模块要增加“维修人员电话”字段,只需要改repair里的模型,而宿舍模块完全不受影响。
权限设计上,我用了Django自带的User模型加Group分组。管理员属于admin组,宿管员属于dorm_manager组,学生用户标记为is_staff=False并用OneToOne关联到Student表。视图层通过装饰器做准入判断,比如发布公告只能管理员操作,处理报修需要宿管员或管理员权限。
2.2 核心数据表与关联关系
数据库是这类系统最值得花时间的地方。我设计了五张核心表,下面把字段和关系写清楚。
第一张是宿舍表Dormitory:
- building:楼栋名,比如“1号楼”“2号楼”
- room_number:房间号,比如“101”
- capacity:床位容量,一般是4、6、8
- gender_type:宿舍类型,男/女,用于分配时防止混住
- 唯一约束:同一楼栋下房间号不能重复
第二张是学生表Student:
- student_no:学号,唯一
- name:姓名
- gender:性别
- department:院系
- major:专业
- phone:手机号
- dormitory:外键关联Dormitory,允许为空,空表示当前未入住
- create_time:创建时间
第三张是住宿记录表HousingRecord:
- student:外键关联Student
- dormitory:外键关联Dormitory
- move_in_time:入住时间
- move_out_time:退宿时间,为空表示在住
- status:状态,在住/已退
第四张是报修表RepairOrder:
- student:外键关联Student,记录谁报修的
- dormitory:外键关联Dormitory,保存报修时所在的宿舍信息
- category:报修类型,比如水电、门锁、家具
- description:详细描述
- status:待处理/处理中/已完成
- created_time:报修时间
- finish_time:完成时间
第五张是公告表Notice:
- title:标题
- content:正文
- publisher:发布人,外键关联User
- publish_time:发布时间
这里最关键的是Student和HousingRecord之间的配合。Student里的dormitory字段保存的是“当前状态”,HousingRecord保存的是“每一次入住退宿的历史流水”。这两个并行存在,既保证了列表页可以快速知道谁住在哪,又不会丢失调宿记录。
2.3 设计取舍:当前状态和历史流水分开
我最早做的时候,图省事只给学生表加了一个宿舍外键,退宿直接置空。后来被问到“上学期这个学生住过哪个房间”时,完全答不上来,因为没有历史表。所以我加了一张HousingRecord,专门记录每一次住宿变更。
为什么不用单一外键覆盖全部业务?因为需求场景不同。当前状态追求的是查询快,历史记录追求的是可追溯。用一张表同时满足两个场景,要么冗余字段多,要么查询逻辑复杂。分开之后逻辑非常清晰:改宿舍先更新Student.dormitory,再往HousingRecord插入一条“新入住”记录;退宿时更新Student.dormitory为空,同时把HousingRecord里在住记录标记为退宿。
还有一个细节值得提:外键的on_delete参数。学生对应的宿舍如果被删除,合理行为是保留学生记录但置空宿舍字段,所以用on_delete=models.SET_NULL并且null=True。而HousingRecord关联的学生如果被删除,这条历史记录本身也没有保留意义了,可以用CASCADE。报修记录我建议用PROTECT,防止学生被删后报修记录被连锁删除,方便后期审计。
3. 从零到一:核心代码实现
3.1 环境准备与项目初始化
开发环境我用的是Python 3.10和Django 4.2 LTS。Django 4.x和旧版本有个明显区别,就是时区处理、路由配置的写法已经比较现代,网上资料也最多,遇到问题容易搜到解决方案。
初始化项目的步骤我列一下:
# 创建项目目录并进入 mkdir dormitory_system cd dormitory_system # 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows系统用 venv\Scripts\activate # 安装Django pip install django # 创建项目和APP django-admin startproject config . python manage.py startapp accounts python manage.py startapp dormitory python manage.py startapp repair python manage.py startapp notice创建完APP后,记得在config/settings.py的INSTALLED_APPS里注册这五个模块。还需要配置语言和时区,把LANGUAGE_CODE改成zh-hans,TIME_ZONE改成Asia/Shanghai,并设置USE_TZ=False。如果不改时区,后面写入数据库的时间会差8个小时,排查起来很头疼。
3.2 模型层:用ORM把表结构写清楚
模型是Django里最不能省事的部分。我把dormitory/models.py里几个核心模型分享出来,这些代码是完整跑通过了的。
from django.db import models class Dormitory(models.Model): GENDER_TYPE_CHOICES = ( ('M', '男'), ('F', '女'), ) building = models.CharField('楼栋', max_length=50) room_number = models.CharField('房间号', max_length=20) capacity = models.PositiveIntegerField('床位容量', default=4) gender_type = models.CharField('宿舍类型', max_length=1, choices=GENDER_TYPE_CHOICES) class Meta: verbose_name = '宿舍' verbose_name_plural = verbose_name unique_together = ('building', 'room_number') def __str__(self): return f'{self.building}-{self.room_number}' @property def current_count(self): return self.student_set.filter(dormitory=self).count() @property def is_full(self): return self.current_count >= self.capacityDormitory里的current_count属性在Admin列表页和分配宿舍时都会用到,直接统计当前住在这个房间的学生数量。unique_together保证了同一个楼栋下不会出现两个相同的房间号,这是数据完整性最基本的保障。
再看Student模型:
class Student(models.Model): GENDER_CHOICES = ( ('M', '男'), ('F', '女'), ) student_no = models.CharField('学号', max_length=30, unique=True) name = models.CharField('姓名', max_length=50) gender = models.CharField('性别', max_length=1, choices=GENDER_CHOICES) department = models.CharField('院系', max_length=100) major = models.CharField('专业', max_length=100) phone = models.CharField('联系方式', max_length=20) dormitory = models.ForeignKey( Dormitory, verbose_name='宿舍', on_delete=models.SET_NULL, null=True, blank=True, related_name='student_set' ) class Meta: verbose_name = '学生' verbose_name_plural = verbose_name def __str__(self): return f'{self.name}({self.student_no})' @property def dormitory_info(self): return f'{self.dormitory.building}-{self.dormitory.room_number}' if self.dormitory else '未入住'Student用ForeignKey关联Dormitory,注意on_delete用的是SET_NULL。我在实际开发中吃过亏,如果直接CASCADE,删除一个宿舍会把所有学生都删掉,这在业务上绝对不允许。管理员误删宿舍,最多就是学生变成“未入住”,可以通过手动重新分配恢复。
HousingRecord记录历史流水:
class HousingRecord(models.Model): STATUS_CHOICES = ( ('IN', '在住'), ('OUT', '已退'), ) student = models.ForeignKey(Student, on_delete=models.CASCADE, verbose_name='学生') dormitory = models.ForeignKey(Dormitory, on_delete=models.PROTECT, verbose_name='宿舍') move_in_time = models.DateTimeField('入住时间', auto_now_add=True) move_out_time = models.DateTimeField('退宿时间', null=True, blank=True) status = models.CharField('状态', max_length=3, choices=STATUS_CHOICES, default='IN') class Meta: verbose_name = '住宿记录' verbose_name_plural = verbose_nameHousingRecord的status和move_out_time其实有一部分信息是重复的:在住时move_out_time为空,退宿时move_out_time不为空。我为什么还要冗余一个status字段?因为对于在住的宿舍,我会频繁统计“当前在住学生”,直接filter(status='IN')比同时判断move_out_time是否为空要直观得多。索引上也更友好,如果数据量变大,还能在status上加索引。
RepairOrder模型:
class RepairOrder(models.Model): CATEGORY_CHOICES = ( ('WATER', '水电'), ('DOOR', '门窗锁具'), ('FURNITURE', '家具'), ('OTHER', '其他'), ) STATUS_CHOICES = ( ('PENDING', '待处理'), ('PROCESSING', '处理中'), ('DONE', '已完成'), ) student = models.ForeignKey(Student, on_delete=models.PROTECT, verbose_name='报修学生') dormitory = models.ForeignKey(Dormitory, on_delete=models.PROTECT, verbose_name='宿舍') category = models.CharField('报修类型', max_length=20, choices=CATEGORY_CHOICES) description = models.TextField('问题描述') status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default='PENDING') created_time = models.DateTimeField('报修时间', auto_now_add=True) finish_time = models.DateTimeField('完成时间', null=True, blank=True) class Meta: verbose_name = '报修记录' verbose_name_plural = verbose_nameRepairOrder里我保存了一个独立的dormitory字段,而不是通过student再查一次宿舍。因为报修单创建后,如果学生后来调了宿舍,报修单上的宿舍仍然应该是“报修时所在宿舍”,否则维修师傅会跑错地方。这种冗余在业务上是合理的。
写模型时有个小建议:每个字段都要写verbose_name,并且尽量写完整的帮助信息。课程设计或团队协作时,别人看到字段名first time就能明白含义,不至于还要翻代码。
3.3 视图层:列表筛选与入住分配
模型建好之后,视图层是业务逻辑最集中的地方。我没有用Django的通用视图Class-Based View,而是选择了函数视图。原因很简单:宿舍管理的每个操作,比如入住、退宿、报修状态变更,都需要插入额外的业务判断,函数视图写起来更直观,调试的时候也更容易定位问题。
学生列表页是多条件筛选的经典场景,我贴一下核心代码:
from django.shortcuts import render, get_object_or_404, redirect from django.db.models import Q from .models import Student, Dormitory def student_list(request): students = Student.objects.select_related('dormitory').order_by('student_no') keyword = request.GET.get('keyword', '').strip() gender = request.GET.get('gender', '') building = request.GET.get('building', '') if keyword: students = students.filter( Q(student_no__icontains=keyword) | Q(name__icontains=keyword) ) if gender: students = students.filter(gender=gender) if building: students = students.filter(dormitory__building=building) return render(request, 'dormitory/student_list.html', { 'students': students, 'dormitories': Dormitory.objects.values_list('building', flat=True).distinct(), })这里有个很重要的优化:Student.objects.select_related('dormitory')。如果不加这一句,列表页每显示一个学生,Django就会去数据库查一次他对应的宿舍,页面渲染100个学生就是101条SQL。加了select_related之后,Django会用一次JOIN把宿舍信息一起查出来。我在开发时用django-debug-toolbar一眼就看到这个问题,不加之前列表页有几十条重复SQL,加完之后降到了一次。
入住分配的逻辑是这样的:先判断宿舍是否存在,再判断是否满员,还要判断性别是否匹配,最后才执行分配。
def assign_dormitory(request, student_id): student = get_object_or_404(Student, pk=student_id) if request.method == 'POST': dorm_id = request.POST.get('dormitory') dorm = get_object_or_404(Dormitory, pk=dorm_id) if dorm.is_full: return render(request, 'error.html', {'message': '该宿舍已住满,无法分配'}) if student.gender != dorm.gender_type: return render(request, 'error.html', {'message': '性别不匹配,无法分配'}) # 当前状态更新 student.dormitory = dorm student.save() # 历史流水记录 HousingRecord.objects.create( student=student, dormitory=dorm, status='IN' ) return redirect('dormitory:student_detail', student_id=student.id) available_dormitories = Dormitory.objects.filter( gender_type=student.gender ).exclude( id__in=[d.id for d in Dormitory.objects.all() if d.is_full] ) return render(request, 'dormitory/assign_dormitory.html', { 'student': student, 'dormitories': available_dormitories, })这段代码里有两个细节值得注意。第一个是“住在满员宿舍”的判断,dorm.is_full这个属性会实时统计学生数量,确保并发情况下也不会超住。第二个是性别校验,在分配前就拦截掉不匹配的情况,而不是写入后再修复。实际宿舍管理中,男生住在女生宿舍这种问题是管理员最想避免的,代码层面直接硬性校验比事后检查靠谱得多。
这里我也踩过一个坑:先用列表推导式Dormitory.objects.all()去判断is_full,如果宿舍数量大,性能会很差。实际课程设计规模下没问题,但放到正式环境,更好的做法是在数据库层面用annotate统计学生数,再filter比较。我在文章后面的优化建议里会再强调。
3.4 Admin后台:不写前端也能管理
Django Admin是这个项目最大的福音。我几乎没为管理端写任何HTML页面,全部靠注册Admin模型就实现了宿舍、学生、报修、公告的后台管理。以下是dormitory/admin.py的配置:
from django.contrib import admin from .models import Dormitory, Student, HousingRecord class StudentInline(admin.TabularInline): model = Student extra = 0 fields = ('student_no', 'name', 'gender', 'major') @admin.register(Dormitory) class DormitoryAdmin(admin.ModelAdmin): list_display = ('building', 'room_number', 'capacity', 'current_count', 'is_full', 'gender_type') list_filter = ('building', 'gender_type') search_fields = ('building', 'room_number') inlines = [StudentInline] @admin.register(Student) class StudentAdmin(admin.ModelAdmin): list_display = ('student_no', 'name', 'gender', 'department', 'major', 'dormitory_info') list_filter = ('gender', 'department', 'dormitory__building') search_fields = ('student_no', 'name', 'phone') autocomplete_fields = ('dormitory',) @admin.register(HousingRecord) class HousingRecordAdmin(admin.ModelAdmin): list_display = ('student', 'dormitory', 'move_in_time', 'move_out_time', 'status') list_filter = ('status', 'dormitory__building')Admin配置的几个点放一起看会更有体会:
- current_count和is_full是模型里的property,可以直接显示在管理列表中,宿管员一眼就能看出哪些房间满了。
- StudentInline嵌入在宿舍详情页里。管理员打开某个房间时,能直接看到住在这个房间的学生,体验非常自然。
- HousingRecord单独注册,方便按学生查询住宿历史。
这些配置看着简单,但把Django Admin的“可配置性”用足了。Student模型里没有自己的list_display函数,我在Admin里定义了dormitory_info,直接调用模型属性,列表页就不用显示“外键对象”那种不友好的形式了。
4. 项目跑通与常见问题排查
4.1 拿到“源码+数据库”怎么快速跑起来
如果你拿到的是别人分享的“源码+数据库+文档”这种形式的项目,第一步不要急着打开一堆代码文件,而是先看README或者文档里的“快速开始”部分。规范的交付包一定会有环境要求、数据库配置说明和启动命令。
以我的项目为例,拿到后按下面的步骤执行:
# 1 使用虚拟环境(项目里已经有 venv 目录就激活,没有就新建) python -m venv venv source venv/bin/activate # 2 安装依赖 pip install -r requirements.txt # 3 如果数据库文件(比如 db.sqlite3)没压缩,直接放项目根目录;如果有SQL脚本,则需要先建库再导入 python manage.py migrate # 4 创建管理员账号 python manage.py createsuperuser # 5 启动开发服务器 python manage.py runserver这里有个常见误区:很多新手以为拿到了别人项目里的db.sqlite3,直接就能用。实际上,如果你的Django版本和对方不一致,或者对方迁移文件不完整,数据库表结构可能对不上。最好的做法是先跑migrate把空库结构建出来,再通过Admin后台录入数据,或者用fixture文件导入测试数据。
数据库文件是SQLite时,只要文件完整,放到根目录通常就能直接用。换成MySQL或PostgreSQL时,需要在settings.py里修改数据库配置,并且提前创建好数据库。我建议项目里同时提供一份SQL导出脚本,方便使用者通过Navicat或命令行工具导入。
4.2 高频报错与排查实录
运行这类Django项目时,下面几个问题出现频率最高,我整理成表格方便对照排查:
| 报错信息 | 主要原因 | 解决方案 |
|---|---|---|
| CSRF verification failed | 表单模板中缺少{% csrf_token %} | 在form标签内部加{% csrf_token %} |
| No such table: dormitory_student | 迁移未执行 | 运行python manage.py makemigrations,再运行migrate |
| OperationalError: no such column | 数据库表结构和模型不一致 | 检查迁移文件,必要时用makemigrations重新生成迁移 |
| NameError: name 'xxx' is not defined | 视图里使用了未导入的模型 | 检查模型导入语句 |
| AttributeError: 'Dormitory' object has no attribute 'student_set' | 外键related_name设置问题 | 检查related_name是否正确,或者通过dormitory.student_set调用反向关系 |
| staticfiles 404异常 | DEBUG=False时未收集静态文件 | 运行collectstatic,并配置STATIC_ROOT和STATIC_URL |
| 时间差了8小时 | 时区配置不对 | settings.py里设置LANGUAGE_CODE='zh-hans',TIME_ZONE='Asia/Shanghai',USE_TZ=False |
| database is locked | SQLite并发写入限制 | 开发环境可接受;生产环境切换MySQL或PostgreSQL |
CSRF问题是我见过最多的。Django默认开启CSRF中间件,只要模板里的form没有加csrf_token,提交必报403。新手容易在写API或者搜索时看到“CSRF verification failed”就直接禁用中间件,这是不推荐的。正确做法是在模板中加上模板标签,既安全又省事。
还有一个特别容易踩的坑是migrate冲突。假设你修改了某个model字段,运行makemigrations后生成了迁移文件,但后来发现写错了,手动把迁移文件删了,重新makemigrations时会提示“No changes detected”。这是因为Django的记录在django_migrations表里。我一般会先到数据库里把对应迁移记录删除,再重新生成,或者直接删库重建。开发阶段数据不重要,删库重建最干净。
4.3 生产环境部署要点
如果这个系统要真正上线用,光靠python manage.py runserver是不行的。runserver自带的是开发服务器,性能低、安全性差,而且并发能力很弱。我在部署时用的是nginx加gunicorn加supervisor的组合,下面说一下关键步骤。
首先,安装gunicorn并启动应用:
pip install gunicorn gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3然后修改settings.py里的一些生产配置:
DEBUG = False ALLOWED_HOSTS = ['your-domain.com', 'ip地址'] STATIC_ROOT = BASE_DIR / 'staticfiles' MEDIA_ROOT = BASE_DIR / 'media' MEDIA_URL = '/media/'接着收集静态文件,否则Admin后台样式全丢:
python manage.py collectstatic最后用supervisor守护gunicorn进程,保证服务器重启后应用能自动拉起。nginx反向代理配置里把80/443端口的请求转发到8000端口,同时负责静态文件和媒体文件的处理。
数据库方面,如果项目从SQLite切换到MySQL,需要安装驱动并修改配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'dormitory_db', 'USER': 'dorm_user', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }MySQL记得建库时指定utf8mb4,否则中文数据存进去容易出现乱码问题。
部署上线之前,还有一个非常容易被忽略的部分:备份。SQLite备份直接复制db.sqlite3文件就行,但要注意服务运行期间不要直接复制,最好先停服务或者使用SQLite的在线备份API。MySQL部署的话,用mysqldump做定时任务脚本,每天导出一次,保留最近七天的备份即可。很多课程设计项目都是因为没做备份,数据丢失后无从恢复,真的很可惜。
5. 从实战中总结的经验
这个项目做完之后,我最大的体会是:宿舍管理系统的难点不在技术选型,而在于把业务规则用数据模型和代码表达清楚。比如满员判断、性别匹配、历史记录,这些规则如果不在一开始就想明白,后面写代码一定会东补西补。
关于ORM使用,我强烈建议养成调试时观察SQL的习惯。Django的query日志或者django-debug-toolbar都能让你看到每次ORM操作背后的SQL语句。我在优化列表页时,就是通过这个工具发现了N+1查询问题,加了select_related之后,页面响应时间肉眼可见地变快。
关于数据库设计,不要急着堆表。先画出核心业务对象的字段和关系,再问自己几个问题:删除某个对象时,关联数据应该怎么办?查询某个列表时,最常用的筛选条件是什么?有没有需要保留的历史流水?这些问题想清楚,表结构基本不会有大改动。
最后再分享一个小技巧:开发阶段一定要学会使用Django的Admin后台。它不仅是管理页面,更是验证模型设计是否合理的最快方式。你注册好模型,进入Admin看一眼列表页和详情页,字段显示是否清晰、筛选是否好用、关联数据是否直观,一目了然。大部分设计问题在这个阶段就能暴露出来,比等到做前端页面再返工高效得多。宿舍管理系统这种体量的项目,用Django做一遍,能把Web开发的整个流程从头到尾打通,值得花时间认真做。