ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django学生宿舍管理系统开发实战:模型设计、事务与部署优化

Django学生宿舍管理系统开发实战:模型设计、事务与部署优化 简介一套基于Python Django框架开发的学生宿舍管理系统完整项目主要面向计算机相关专业的课程设计、期末大作业或毕业设计参考人群适合通过一个真实系统快速掌握Django的MTV架构、前后端交互以及数据库设计方法。压缩包共862个文件整体大小约30.65MB包含84个Python后端脚本、103个Vue组件、261个JavaScript文件、30个HTML页面和35个CSS样式文件同时附带4个SQL数据库脚本以及多张PNG、JPG、GIF图片素材覆盖从数据表初始化到页面展示的完整开发链路。项目已获导师指导并通过97分代码结构清晰、运行配置齐全下载后可直接导入数据库并启动服务无需修改即可作为课程设计或期末大作业提交。目前已有247人学习/下载适合希望参照高分范例、快速完成同类宿舍管理系统设计与实现的开发者。1. 用 Django 做学生宿舍管理系统为什么是课程设计的“最优解”如果你正在为“Python基于Django学生宿舍管理系统”这个题目发愁说明你已经踩到了课程设计最典型的坑题目看起来不难但真正动手时发现增删改查谁都会写可“宿舍分配”“调宿”“退宿”这些业务动作一旦叠加代码就开始失控。这个题目之所以被反复选为高分课程设计是因为它天然包含一对多宿舍对学生、多对多学生与入住记录关系还涉及事务边界——这几样正好是 Django ORM 最擅长、也最值得写进答辩词的能力展示点。与其在网上找一份来源不明的压缩包不如顺着标题里的“源码数据库”两个关键词把这一套从模型设计到数据库交付完整打通。这篇文章就是把你把从零实现这个系统时最该写对的部分拆开讲透适合正在做课程设计的学生也适合想快速搭一个后台管理 Demo 的开发者。2. 先把宿舍系统的数据模型定清楚从学生、宿舍到入住记录的 3 张核心表2.1 模型设计用“入住记录”闭合多对多关系而不是让宿舍表直接挂学生很多初学者写宿舍管理系统会直接给Dormitory表加一个students多对多字段或者给Student表加一个dormitory外键。这两种设计在演示时“看起来能用”但一遇到“查某学生住过哪些宿舍”就写不出干净 SQL。合理的做法是引入一张独立的入住记录表把学生和宿舍之间的关联变成“历史轨迹”而不是“当前状态”。下面是核心模型代码我用的是 Django 默认的sqlite3起步后面要切 MySQL 也只需改settings.pyfrom django.db import models from django.core.exceptions import ValidationError class Building(models.Model): 宿舍楼栋 name models.CharField(楼栋名称, max_length50, uniqueTrue) manager models.CharField(管理员, max_length30, blankTrue) class Meta: db_table dorm_building verbose_name 楼栋 verbose_name_plural verbose_name def __str__(self): return self.name class Dormitory(models.Model): 具体宿舍房间 building models.ForeignKey(Building, on_deletemodels.CASCADE, related_namedormitories, verbose_name所属楼栋) room_no models.CharField(房间号, max_length20) capacity models.PositiveIntegerField(容纳人数, default4) current_count models.PositiveIntegerField(当前人数, default0) class Meta: db_table dorm_dormitory unique_together (building, room_no) verbose_name 宿舍 def __str__(self): return f{self.building.name}-{self.room_no} def clean(self): if self.current_count self.capacity: raise ValidationError(当前人数不能超过容纳人数) class Student(models.Model): 学生基础信息 sno models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length30) gender models.CharField(性别, max_length2, choices((M, 男), (F, 女))) phone models.CharField(联系电话, max_length11, blankTrue) enroll_date models.DateField(入学时间, auto_now_addTrue) class Meta: db_table dorm_student verbose_name 学生 def __str__(self): return f{self.sno}-{self.name} class CheckInRecord(models.Model): 入住记录学生与宿舍之间的历史关联 student models.ForeignKey(Student, on_deletemodels.CASCADE, related_namecheckin_records, verbose_name学生) dormitory models.ForeignKey(Dormitory, on_deletemodels.CASCADE, related_namecheckin_records, verbose_name宿舍) checkin_date models.DateField(入住日期, auto_now_addTrue) checkout_date models.DateField(退宿日期, nullTrue, blankTrue) class Meta: db_table dorm_checkin_record ordering [-checkin_date] verbose_name 入住记录 def __str__(self): return f{self.student} 住 {self.dormitory}这段代码的逻辑核心在CheckInRecord。学生入住时插入一条记录退宿时只更新checkout_date而不删除数据。这样做有一个立竿见影的好处你可以随时统计“某宿舍历史上住过哪些人”也能统计“当前实际入住人数 入住记录中 checkout_date 为空的条数”。Dormitory.current_count是冗余字段它存在的意义是让宿舍列表页不用每次都聚合统计入住记录。冗余字段在课程设计里不算坏味道只要记得在入住和退宿两个入口同步维护它。2.2 迁移与初始数据写 fixture 代替手动录数据省下答辩前的准备时间模型写完后执行迁移python manage.py makemigrations python manage.py migratemakemigrations之后一定要看一眼生成的迁移文件确认每张表的字段类型是否符合预期。常见错误是把PositiveIntegerField写成IntegerField导致容量出现负数时数据库并不报错。初始数据建议用 Django fixture 来灌而不是打开 admin 一条条录入。在应用目录下新建fixtures/initial_data.json内容示例[ { model: dorm.building, pk: 1, fields: {name: 梅园1栋, manager: 张阿姨} }, { model: dorm.dormitory, pk: 1, fields: {building: 1, room_no: 101, capacity: 4, current_count: 0} }, { model: dorm.student, pk: 1, fields: {sno: 20240001, name: 李明, gender: M, phone: 13800000000} } ]然后执行python manage.py loaddata initial_data.json执行后注意观察终端输出Django 会提示加载了多少条记录。如果报DeserializationError通常是 fixture 里写错了model路径检查应用名是否与INSTALLED_APPS中注册的名称一致。3. 路由与视图把“分配宿舍”和“调宿/退宿”这两个硬骨头实现掉3.1 URL 分层把业务动作拆成资源化路由而不是堆砌 request.GET宿舍管理系统最常见的演示操作是“给新生分配宿舍”和“学生调宿”。很多人的实现方式是在模板里写多个a标签每个 href 带不同的?actionxxx视图里用if判断分发。这种做法能跑但 URL 既不 RESTful也不利于你在答辩时解释设计。更清晰的做法是把动作映射到不同的 URL 和视图函数# dorm/urls.py from django.urls import path from . import views app_name dorm urlpatterns [ path(students/, views.student_list, namestudent_list), path(students/str:sno/checkin/, views.checkin_view, namecheckin), path(students/str:sno/move/, views.move_dorm_view, namemove_dorm), path(students/str:sno/checkout/, views.checkout_view, namecheckout), path(dormitories/, views.dormitory_list, namedormitory_list), ]注意checkin、move、checkout这三个动作都隶属于某个学生。把学号放在 URL 路径里而不是放在查询字符串里好处有两点一是 URL 语义明确二是视图函数可以直接通过参数拿到学号不需要手动解析。3.2 视图函数用事务把“分配计数”包裹起来防止数据不一致分配宿舍的过程实际涉及三步创建入住记录、把宿舍current_count加 1、把学生的状态更新为“已入住”。这三步必须在一个数据库事务里完成否则中途报错就会出现“记录建了但人数没加”的脏数据。from django.db import transaction from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages from .models import Student, Dormitory, CheckInRecord transaction.atomic def checkin_view(request, sno): student get_object_or_404(Student, snosno) if request.method POST: dorm_id request.POST.get(dormitory_id) dorm get_object_or_404(Dormitory, pkdorm_id) # 判断宿舍是否已满 if dorm.current_count dorm.capacity: messages.error(request, f宿舍 {dorm.room_no} 已满员) return redirect(dorm:checkin, snosno) # 判断学生是否已有未退宿记录 active_record CheckInRecord.objects.filter( studentstudent, checkout_date__isnullTrue ).first() if active_record: messages.warning(request, 该学生当前已在住不能重复分配) return redirect(dorm:student_list) CheckInRecord.objects.create(studentstudent, dormitorydorm) dorm.current_count 1 dorm.save(update_fields[current_count]) messages.success(request, f{student.name} 已分配到 {dorm.building.name}-{dorm.room_no}) return redirect(dorm:student_list) available_dorms Dormitory.objects.filter( current_count__ltmodels.F(capacity) ) return render(request, dorm/checkin.html, { student: student, available_dorms: available_dorms, })参数说明transaction.atomic装饰器保证视图内所有数据库写操作要么全部提交要么全部回滚。这是“分配宿舍”这类多步写操作的基本保障。request.POST.get(dormitory_id)拿的是模板表单里提交的宿舍主键对应Dormitory模型的主键。查询可用宿舍时用了models.F(capacity)表达式它的作用是让current_count capacity这个比较在数据库端完成而不是把数据全部取到 Python 内存里再判断。当宿舍表数据量上千时这个写法能明显减少内存占用。dorm.save(update_fields[current_count])指定只更新当前人数字段避免触发整行保存也减少不必要的数据库写入。3.3 调宿的边界条件先退旧再住新还是一个事务里完成调宿的逻辑更值得细想。学生从 A 宿舍搬到 B 宿舍本质上是把原入住记录的checkout_date设置为今天再新建一条入住记录。这两个操作加在一起仍然是一个事务。transaction.atomic def move_dorm_view(request, sno): student get_object_or_404(Student, snosno) if request.method POST: new_dorm_id request.POST.get(new_dormitory_id) new_dorm get_object_or_404(Dormitory, pknew_dorm_id) active_record CheckInRecord.objects.select_for_update().filter( studentstudent, checkout_date__isnullTrue ).first() if not active_record: messages.error(request, 该学生当前没有在住记录) return redirect(dorm:student_list) old_dorm active_record.dormitory # 新宿舍满员判断 if new_dorm.current_count new_dorm.capacity: messages.error(request, 目标宿舍已满员) return redirect(dorm:move_dorm, snosno) # 旧宿舍人数减一新宿舍人数加一 old_dorm.current_count - 1 old_dorm.save(update_fields[current_count]) active_record.checkout_date models.DateField.today() active_record.save(update_fields[checkout_date]) new_dorm.current_count 1 new_dorm.save(update_fields[current_count]) CheckInRecord.objects.create(studentstudent, dormitorynew_dorm) messages.success(request, f{student.name} 已从 {old_dorm.room_no} 调至 {new_dorm.room_no}) return redirect(dorm:student_list) available_dorms Dormitory.objects.exclude( pk__inCheckInRecord.objects.filter( studentstudent, checkout_date__isnullTrue ).values_list(dormitory_id, flatTrue) ).filter(current_count__ltmodels.F(capacity)) return render(request, dorm/move.html, { student: student, available_dorms: available_dorms, })这里有一个容易被忽略的细节在更新旧宿舍人数时直接用old_dorm.current_count - 1。如果旧宿舍人数已经为 0这个操作会把人数变成负数。因此更稳妥的方式是在事务里再查一次旧宿舍的当前人数if old_dorm.current_count 0: messages.error(request, 宿舍人数数据异常) return redirect(dorm:student_list)这个判断在演示时可能永远不会触发但写在答辩代码里评委能看出你考虑过数据异常场景。3.4 查询效率select_related 在宿舍列表页的实战价值宿舍列表页通常需要展示“楼栋-房间-当前人数-剩余床位-含入住学生列表”如果不用select_related或prefetch_related每渲染一个宿舍就会触发一次额外的数据库查询这就是著名的 N1 问题。def dormitory_list(request): dorm_list Dormitory.objects.select_related(building).prefetch_related( checkin_records__student ).all() return render(request, dorm/dormitory_list.html, {dorm_list: dorm_list})逻辑说明select_related(building)用于 ForeignKey 关系的联表查询会在一条 SQL 里通过JOIN把楼栋信息查出来。prefetch_related(checkin_records__student)用于反向关联和多层关联Django 会先查宿舍列表再查所有相关入住记录最后查入住记录里的学生信息总共 3 条 SQL而不是 N1 条。这个优化在只有几百条宿舍数据时看不出差别但答辩时如果被问到“查询性能如何优化”这段代码就是你能拿出的证据。4. Admin 后台与模板层把 Django 自带后台改造成课程设计的加分项4.1 定制 Admin 列表页list_display 与 list_filter 的配置方法Django Admin 是课程设计里性价比最高的部分。很多同学只会在admin.py里写一行admin.site.register(Student)这样后台列表只显示模型的__str__返回值既不直观也浪费了 Django 自带的搜索、筛选能力。# dorm/admin.py from django.contrib import admin from .models import Building, Dormitory, Student, CheckInRecord admin.register(Student) class StudentAdmin(admin.ModelAdmin): list_display (sno, name, gender, phone, current_dorm) list_filter (gender, checkin_records__dormitory__building) search_fields (sno, name) list_per_page 20 def current_dorm(self, obj): record obj.checkin_records.filter(checkout_date__isnullTrue).first() return record.dormitory if record else 未入住 current_dorm.short_description 当前宿舍 admin.register(Dormitory) class DormitoryAdmin(admin.ModelAdmin): list_display (building, room_no, capacity, current_count) list_filter (building,) search_fields (room_no,) admin.register(CheckInRecord) class CheckInRecordAdmin(admin.ModelAdmin): list_display (student, dormitory, checkin_date, checkout_date) list_filter (checkin_date, dormitory__building)参数说明list_display里的字段可以是模型字段也可以是自定义方法。current_dorm方法在列表页会逐行查询该学生的入住记录数据量大时有性能损耗但在课程设计这种几十条数据的场景下完全够用。list_filter支持跨表字段写法是在关联字段后用双下划线。这里按宿舍所属楼栋筛选学生后台会自动生成下拉选项。search_fields指定的sno和name会被 Django 转成 SQL 里的LIKE查询适合“知道学号但不知道叫什么”的检索场景。list_per_page控制分页大小后台数据超过 20 条时会自动分页。4.2 自定义 Admin 动作给宿舍管理员做一个“一键调宿”操作Admin 自带的删除、修改操作对宿舍管理场景不够用。你可以注册一个自定义 action让管理员在后台勾选多个学生批量把他们调到指定宿舍。from django.contrib import admin, messages from django.shortcuts import redirect from .models import Student, Dormitory admin.action(description将选中的学生批量调宿) def batch_move_student(modeladmin, request, queryset): # 这里只做中间页跳转真正的调宿逻辑在 move_confirm 视图里 selected_ids queryset.values_list(id, flatTrue) request.session[batch_student_ids] list(selected_ids) return redirect(/admin/dorm/batch-move/) admin.register(Student) class StudentAdmin(admin.ModelAdmin): actions [batch_move_student]逻辑说明admin.action装饰器是 Django 3.2 之后的推荐写法比旧版actions [batch_move_student]多了一个描述字段。这里的实现思路是先选中学生跳转到自定义的批量调宿页面在那个页面选择目标宿舍后统一处理。你也可以把调宿动作直接放在 action 函数里完成但那样需要额外处理“目标宿舍不足”“学生没有在住记录”等异常交互体验不如中间页友好。4.3 前端模板模板继承与表单回显让页面代码别写成一张“大饼”课程设计的模板层不需要多华丽但结构必须清晰。最忌讳的是每个页面都从头写一套 HTML公共的导航栏、CSS 引用、JS 引用重复粘贴。Django 的模板继承机制就是为这个场景设计的。!-- templates/base.html -- !DOCTYPE html html langzh-cn head meta charsetUTF-8 title{% block title %}宿舍管理系统{% endblock %}/title link relstylesheet hrefhttps://cdn.staticfile.org/bootstrap/5.3.0/css/bootstrap.min.css /head body nav classnavbar navbar-expand-lg navbar-dark bg-dark div classcontainer-fluid a classnavbar-brand href{% url dorm:student_list %}宿舍管理系统/a div classcollapse navbar-collapse ul classnavbar-nav li classnav-itema classnav-link href{% url dorm:student_list %}学生列表/a/li li classnav-itema classnav-link href{% url dorm:dormitory_list %}宿舍列表/a/li /ul /div /div /nav div classcontainer mt-3 {% if messages %} {% for msg in messages %} div classalert alert-{{ msg.tags }}{{ msg }}/div {% endfor %} {% endif %} {% block content %}{% endblock %} /div /body /html子模板只需要重写content和title两个 block!-- templates/dorm/student_list.html -- {% extends base.html %} {% block title %}学生列表{% endblock %} {% block content %} table classtable table-striped thead tr th学号/thth姓名/thth性别/thth当前宿舍/thth操作/th /tr /thead tbody {% for stu in student_list %} tr td{{ stu.sno }}/td td{{ stu.name }}/td td{{ stu.get_gender_display }}/td td {% for rec in stu.checkin_records.all %} {% if not rec.checkout_date %}{{ rec.dormitory }}{% endif %} {% empty %}未入住{% endfor %} /td td a href{% url dorm:checkin stu.sno %}分配/a a href{% url dorm:move_dorm stu.sno %}调宿/a a href{% url dorm:checkout stu.sno %}退宿/a /td /tr {% endfor %} /tbody /table {% endblock %}提示模板里的stu.get_gender_display是 Django 为带有choices选项的字段自动生成的方法用来显示“男/女”而不是存储的“M/F”。如果你的模型字段没写choices这个方法不存在。5. 部署前的验证与交付从 SQLite 迁移到 MySQL 的配置对照与 3 个排错点5.1 用 Django 自带测试客户端做一遍全流程验收不要等到答辩前才手动点页面验证。Django 自带的Client可以模拟 GET 和 POST 请求帮你把“分配宿舍→调宿→退宿”整个流程跑一遍。# dorm/tests.py from django.test import TestCase from .models import Student, Dormitory, CheckInRecord, Building class CheckInFlowTest(TestCase): def setUp(self): self.building Building.objects.create(name梅园2栋) self.dorm_a Dormitory.objects.create( buildingself.building, room_no201, capacity2, current_count0) self.dorm_b Dormitory.objects.create( buildingself.building, room_no202, capacity2, current_count0) self.stu Student.objects.create( sno20240002, name王芳, genderF) def test_checkin_then_move_then_checkout(self): client self.client # 入住宿舍 A resp client.post(f/dorm/students/{self.stu.sno}/checkin/, {dormitory_id: self.dorm_a.id}) self.assertEqual(resp.status_code, 302) record CheckInRecord.objects.get(studentself.stu, checkout_date__isnullTrue) self.assertEqual(record.dormitory, self.dorm_a) self.dorm_a.refresh_from_db() self.assertEqual(self.dorm_a.current_count, 1) # 调宿到 B resp client.post(f/dorm/students/{self.stu.sno}/move/, {new_dormitory_id: self.dorm_b.id}) self.assertEqual(resp.status_code, 302) self.dorm_a.refresh_from_db() self.dorm_b.refresh_from_db() self.assertEqual(self.dorm_a.current_count, 0) self.assertEqual(self.dorm_b.current_count, 1) # 退宿 resp client.post(f/dorm/students/{self.stu.sno}/checkout/) self.assertEqual(resp.status_code, 302) self.dorm_b.refresh_from_db() self.assertEqual(self.dorm_b.current_count, 0)运行命令python manage.py test dorm逻辑说明测试里使用refresh_from_db()是为了重新读取数据库中的最新值因为 ORM 对象在内存中可能缓存了旧数据。如果这个测试跑完所有断言都通过说明核心业务链路没有致命问题。5.2 从 SQLite 迁移到 MySQLsettings.py 的 DATABASES 配置对照课程设计提交时老师通常会要求提供“源码数据库”。SQLite 数据库是单文件交作业很方便但有些老师会指定要求 MySQL。迁移时主要改三处。sqlite3配置DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }MySQL配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: dorm_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }改完配置后需要重新生成迁移并建表。注意不要把 SQLite 里的数据直接导入 MySQL因为 SQLite 的auto_now_add日期字段、布尔字段在两种数据库中的存储方式不同。正确做法是先python manage.py migrate在 MySQL 中建表再通过 admin 或脚本重新录入数据。5.3 三个最容易在答辩现场翻车的配置问题第一个问题是静态文件 404。Django 默认在DEBUGFalse时不再托管静态文件如果你没有配置whitenoise或 nginx页面 CSS 会全部失效。课程设计答辩时一般不需要真正部署到服务器所以直接把DEBUG保持为True即可但如果老师要求模拟生产环境就在settings.py里加STATIC_ROOT BASE_DIR / staticfiles然后执行python manage.py collectstatic把所有静态文件收集到staticfiles目录。第二个问题是 CSRF 验证失败。如果你在模板里写了form methodpost但忘了加{% csrf_token %}提交时会报 403。这个问题几乎每个用 Django 做课设的人都会遇到排查方法很简单打开浏览器的开发者工具看响应页面里有没有csrftoken这个 Cookie如果没有说明中间件配置有问题。第三个问题是时区导致的时间错乱。Django 默认的USE_TZTrue会把所有时间按 UTC 存储。如果你在模板里显示“入住日期”会发现比本地时间慢 8 小时。处理方式TIME_ZONE Asia/Shanghai USE_TZ False注意USE_TZFalse是全局关闭时区支持对于纯展示型的管理系统完全够用。如果你的模型里用了auto_now_addTrue关闭时区后这些字段会直接使用本地时间。5.4 交付压缩包时固定依赖版本避免老师本机跑不起来标题里写着“源码数据库高分课程设计.zip”交付时最尴尬的情况是老师双击manage.py runserver直接报错原因是本机 Django 版本和你开发时用的不一样。稳妥的做法是在项目根目录放一个requirements.txt固定版本pip freeze requirements.txt但pip freeze会把所有环境包都打进去太冗余。手动写出关键依赖即可Django4.2,5.0 mysqlclient2.2 pytz然后在 README 中写清启动步骤创建虚拟环境、安装依赖、迁移数据库、loaddata导入初始数据、运行runserver。整个验证过程用刚才的tests.py收尾如果测试能通过说明这份压缩包在老师机器上大概率也能跑通。最后多叮嘱一句这个项目的核心是“入住记录”这个模型设计以及围绕它的事务处理。你把这两点讲透了比在页面上堆十个 CSS 动画更能拿到高分。本文还有配套的精品资源点击获取
返回列表