
简介这是一套基于Django框架、Python语言与MySQL数据库开发的高校学生学业预警系统毕业设计资源面向计算机相关专业毕业生及需要实现学业预警功能的管理开发者。系统包含管理员端与学生端覆盖预警分析、学生信息管理、成绩管理、用户管理、个人信息与学习计划等模块从数据采集到预警生成形成完整业务闭环。资源共323个文件以Python源码与pyc编译文件为核心配合HTML/CSS/JS前端页面、SQL数据库初始化脚本、GIF演示截图以及doc/docx开发文档组成压缩包约10.53MB结构清晰便于查阅。已有53人学习下载适合用于毕业设计参考、项目二次开发或系统搭建练习。包内附可直接运行的源代码、数据库文件与论文文档下载后对照文档即可快速部署运行后续调试问题亦可获得技术支持。1. 高校学业预警系统这个毕业设计在解决什么问题、适合谁做高校学业预警系统是这几年 Python 方向毕业设计里出现频率极高的一个题目核心任务说穿了就一件事把学生的成绩、挂科门次、学分进度、出勤情况收集起来按预设规则自动判断每个学生处于哪种学业风险等级再把这个结论推送给出辅导员和学生本人。题目之所以热门一是业务场景真实答辩时不用费劲解释“为什么要做”二是技术栈能把 Django、数据库、算法判重、数据可视化一整条链路串起来工作量好分配。适合有 Python 基础、想完整走一遍 Web 项目开发全流程的同学。如果你正在为选题发愁或者已经拿到这个题目想找一套靠谱的落地方案和避坑清单这篇东西能帮你省下不少返工时间。2. 技术选型与项目骨架为什么这个题目最省力的路线是 Django 全家桶2.1 Django、Flask、FastAPI 怎么选三个现实考量先说结论这个题目默认选 Django除非指导老师点名要求轻量框架。原因有三条每一条都对应真实的答辩场景。第一管理后台白送。学业预警系统天然需要维护学生档案、课程表、成绩数据这些基础信息Django Admin 在 models 写完之后自动生成增删改查页面不用单独写半行 CRUD 代码。这对省时间极其关键期末阶段最缺的就是时间。第二ORM 和迁移自带。数据库表结构改了之后跑一条makemigrations不用自己写 SQL 迁移脚本。用 Flask 的话SQLAlchemy 配置和 Alembic 迁移都要额外处理小项目里这一层配置能折腾两三天。第三认证和权限模块现成。系统面向辅导员、院系管理员、学生三种角色Django 自带的 User 模型加上 Group 权限就能覆盖根本不需要自己实现登录注册。FastAPI 在这个题目里没有优势它的异步特性对成绩统计这种 CPU 密集型任务帮助不大而且自动文档、依赖注入这些特性在毕业设计里属于过度设计。如果你老师指定 Flask也不是不行但后面提到的所有模型和判定逻辑照样能搬过去只是要把 ORM 层换成 SQLAlchemy。2.2 核心表结构设计学生、成绩、预警记录三张表就够跑了不要一开始就设计十张八张表学业预警系统真正不可少的是四个实体学生、课程、成绩、预警记录。下面是精简后的 models 代码毕业设计直接抄这个骨架没有大问题。# models.py from django.db import models class Student(models.Model): 学生档案表 student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length50) major models.CharField(专业, max_length100) grade models.CharField(年级, max_length10, help_text如 2023) advisor models.CharField(辅导员, max_length50, blankTrue) is_active models.BooleanField(是否在读, defaultTrue) def __str__(self): return f{self.student_no} {self.name} class Course(models.Model): 课程表一门课只存一次不分学期 course_code models.CharField(课程代码, max_length20, uniqueTrue) course_name models.CharField(课程名称, max_length100) credit models.FloatField(学分) is_required models.BooleanField(是否必修, defaultTrue) class Score(models.Model): 成绩明细学生-课程-学期 唯一确定一条成绩 student models.ForeignKey(Student, on_deletemodels.CASCADE) course models.ForeignKey(Course, on_deletemodels.PROTECT) semester models.CharField(学期, max_length20, help_text如 2024-2025-1) score models.FloatField(成绩) is_retake models.BooleanField(是否重修, defaultFalse) status models.CharField( 成绩状态, max_length10, choices[(normal, 正常), (deferred, 缓考), (absent, 缺考)], defaultnormal ) class Meta: # 同一学生同一学期同一门课只能有一条成绩 unique_together (student, course, semester)这段代码的关键点在最后一行的unique_together。实际项目里最常见的脏数据就是重复导入同一个学生同一门课的成绩在表里出现两条后面统计挂科门次时直接翻倍。设计阶段就把唯一约束加上比事后清理数据省心得多。三个设计细节值得说透。一是Course表不挂学期一个学生可能在大一和大三修同一门课成绩是记录在Score.semester里的课程表只维护课程本身的信息。二是Score表加了status字段缓考和缺考必须和正常成绩区分开否则一个缓考的学生会被当成挂科处理预警全错。三是on_deletemodels.PROTECT用在课程上防止误删课程导致成绩记录跟着消失。2.3 用 Django Admin 快速拿到可用的数据后台模型定义好之后注册到 Admin 只需要四行代码# admin.py from django.contrib import admin from .models import Student, Course, Score admin.site.register(Student) admin.site.register(Course) admin.site.register(Score)注册完进入后台就能直接录入学生、课程、成绩。这里注意课程要先录入因为成绩录入时要选课程和学号是外键关联如果课程表是空的成绩页面里下拉框是空的什么都选不了。实际操作的顺序是先建好 Django 项目、跑通迁移然后打开 Admin 手录几门课和十几条成绩用于后续开发预警判定逻辑时的联调数据。手工造数不需要多覆盖及格、挂科、缓考三种状态就够了。3. 预警判定引擎把“挂科两门”变成可执行的 Python 代码3.1 先把预警规则数据化写死在 if 里是最难维护的做法很多同学的第一个版本是这么写的# 反例警告 def evaluate(student): fail_count ... if fail_count 3: return red elif fail_count 2: return orange ...这种写法当时能跑但等到指导老师说“两门改成三门”“GPA 低于 2.0 也要预警”的时候你要改代码、改测试、还要祈祷没有其他逻辑依赖这段代码。更麻烦的是不同院系可能有不同的预警规则硬编码的 if 结构根本没法按院系区分。正解是建一张预警规则表把阈值存进数据库class WarningRule(models.Model): LEVEL_CHOICES ( (yellow, 黄色预警), (orange, 橙色预警), (red, 红色预警), ) level models.CharField(预警等级, max_length10, choicesLEVEL_CHOICES) priority models.IntegerField(优先级, help_text数字越小越先匹配) fail_count_min models.IntegerField(挂科门次下限, default0) gpa_max models.FloatField(GPA上限, default0) credit_rate_min models.FloatField(学分完成率下限, default0) is_active models.BooleanField(启用, defaultTrue)每条规则表示“达到这个条件就命中这个等级”。匹配时按优先级从上到下尝试命中第一条就返回不再往下看。这样调整阈值就变成在后台改数字动一行代码都不用。建议预置三条规则等级挂科门次下限GPA上限学分完成率下限优先级红色预警31.560%1橙色预警22.070%2黄色预警12.580%3这个参数表只是起步参考后面第四章会详细讲怎么调。3.2 判定主流程三个指标怎么算出来的预警判定基于三个指标挂科门次、平均绩点 GPA、学分完成率。它们各自对应学业风险的不同侧面。挂科门次反映单学期学业的即时压力GPA 反映长期学习质量学分完成率反映学业进度的偏离程度。下面是核心计算逻辑# services/early_warning.py def score_to_gpa(score: float) - float: 百分制成绩转 4.0 制绩点 if score 90: return 4.0 if score 85: return 3.7 if score 80: return 3.3 if score 75: return 2.7 if score 70: return 2.0 if score 60: return 1.0 return 0.0 def calculate_indicators(student, semester): 计算单个学生在某学期的学业指标 scores Score.objects.filter(studentstudent, semestersemester) # 指标一挂科门次缓考不算挂科 fail_count scores.exclude(statusdeferred) \ .filter(score__lt60).count() # 指标二加权平均绩点排除缓考记录 normal_scores scores.exclude(statusdeferred) total_credit sum(s.course.credit for s in normal_scores) total_point sum(s.course.credit * score_to_gpa(s.score) for s in normal_scores) gpa total_point / total_credit if total_credit else 0.0 # 指标三学分完成率 已获学分 / 应修学分 earned_credit sum(s.course.credit for s in normal_scores if s.score 60) credit_rate earned_credit / total_credit if total_credit else 1.0 return { fail_count: fail_count, gpa: round(gpa, 2), credit_rate: round(credit_rate, 2), }这里有两个细节值得解释。缓考的处理是第一个关键点缓考的课程在这个学期没有成绩把它排除掉才合理不能算挂科也不能算通过。第二个是加权平均绩点的口径绩点按课程学分加权一门 4 学分的数学课和一门 1 学分的选修课对 GPA 的影响显然应该不同。3.3 批量扫描全校学生并落库预警记录单学生指标算完还需要一个批量扫描入口以及匹配规则、保存预警记录的完整链路def match_rule(indicators): 按优先级匹配第一条命中的规则 rules WarningRule.objects.filter(is_activeTrue) \ .order_by(priority) for rule in rules: if indicators[fail_count] rule.fail_count_min \ and indicators[gpa] rule.gpa_max: return rule return None def scan_semester(semester: str): 全量扫描所有在读学生 from django.db import transaction students Student.objects.filter(is_activeTrue) created 0 with transaction.atomic(): for stu in students: indicators calculate_indicators(stu, semester) rule match_rule(indicators) if not rule: continue record, is_new WarningRecord.objects.update_or_create( studentstu, semestersemester, defaults{ level: rule.level, fail_count: indicators[fail_count], gpa: indicators[gpa], credit_rate: indicators[credit_rate], } ) if is_new: created 1 return createdupdate_or_create是这段代码的核心。同一学生同一学期如果重复扫描不会生成第二条预警记录而是先查存在性、存在就更新。加上WarningRecord表里针对(student, semester)的唯一约束从数据层杜绝了重复预警。这个设计在你把扫描任务接到定时任务后尤其重要——定时任务跑一次就发一次通知的话学生会被提醒短信轰炸的。class WarningRecord(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE) semester models.CharField(学期, max_length20) level models.CharField(预警等级, max_length10) fail_count models.IntegerField(挂科门次, default0) gpa models.FloatField(GPA, default0.0) credit_rate models.FloatField(学分完成率, default0.0) created_at models.DateTimeField(首次预警时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: unique_together (student, semester)created_at记录首次预警时间updated_at记录最近更新时间。如果某个学生这个学期从黄色升到红色查这个表能看出风险升级的轨迹答辩时这是很好的展示素材。4. 预警等级阈值怎么定黄、橙、红三级参数表与调优经验4.1 一张可直接参考的初始阈值表阈值参数是学业预警系统里争议最多、也最容易被评委追问的部分。答辩时被问“你这个红线的依据是什么”答不上来就会显得整个系统是拍脑袋做的。给出下面这张参考表同时准备好这句话“这组参数参考了同类院校的通行做法实际部署时由教务处以数据回溯的方式校准。”等级判定条件响应动作通知渠道黄色预警挂科 1 门 或 GPA 低于 2.0辅导员谈话了解原因站内消息橙色预警挂科 2 门 或 GPA 低于 1.5院系教务介入短信 站内消息红色预警挂科 3 门及以上 或 GPA 低于 1.0通知家长学业警示短信 电话 书面通知这个表的核心逻辑是双维触发挂科门次反映短期爆发风险GPA 反映长期低迷风险。一个学生可能这个学期只挂了 1 门课但 GPA 掉到了 1.2单看挂科门次只有黄色预警结合 GPA 看就该直接上橙色。所以判定时两个维度都算谁先命中按谁处理。4.2 三个需要按院校调整的参数维度第一课程学分权重。有些学校的必修课学分高达 6 分挂一门必修的杀伤力等于挂三门选修。如果你直接用“挂科门次”统计会把挂选修和挂必修当成同一件事。改进方案是把“挂科门次”换成“挂科学分加权”代码如下fail_credit sum(s.course.credit for s in scores if s.score 60)然后用挂科学分数替换门次作为触发条件。如果指导老师不强调这个维度用门次就行但你要能说出为什么——门次算法更直观对非专业的教务老师来说“挂了三门课”比“挂了 8 个学分”更易懂。第二年级差异。大一新生第一学期的课程以公共课为主GPA 普遍偏高阈值可以紧一点大三学生专业课难度大GPA 整体下降是正常现象阈值要松一到两档。常见的做法是给Student表加一个grade字段在计算指标时区分年级高年级放宽 GPA 上限 0.3 左右。第三专业差异。理工科和文科的给分风格不同有的学院平均分 75有的学院平均分 85。想让预警更准就要按专业分别维护规则表——WarningRule加一个major外键字段为空表示全校通用。这个扩展不复杂模型加一行字段就行。4.3 阈值改完之后如何重新计算历史预警阈值不是一成不变的。你可能在答辩前调了两次参数这时候有一个很实际的问题改阈值后之前的预警记录要不要重算我的建议是保留一个“重算历史数据”的入口但不直接覆盖原记录。做法是给WarningRecord加一个version字段重算时按新版本写入记录老记录留着供对比。这样做有两个好处一是能向答辩老师展示你设计的数据版本思维二是教务老师如果对结果有争议可以回查是用了哪版规则。def rescan_semester(semester: str, version: int 2): 按新版规则重算某学期保留旧版本数据 WarningRecord.objects.filter(semestersemester).update(archivedTrue) created scan_semester(semester, versionversion) return created重算之后务必做一次数据对比——有多少学生从黄色升到橙色有多少从橙色回落到黄色。这组数字本身就能说明规则调整的合理性是答辩加分项。5. 学业预警系统经常翻车的地方成绩数据、重复预警、性能瓶颈5.1 成绩导入时把缓考、缺考当成 0 分全校学生一夜之间全红了现象用 Excel 导入上学期成绩后跑预警结果 80% 的学生都命中了红色预警数据明显异常。原因教务系统导出的成绩单里“缓考”“缺考”“作弊”这些文本状态直接放在成绩列里比如“缓考”两个字。导入脚本没有做状态判断直接float(value)转换失败就默认成 0 分。0 分当然小于 60于是所有缓考生全被算成挂科。解决导入的时候加一个归一化函数先判断文本状态再转数字并且将状态写进Score.status字段def parse_score(raw_value: str): 把教务导出的成绩文本解析为 (分数, 状态) raw_value raw_value.strip() status_map { 缓考: deferred, 缺考: absent, 作弊: absent, 取消: absent, } if raw_value in status_map: return 0.0, status_map[raw_value] return float(raw_value), normal导入完成后统计一下各个状态的数量做 sanity check比如“缓考 12 人、缺考 3 人”对比原文件确认无误再跑预警。5.2 同一学生同一学期收到三条重复预警通知辅导员开始拒接电话现象定时任务每天跑一次扫描每次扫描都生成新的预警记录系统每天发一次短信学生和辅导员都收到轰炸。原因没有做幂等控制。第一次扫描生成了记录第二次扫描发现学生还在预警名单里又生成了一条通知也跟着发了一遍。WarningRecord表没有唯一约束数据重复不可避免。解决两个层面的控制。数据层面WarningRecord加unique_together (student, semester)这个第三章已经讲过代码层面通知发送前查一下该学生该学期是否已经发过同等级通知def notify_student(record): sent NotificationLog.objects.filter( recordrecord, notify_typesms ).exists() if sent: return False # 发送短信 NotificationLog.objects.create(recordrecord, notify_typesms) return True核心原则是预警记录可以更新但通知不能重复发。更新记录不影响历史记录展示但重复通知会消耗学生和老师的耐心。5.3 几千个学生循环查询成绩预警跑一次要五分钟现象学期初全量扫描控制台里一条一条 SQL 刷过去跑完全校 5000 个学生要五到八分钟期间后台页面加载都变慢。原因循环里对每个学生发了多条查询——查成绩、查课程、算学分这是 N1 查询问题。5000 个学生就是上万条 SQL性能自然崩。解决把逐条查改成批量化预聚合。成绩表按学期一次性取出在内存里按学生分组再进行指标计算from collections import defaultdict def calculate_indicators_bulk(semester: str): 批量计算某学期所有学生的指标 scores Score.objects.filter( semestersemester, statusnormal ).select_related(course, student) groups defaultdict(list) for s in scores: groups[s.student_id].append(s) results {} for stu_id, stu_scores in groups.items(): fail_count sum(1 for s in stu_scores if s.score 60) total_credit sum(s.course.credit for s in stu_scores) total_point sum(s.course.credit * score_to_gpa(s.score) for s in stu_scores) gpa total_point / total_credit if total_credit else 0.0 earned sum(s.course.credit for s in stu_scores if s.score 60) results[stu_id] { fail_count: fail_count, gpa: round(gpa, 2), credit_rate: round(earned / total_credit, 2) if total_credit else 1.0, } return results这个版本的查询从 N1 降到了 1 次。5000 个学生的数据一条 SQL 全查出来内存分组的耗时可以忽略不计整个扫描时间从五分钟压到两三秒。如果数据量再大比如十万级那就要考虑分页处理或者把指标统计写成 SQL 的annotate聚合但毕业设计到这一步已经足够。5.4 重修成绩算进了挂科门次预警等级虚高现象某学生大二挂了高等数学大三重修通过但预警记录显示他一直挂科红色预警持续了一年。原因Score表存着重修前后的两条成绩统计挂科门次时两条都被算进去了。解决统计时做两个过滤——同一门课取最高成绩重修通过的成绩不计入门次def calculate_fail_count(student, semester): scores Score.objects.filter(studentstudent, semestersemester) passed set() fail set() for s in scores: course_key s.course_id if s.score 60: passed.add(course_key) else: fail.add(course_key) # 同一门课既挂过又修过视为已通过 return len(fail - passed)用集合去重同一门课挂了重修再挂只算一次。这个逻辑最好写进单元测试里——专门构造一个学生挂科后重考的数据断言挂科门次是 1 而不是 2。测试写一次之后改判定逻辑的时候能少踩很多坑。5.5 时间维度的坑学期字段格式不统一现象上一届同学留下的数据里“2024-2025-1”“2024/2025(1)”“第一学期 2024”三种格式混在一起按学期统计时数据对不上。原因数据来源不同教务系统导出格式和手动录入格式不统一学期字段没有做标准化。解决在数据导入层做统一转换全系统只用YYYY-YYYY-N一种格式def normalize_semester(raw: str) - str: 把各种学期写法统一为 2024-2025-1 格式 if re.match(r^\d{4}-\d{4}-\d$, raw.strip()): return raw.strip() m re.search(r(20\d{2}).*?(20\d{2}), raw) if m: year1, year2 m.group(1), m.group(2) if 1 in raw or 上 in raw: return f{year1}-{year2}-1 return f{year1}-{year2}-2 raise ValueError(f无法识别的学期格式: {raw})这个问题不解决统计出的学期对比数据毫无意义而且非常隐蔽——表面数字能跑出来但横向对比时对不上。6. 用历史数据验证预警效果召回率、误报率与答辩数据准备系统能跑只是第一步答辩时评委最常问的一个问题是“你怎么知道你的预警是准的”闭着眼说“我们测试过了”是站不住脚的这里给你一套可以落到实处的历史回测方法。拿上一学年的数据来回测。假设现在要验证 2023-2024 学年的预警规则你需要两组数据一组是当时已经发生的学业结果——比如该学年结束时有 XX 人确实出现了严重学业问题挂科门次超过 3、被学业警示或退学这作为“真实问题学生”的名单另一组是你的预警系统如果当时运行会圈出多少学生作为预警对象。两组一对比就得到了四类结果真正预警且出问题的、预警了但没出问题的、没预警但出问题的、既没预警也没出问题的。由此计算两个指标召回率和误报率。召回率 预警且出问题的人数 ÷ 实际出问题总人数衡量的是“真正有风险的学生有多少被抓住了”误报率 预警但实际没出问题的人数 ÷ 总预警人数衡量的是“误伤了多少人”。一个可用的预警系统至少要保证召回率超过 80%否则漏掉的人太多预警没有实际意义误报率可以放宽到 50% 以下因为大学里学业预警本身偏保守多提醒一个总比漏一个强。具体操作不需要单独写个复杂的验证模块写一段回测脚本就能跑def backtest(history_results, warning_records): history_results: 实际出问题的学生集合 warning_records: 预警系统圈出的学生集合 tp len(warning_records history_results) # 抓住的 fn len(history_results - warning_records) # 漏掉的 fp len(warning_records - history_results) # 误报的 recall tp / (tp fn) if (tp fn) else 0 precision tp / (tp fp) if (tp fp) else 0 false_alarm fp / (tp fp) if (tp fp) else 0 return { 召回率: round(recall, 3), 精确率: round(precision, 3), 误报率: round(false_alarm, 3), }回测跑完之后把阈值参数微调一版、再跑一遍把两版结果放进一个简单的表格里对比。这个“我调了阈值召回率从 62% 提升到 85%误报从 48% 降到 35%”的过程就是你整个项目的灵魂。它证明你不是在做一个“能点按钮的系统”而是在做一个对业务有实际价值、参数可验证的工具。建议你把这个回测脚本的文档留到最后一刻——做预警系统的同学里十个人有九个把精力全花在页面和代码上真正拿历史数据回测验证的不到两成。你只要把这个环节做了在答辩现场你已经是能说清楚“系统为什么值得用”的那一个。至于那些花哨的图表、复杂的动画反而是次要的。我当年做类似项目的时候也走过先把页面做花却忽略了判定规则本身可靠性的弯路最后重构模型和数据时才补上验证这一步。希望这个教训能帮到你少走弯路愿你的预警系统既跑得顺也站得住。本文还有配套的精品资源点击获取