
简介这是一份基于Python Django框架与MySQL数据库的高校学生学业预警系统毕业设计资料包适用于计算机相关专业学生完成毕设或学习Web系统开发。系统涵盖管理员端菜单模块、预警分析、学生信息与成绩管理、用户管理以及学生端个人信息、学习计划等核心功能能够对学业数据进行实时监控与异常预警。压缩包共323个文件除Python源码外还包含HTML/CSS/JS前端页面、pyc编译文件、SQL数据库脚本及doc/docx开发文档整体大小约10.53MB目录结构清晰便于快速定位与二次开发。目前已有53人学习下载。资料附带数据库文件与完整开发文档涵盖系统安装配置、模块说明及使用指南项目已测试可正常运行适合需要快速上手或参考完整项目流程的毕业设计者。1. 高校学业预警系统值不值得做先看清这题到底在考什么一个学期结束辅导员手上最头疼的名单不是奖学金名单而是挂科名单。学业预警系统本质上是把这个“人工翻成绩单、逐个算挂科和绩点”的过程变成一套可复用的Python程序数据从教务系统或者Excel里来程序按规则算出哪些学生需要提醒、提醒到什么程度再把结果用Web页面展示给辅导员和管理员。这套系统真正难的地方从来不在页面好不好看而在三条数据口径能不能对齐——教务处那边的成绩口径、辅导员手里的名单口径、学生自己感知的挂科情况稍微差一点预警名单就废了。做这个方向是有价值的。毕业后如果你想走数据方向这套系统能拿来讲你和业务方对齐口径的经历如果你走开发方向它也是一个完整的增删改查加统计展示的项目够你写进简历。这篇笔记按我实际做过的方案把需求拆分、数据库建模、规则引擎、Web展示和踩过的坑整个走一遍照着搭一套能用、经得起答辩问的完整系统。2. 把需求翻译成系统从教务数据到预警名单的数据流转2.1 预警系统的核心业务先搞清楚谁在用、用什么数据学业预警系统放在高校里一般服务三类人。辅导员要的是“这个学期我们班谁挂科了、挂了几门、累计学分差多少”学院教务员要的是“哪个专业、哪个年级的挂科率在上涨需不需要干预”学生本人要的是“我现在离学业警示线还有多远”。一套系统如果只做了其中一个视角答辩时很容易被问住。所以数据模型设计时就要让这三个视角都查得到。数据来源通常是两个。一个是教务系统的成绩库但一般情况下学生不可能拿到教务库的直连权限毕设里最常见的是从教务处导出的Excel成绩单或者用你自己造的一套仿真成绩数据。另一个是培养方案它决定了课程属于必修还是选修、对应多少学分这是算累计挂科学分的基础。没有培养方案你只能说“挂了两门课”说不清“累计挂科学分已经到8分”后者才是学校做退学警示的依据。整个系统的数据流按顺序是成绩Excel导入、清洗落库、按学期和学号组织成绩、预警引擎跑规则、生成预警记录、Web端展示和导出。后面所有章节都绕着这条线走先看数据库怎么建。2.2 数据库表结构设计与Python建模学号为什么必须用字符串学业预警系统的表数量不多按最小可用方案四张表就够学生表、课程表、成绩表、预警记录表。我用SQLAlchemy写了一套简化模型跑在SQLite上。SQLite对毕设来说是性价比最高的选择文件即数据库演示时直接拷走就能跑不用装MySQL服务。from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey from sqlalchemy.orm import declarative_base Base declarative_base() class Student(Base): __tablename__ students student_no Column(String(20), primary_keyTrue) # 学号 name Column(String(50), nullableFalse) major Column(String(50)) # 专业 grade Column(String(4)) # 年级如 2021 class_name Column(String(50)) # 行政班 class Course(Base): __tablename__ courses course_code Column(String(20), primary_keyTrue) course_name Column(String(100), nullableFalse) credit Column(Float) # 学分 is_compulsory Column(String(1), default1) # 是否必修1-是 0-否 class Score(Base): __tablename__ scores id Column(Integer, primary_keyTrue, autoincrementTrue) student_no Column(String(20), ForeignKey(students.student_no), indexTrue) course_code Column(String(20), ForeignKey(courses.course_code)) semester Column(String(10)) # 例如 2023-2024-1 score Column(Float) # 百分制成绩缺考记0 is_retake Column(String(1), default0) # 1-重修成绩 0-正常成绩 class WarningRecord(Base): __tablename__ warning_records id Column(Integer, primary_keyTrue, autoincrementTrue) student_no Column(String(20), ForeignKey(students.student_no), indexTrue) semester Column(String(10)) # 触发预警的学期 level Column(String(10)) # yellow / orange / red reason Column(String(255)) # 触发规则描述例如挂科2门累计挂科学分6分 is_processed Column(String(1), default0) # 辅导员是否已处理这里最想强调的一点学号不要用Integer必须用String。真实学号短的七八位、长的十位出头Excel里一旦被处理成数字前导零直接消失超过15位还会变科学计数法。虽然学号长度不至于触发15位精度问题但前面有0的话转成数字再转回来就回不去了。这个坑在后面的导入环节还会再出现。成绩表里is_retake字段很多人会忽略。一个学生补考过了成绩库里可能留着一条原始的挂科记录和一条新的通过记录如果你不去区分挂科门数会被重复统计预警名单就会多出一批不该出现的人。先把这个标记留好规则引擎里再决定取哪条。2.3 选Flask还是Django毕设场景下的框架选型逻辑学业预警系统的交互密度很低核心操作就是导入成绩、跑预警、看列表、看统计图。对于这种场景Flask和Django都能做但我的建议是Flask。理由不是Django不好而是Django自带Admin后台、ORM、Migration这些能力对毕设来说大部分用不上反而增加理解负担。Flask的核心优势是路由和视图函数的结构非常直观一个app.route装饰器对应一个页面或接口答辩时你好讲老师也好顺着问。Django的MTV分层在项目大了是优势但一个预警系统的代码量撑不起这个复杂度硬套反而是负担。Python环境配置就按最常规的来虚拟环境建完后用pip安装flask、sqlalchemy、pandas、openpyxl、scikit-learn这几个包就够了。开发调试在PyCharm或VS Code里都行重点是虚拟环境要选对解释器不然经常出现“命令行能跑、IDE里报No module named flask”的尴尬情况。装包时国内用户记得把pip源切成清华或阿里镜像不然在默认源上下载pandas那几十兆文件能等得人想摔电脑。3. 用Python实现预警引擎规则阈值、触发逻辑与名单生成3.1 预警维度怎么选挂科、绩点、缺勤三个方向预警规则不要拍脑袋定要能说出依据。高校里常见的学业预警维度有三个成绩维度、学分维度、考勤维度。成绩维度指单科挂科或学期平均绩点低于某条线学分维度指累计未获学分达到一定值考勤维度指出勤率低于某个比例一般归到学生管理而不是教务系统里但可以在系统里预留字段。在毕设论文里规则要以表格形式写清楚这是我建议的默认参数预警等级触发条件默认值响应方式黄色预警单学期挂科1门系统记录辅导员谈话提醒橙色预警单学期挂科2门或累计挂科学分≥6通知学生本人并约谈红色预警单学期挂科≥3门或累计挂科学分≥12或连续两学期橙色预警通知家长学院介入这些数字不是乱写的挂科学分阈值参考的是很多高校“累计挂科超过20学分做退学警示”的底线预警线要设置在比底线更早触发的位置才有干预意义。你拿到具体学校的数据后这些参数值全部可以做成配置项别写死在代码里。3.2 用pandas实现成绩统计一次算清挂科门数和加权绩点确定规则后引擎实现我用pandas。有人会问为什么不用纯SQL理由是成绩数据大概率来自Excel反正都要用pandas清洗一遍不如统计也一起在DataFrame里做了逻辑更直观。import pandas as pd from sqlalchemy import create_engine engine create_engine(sqlite:///warning_system.db) # 关联学生、课程、成绩三张表一次性取全量数据 sql SELECT s.student_no, s.major, s.grade, sc.semester, sc.course_code, sc.score, sc.is_retake, c.credit, c.is_compulsory FROM scores sc JOIN students s ON s.student_no sc.student_no JOIN courses c ON c.course_code sc.course_code df pd.read_sql(sql, engine) # 先处理补考/重修同一门课只取该学期该生最新一条成绩 df df.sort_values(is_retake).drop_duplicates( subset[student_no, course_code, semester], keeplast ) # 标记挂科 df[is_fail] (df[score] 60).astype(int)排序后按学号加课程加学期去重保留最后一条这样重修通过的学生不会因为历史挂科记录被重复预警。这种写法在真实数据里也站得住因为教务系统导出时重修记录一般排列在原始记录之后。接下来统计挂科门数和挂科学分。这里有一个细节要注意挂科学分应该只累加必修课选修课挂科一般不影响毕业学分要求但这个规则不同学校不一样我做成一个开关。累计方法上先筛出挂科记录再按学号分组求和比在groupby里用lambda安全得多——后者很容易踩索引不对齐的坑。# 只保留必修课挂科记录用于累计挂科学分 fail_df df[(df[is_fail] 1) (df[is_compulsory] 1)] fail_stats fail_df.groupby(student_no).agg( fail_count(course_code, count), fail_credit(credit, sum) ).reset_index() # 统计每个学生本学期各科的成绩和学分用于算加权平均绩点 gpa_df df[df[is_retake] 0].copy() # 绩点计算不算重修 def score_to_gpa(score): if score 90: return 4.0 elif score 85: return 3.7 elif score 82: return 3.3 elif score 78: return 3.0 elif score 75: return 2.7 elif score 72: return 2.3 elif score 68: return 2.0 elif score 64: return 1.5 elif score 60: return 1.0 return 0.0 gpa_df[gpa] gpa_df[score].apply(score_to_gpa) gpa_df[weighted] gpa_df[gpa] * gpa_df[credit] gpa_stats gpa_df.groupby(student_no).apply( lambda g: pd.Series({ gpa_avg: g[weighted].sum() / g[credit].sum(), term_fail: (g[score] 60).sum() }) ).reset_index()score_to_gpa函数用的4.0制是简化版实际学校有百分制加权和等级制绩点两种算法你只需要替换这一个函数就能适配。groupby之后reset_index是为了把学号从索引里放回列不然后面按学号merge时会多一层结构新手经常在这里翻车。把这几个统计结果按学号合并就得到预警判定所需的全部特征。原则上我建议统计和判定分开写函数统计函数返回DataFrame判定函数接收DataFrame返回预警等级。答辩老师问“如果我换个绩点算法需要改哪里”的时候你就能理直气壮地说只改score_to_gpa这一个函数。3.3 预警等级判定规则可配置预警记录可追溯统计完成之后判定逻辑反而是最简单的就是把阈值规则翻译成代码。但我建议预警记录表里必须写入触发原因格式是“2023-2024-1学期挂科3门累计挂科学分14分”这条reason字段在导出通知单和答辩展示时都是关键素材。def judge_warning(row): 根据统计结果判定预警等级只依赖行内字段 if row[fail_count] 3 or row[fail_credit] 12: return red if row[fail_count] 2 or row[fail_credit] 6 or row[gpa_avg] 2.0: return orange if row[term_fail] 1: return yellow return None # 合并统计结果 result fail_stats.merge(gpa_stats, onstudent_no, howouter) result result.fillna({fail_count: 0, fail_credit: 0, term_fail: 0}) # 判定并生成预警记录 result[level] result.apply(judge_warning, axis1) warn_records result[result[level].notnull()].copy() warn_records[reason] ( warn_records[semester] 挂科 warn_records[term_fail].astype(int).astype(str) 门 )rule函数放在独立模块里不要和视图函数混在一起。因为你后续很可能要调整阈值比如某学院觉得单学期挂3门才算严重只需要改judge_warning里的常量不需要动任何其他代码。预警等级逻辑里有一个边界情况要注意红色和橙色的判定顺序。如果学生挂科4门他同时满足红色和橙色条件judge_warning里先判断红色再判断橙色返回的是红色覆盖关系靠函数顺序保证。这个顺序建议写注释说明不然以后改代码的人会把条件顺序调乱导致预警等级突然降级。4. 让预警结果可见Web接口、列表展示与图表设计4.1 后端接口怎么设计围绕辅导员的操作路径来后端不需要做成一堆CRUD接口先想清楚辅导员打开这个系统后要干嘛。他需要一个总览页看到全学院预警人数和等级分布需要一张预警名单按班级筛选需要每个学生的预警详情还需要能导出Excel。四个页面对应四个接口够了。from flask import Flask, jsonify, request, render_template from sqlalchemy import create_engine import pandas as pd app Flask(__name__) engine create_engine(sqlite:///warning_system.db) app.route(/api/summary) def api_summary(): 总览返回各预警等级人数和最高频挂科课程TOP5 summary_sql SELECT level, COUNT(*) AS cnt FROM warning_records WHERE is_processed 0 GROUP BY level level_df pd.read_sql(summary_sql, engine).to_dict(orientrecords) course_sql SELECT c.course_name, SUM(sc.is_fail) AS fail_cnt FROM score_stats sc JOIN courses c ON c.course_code sc.course_code GROUP BY c.course_name ORDER BY fail_cnt DESC LIMIT 5 course_df pd.read_sql(course_sql, engine).to_dict(orientrecords) return jsonify({levels: level_df, top_courses: course_df}) app.route(/api/warnings) def api_warnings(): 预警名单支持按班级和专业过滤返回列表 major request.args.get(major, ) class_name request.args.get(class_name, ) level request.args.get(level, ) sql SELECT w.id, s.student_no, s.name, s.major, s.class_name, w.semester, w.level, w.reason, w.is_processed FROM warning_records w JOIN students s ON s.student_no w.student_no WHERE 1 1 params [] if major: sql AND s.major ? params.append(major) if class_name: sql AND s.class_name ? params.append(class_name) if level: sql AND w.level ? params.append(level) df pd.read_sql(sql, engine, paramsparams) return jsonify(df.to_dict(orientrecords)) if __name__ __main__: app.run(debugTrue, port5000)接口返回DataFrame转dict比手写循环构造字典快得多对预警系统这种数据量级别完全够用。SQL里where 11是为了后面拼接条件方便实际执行时SQLite会把它优化掉不会拖慢查询。pandas的read_sql是支持params参数传列表的但要注意参数个数必须和SQL里的问号一一对应少传一个会报ProgrammingError这个错误信息经常让人误以为是SQL写错了。查询性能上预警记录表加上student_no和semester两个索引后几万条数据过滤基本都是毫秒级。不用优化到上缓存那是给自己加戏。4.2 前端展示用ECharts两个关键图表就足够展示层我用ECharts因为它的图表交互能力强而且基于JavaScript不需要后端参与绘图。前端不需要框架一个HTML页面加CDN引ECharts就够毕设演示时双击打开都能跑不需要配Node环境。预警系统最值得展示的图有两个。第一个是预警等级分布饼图直接告诉看的人“有多少黄色、橙色、红色”这是系统存在价值的直观证明。第二个是每个学期挂科率的趋势折线图能看到整体学业状态在变好还是变差这对应了系统“预警不是目的干预才是”的价值。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title学业预警总览/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idlevelChart stylewidth: 500px; height: 360px;/div script fetch(/api/summary) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(levelChart)); chart.setOption({ title: { text: 学期预警等级分布 }, tooltip: { trigger: item }, series: [{ type: pie, radius: [40%, 65%], data: data.levels.map(item ({ name: {yellow: 黄色预警, orange: 橙色预警, red: 红色预警}[item.level], value: item.cnt })) }] }); }); /script /body /htmlfetch请求接口拿数据然后map成ECharts需要的格式。这里唯一容易出问题的是level的英文编码和中文展示名的映射不能丢不然图上显示的是“yellow”这种原始值。如果你想把图表集成到Flask里用render_template传渲染好的数据也行但接口方式更清晰答辩时还可以顺便演示接口调试。4.3 把预警名单导出Excel要考虑辅导员真的会拿去用导出功能是预警系统里被低估的一环。很多毕设做了在线列表就结束了但实际业务里辅导员需要打印名单去开会、发邮件给家长。没有导出功能的系统辅导员用两次就弃了。导出用pandas的to_excel底层引擎是openpyxl。这里有两个细节一是文件名不要用固定值加上当前日期防止覆盖二是导出的列顺序要按人工习惯排列——学号、姓名、专业、班级、预警等级、触发原因、是否已处理而不是按数据库字段顺序。from datetime import datetime from flask import send_file import pandas as pd app.route(/api/warnings/export) def export_warnings(): 导出当前筛选条件下的预警名单为Excel class_name request.args.get(class_name, ) sql SELECT s.student_no AS 学号, s.name AS 姓名, s.major AS 专业, s.class_name AS 班级, w.level AS 预警等级, w.reason AS 触发原因, w.is_processed AS 是否已处理 FROM warning_records w JOIN students s ON s.student_no w.student_no if class_name: sql WHERE s.class_name ? df pd.read_sql(sql, engine, params[class_name] if class_name else []) # 英文等级转中文方便直接打印 df[预警等级] df[预警等级].map({ yellow: 黄色预警, orange: 橙色预警, red: 红色预警 }) filename fwarning_export_{datetime.now().strftime(%Y%m%d_%H%M)}.xlsx df.to_excel(filename, indexFalse, sheet_name预警名单) return send_file(filename, as_attachmentTrue)Excel输出的列名直接用中文是因为这份文件最终会给不写代码的人看。如果读者以后自己做的系统要对接其他程序列名就可以用英文字段减少编码麻烦。openpyxl引擎写出来的.xlsx文件在WPS和微软Excel里都能正常打开不用考虑老式.xls的兼容问题。5. 避坑成绩导入、口径对齐、环境与导出的四个实战问题5.1 现象学号导入后前导零丢失、成绩列全是科学计数法第一次拿真实教务系统导出的Excel跑的时候学号列显示成1.23457E11成绩列显示成78.00000001姓名是乱码。原因分两层一是Excel文件本身把单元格格式设成了数值二是pandas读取时会把看起来像数字的列自动推断成int64或float64格式。解决方式是读取时不要让它自动猜类型。用dtype参数强制指定列类型是最省心的做法df pd.read_excel( score_export.xlsx, dtype{ 学号: str, # 强制按字符串读保留前导零 课程代码: str, }, na_values[, NULL, 缺考] # 缺考在教务系统里可能是文本 )这里要注意的是dtype指定必须在你确认哪些列是编号列之后。如果你用了openpyxl直接读还可以先打印所有sheet的列名确认列名匹配再跑批处理。缺考值在很多教务系统里不是0而是“缺考”两个字或者直接空白不处理的话会被pandas读成NaN后续算挂科时NaN小于60的判断是False这个人就莫名其妙不算挂科了。5.2 现象预警结果和教务处人工统计的名单对不上这是最让人崩溃的坑。同一份成绩单系统算出挂科5门教务处那边的名单上写的是挂科3门。我当时逐条核对后发现问题出在“同一门课程不同学期重修”的情形上学生第一学期挂科第二学期重修通过成绩表里两条记录都在。按我的统计逻辑这门课计算为通过但教务处人工统计的口径是“曾经挂过就算累计挂科门数”。这个问题的本质是口径偏差不是代码bug。两个口径都有道理只算当前未通过课程是“当前学业风险”算历史挂科次数是“学习态度评估”。解决方法不是改代码而是在系统参数里增加一个开关叫“挂科口径”可选值为“当前挂科”和“累计挂科”。你要做的是在答辩时明确说出你这个系统选了哪个口径以及为什么选它。我系统里默认用的是“当前挂科”因为预警的目的是面向下一步的干预已经通过了就不需要再警示。预防这类问题的最好办法是拿一份真实的历史预警名单做对账哪怕只有几十个学生也行。写一段比对脚本把系统算出来的人和学校原来发过的人做差集然后逐个找原因。这个环节做下来你的系统在业务层面的说服力会上一个台阶。5.3 现象代码在自己电脑跑得好好的换台电脑就报错典型报错是ModuleNotFoundError: No module named pandas或者flask、sqlalchemy找不到。原因基本都是新机器上没有安装依赖包或者全局环境里装的是不同版本。解决方法是写一份requirements.txt并且在文档里写明安装步骤。生成依赖清单用pip freeze命令它会把你当前环境里所有包连同版本号导出来。但pip freeze有个问题它会连一些传递依赖也列进去比如pandas依赖的numpy。所以更推荐的做法是手动维护一份只包含顶层依赖的文件flask3.0 pandas2.0 sqlalchemy2.0 openpyxl3.1 scikit-learn1.3 urllib3 # 仅当需要requests调接口时换环境后直接执行pip install -r requirements.txt五分钟内就能跑起来。另外数据库文件.db文件如果是复制的要注意别把数据库文件也加入.gitignore不然换机器后程序能启动但查不到数据那种问题特别难定位因为报错信息只会显示“数据库表不存在”。5.4 现象导出Excel后打开学号变成了科学计数法这个坑在4.3节导出代码里埋了一半。pandas的to_excel会把学号列按字符串写入但如果学号本身在DataFrame里是int类型Excel打开时它还是会被识别成数值型长学号就会显示成科学计数法。最彻底的解决方法是保证DataFrame里学号列是object类型也就是字符串。从数据库读出来时SQLAlchemy返回String字段本身没问题但如果你在做groupby统计后又和别的表merge学号列的dtype有可能被自动转成int64——因为pandas的merge会自动对齐类型如果有一边的学号是字符串另一边的学号被读成了整数它会做隐式转换。排查技巧是在导出前打印df.dtypes盯着学号列看它到底是什么类型。如果不是object用df[学号] df[学号].astype(str)强制转回来。这个检查动作应该写成习惯因为它能顺带发现很多数据问题。6. 从规则预警走向预测预警让系统具备一点前瞻性规则预警有一个无法回避的弱点它只能对已经发生的事情做响应。学生挂科了系统才知道他有风险。如果能在学期初就预判出哪些人这学期大概率会挂科提前谈话干预效果会比事后预警好得多。用scikit-learn里的逻辑回归就能做到这一步这也是答辩时最能体现技术深度的地方。特征选取上用学生前两个学期的数据来预测本学期是否挂科最有效的三个特征是前两学期加权平均分、累计挂科门数、前两学期平均绩点。这三个指标都来自预警系统已有的表不需要额外采集数据这是选它们的最大理由。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 假设 df_features 是合并好的训练数据 # 列avg_score_prev(前两学期均分) fail_count_prev(累计挂科) gpa_prev(前两学期绩点) # 目标fail_this_term(本学期是否挂科1/0) X df_features[[avg_score_prev, fail_count_prev, gpa_prev]] y df_features[fail_this_term] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model LogisticRegression(max_iter1000) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))stratifyy是为了保证正负样本在训练集和测试集中的比例一致。如果你们的正样本挂科学生本身就少不做分层抽样很可能出现测试集里一个挂科样本都没有的情况精确率召回率全是0特别难看。模型效果上逻辑回归在这个任务的准确率能做到70%到80%之间就已经够用。因为学业预警不是一个“宁缺毋滥”的场景漏掉一个真正会挂科的学生比多提醒一个最后没挂科的学生代价大得多。所以重点看召回率而不是准确率。如果召回率不够高可以把逻辑回归的判定阈值从默认的0.5往下调比如调到0.3让更多风险学生进入预警名单。这个阈值参数建议放到配置文件里方便答辩时演示它对结果的影响。最后说一个做这类系统的体会真正花时间的从来不是写代码而是理解业务数据长什么样、口径之间怎么对齐。学业预警系统放在高校里是一个很小的应用但它把数据获取、清洗、规则建模、系统展示、结果验证这个完整链路串了起来。这恰恰是毕业后做数据相关工作最需要的能力。我当年做的时候大概花了三分之二的时间在找数据和核对规则上写代码只用了三分之一这个比例放到真实企业项目里也成立。答辩时把这个过程讲清楚比任何花哨功能都能打动人。希望这篇笔记能帮你把这个方向做得扎实、做得明白。本文还有配套的精品资源点击获取