Django通用排课系统实战:从数据模型到自动排课算法

发布时间:2026/10/6 17:33:38
Django通用排课系统实战:从数据模型到自动排课算法 简介面向高校、中学及培训机构教务管理场景的通用排课系统源码包基于Python与Django框架开发围绕自动排课、手动调整、课表生成等核心环节设计适合毕业设计选题、教学实践或在此基础上进行二次开发。压缩包共276个文件大小约35.69MB主要包含Python源码和pyc编译文件构成的后端逻辑HTML、CSS与JavaScript实现的前端界面SQL数据库脚本以及GIF图片演示素材和PDF、DOC、XLSX等技术文档与表格样例目录模块划分清晰便于按功能查阅和部署。已有56人学习系统同时提供操作手册与开发文档能帮助使用者快速上手。通过源码可以学习Django的MTV设计模式、排课约束处理算法和可视化课表生成思路结合前端模板、数据库样例与静态资源可支撑从环境搭建、功能调试到二次扩展的完整流程对教务人员、开发者和毕业生而言都是一份兼具实用性与参考价值的排课项目样例。1. 用 Django 做通用排课系统它到底解决了什么谁适合拿它起步排课这件事本质上是一个带硬约束的调度问题一门课要有老师、要有时段、要有教室三者不能冲突还要避开老师不能用、教室已被占这类限制。手动排课在小规模下勉强能忍一旦课程数量超过 50 门冲突排查的时间成本会指数级上升。python270通用排课系统(django)这个项目标题指的就是基于 Python 的 Django 框架实现的、包含课程管理、教师管理、教室管理、自动排课与冲突检测的通用排课系统。它不算一个科研级调度引擎而是一个适合二次开发的教学管理方案用 Django 的 ORM 做数据建模用算法做基础排课再靠人工调整收尾。适合三类人正在学 Django 想做完整项目的开发者、学校或培训机构需要内部排课工具的老师、以及想快速理解「调度类系统怎么落地」的转行者。这个系统的价值在于给了你一套能跑的骨架你只需要替换数据模型、调整约束条件就能接到自己的场景里。我把这套系统的核心逻辑和复现路径拆成六步讲清楚数据模型、排课算法、冲突检测、通用功能、踩坑记录、部署与二次开发。每一步你都能照着落地代码可以直接抄参数怎么改、坑在哪我会逐条说明。2. 先把数据模型立住Django 排课系统的表设计与关系拆解2.1 为什么用 Django 而不是 Flask 或 FastAPI 做排课系统排课系统的瓶颈不在接口性能而在数据关系的复杂度和事务一致性。一个排课动作要同时检查教师表、教室表、课程表、时段表的占用状态Flask 这类微框架需要你自己拼 SQL 和事务FastAPI 虽然有 SQLAlchemy 兜底但 admin 后台、ORM 迁移、内置的权限系统都得零散搭。Django 自带 admin 后台、migrate 迁移机制和基于对象的 ORM这让「先建表、再跑通、后加权限」的开发路径非常顺。我在实际使用中的选型理由是Django 的 ORM 查询在排课场景里有天然优势。比如「找某个时段所有教师的时间安排」用TimeSlot.objects.filter(periodperiod.id).select_related(teacher)一次查询就能拿到完整对象图不需要写联表 SQL。Django 的信号机制还能在教师被删除时自动清理排课记录这种兜底逻辑在手动管理后台时特别重要。如果你只需要一个排 API 的服务Flask 够用但排课系统本质是「管理后台 调度逻辑」双核心这是 Django 的主场。2.2 五张核心表课程、教师、教室、时段、排课结果排课系统的数据模型不需要过度设计五张表基本覆盖全部业务场景。from django.db import models class Teacher(models.Model): name models.CharField(max_length50, verbose_name教师姓名) title models.CharField(max_length50, blankTrue, verbose_name职称) max_lessons_per_day models.IntegerField(default4, verbose_name每日最大课时数) class Meta: db_table teacher class Course(models.Model): name models.CharField(max_length100, verbose_name课程名称) code models.CharField(max_length20, uniqueTrue, verbose_name课程编号) credit models.FloatField(default1.0, verbose_name学分) teacher models.ForeignKey(Teacher, on_deletemodels.PROTECT, verbose_name授课教师) requires_capacity models.IntegerField(default30, verbose_name最小教室容量) class Meta: db_table course class Classroom(models.Model): room_number models.CharField(max_length20, uniqueTrue, verbose_name教室编号) capacity models.IntegerField(default60, verbose_name容量) has_projector models.BooleanField(defaultTrue, verbose_name是否有投影) class Meta: db_table classroom class TimeSlot(models.Model): weekday models.IntegerField(choices[(1, 周一), (2, 周二), (3, 周三), (4, 周四), (5, 周五), (6, 周六), (7, 周日)], verbose_name星期) start_time models.TimeField(verbose_name开始时间) end_time models.TimeField(verbose_name结束时间) is_available models.BooleanField(defaultTrue, verbose_name是否可用) class Meta: unique_together (weekday, start_time, end_time) db_table time_slot class Schedule(models.Model): course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) classroom models.ForeignKey(Classroom, on_deletemodels.CASCADE, verbose_name教室) time_slot models.ForeignKey(TimeSlot, on_deletemodels.CASCADE, verbose_name时段) is_fixed models.BooleanField(defaultFalse, verbose_name是否人工锁定) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: unique_together (classroom, time_slot) db_table schedule逻辑说明组织层级是「课程选教师」——每个课程对外键绑定了一个教师这个设计让你能直接在 Course 上拿到教师与课程的关联在执行排课时不需要联查。Schedule表是排课结果的核心存储的是「课程 × 教室 × 时段」的三元组。两张表的索引与约束是关键TimeSlot用unique_together防止重复时段Schedule用unique_together保证同一个教室同一个时间只能安排一门课。含义需要说清楚。unique_together看起来只是保证数据完整性它真正的作用是给排课算法提供了「原子约束」——你在调用save()时如果违反了这个约束Django 会抛IntegrityError这比在业务层手写大量判断逻辑要可靠得多。on_deletemodels.PROTECT的选择也是有意为之课程表中的教师被删除时必须手动先处理相关课程避免排课结果中出现孤立数据。实际操作时这些字段名称建议保持不变因为后续我会讲的三个模块都直接依赖这五张表的结构。2.3 用 Django migration 建表从模型到 MySQL 的落库流程数据模型定义好之后migration 是连接 Django 模型与数据库的关键步骤。很多新手在这里会掉进「迁移文件不生效」的坑命令顺序和参数值得单独说。# 在项目根目录执行生成表结构的迁移文件 python manage.py makemigrations scheduler # 查看生成的 SQL 语句确认表结构与约束是否正确 python manage.py sqlmigrate scheduler 0001 # 执行迁移真正建表 python manage.py migrate参数与排查说明makemigrations后面的scheduler是 app 的名字如果不写它Django 会扫描所有 app在项目里有多个 app 时容易产生脏迁移。sqlmigrate是一个验证步骤它不会写入数据库只打印要执行的 SQL建表前必须跑这一步检查外键和约束。migrate执行后如果提示Table already exists那说明你之前手动建过表或迁移文件有冲突解决方法是删除对应的 migration 文件不是数据库表重新执行前两步。一个使用经验是Django 默认的迁移逻辑会把外键名自动拼接成不规则字符串这会让 DBA 审查困难。如果想生成可读性高的外键约束名需要在 Model 的 Meta 类中显式指定db_constraint命名规则不过中小规模项目这一条可以跳过。开发环境建议直接用 SQLite 起步正式部署时切到 MySQL——Django 的 ORM 层屏蔽了两种数据库的差异你唯一需要注意的坑是 MySQL 默认字符集必须是 utf8mb4否则中文课程名会存不进去。3. 排课核心算法贪心分组与时槽分配的实现要点3.1 排课问题的本质约束教师、教室、时间的三维互撞排课问题在数学上可以抽象为三维匹配问题Course要占用一个TimeSlot同时要占用一个Classroom教师本身在一周内的可用时间也是受限的。判断一次排课是否合法等价于在三个集合上验证没有元素冲突。这个问题的暴力解法是三层循环穷举组合复杂度是O(课程数 × 时段数 × 教室数)50 门课 × 40 个时段 × 10 间教室就是 2 万次匹配看起来不大但如果每次匹配都要查数据库实际耗时会非常难看。所以排课系统不可以天真地做逐条试探。我推荐的做法是贪心分组 预加载时段表把数据库交互压缩到最少。算法思路分三步第一步把所有课程按学分和教师可用时间分组第二步为每门课生成候选时段列表第三步按顺序分配教室。3.2 自动排课脚本一次跑完所有课程分配from django.core.management.base import BaseCommand from django.db.models import Q from scheduler.models import Course, Teacher, Classroom, TimeSlot, Schedule class Command(BaseCommand): help 自动排课按教师工作量与教室容量分配课程 def handle(self, *args, **options): time_slots list(TimeSlot.objects.filter(is_availableTrue) .order_by(weekday, start_time)) classrooms list(Classroom.objects.all() .order_by(capacity)) # 建立两张占用表用于快速判断冲突 schedule_occupied set() teacher_workload {} for course in Course.objects.select_related(teacher).all(): teacher course.teacher # 只选择这个教师尚未占用的时段 for slot in time_slots: if teacher_workload.get((teacher.id, slot.weekday), 0) teacher.max_lessons_per_day: continue if (slot.id, any_classroom_id) in schedule_occupied: continue # 从大到小选择第一个容量足够的教室 assigned_classroom None for room in classrooms: if room.capacity course.requires_capacity and (room.id, slot.id) not in schedule_occupied: assigned_classroom room break if assigned_classroom: Schedule.objects.create( coursecourse, classroomassigned_classroom, time_slotslot ) schedule_occupied.add((assigned_classroom.id, slot.id)) teacher_workload[(teacher.id, slot.weekday)] \ teacher_workload.get((teacher.id, slot.weekday), 0) 1 break这段脚本的核心是内存中的两个集合schedule_occupied和teacher_workload。前者存的是(教室, 时段)二元组后者存的是(教师, 星期)每日课时数。每次分配前先查这两个集合避免了反复请求数据库做冲突判断这是性能的关键。代码里有一个细节值得注意select_related(teacher)在查询课程时就把教师信息通过 SQL JOIN 一起取出如果漏掉这一步循环内部访问course.teacher会引发 N1 查询50 门课就多出 50 次 SQL。另一个可以调整的参数是order_by(capacity)让教室按容量升序配合 course.requires_capacity的条件取第一个够用的这是典型的贪心策略——小教室优先分配大教室留给后续大班课。这个脚本适合的场景是学期初的首次排课它能保证「不冲突」但不保证「最优」。比如它不考虑同一个班级的课程尽量均匀分布在五天也不考虑教师希望上午或下午上课的习惯。所以它的定位是「初始化 人工微调」而不是一次到位。3.3 手动调整与数据回填执行排课命令后需要处理什么跑完自动排课后直接去 Django admin 后台查看 Schedule 表会发现数据已经写进去了但这个阶段还不能直接定稿。我建议执行完自动排课后立刻跑一次冲突检测脚本用 SQL 分组查询核对有没有并发插入导致的重复记录。还有一个在实际场景里很容易出现的问题后台手动新增一条 Schedule 时教师工作量约束不会自动校验所以管理员必须在教师详情页看到「本周课表」和「每日课时数」。更好的做法是给 Schedule 表加一个save()方法复写在保存前做一次合法性校验而不是依赖 app 层的自觉。对于简单的管理员后台我一般会生成一个只读的「课表视图」——按星期 × 节次分组展示已经排好的课程。这一步可以人工微调冲突后再回到后台确认状态排课结果才算真正生效。4. 冲突检测与约束校验让排课系统「守规矩」4.1 三种核心冲突的高效检测写法排课系统里说的「冲突」落到代码里无非三种情况同一教室同一时段被分配两次、同一教师同一时段被分配两次、同一个班级/课程组同一时段被安排了两门课。前一章的自动排课在内存层面已经拦住了前两种但手动在 admin 后台插入数据、或导入历史数据时这三种冲突都会出现。所以我单独写了一个检测脚本放在 Django 管理命令里周期执行。from django.core.management.base import BaseCommand from django.db.models import Count, Q from scheduler.models import Schedule, Classroom, TimeSlot class Command(BaseCommand): help 检测排课冲突教室冲突、教师冲突、课程冲突 def handle(self, *args, **options): # 1. 教室冲突同一教室 同一时段 出现多次 room_conflicts (Schedule.objects .values(classroom_id, time_slot_id) .annotate(cntCount(id)) .filter(cnt__gt1)) # 2. 教师冲突课程关联了同一个老师且时段相同 teacher_conflicts (Schedule.objects .values(course__teacher_id, time_slot_id) .annotate(cntCount(id)) .filter(cnt__gt1)) # 3. 排课结果时段重叠开始和结束时间跨区段重叠 slots list(TimeSlot.objects.all().order_by(weekday, start_time)) for i in range(len(slots) - 1): if slots[i].weekday slots[i1].weekday: if slots[i].end_time slots[i1].start_time: self.stdout.write(self.style.WARNING( f时段重叠: {slots[i]} 与 {slots[i1]} ))逻辑说明前两个查询利用 Django ORM 的annotatefilter(cnt__gt1)组合直接由数据库完成分组计数比 Python 层双层循环判断快一个数量级。第三个查询解决的是「前一个时段没结束、后一个时段已开始」的重叠问题这种数据往往来自手工录入不规范的时段表。常见的误用是直接在values()里写course__teacher_id却拿不到结果——这是因为你没有先select_related那层关系编译器不会自动联表。正确写法是values(course__teacher_id)本身会触发 JOIN这个不用额外操作。跑完脚本后输出警告再在 admin 后台手动修正即可。4.2 硬约束与软约束什么冲突必须禁什么冲突允许存在上一个脚本能查出所有冲突但查出之后要「怎么处理」才是考验业务理解的地方。排课系统的约束分为硬约束和软约束。硬约束必须为 False包括教室同一时段不能上两门课、同一班级同一时段不能并行上课。软约束是可以容忍但应该尽量避免的典型有课程安排不要太集中、同一个班级尽量少出现一天满六节课的情况、课程安排在教师指定的不授课时间段外。在自动排课算法里我只会用硬约束做过滤软约束留给人工微调阶段。因为引入过多的软约束会让贪心算法变成背包问题复杂度上升但收益有限。一个务实的做法是算法跑完后在报表页展示「每日课时分布」和「教室使用率」管理员用肉眼判断哪些地方需要手动拖动这比让算法去平衡所有指标更可控。4.3 用「时段矩阵」可视化课表把冲突检测结果直接展示出来光有检测脚本不够管理人员需要一个直观的课表视图。Django admin 默认以列表展示数据不适合课表场景。我建议生成一张二维表格行是时段周一至周五第 1 节到第 8 节列是教室单元格是课程名。后端用字典临时构建数据前端模板渲染成 HTML table 即可。from django.shortcuts import render from scheduler.models import Schedule, TimeSlot, Classroom def schedule_matrix(request): slots list(TimeSlot.objects.order_by(weekday, start_time)) rooms list(Classroom.objects.order_by(room_number)) # 建立直接索引key 是 (教室id, 时段id)value 是课程名 matrix {} for sch in Schedule.objects.select_related(course, classroom, time_slot): matrix[(sch.classroom_id, sch.time_slot_id)] sch.course.name grid [] for slot in slots: row {slot: slot, cells: [matrix.get((room.id, slot.id), ) for room in rooms]} grid.append(row) return render(request, scheduler/matrix.html, { grid: grid, rooms: rooms, })逻辑说明select_related(course, classroom, time_slot)一次取出全部所需关联对象避免模板渲染阶段逐行查库。matrix字典让查找从遍历变成 O(1)这种「先建索引再渲染」的模式在处理表格类页面时非常通用。前端的matrix.html只需要两层循环外层遍历 grid 生成行内层遍历 room 列表生成列。单元格为空说明该时段该教室空闲。实际效果是管理员日常排课基本不再打开课程列表而是在这个课表矩阵上直接判断冲突。如果某个单元格里出现了两门课说明数据库里查出了冲突矩阵页面会高亮这个区域点进去就能看到是哪两条记录。5. 排课系统避坑指南环境、版本、数据迁移里翻过的车5.1 Python 与 Django 版本不匹配迁移一跑就报错现象manage.py migrate执行时报ImportError: cannot import name ugettext_lazy。原因Django 3.0 开始把ugettext_lazy改名为gettext_lazy网上很多排课系统的老代码用的是旧包名直接拿 Python 3.8 配 Django 4.x 跑就会翻车。解决要么把 Django 降到 2.2 LTS要么全局把ugettext_lazy替换为gettext_lazy。我的经验是用后者因为新版 Django 才能兼容新版 Python 和 MySQL 8 的认证插件。5.2 中文课程名入库变乱码现象教室名称和课程名称在 admin 后台显示为????。原因MySQL 数据库或数据表字符集是 latin1不是 utf8mb4。Django 的settings.py里DATABASES配置了OPTIONS: {charset: utf8mb4}但 MySQL 表在早期用 Navicat 手动创建时就已经用了错误字符集。解决执行ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;对数据表做转换。这类问题在 MySQL 8.0 默认 utf8mb4 之后不再出现如果你用的是 MySQL 5.7务必在上线前检查一遍全表字符集。5.3 同一时段同一教室插入了两条排课记录现象数据库中出现了同教室同时段的两条 Schedule且都显示正常。原因unique_together约束只对单条save()操作生效如果数据是通过bulk_create()批量插入ORM 会绕过部分完整性检查另外两个管理员同时打开后台点保存也会产生并发间隙。解决在 Django 层加with transaction.atomic():包裹写入操作同时用get_or_create替代create。我这里强烈建议在Schedule.save()方法里重写一个存在性检查宁可让系统变慢也不要让脏数据进入数据库。5.4 教师工作量统计虚高周一显示 6 节课现象同一个教师在周一被排了 6 节课超过了max_lessons_per_day4的限制。原因自动排课脚本里teacher_workload只在当前运行进程内维护重启命令或手动在 admin 新增排课后这个内存变量就失效了。解决在 Schedule 保存时直接从数据库统计Schedule.objects.filter(course__teacherteacher, time_slot__weekdayslot.weekday).count()把约束判断下沉到数据库层。5.5 时段表里跨天重叠课表渲染乱套现象排课矩阵里周一第 1 节一直连到周一第 3 节分不开。原因手工往TimeSlot表里塞了类似08:00-08:50和08:30-09:20这样的重叠时段它们不属于同一个节次定义。解决在 admin 后台写成自定义验证函数禁止这种部分重叠的数据被保存。这也是前文检测脚本第三个查询存在的意义。最常见的做法是把「节次」做成一个独立的基础配置表教师只从下拉框选择第 1 节、第 2 节而不是自由输入时间。6. 部署、验证与二次开发把排课系统真正用起来的三个关键步骤6.1 生产环境部署要点用 uWSGI 跑 Django 的命令集合开发环境可以直接python manage.py runserver但真实使用场景必须走uWSGI Nginx MySQL的经典组合。部署文件建议拆成两套开发用settings_dev.py生产用settings_prod.py。# 安装依赖 pip install django uwsgi mysqlclient # 生产环境运行方式先收集静态文件再启动 uWSGI python manage.py collectstatic --settingssettings_prod uwsgi --http :8001 --module myproject.wsgi \ --static-map /static/var/www/static \ --processes 4 --threads 2 \ --harakiri 60 --max-requests 5000参数说明--processes 4 --threads 2表示 4 个进程每个 2 线程这个配比适合课表查询密集、写入较少的场景--harakiri 60是超时上限超过 60 秒的请求会被杀掉排课算法循环如果异常卡住会在这里兜底--max-requests 5000让每个进程处理 5000 个请求后自动回收规避内存泄漏问题。Nginx 只需要反代到8001端口并处理静态文件即可。6.2 数据备份与恢复排课系统最容易被忽略的最后一道防线排课数据的体量不大但重建成本极高人工排完一学期的课表需要好几天。所以备份策略比数据库性能更值得投入。最可靠的做法是每天深夜跑一次mysqldump加上 Linux crontab 完成定时任务。# 每天凌晨 2 点备份保留 7 天 0 2 * * * mysqldump -u root -pxxx --single-transaction --set-gtid-purgedOFF scheduler_db /data/backup/edu_$(date \%Y\%m\%d).sql find /data/backup -name *.sql -mtime 7 -delete--single-transaction很关键它让 dump 过程不锁表不影响白天正在使用的排课后台。恢复时用mysql -u root -pxxx scheduler_db edu_20240101.sql即可。有一个坑值得注意如果备份时--set-gtid-purgedOFF没写恢复后会报 GTID 不一致的错误这个参数是 MySQL 5.7 和 8.0 的常见坑。6.3 二次开发的方向与验证方法汇率课表、多校区排课、导入导出这个系统能跑通之后值得扩展的方向有三个。第一个是「按课程组排课」一个教学班可能由多个平行班合并而成需要在Course表增加requires_merge字段并在排课算法中把它当作占两个教室的课程处理。第二个是「多校区资源隔离」在Classroom表增加校区字段算法候选列表里先按校区过滤。第三个是 Excel 导入导出教务老师电脑里永远有一堆排课 Excel 模板用openpyxl导出排课结果、导入历史数据是刚需。验证一套排课系统是否合格我习惯按三条标准检查第一随机挑出 20 门课手工回溯它们的排课结果确认无任何硬约束冲突第二清空 Schedule 表后连续跑三次自动排课每次结果不同但都不冲突说明随机初始化和约束检测在工作第三请一位教务老师连续使用一周记录她手动调整了多少次——如果三次以上是为了调整「上午全是数学课」这类均匀性问题就说明软约束成本太高需要改成按课程类别加权重。我个人的习惯是不要试图把排课算法做得完美把硬约束守住、把可视化做好、给教务老师留足够的微调空间这比任何算法优化都管用。用宋老师的说法排课系统是「先求稳、再求好」的系统希望这套思路帮你在自己的场景里少走弯路希望帮到你。本文还有配套的精品资源点击获取