Django通用排课系统实战:从数据库设计到贪心算法实现

Django通用排课系统实战:从数据库设计到贪心算法实现 简介本资源是一款基于Python 2.7与Django框架开发的通用排课系统面向高校、中学及培训机构的教务管理人员与毕业设计学习者旨在解决课程安排中教师、教室、时段等多约束条件下的高效排课难题。系统集成智能排课算法、可视化课表管理界面与灵活的手动调整功能支持自动初排、人工微调、课表导出含xlsx、csv及多终端适配具备良好的可维护性与跨平台扩展能力。压缩包共276个文件35.69MB涵盖39个核心Python源码py、38个前端交互脚本js、19个HTML页面、39个CSS样式文件含Bootstrap、Layui等主流UI框架资源以及PDF操作手册、SQL数据库脚本和课表模板等实用文档。目前已有56人学习下载提供完整MTV架构实现、清晰目录结构、开箱即用的本地部署方案及配套说明适合Python Web开发入门者实践Django项目全流程亦可作为毕业设计直接参考或二次开发基础。 排课系统这个活儿看着简单做过的人才知道水有多深。标题里这个“python270通用排课系统(django)”一眼扫过去就知道是个有点年头的项目——python270大概率是python2.7用django做Web框架前端拿现成模板改一改后端靠Django ORM操作数据库核心逻辑集中在排课算法里。这种项目在学校、培训机构、驾校、企业培训部门里需求量一直不小因为Excel手排太痛苦了尤其是遇上几十个老师、几十个班、一堆教室还要考虑时间冲突的时候人工排课基本是噩梦。这篇文章我就以这个排课系统为切入点把它从需求拆解、数据库设计、算法实现到部署运维的完整链路讲清楚。内容适合正在做课设、毕业设计的学生也适合想自己撸一套内部工具的后端开发或者单纯想看看“排课系统到底怎么实现”的非技术老师。我会把核心的排课逻辑用大白话拆开讲保证你看完能动手复现。1. 排课系统的本质问题与方案选型1.1 排课到底难在哪很多人第一次接触排课系统以为就是“把课放进表格里”写两个循环就完了。但真正做完一遍会发现排课是一个典型的约束满足问题CSP。你面对的不是一张空白课表而是一堆互相拉扯的条件一个教师同一时间只能在一间教室上课一个班级同一时间只能上一门课一间教室同一时间只能容纳一个班级某些课程必须安排在特定时间段比如体育课不能全排在上午第四节某些教师指定时间不能排课开会、外出、有固定行程每周课时数要均匀分布不能一天上六节同一门课教室类型要匹配机房课必须进机房、多媒体课必须进多媒体教室这些条件里前三条是硬约束不满足就是排错了后面几条是软约束多条软约束互相打架的时候就得靠优先级来取舍。人工排课的痛苦就在于当你调整第20节课的时候可能把第5节课的某个条件破坏了整个表又得推倒重来。而程序排课的核心价值就是把这些冲突判断交给计算机去做并且通过算法在短时间内给出一个可行的初始方案然后人工再在方案基础上微调。1.2 为什么选Django而不是其他方案Python做后端能选的无非就是Flask、Django、FastAPI这几个。这个项目选Django我站在实际开发的角度给几个理由自带Admin后台排课系统需要大量基础数据维护——教师信息、班级信息、课程信息、教室信息。Django自带的后台管理页面改一下models注册就能直接录入数据省去写一堆表单页面的功夫。ORM省心排课过程中要频繁做“查某教师某时间是否已有课”“查某教室某时间是否被占用”这类操作Django ORM对这些查询的表达很清晰而且不用直接写SQL拼接降低出错概率。模板系统够用课表最终要展示成网格状的周视图Django模板加一点循环语法就能输出表格不需要额外引入复杂的前端框架。生态成熟不管是excel导入导出用openpyxl或xlrd、登录权限django自带的auth还是部署nginxgunicorn/uwsgi都有成熟的解决方案。当然如果今天让我重新选我会用Django 4.x Python 3.10但标题里这个项目是python2.7时代的产物这也不影响我们拆解它的设计思路——因为排课系统的核心算法、表结构设计跟Python版本无关换了语言照样是这套逻辑。注意Python 2.7官方早已停止维护Django 1.11是支持Python 2.7的最后一个版本。如果你拿到的项目源码是python2.7 Django 1.x写的在新环境里跑大概率会报错。我的建议是要么花两天时间把代码迁移到Python 3要么用现有环境跑通之后把关键业务逻辑抽出来重写。空有源码跑不起来等于没有。1.3 项目的技术栈拆解这类排课系统的标准技术栈一般是这样层次技术选型说明后端框架Django1.x或2.x负责URL路由、ORM、Admin、视图逻辑数据库SQLite开发/ MySQL生产SQLite适合小规模课设生产环境建议MySQL前端Django模板 Bootstrap表格化展示课表AdaptiveGridLayout排课核心Python函数实现贪心/回溯算法独立逻辑层不做成Web请求内的重逻辑文件处理openpyxl / xlrdExcel导入基础数据导出课表部署Nginx uWSGI生产环境常用组合这套组合放在今天也不过时。它最大的好处是业务逻辑与Web层分离——排课算法是一个独立的模块输入的是任务列表和规则配置输出的是排课结果完全不依赖HTTP请求上下文。这样既方便后期做命令行调试也方便把算法单独拿出去写单元测试。2. 数据库设计与排课规则建模2.1 核心表结构排课系统的数据库设计是整个项目的地基表设计好了后面算法写起来才顺手。我按这个项目常见的设计思路把核心表分为四类基础信息表、教学任务表、规则配置表、排课结果表。基础信息表包括教师表Teacher、班级表Class、教室表Classroom、课程表Course这几个表用途直白不多说。但有几个字段容易被忽略教师表里一定要有max_hours_per_week每周最大课时数和unavailable_time不可排课时间JSON格式存储否则排课容易把老师累死。教室表里要加room_type教室类型比如“普通教室”“多媒体”“机房”因为课程可能对教室类型有要求。班级表里要加student_count排课的时候用来匹配教室容量。教学任务表CourseArrange是最关键的一张表它描述的是“哪个班级需要上哪门课、谁来讲、每周几节、是否需要特殊教室”。字段大概是字段名类型说明course外键课程IDteacher外键授课教师IDclass_group外键班级IDhours_per_weekInteger每周课时数preferred_timeCharField教师或课程偏好的时间段如“上午”requires_labBoolean是否需要机房等特殊教室is_splitBoolean是否允许多天分散排课这张表可以由教务管理员在后台手动录入也可以由系统从“培养方案”里一键生成。时间片表TimeSlot是排课系统的节拍器。常见的做法是定义星期几第几节比如class TimeSlot(models.Model): WEEKDAY_CHOICES [(i, f星期{i}) for i in range(1, 8)] weekday models.IntegerField(choicesWEEKDAY_CHOICES) period models.IntegerField() # 第几节1~N is_active models.BooleanField(defaultTrue) # 是否允许排课 class Meta: unique_together (weekday, period)很多新手不建时间片表直接拿(weekday, period)字符串当字段用短期行后期要加“限定时段不排课”这种规则就非常痛苦。时间片抽成一张表你就可以在后台直接勾选哪些时间段禁用排课算法只需要跳过is_activeFalse的时间片就行。排课结果表ScheduleResult是算法跑完之后的落表字段包括timeslot外键、teacher外键、class_group外键、classroom外键、course外键。必须加一个is_manual布尔字段标记这一条是算法自动排的还是人工手动调的。后期做“一键锁定已调课表”之类功能时这个字段特别有用。2.2 为什么把规则配置独立成表我见过很多排课系统把规则写死在代码里比如直接在函数里判断if teacher.id 1: 不要排周一。这种做法看起来很直接但换一个学校、换一个学期就要改代码所谓“通用”就名存实亡了。所以真正通用的排课系统一定要把规则配置化。常见的规则配置表设计class RuleConfig(models.Model): rule_type models.CharField(max_length50) # 如 teacher_unavailable, class_unavailable target_type models.CharField(max_length20) # teacher / class / classroom target_id models.IntegerField() # 对应具体对象的ID weekday models.IntegerField() periods models.CharField(max_length50) # 如 1,2,3 表示第1、2、3节 description models.CharField(max_length255, blankTrue)排课算法启动之前会把规则配置表里的数据加载成一个冲突判断集合每次尝试把一个教学任务放到某个时间片时先查这个集合命中就直接跳过。这样“哪个老师周三下午不能排课”“哪个班级周五第三节课有班会”“机房周三晚上不开放”这些需求全都可以在后台配置代码一行不用改。实操心得我踩过一个坑——当时把规则配置设计成一次只能配“某一天某几节”后来教务处老师说“我们要隔周排一次课双周上课、单周不上”。这种“周次奇偶”需求很常见但一开始完全没考虑。建议时间片表里加一个week_type字段0表示每周都排1表示单周排2表示双周排。这个字段加晚了后期补数据真的很痛苦。2.3 数据导入导出设计通用排课系统如果没有Excel导入导出功能教务老师用起来就是一场灾难。Django生态里处理Excel最方便的是openpyxl处理xlsx和xlrd处理xls配合Django Admin的import/export功能可以实现“下载模板 → 填写 → 上传导入”的完整闭环。导入数据的核心思路是先解析Excel行逐行创建Teacher、Class、Course等基础对象。要注意处理重复导入的情况比如同一个教师姓名在Excel里出现两次系统要识别出来是同一人还是不同人。这种场景我一般用“工号姓名”作为唯一索引先查数据库里有没有工号相同的记录有就更新没有才创建。导出课表也更讲究很多老师拿到系统之后会直接拿课表去打印或发给学生所以Excel导出格式必须贴近“人类手排课表”的样式。我一般用openpyxl按周视图生成Excel表合并单元格、边框线、页眉都设置好再导出一个带汇总统计的sheet列出每个老师的周课时数、每个班级的周课时数方便教务处核对。3. 排课算法设计从贪心到冲突消解3.1 最常用的初始解生成策略加权贪心排课算法不要一上来就上回溯、遗传算法、模拟退火那是最后优化阶段的事。工程上最稳的起步方案是加权贪心思路非常简单把所有教学任务班级课程教师按“限制程度”排序限制越多的任务越先排。对每个任务遍历所有可用时间片可用教室组合。计算每个组合的得分选得分最高的落位。如果某个任务所有组合都冲突把它放入“待处理队列”等首轮排完后统一处理。什么是“限制程度”我常用的计算方式是三个维度的加权需要特殊教室的任务限制程度更高每周课时数多的任务限制程度更高指定了时间段偏好的任务限制程度更高比如一个任务要求“必须使用机房”且“每周排6节”那它要比“普通教室每周2节”的任务先排因为机房资源少、可用的时间片也少晚排很可能就找不到位置了。得分函数可以设计成这样def score_choice(task, timeslot, classroom, conflict_checker): score 0 # 硬约束检查 if conflict_checker.has_conflict(task, timeslot, classroom): return -float(inf) # 时间偏好上午优先 if timeslot.period 4: score 10 # 教室匹配度类型完全匹配加分 if task.requires_lab and classroom.room_type lab: score 20 # 均匀性如果这个班级当天已有两节同课程扣分 # 教师负担如果老师当天已排4节以上扣分 return score一次遍历所有时间片和教室把得分最高的选出来这就是典型的贪心。贪心不一定能得到全局最优解但得到的初始解通常已经“能看”至少满足硬约束这就大大减轻了人工调整的负担。3.2 冲突检测的数据结构冲突检测是排课算法的核心动作也是性能瓶颈点。如果每次判断都用SQL查询“查一下这个老师这个时间有没有课”排300个任务时每个任务遍历20个时间片×3个教室就是18000次查询再叠加逻辑响应时间很容易到不可接受的程度。工程上更高效的做法是在内存中建立三个哈希索引。teacher_schedule[teacher_id][weekday][period]→ 是否已占用class_schedule[class_id][weekday][period]→ 是否已占用classroom_schedule[classroom_id][weekday][period]→ 是否已占用每次尝试把一个任务放到某个时间片时只需查三个字典时间复杂度是O(1)。三个索引全部为空才可以落位落位后同时更新三个索引。这样整个排课过程全程在内存中跑最后再把结果批量写回数据库性能会快一个数量级。代码示意teacher_schedule {} class_schedule {} classroom_schedule {} def has_conflict(task_teacher_id, task_class_id, task_classroom_id, weekday, period): if teacher_schedule.get((task_teacher_id, weekday, period)): return True if class_schedule.get((task_class_id, weekday, period)): return True if classroom_schedule.get((task_classroom_id, weekday, period)): return True return False def allocate(task, timeslot, classroom): if has_conflict(task.teacher_id, task.class_group_id, classroom.id, timeslot.weekday, timeslot.period): return False teacher_schedule[(task.teacher_id, timeslot.weekday, timeslot.period)] True class_schedule[(task.class_group_id, timeslot.weekday, timeslot.period)] True classroom_schedule[(classroom.id, timeslot.weekday, timeslot.period)] True return True注意python2.7时代字典的get方法返回None如果你拿到的老项目里有类似if dict.has_key(key)的写法建议改成if key in dict语义不变但更清晰后续迁移到python3也少很多麻烦。3.3 处理首轮排不进去的任务贪心结束后总会有那么几个任务死活排不进去。常见的兜底策略有三种策略一放宽软约束重排。比如某个任务优先考虑“上午”但上午全满了那就允许在下午找一个可用的时间片落位。这需要把评分函数里的约束从“硬”变成“软”实现方式就是先跑一遍带软约束评分的贪心如果有没有排进去的任务就降低评分门槛再跑一遍直到能落位为止。策略二局部回溯。找到导致当前任务无法落位的“冲突来源”——即占用了某个时间片的其他任务尝试把这些任务挪到别的时间片腾出位置给当前任务。这个逻辑很接近“人工调课”的思维先看谁占了我的坑再看那个任务能不能搬到其他地方。策略三手动干预。系统对无法自动排入的任务打上“未排”标记在后台页面上列出由教务管理员手动指定时间。这不是妥协而是务实。真实业务场景里总有算法考虑不到的隐性规则比如学校临时通知某天下午全体教师开会这种信息不一定提前录入了系统。给人工留一条兜底通道系统才真的能用起来。3.4 算法效果评估排课算法写完之后不要急着交付。我习惯设计一个简单的评估函数输出几个分数硬约束满足率必须100%教师时间偏好满足率期望90%以上课程时间均匀度同一门课未连续出现3节以上教室类型匹配率期望95%以上在开发环境造一组模拟数据比如30个教师、40个班级、60门课程、每周600节课跑一次看分数。如果算法出来的均匀度很差比如某个班周一连续6节课都是数学那就要在评分函数里调“连续课时惩罚”的权重系数。先把评估函数做好调优才有依据否则完全靠肉眼检查课表效率太低了。4. 核心功能实操从初始化到导出课表4.1 项目初始化与依赖安装假设你拿到的是那一份python270通用排课系统的原始压缩包按解压后的目录结构第一步通常是创建虚拟环境并安装依赖。老项目的依赖一般记录在requirements.txt里大致内容如下Django1.11.29 mysqlclient1.3.14 openpyxl2.6.4 xlrd1.2.0如果是Python 2.7环境虚拟环境最好用virtualenv而不是python -m venvpython2.7不自带venv模块。命令大概是virtualenv venv source venv/bin/activate # Linux/Mac # 在Windows上是 venv\Scripts\activate pip install -r requirements.txt python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000如果你手上的环境是Python 3.x直接跑Django 1.11大概率报ImportError: No module named django.core.urlresolvers这类错误Django 2.0起urlresolvers模块被移动了需要做小范围兼容性修改。这是我的血泪教训不要一上来就想着整个项目升级Django版本先让它在现有环境里跑起来把核心逻辑吃透再谈迁移。4.2 通过Admin后台维护基础数据登录Django Admin后第一件事是录入基础数据。通常在排课系统的Admin里你会看到这些可维护的模块教师管理姓名、工号、邮箱、每周上限课时、不可排课时间段班级管理班级名称、人数、所属年级教室管理教室名称、容量、类型普通/多媒体/机房课程管理课程名称、默认周课时数、是否需要机房教学任务管理班级课程教师周课时的组合配置其中“教学任务管理”是最核心的入口一个任务就对应“某班这学期要学某门课、由某位老师教、每周X节”。录完基础数据就可以点“自动排课”按钮了。4.3 自动排课的完整流程自动排课的后端视图函数大致流程如下从数据库中读取所有教学任务读取所有可用时间片读取所有可用教室加载规则配置教师不可排时间、班级不可排时间等对教学任务按限制程度排序初始化三个调度索引teacher_schedule、class_schedule、classroom_schedule对每个任务遍历时间片教室组合按评分函数选择最优排不进去的任务放入待处理队列对待处理队列执行局部回溯或放宽约束重排把所有结果批量写入ScheduleResult表返回统计结果成功排入数、剩余未排任务数、算法耗时这个流程最好封装成一个独立的服务函数比如def run_auto_schedule(): tasks load_tasks() slots load_time_slots() rooms load_classrooms() constraints load_rule_config() scheduler Scheduler(tasks, slots, rooms, constraints) scheduler.execute() result scheduler.get_result() save_to_db(result) return scheduler.get_statistics()建议在开发时加一个--dry-run参数只跑算法不写库方便反复调参。我第一次做的时候没有这个概念每次调算法都直接覆盖正式数据后来被同事喷了一顿。这个教训后来一直记着任何有“破坏性”的行为都要提供一次模拟运行的机会。4.4 手动调课与冲突提示自动排课结果出来教务处老师大概率还是会手动调几节课比如“王老师周三下午临时有事周三第5节的课挪到周四第2节”。手动调课功能的实现不难但有几个细节必须处理好调课前先做冲突检测。把目标时间片、目标教室的三个索引都查一遍有冲突立即弹提示写明冲突来源教师冲突/班级冲突/教室冲突。调课成功后要同步更新历史记录。ScheduleResult表里旧记录删除或置为非活跃状态新记录插入同时重新渲染课表。要有操作日志表。记录“谁在什么时间把哪节课从什么位置调到了什么位置”出了问题能追溯这在教务场景中是硬需求。前端如果不想写复杂的交互用Django模板渲染一个可编辑表格就行每个单元格是一个课程卡片点击卡片弹出模态框显示该课程的详细信息并提供“调整时间”下拉框。这个交互用原生JS配jQuery也能搞定不用上Vue/React杀鸡焉用牛刀。4.5 周课表与教师课表的查看导出排课系统的最终交付物一定要有清晰直观的课表视图。常见的是三个视图班级课表按班级查展示一周的课程格子。教师课表按教师查可以看到每天的课程分布。教室课表按教室查验收一下教室利用率。Django模板里用二维数组表示课程网格本质上就是按(weekday, period)构建一个“位置→课程信息”的映射模板里双重循环输出表格。比如# 视图里构造数据 schedule_matrix {} # key: (weekday, period), value: ScheduleResult对象 for s in ScheduleResult.objects.filter(class_groupcls): schedule_matrix[(s.timeslot.weekday, s.timeslot.period)] s模板大致是table trth节次/thth周一/thth周二/th.../tr {% for period in periods %} tr td{{ period }}/td {% for weekday in weekdays %} td {% with itemschedule_matrix|get_item:weekday|get_item:period %} {{ item.course.name }}br{{ item.teacher.name }} {% endwith %} /td {% endfor %} /tr {% endfor %} /table导出Excel则用openpyxl做同样的双循环设置好单元格边框和列宽效果基本跟网页一致。实操心得模板里的get_item过滤器Django默认没有需要自己写一个模板过滤器不然就得把数据提前处理成二维数组。我更推荐在视图里直接生成二维数组或字典模板只做纯显示不依赖自定义过滤器调试起来少一层麻烦。5. 常见问题与排查技巧实录5.1 python2.7环境下最常见的坑跑老项目最痛苦的就是环境问题。python2.7时代编码就是最大的坑。如果你在console里看到UnicodeDecodeError或者课表里中文全部变成乱码八成是文件头部没有声明编码或者数据库连接字符串少了charsetutf8。排查和解决办法Python文件头部加上# -*- coding: utf-8 -*-这是python2的标配。数据库连接配置改成ENGINE: django.db.backends.mysql, OPTIONS: {charset: utf8mb4}MySQL存储中文才稳。所有字符串统一用u前缀python2里str和unicode是两种类型最省心的方式是所有自己写的字符串都写成unicode。从Excel读入的中文要先.decode(utf-8)再入库取出来展示时确保是unicode再交给模板。5.2 排课结果有冲突但系统没检测出来这是一个很隐蔽的问题。如果算法检测不出来冲突通常不是逻辑写错了而是索引没有同步更新。举个例子某个任务先尝试占用了时间片A评分不高被放弃了但代码里忘记回滚占用的索引下一个任务再来的时候时间片A被误判为“已占用”导致明明有位置却不能排或者反过来已经落位的不小心覆盖索引造成漏判。排查方法在每次allocate和rollback的地方打日志打印三个索引的状态。开发时可以用一个小工具函数每跑完一轮就做一次全量校验把数据库里已有的排课记录重新加载成索引和内存中的索引做对比。这个办法虽然土但很快能定位到是哪一步更新漏了。5.3 排课特别慢几百个任务要卡几十秒性能问题通常出在“教室选择”这一步。比如每个任务遍历10个可用教室每个教室再遍历20个时间片就是200次组合判断。但大多数任务根本不需要遍历所有教室——只要教室类型匹配、容量足够就行完全可以在进入时间片循环之前先过滤出候选教室列表candidate_classrooms [ room for room in rooms if room.room_type required_type and room.capacity class_size ]把双层循环缩小到“候选教室×时间片”循环量直接砍掉一大半。再配合内存哈希索引几百个任务的排课时间应控制在2~3秒以内。如果还慢就要检查是不是在循环里执行了SQL查询。原则很简单能进内存做判断的绝不查数据库。5.4 教师课表满屏红色冲突标记这种情况通常是教师在系统中被分配了超过每周上限的课时数。比如一个老师每周上限是16节但待排教学生务加一起有28节再怎么排都满足不了硬约束。解决办法不是改算法而是要做排课前的数据体检自动排课启动之前先遍历所有教学任务检查有没有“同一教师周课时数超上限”“同一班级每周课程超过可上课时段数”这类异常数据直接在前端页面列出所有超限项提示老师在基础数据里调整再重新排。把校验做在算法之前能避免排完课发现“根本排不出来”的尴尬。5.5 从python2.7迁移到python3的注意事项最后聊一下如果你确实想把老项目迁到Python 3需要关注的点。首选是Django1.11最高支持python2.7如果你要迁到Python 3.8建议直接升到Django 3.2 LTS或4.x。升级过程中90%的报错集中在url()函数废弃改成path()或re_path()ugettext改成gettextsmart_str、force_text这些别名改动MIDDLEWARE_CLASSES改成MIDDLEWARE模板标签中ifequal、ifnotequal被移除Python层面的改动主要是print语句变函数、dict.keys()返回视图而非列表、xrange改range、unicode概念并入str。整个过程不需要重新设计业务逻辑但非常琐碎建议用小步迁移的方式每改完一个app就跑一边测试。实操心得迁移别在排课算法上先动手先迁基础的表结构和Admin后台跑通“录入数据”这个链路再迁移排课算法。算法模块独立性强函数入口清晰只要输入输出结构不变迁移起来反而没那么难。最后分享一个小技巧不管你是拿这份源码做课设、毕设还是真实部署一定要把“自动排课之前先做数据校验”“排完课先看统计不看明细”“手动调课强制做冲突检测”这三件事写进代码里。排课系统用得好不好往往不是看算法多先进而是看边界情况有没有兜住。我做过的几个排课项目最终让客户满意的反而是那些“提前发现数据异常”的提示和“手动调整不破坏已有安排”的机制。这些细节比排课算法本身更值得投入精力。本文还有配套的精品资源点击获取