☰
Django通用排课系统实战:从数据模型到自动排课算法
2026/10/6 17:33:25 网站建设 项目流程

简介:面向高校、中学及培训机构教务管理场景的通用排课系统源码包,基于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(period=period.id).select_related('teacher')一次查询就能拿到完整对象图,不需要写联表 SQL。Django 的信号机制还能在教师被删除时自动清理排课记录,这种兜底逻辑在手动管理后台时特别重要。如果你只需要一个排 API 的服务,Flask 够用;但排课系统本质是「管理后台 + 调度逻辑」双核心,这是 Django 的主场。

2.2 五张核心表:课程、教师、教室、时段、排课结果

排课系统的数据模型不需要过度设计,五张表基本覆盖全部业务场景。

from django.db import models class Teacher(models.Model): name = models.CharField(max_length=50, verbose_name='教师姓名') title = models.CharField(max_length=50, blank=True, verbose_name='职称') max_lessons_per_day = models.IntegerField(default=4, verbose_name='每日最大课时数') class Meta: db_table = 'teacher' class Course(models.Model): name = models.CharField(max_length=100, verbose_name='课程名称') code = models.CharField(max_length=20, unique=True, verbose_name='课程编号') credit = models.FloatField(default=1.0, verbose_name='学分') teacher = models.ForeignKey(Teacher, on_delete=models.PROTECT, verbose_name='授课教师') requires_capacity = models.IntegerField(default=30, verbose_name='最小教室容量') class Meta: db_table = 'course' class Classroom(models.Model): room_number = models.CharField(max_length=20, unique=True, verbose_name='教室编号') capacity = models.IntegerField(default=60, verbose_name='容量') has_projector = models.BooleanField(default=True, 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(default=True, verbose_name='是否可用') class Meta: unique_together = ('weekday', 'start_time', 'end_time') db_table = 'time_slot' class Schedule(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='课程') classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE, verbose_name='教室') time_slot = models.ForeignKey(TimeSlot, on_delete=models.CASCADE, verbose_name='时段') is_fixed = models.BooleanField(default=False, verbose_name='是否人工锁定') created_at = models.DateTimeField(auto_now_add=True, 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_delete=models.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_available=True) .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( course=course, classroom=assigned_classroom, time_slot=slot ) 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会引发 N+1 查询,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(cnt=Count('id')) .filter(cnt__gt=1)) # 2. 教师冲突:课程关联了同一个老师且时段相同 teacher_conflicts = (Schedule.objects .values('course__teacher_id', 'time_slot_id') .annotate(cnt=Count('id')) .filter(cnt__gt=1)) # 3. 排课结果时段重叠:开始和结束时间跨区段重叠 slots = list(TimeSlot.objects.all().order_by('weekday', 'start_time')) for i in range(len(slots) - 1): if slots[i].weekday == slots[i+1].weekday: if slots[i].end_time > slots[i+1].start_time: self.stdout.write(self.style.WARNING( f'时段重叠: {slots[i]} 与 {slots[i+1]}' ))

逻辑说明:前两个查询利用 Django ORM 的annotate+filter(cnt__gt=1)组合,直接由数据库完成分组计数,比 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_day=4的限制。原因:自动排课脚本里teacher_workload只在当前运行进程内维护,重启命令或手动在 admin 新增排课后,这个内存变量就失效了。解决:在 Schedule 保存时直接从数据库统计Schedule.objects.filter(course__teacher=teacher, time_slot__weekday=slot.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 --settings=settings_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 -p'xxx' --single-transaction --set-gtid-purged=OFF scheduler_db > /data/backup/edu_$(date +\%Y\%m\%d).sql && find /data/backup -name "*.sql" -mtime +7 -delete

--single-transaction很关键,它让 dump 过程不锁表,不影响白天正在使用的排课后台。恢复时用mysql -u root -p'xxx' scheduler_db < edu_20240101.sql即可。有一个坑值得注意:如果备份时--set-gtid-purged=OFF没写,恢复后会报 GTID 不一致的错误,这个参数是 MySQL 5.7 和 8.0 的常见坑。

6.3 二次开发的方向与验证方法:汇率课表、多校区排课、导入导出

这个系统能跑通之后,值得扩展的方向有三个。第一个是「按课程组排课」:一个教学班可能由多个平行班合并而成,需要在Course表增加requires_merge字段,并在排课算法中把它当作占两个教室的课程处理。第二个是「多校区资源隔离」:在Classroom表增加校区字段,算法候选列表里先按校区过滤。第三个是 Excel 导入导出,教务老师电脑里永远有一堆排课 Excel 模板,用openpyxl导出排课结果、导入历史数据是刚需。

验证一套排课系统是否合格,我习惯按三条标准检查:第一,随机挑出 20 门课,手工回溯它们的排课结果,确认无任何硬约束冲突;第二,清空 Schedule 表后连续跑三次自动排课,每次结果不同但都不冲突,说明随机初始化和约束检测在工作;第三,请一位教务老师连续使用一周,记录她手动调整了多少次——如果三次以上是为了调整「上午全是数学课」这类均匀性问题,就说明软约束成本太高,需要改成按课程类别加权重。

我个人的习惯是:不要试图把排课算法做得完美,把硬约束守住、把可视化做好、给教务老师留足够的微调空间,这比任何算法优化都管用。用宋老师的说法,排课系统是「先求稳、再求好」的系统,希望这套思路帮你在自己的场景里少走弯路,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询