简介教务管理系统中排课一直是复杂度最高的模块之一其本质是一个多资源约束分配问题涉及教师、班级、教室、时间等多维度的冲突规避。借助Python生态中成熟的Django框架可以高效地搭建一套通用排课系统。本文从基础概念出发拆解了系统设计中的核心表结构包括教师、班级、课程、教室、时间槽与排课记录六张模型并重点讲解了基于贪心策略的自动排课算法与冲突检测机制。通过优先级排序、容量校验和类型匹配系统能快速生成不冲突的课表极大减轻教务人员的工作负担。文中还分享了后台管理、课表展示以及性能优化、踩坑排查的实用经验。这套方案适用于中学、大学及培训机构只要调整基础数据即可直接落地为教务系统开发提供了一个完整、可复用的技术参考。 排课系统这四个字做过教务系统的人看到都会会心一笑。这可能是学校信息化建设里最“硬核”的一块也是看起来简单、做起来全是坑的一个模块。手动排课有多痛苦问任何一个教务老师都知道几十个班级、几十位老师、每周几十门课要保证同一时间教师不撞车、教室不冲突、班级不重课还要考虑课程连排、教室容量、特殊教室要求光是把这些约束条件摆出来就已经很复杂了。这个项目是一个基于 Python 和 Django 框架开发的通用排课系统它把课程、教师、班级、教室、时间这五大要素建模成可配置的数据对象通过一套自动排课算法快速生成课表。整套系统不绑定特定学校业务中学、大学、职业培训机构拿来改改基础数据就能用。这篇文章我会把这个项目的表结构设计、排课算法、后台页面和实际踩坑经历完整拆开讲一遍从零开始复现整个系统的核心逻辑。1. 设计思路排课系统到底在解决什么问题1.1 三类最容易出问题的冲突排课本质上是一个多资源约束分配问题。教室里坐的是学生讲台上站的是老师课表上填的是课程这三样东西在时间维度上必须是“一对一”的关系任何一个环节出现重叠课表就废了。最常见的冲突就是三类同一个老师在同一时间段排了两门课同一个班级在同一时间段被安排了两门不同的课同一个教室在同一时间段被两个班级抢占。这个系统的整个建模过程都是围绕这三类冲突展开的数据库设计也好算法逻辑也好最后兜底的都是这三条规则。还有一类冲突是隐性冲突就是教室容量不够。50个人的班硬塞进一个30人的小教室这在数据库层面不冲突但在实际教学场景里完全不可行。所以这个系统在做教室资源分配的时候必须把容量校验放进算法逻辑里而不是完全依赖数据库约束。1.2 为什么把技术栈定在Django能实现排课系统的技术栈很多PHP、Java、Node.js 都能做但这个项目选择了 Python Django用的是非常成熟的方案。Django 在这个场景里最大的优势不是性能而是开发效率尤其适合排课这种数据管理密集型项目理由很实在自带 Admin 后台课程、教师、班级的增删改查直接进后台管理不用单独写页面。ORM 做模型映射非常省事建表、迁移一条命令搞定开发阶段用 SQLite上了生产环境切 MySQL业务代码一行都不用改。自带用户认证和权限体系管理员、教务、普通教师三种角色天然支持。MTV 架构清晰模板层和业务逻辑层分开课表这种表单密集型的页面开发起来特别顺。有人在技术群问过“python django 国内使用广泛么”我的看法一直是Django 在国内 Web 领域虽然不像某些框架那么铺天盖地但在教务、ERP、后台管理这类系统里它一直是稳定可靠的选择。排课系统恰好就落在它最擅长的区域里。1.3 项目目录结构与模块划分这个项目的根目录结构很简单核心代码都集中在 schedule 这个主应用里配置在 config 目录下schedule_project/ ├── manage.py ├── schedule/ # 主应用 │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── admin.py │ └── templates/ ├── config/ # 项目配置 │ ├── settings.py │ └── urls.py └── requirements.txt功能模块上拆成四块基础数据管理、教学计划配置、自动排课引擎、课表展示与导出。基础数据管理管的是教师、班级、课程、教室这四类主数据教学计划配置把课程跟班级、教师绑定起来同时设定每周课时自动排课引擎是核心负责把前面的配置变成一张不冲突的课表课表展示则按班级、教师、教室三个维度输出视图。2. 数据库模型设计六张核心表一次讲透数据库层是整个排课系统的地基。Django 的 ORM 把数据库操作封装得很干净但前提是模型设计得合理。我见过很多排课系统翻车不是算法不行而是模型没设计好导致后面算法层怎么跑都别扭。这个项目的表设计很克制一共六张核心表每一张都有明确的存在价值。2.1 核心表结构与字段说明教师表、班级表、课程表、教室表、时间槽表、排课结果表这六张表构成了整个系统的数据骨架。教师表存姓名、工号、职称、邮箱工号设置唯一约束班级表要记录年级、班级名称、人数人数是一个关键字段直接关系到教室容量的校验课程表是核心中的核心除了课程名称、每周课时、总学时还通过外键关联授课教师通过多对多关联适用班级教室表记录名称、容量、类型类型区分普通教室、机房、实验室因为不同教室类型的排课逻辑不一样时间槽表把一周的上课时间离散化成固定数量的时间段每个时间槽作为排课的原子资源排课结果表记录最终的课程、班级、教师、教室、时间槽对应关系。2.2 Django模型代码实现模型代码是这么写的直接贴出来from django.db import models class Teacher(models.Model): name models.CharField(姓名, max_length50) employee_no models.CharField(工号, max_length20, uniqueTrue) title models.CharField(职称, max_length20, blankTrue) email models.EmailField(邮箱, blankTrue) def __str__(self): return self.name class Meta: verbose_name 教师 verbose_name_plural 教师 class ClassGroup(models.Model): name models.CharField(班级名称, max_length50) grade models.CharField(年级, max_length20) student_count models.IntegerField(人数, default0) def __str__(self): return self.name class Meta: verbose_name 班级 verbose_name_plural 班级 class Course(models.Model): name models.CharField(课程名称, max_length100) teacher models.ForeignKey(Teacher, on_deletemodels.CASCADE, verbose_name授课教师) class_groups models.ManyToManyField(ClassGroup, verbose_name适用班级) weekly_hours models.IntegerField(每周课时, default2) total_hours models.IntegerField(总学时, default32) classroom_type models.CharField(教室类型, max_length20, defaultnormal) def __str__(self): return self.name class Meta: verbose_name 课程 verbose_name_plural 课程 class Classroom(models.Model): name models.CharField(教室名称, max_length50) capacity models.IntegerField(容量, default50) room_type models.CharField(类型, max_length20, defaultnormal) def __str__(self): return self.name class Meta: verbose_name 教室 verbose_name_plural 教室 class TimeSlot(models.Model): WEEKDAY_CHOICES [(i, f星期{i}) for i in range(1, 8)] weekday models.IntegerField(星期几, choicesWEEKDAY_CHOICES) slot models.IntegerField(第几时段) start_time models.TimeField(开始时间) end_time models.TimeField(结束时间) def __str__(self): return f星期{self.weekday} 第{self.slot}时段 class Meta: verbose_name 时间槽 verbose_name_plural 时间槽 class Schedule(models.Model): course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) class_group models.ForeignKey(ClassGroup, on_deletemodels.CASCADE, verbose_name班级) teacher models.ForeignKey(Teacher, on_deletemodels.CASCADE, verbose_name教师) classroom models.ForeignKey(Classroom, on_deletemodels.CASCADE, verbose_name教室) time_slot models.ForeignKey(TimeSlot, on_deletemodels.CASCADE, verbose_name时间槽) class Meta: unique_together [ (time_slot, class_group), (time_slot, teacher), (time_slot, classroom), ] verbose_name 排课记录 verbose_name_plural 排课记录2.3 为什么时间槽要单独建表这是很多初学 Django 的人容易忽略的设计点。有同学会问为什么不直接在课程表里存星期几和第几节非要额外建一张时间槽表原因有三层。第一层是灵活性。每个学校的上课节奏不一样有的学校上午五节课有的上午四节有的晚自习也要排课。时间槽单独建表之后教务人员可以直接在后台增删时间槽不需要改代码就能适配不同的作息制度。第二层是算法便利。排课引擎在遍历可用时间段的时候直接对 TimeSlot 对象做遍历就行条件判断用对象 ID 比较比操作字符串或者多个字段组合要干净得多。第三层是可扩展性。后面如果要做单双周排课、节假日调休、临时调课这些功能时间槽表加字段或者加关联就行。比如要支持单双周模式加一个 week_type 字段就够了不需要重构整个排课逻辑。在这个模型设计里最值得反复强调的是 Schedule 表里的unique_together三重联合唯一约束。这三条约束正好对应前面说的三类核心冲突它们在数据库层面就把违规排课挡住了。即使你的算法有 bug想往同一个人、同一时间、两个教室插入两条排课记录数据库也会直接报错不会产生脏数据。3. 排课算法核心逻辑贪心策略与冲突检测排课算法是整个系统最核心的部分。这个项目用的是一套基于贪心策略的分配算法配合冲突检测整体思路可以概括为先难后易、逐个分配、能排就排。谈不上什么高深的数学优化但实际跑下来效果很稳中小规模学校的排课需求完全能覆盖。3.1 先排谁课程优先级排序贪心算法的关键在“先难后易”也就是先把约束最强的课程排掉再处理约束弱的。这跟生活中收拾行李一个道理先把大件放进去再用小件填缝隙最后随便塞袜子。如果反着来大件很可能就放不进去了。这个项目里课程排序的优先级规则有三条每周课时多的课程优先排。4课时、6课时的大课如果放到后面很难找到连续的可用时间槽。适用班级数量多的课程优先排。一门课同时面向多个班级占用的资源多约束更强先排可以先把资源锁定住。特殊教室要求的课程优先排。比如必须用机房的计算机课机房资源本来就少先排可以先抢到机房。排序得分可以这样算直接贴一个可用的代码片段def course_sort_key(course): score course.weekly_hours * 10 score course.class_groups.count() * 5 if course.classroom_type ! normal: score 3 return -score courses sorted(Course.objects.all(), keycourse_sort_key)我实测这个权重分配效果不错。weekly_hours 权重最高因为它直接决定排课的难度系数班级数量次之特殊教室要求加成最低因为特殊教室只是限制了可选教室池不影响时间槽分配。当然权重值不是绝对的你可以根据实际场景调整。3.2 冲突检测与教室匹配冲突检测是排课系统的安全网也是算法层最不能出错的地方。核心逻辑是对每一个待分配的时间槽检查班级、教师、教室是否都空闲。代码很直白直接查 Schedule 表就行def is_conflict(class_group, teacher, classroom, time_slot): # 同一班级同一时间不能上两门课 if Schedule.objects.filter( time_slottime_slot, class_groupclass_group ).exists(): return True # 同一教师同一时间不能上两门课 if Schedule.objects.filter( time_slottime_slot, teacherteacher ).exists(): return True # 同一教室同一时间不能被占两次 if Schedule.objects.filter( time_slottime_slot, classroomclassroom ).exists(): return True return False教室匹配逻辑里有两个必须做的校验。第一个是容量校验classroom.capacity class_group.student_count这个条件必须写死否则就会出现前面说的 50 人的班排进 30 人教室的问题。第二个是类型匹配课程要求机房就只能从room_typelab的教室池里挑不能拿普通教室顶上。3.3 排课流程完整拆解整个排课流程可以分为四步走每一步都有明确的输入输出。第一步初始化数据和排序。把全部课程取出来按优先级排序把全部时间槽取出来备用把全部教室取出来按类型分好组。第二步遍历课程逐个分配。对每个课程确定它需要的总课时数也就是 weekly_hours。然后遍历所有时间槽对每个时间槽先检查课程绑定班级是否有空、教师是否有空再找一个满足容量和类型要求的空闲教室。第三步批量写入排课结果。如果一门课找到了足够数量的可用时间槽就把这些记录一次性写入 Schedule 表。如果不够就把这门课标记为排课失败在页面上给出提示让教务人员手动安排。第四步汇总统计。排课完成后输出一个统计结果哪些课程排成功了哪些失败了失败原因是什么。这样管理员可以针对性地去调整教学计划。下面是整个排课引擎核心逻辑的完整代码你可以直接抄到自己的项目里改改用def generate_schedule(): Schedule.objects.all().delete() # 清空旧课表 courses sorted(Course.objects.all(), keycourse_sort_key) time_slots list(TimeSlot.objects.all()) classrooms list(Classroom.objects.all()) fail_courses [] for course in courses: assigned [] teachers [course.teacher] class_groups list(course.class_groups.all()) for time_slot in time_slots: if len(assigned) course.weekly_hours: break # 先检查教师和班级是否空闲 teacher_busy Schedule.objects.filter( time_slottime_slot, teachercourse.teacher ).exists() class_busy Schedule.objects.filter( time_slottime_slot, class_group__inclass_groups ).exists() if teacher_busy or class_busy: continue # 再找合适的教室 available_classroom None for classroom in classrooms: if classroom.room_type ! course.classroom_type: continue if classroom.capacity max( cg.student_count for cg in class_groups ): continue if Schedule.objects.filter( time_slottime_slot, classroomclassroom ).exists(): continue available_classroom classroom break if available_classroom: assigned.append((time_slot, available_classroom)) if len(assigned) course.weekly_hours: for time_slot, classroom in assigned[:course.weekly_hours]: for class_group in class_groups: Schedule.objects.create( coursecourse, class_groupclass_group, teachercourse.teacher, classroomclassroom, time_slottime_slot, ) else: fail_courses.append(course.name) return fail_courses3.4 关于连排和回溯的补充实际排课里经常有连排需求比如数学课一上就是两节连排。这个项目处理连排的方式比较取巧直接把时间槽粒度定义成“第1-2节”“第3-4节”这样的大时间段每个时间槽天然就是一个连排单元。这种方式实现最简单对大多数中小学来说完全够用。如果你需要更细粒度的时间槽比如按单节课来排再做相邻时间槽匹配算法复杂度会明显上升。我的建议是如果不是特别复杂的排课场景优先用大时间段方案因为教务人员更能直观理解课表调整起来也方便。关于回溯机制这个项目实现得比较保守如果某门课遍历完所有时间槽还排不满课时就把它放进失败列表不会动已排好的课程。面对规模大、约束紧的场景可以考虑在失败时撤销上一次分配结果并重新尝试但回溯深度控制在两层以内就够了。超过两层还排不出来大概率是初始教学计划配置有问题比如课时总量超过了时间槽容量这时候应该检查数据而不是继续在算法层面硬扛。4. 后台管理与课表展示模块落地排课算法搞定后系统还差两块拼图基础数据怎么维护课表怎么展示。这两块做得好不好直接影响系统的可用性。Django 的 Admin 后台在数据维护环节帮了大忙课表展示则需要自己写视图和模板。4.1 用Admin后台管理基础数据模型定义好之后在 admin.py 里注册一下就能获得一个完整的后台管理系统from django.contrib import admin from .models import Teacher, ClassGroup, Course, Classroom, TimeSlot, Schedule admin.register(Teacher) class TeacherAdmin(admin.ModelAdmin): list_display (name, employee_no, title) search_fields (name, employee_no) admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (name, teacher, weekly_hours, total_hours) filter_horizontal (class_groups,) admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): list_display (course, class_group, teacher, classroom, time_slot) list_filter (teacher, classroom, time_slot)这里有一个很实用的小技巧Course 的适用班级字段是多对多关系在 Admin 里默认是下拉多选框班级多的时候选起来特别痛苦。用filter_horizontal (class_groups,)之后会变成左右两栏的双向选择框左边是所有班级列表右边是已选班级操作体验提升一个档次。这个配置在 Django 文档里叫“水平过滤器”很多老手都在用。4.2 班级课表展示页面课表展示是按班级维度组织的因为这是教务老师和家长最常看的视图。页面逻辑很简单传入一个班级查询该班级的所有排课记录按星期几和节次排成一个二维表格。视图代码大致是这样的from django.shortcuts import render, get_object_or_404 from .models import ClassGroup, Schedule, TimeSlot def class_schedule(request, class_group_id): class_group get_object_or_404(ClassGroup, idclass_group_id) schedules Schedule.objects.filter(class_groupclass_group).select_related( course, teacher, classroom, time_slot ) # 生成课表矩阵: matrix[weekday][slot] weekdays range(1, 8) slots TimeSlot.objects.values_list(slot, flatTrue).distinct() matrix { weekday: {slot: None for slot in slots} for weekday in weekdays } for item in schedules: matrix[item.time_slot.weekday][item.time_slot.slot] item return render(request, class_schedule.html, { class_group: class_group, matrix: matrix, slots: sorted(slots), weekdays: weekdays, })模板层面用一个循环嵌套把课表矩阵渲染成 HTML 表格table classtable table-bordered thead tr th节次/th {% for day in weekdays %} th星期{{ day }}/th {% endfor %} /tr /thead tbody {% for slot in slots %} tr td第{{ slot }}时段/td {% for day in weekdays %} td {% with itemmatrix.day.slot %} {% if item %} strong{{ item.course.name }}/strongbr {{ item.teacher.name }}br {{ item.classroom.name }} {% endif %} {% endwith %} /td {% endfor %} /tr {% endfor %} /tbody /table有一点要提醒大家matrix.day.slot这种带变量名取字典值的方式在 Django 模板里是行不通的模板引擎不支持这样动态取值。正确做法是在视图里把矩阵整理成模板可以直接遍历的结构或者用自定义模板过滤器在后台就把星期几和节次的映射关系处理好这样模板里就是纯展示逻辑不会踩动态查找的坑。4.3 前端页面的数据优化细节排课结果展示页面如果数据量大了性能问题会马上冒出来。Schedule 表每条记录都关联了课程、教师、教室、时间槽如果直接遍历Django 会对每一个外键都发一条查询语句这就是典型的 N1 查询问题。解决办法是在视图的查询里加上select_related像上面代码里写的那样一次性把相关联的对象预加载进来。加了select_related之后一次查询就能把所有需要的数据全部拿到查询次数从 N1 次降到了 1 次。数据量小的时候感受不明显数据量一上来差距巨大这个优化在排课这种外键密集型的业务里是必修课。5. 实操中踩过的坑与排查方法这个项目在开发调试过程中踩了不少坑有些是新手常见问题有些是排课系统特有的坑整理出来供大家参考。5.1 数据库迁移和中文编码问题项目刚拉下来的时候第一件事就是python manage.py migrate。如果报错优先检查 Django 版本和 Python 版本的兼容性。这个项目是在 Python 3.8 以上的环境里开发的如果你本地还是 Python 3.6 老环境可能装不上最新依赖建议直接用 conda 建一个干净的 Python 3.10 环境再跑。中文编码问题也出现过。Windows 环境下命令行窗口执行makemigrations时偶尔会出现 UnicodeEncodeError 报错解决方案是在项目根目录加一个sitecustomize.py或者在命令前带上环境变量PYTHONIOENCODINGutf-8。这个问题在 Linux 服务器上不会出现基本是 Windows 开发机特有的坑。数据库用 SQLite 开发没问题但要上生产环境还是建议切 MySQL。切换方式很简单改 settings.py 里的 DATABASES 配置就行Django ORM 会自动适配业务代码不用动。5.2 时区与时间格式化Django 默认开启时区支持settings.py 里有TIME_ZONE UTC和USE_TZ True这两个配置。如果不改你录入的时间会差 8 个小时尤其在做统计查询的时候按照日期分组必然会出问题。排课系统项目里我建议直接设置TIME_ZONE Asia/Shanghai并把USE_TZ设为 False这样时间字段保存的就是本地时间不会出现时区偏移导致的时间显示混乱。如果必须保留USE_TZ True那所有时间操作都要小心Django 会按 UTC 时间存储展示时再转换到本地时区逻辑上会复杂不少。时间槽表里的 start_time、end_time 只是展示用不参与排课算法的计算算法只关心 weekday 和 slot 这两个字段所以时区问题只要在配置层面处理好实际业务逻辑基本不受影响。5.3 排课失败先检查数据再检查代码排课完成后如果有课程进了失败列表这是最让人头疼的问题。我排错的时候有一套固定流程先查时间槽数量是否足够每周课时总量是否超过了时间槽容量。比如一周排 20 个时间槽但某门课每周要排 6 课时加上其他课程占用的课时总量超了自然会失败。再查教室资源是否充足。机房数量不够的时候所有要求机房的课程都会失败这属于资源配置问题不是算法问题。这时候要么增加机房要么减少同时段排课需求。最后才考虑代码层面。确认排课算法里is_conflict的判断条件没有写反没有把空格检查逻辑弄成永远返回 True 的“死亡逻辑”。我调试的时候吃过这种亏一条 SQL 条件写错导致所有课程全部排课失败查了半天才发现是条件反了。5.4 性能优化心得这个排课系统如果只处理几十门课性能完全不是问题脚本几秒内就能跑完。但当数据量上来比如高校一个年级上百个班级、数百门课、上千条排课记录的时候性能就开始吃紧了。性能瓶颈主要出在两个地方。第一是冲突检测里的exists()查询每分配一个时间槽就要发很多条 SQL。优化思路是预加载数据先把已有排课记录全部加载到内存里用字典或集合做索引冲突检测直接查内存结构不再访问数据库。第二是写库操作批量创建 Schedule 对象的时候一定要用bulk_create()别用一层层 for 循环里的create()速度差几十倍。我实际测试过用内存索引加bulk_create()重写一遍后500 门课的排课时间从原来的近 1 分钟缩短到 2 秒以内。这个优化对排课系统来说非常重要因为排课并不是只跑一次教务人员经常会调一调数据重新排跑得快一点体验完全不一样。结语通用排课系统扩展的想象空间按照我个人经验这个项目的设计已经覆盖了排课系统最核心的骨架。如果后续想继续扩展有两个比较容易落地的方向单双周课程模式在 TimeSlot 表加一个周类型字段就能撑起来自动检测教师总工作量上限排课时过滤掉超过周课时上限的教师让排好的课表更符合实际教学规范。最后一个实用小技巧项目里的排课算法是可以用脚本单独测试的不需要把整个 Django 服务跑起来。写一个独立的测试脚本调用generate_schedule()函数对着输出结果反复调整权重和逻辑比每次在网页上点一遍快得多。调试排课系统熟练用这种“脱离页面跑核心逻辑”的方式能省下大量时间。本文还有配套的精品资源点击获取