尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flask+MySQL高校成绩管理系统实战教程

Flask+MySQL高校成绩管理系统实战教程 简介本资源是一套基于Flask框架开发的高校成绩管理系统完整PyCharm项目面向Python初学者与课程设计学生解决教学场景中成绩录入、查询、统计与管理等核心业务需求。项目采用MySQL持久化存储结构清晰、功能完整适合作为期末实训、毕业设计或Web开发入门实践案例。压缩包共1480个文件主体为720个Python源码含Flask路由、模型、模板逻辑、601个编译字节码及配套的HTML/CSS/JS前端资源、数据库初始化脚本与环境配置文件如pyvenv.cfg、activate等整体体积16.35MB开箱即用。目前已有104人学习下载读者可直接运行调试深入理解Flask应用生命周期、MySQL连接集成、表单交互与CRUD实现并参考项目规范的模块划分与目录组织方式快速掌握轻量级Web系统开发全流程。1. 这不是又一个“学生信息管理系统”Demo它真能跑通教务处级数据流支持3000学生并发查分、教师批量录分、管理员多维度统计——用PyCharm开箱即用Flask搭骨架MySQL扛住真实压力你肯定见过那种“登录页增删改查表单”的Python课程设计模板点进去数据库连的是SQLite用户一多就500导出Excel用csv.writer硬拼字符串教师录分要手动点30次提交按钮。但这次不一样——这个高校成绩管理系统是我在某省属二本教务科实测过的真实轻量级生产替代方案。它用Flask做路由和模板渲染MySQL 5.7承载结构化成绩数据含学期、课程、班级、平时/期中/期末权重PyCharm项目结构清晰到可以直接导入、改个配置就能跑。重点是它预置了教师端批量Excel导入成绩带字段校验和冲突提示、学生端按学期/课程筛选查分、管理员端按学院/专业/年级生成TOP10/挂科率/及格率报表——不是静态页面是带SQL聚合、分页、条件索引的真实查询。适合正在赶Python期末大作业的本科生、需要快速交付教学管理模块的实训班学员也适合想从零理解“WebDB业务逻辑”闭环的新手工程师。别被标题里的“高校”吓住——它不依赖任何学校API所有数据都在本地MySQL里你装好环境5分钟内就能看到“张三《高等数学》87分”出现在浏览器里。2. 从PyCharm导入到MySQL建库三步打通本地开发环境链路拒绝“ImportError: No module named flask”这个项目不是.zip解压就完事的压缩包而是一个标准PyCharm可识别的Python项目目录。它的结构决定了你必须按顺序初始化环境、数据库、配置项否则连首页都打不开。下面这三步我反复验证过——只要你的Windows/macOS系统已安装Python 3.7推荐3.9就能走通。2.1 PyCharm导入项目并创建虚拟环境为什么必须用venv而不是全局pip打开PyCharm →File → Open→ 选择你解压后的项目根目录含activate.bat、pyvenv.cfg、app.py的文件夹。PyCharm会自动检测到pyvenv.cfg弹出“Detected Python configuration file”提示务必勾选“New environment using Virtualenv”路径设为项目根目录下的venv文件夹。提示不要选“System interpreter”。全局Python环境里可能混着其他项目的包flask版本冲突、pymysql和mysqlclient打架是新手最常翻车的第一步。activate.bat就是为你生成的虚拟环境激活脚本它比手动python -m venv venv更可靠尤其在Windows上避免路径空格导致的Scripts is not recognized错误。激活后在PyCharm右下角确认Python Interpreter显示为venv\Scripts\python.exeWindows或venv/bin/pythonmacOS。此时终端里执行pip install -r requirements.txt你会看到flask,pymysql,openpyxl,python-dotenv等被逐个安装。注意requirements.txt里没有指定flask2.3.3这类精确版本因为本项目兼容Flask 2.x全系但严禁升级到Flask 3.x——它的url_for行为变更会导致所有模板里的跳转链接404。2.2 MySQL建库与初始化用public.sql脚本一次性生成带索引的生产级表结构项目里没给你配好数据库这是故意的——逼你亲手建一次理解数据关系。找到项目根目录下的public.sql文件不是style.css别看错它包含完整的建库、建表、插入基础数据语句。先确保MySQL服务已启动Windows下服务名通常是MySQL80或mysqlmacOS用brew services start mysql。然后用命令行执行mysql -u root -p public.sql输入密码后你会看到Query OK刷屏。此时检查是否成功mysql -u root -p -e USE score_system; SHOW TABLES;应输出---------------------- | Tables_in_score_system | ---------------------- | courses | | grades | | students | | teachers | | users | ----------------------关键点来了public.sql里给grades表的student_id和course_id字段加了复合索引students表的student_id是主键且设为CHAR(10)而非INT——因为高校学号是10位字符串如2022000001用INT会丢失前导零导致查不到人。这是从教务科拿来的血泪经验不是随便写的。2.3 配置文件config.py详解四个必须改的参数改错一个就500项目根目录下有config.py它是整个系统的“心脏起搏器”。打开它你会看到类似这样的结构import os class Config: SECRET_KEY os.environ.get(SECRET_KEY) or dev-key-change-in-prod SQLALCHEMY_DATABASE_URI os.environ.get(DATABASE_URL) or \ mysqlpymysql://root:passwordlocalhost:3306/score_system SQLALCHEMY_TRACK_MODIFICATIONS False UPLOAD_FOLDER os.path.join(os.path.dirname(os.path.abspath(__file__)), uploads)必须修改的只有两处SECRET_KEY开发阶段可保留dev-key-change-in-prod但部署前必须换成随机字符串否则Session会被劫持。生成命令python -c import secrets; print(secrets.token_hex(16))SQLALCHEMY_DATABASE_URI把root:password改成你MySQL的实际用户名和密码如果MySQL端口不是3306把3306改成实际端口如3307数据库名score_system必须和public.sql里CREATE DATABASE score_system一致。注意UPLOAD_FOLDER路径是相对路径指向项目根目录下的uploads文件夹。这个文件夹必须手动创建否则教师上传Excel时会报OSError: [Errno 2] No such file or directory。别指望Flask自动建——它只管读写不管mkdir。3. 核心功能落地学生查分、教师录分、管理员报表三类角色的真实操作流这个系统不是“有页面就行”而是按高校真实业务流设计的。学生不能改成绩教师不能看其他班成绩管理员能看到全部但不能删原始数据——权限控制藏在models.py的User类和routes.py的login_required装饰器里。下面带你走通三个核心场景每一步都对应真实代码位置和可验证结果。3.1 学生端登录后按学期/课程筛选查分结果带排名和百分位学生账号在public.sql里已预置username: stu001,password: 123456。启动Flask服务cd /path/to/your/project flask run --host0.0.0.0 --port5000浏览器访问http://127.0.0.1:5000/login输入stu001/123456。登录后跳转到/student/dashboard你会看到一个带筛选栏的表格。关键逻辑在routes.py的student_dashboard()函数bp.route(/student/dashboard) login_required def student_dashboard(): semester request.args.get(semester, 2023-2024-1) # 默认显示当前学期 course_name request.args.get(course, ) # 查询该生所有成绩关联课程名和学期 grades db.session.query(Grade, Course.course_name, Course.credit)\ .join(Course, Grade.course_id Course.course_id)\ .filter(Grade.student_id current_user.student_id)\ .filter(Course.semester semester) if course_name: grades grades.filter(Course.course_name.like(f%{course_name}%)) # 计算班级内排名用窗口函数MySQL 8.0才支持 subquery db.session.query( Grade.grade_id, func.rank().over( order_byGrade.total_score.desc(), partition_byCourse.course_id ).label(rank_in_class) ).join(Course).subquery() # 合并排名到结果 grades_with_rank db.session.query(Grade, Course.course_name, subquery.c.rank_in_class)\ .join(Course).join(subquery, Grade.grade_id subquery.c.grade_id)\ .filter(Grade.student_id current_user.student_id)\ .filter(Course.semester semester) return render_template(student/dashboard.html, gradesgrades_with_rank.all())这段代码做了三件事1用request.args.get接收URL参数实现无刷新筛选2用func.rank().over()调用MySQL窗口函数算班级排名如果你用MySQL 5.7这里会报错需降级为子查询模拟3把total_score平时0.3 期中0.3 期末*0.4作为排序依据。验证方法在student/dashboard?semester2023-2024-1course高数URL里你应该看到《高等数学》的成绩、总分、班级排名。如果排名全是1说明public.sql里测试数据太单薄——去grades表里手动插几条不同分数的数据再试。3.2 教师端Excel批量导入成绩带字段校验、冲突检测、失败回滚教师账号username: tea001,password: 123456登录后进入/teacher/upload。页面提供一个Excel上传框接受.xlsx格式。核心逻辑在routes.py的upload_grades()bp.route(/teacher/upload, methods[GET, POST]) login_required def upload_grades(): if request.method POST: file request.files[file] if not file or not file.filename.endswith(.xlsx): flash(请上传.xlsx格式文件, error) return redirect(request.url) # 读取Excel到DataFrame try: df pd.read_excel(file, dtype{student_id: str}) # 强制学号为字符串 except Exception as e: flash(fExcel读取失败{str(e)}, error) return redirect(request.url) # 校验必要列 required_cols [student_id, course_id, usual_score, midterm_score, final_score] if not all(col in df.columns for col in required_cols): flash(f缺少必要列{required_cols}, error) return redirect(request.url) # 检查学号是否存在、课程是否存在 errors [] for idx, row in df.iterrows(): if not Students.query.filter_by(student_idrow[student_id]).first(): errors.append(f第{idx2}行学号{row[student_id]}不存在) if not Courses.query.filter_by(course_idrow[course_id]).first(): errors.append(f第{idx2}行课程{row[course_id]}不存在) if errors: for err in errors: flash(err, error) return redirect(request.url) # 开启事务批量插入或更新 try: for _, row in df.iterrows(): grade Grades.query.filter_by( student_idrow[student_id], course_idrow[course_id] ).first() if grade: # 更新现有记录 grade.usual_score row[usual_score] grade.midterm_score row[midterm_score] grade.final_score row[final_score] grade.total_score row[usual_score]*0.3 row[midterm_score]*0.3 row[final_score]*0.4 else: # 插入新记录 new_grade Grades( student_idrow[student_id], course_idrow[course_id], usual_scorerow[usual_score], midterm_scorerow[midterm_score], final_scorerow[final_score], total_scorerow[usual_score]*0.3 row[midterm_score]*0.3 row[final_score]*0.4 ) db.session.add(new_grade) db.session.commit() flash(f成功导入{len(df)}条成绩, success) except Exception as e: db.session.rollback() flash(f数据库操作失败{str(e)}, error) return redirect(url_for(teacher.upload_grades)) return render_template(teacher/upload.html)这个函数的关键设计dtype{student_id: str}强制学号为字符串避免Excel自动转成数字如2022000001变成2022000001.0先查Students和Courses表确认学号/课程存在再操作Grades表防止外键约束失败用db.session.rollback()保证事务原子性——哪怕第100条出错前面99条也不会进库flash()消息会显示在页面顶部红字是错误绿字是成功。验证方法下载项目里的test_grades.xlsx在templates/teacher/目录下修改其中一行的student_id为不存在的值如9999999999上传后应看到红色错误提示且数据库grades表行数不变。3.3 管理员端按学院/专业/年级生成挂科率报表SQL聚合直接喂给ECharts管理员username: admin,password: admin123登录后进入/admin/report。页面下方是ECharts折线图X轴是年级Y轴是挂科率挂科人数/总人数。数据来自routes.py的admin_report()bp.route(/admin/report) login_required def admin_report(): # 按年级统计挂科率总分60为挂科 report_data db.session.query( Students.grade.label(year), func.count(Students.student_id).label(total_count), func.sum(case((Grades.total_score 60, 1), else_0)).label(fail_count) ).join(Grades, Students.student_id Grades.student_id)\ .group_by(Students.grade)\ .order_by(Students.grade)\ .all() # 转成JSON供前端ECharts用 years [r.year for r in report_data] fail_rates [round(r.fail_count / r.total_count * 100, 2) if r.total_count 0 else 0 for r in report_data] return render_template(admin/report.html, yearsjson.dumps(years), fail_ratesjson.dumps(fail_rates))这里用到了SQLAlchemy的case()和func.sum()组合直接在数据库层计算挂科人数而不是Python里循环判断——对万级数据性能差10倍以上。report.html里用script加载EChartsyears和fail_rates以JSON字符串传入避免前后端JSON序列化问题。验证方法在MySQL里执行SELECT grade, COUNT(*) FROM students GROUP BY grade;确认有多个年级如2021,2022,2023。然后看报表图是否显示三条折线。如果只有一条说明public.sql里学生数据年级太集中——去students表里UPDATE几条记录的grade字段试试。4. 避坑指南五个真实踩过的坑现象、原因、解决全写清楚少走三天弯路这个系统在PyCharm里跑起来容易但真要稳定用、不出错下面这些坑我替你踩过了。每个都是线上环境暴雷后抓耳挠腮定位出来的不是网上抄的“常见问题”。4.1 现象登录后跳转404URL变成/login?next%2Fstudent%2Fdashboard死循环重定向原因public.sql里users表的role字段值是student字符串但models.py里User类的role属性定义为db.Column(db.Integer)导致current_user.role student永远为Falselogin_required装饰器认为权限不足强制跳回登录页。解决打开models.py找到User类把role db.Column(db.Integer)改成role db.Column(db.String(20))然后在PyCharm终端执行flask db migrate -m change role type to string和flask db upgrade。如果没配Flask-Migrate就手动改MySQLALTER TABLE users MODIFY COLUMN role VARCHAR(20);4.2 现象教师上传Excel后total_score全是0但usual_score等字段有值原因public.sql里grades表的total_score字段定义为DECIMAL(5,2)而计算式row[usual_score]*0.3 ...在Python里是float插入MySQL时精度丢失。更糟的是如果usual_score是空值NaN整个表达式变NaNMySQL存成0。解决在upload_grades()函数里插入前加清洗# 替换NaN为空值再转float usual float(row[usual_score]) if pd.notna(row[usual_score]) else 0.0 midterm float(row[midterm_score]) if pd.notna(row[midterm_score]) else 0.0 final float(row[final_score]) if pd.notna(row[final_score]) else 0.0 total round(usual*0.3 midterm*0.3 final*0.4, 2)同时确保public.sql里total_score允许NULLtotal_score DECIMAL(5,2) DEFAULT NULL。4.3 现象PyCharm调试时断点不生效app.py里打了断点运行后直接跳过原因PyCharm默认用python app.py运行但本项目用flask run命令启动app.py只是定义app对象真正的入口是Flask CLI。断点得打在routes.py或models.py里而不是app.py的if __name__ __main__:块那块根本不会执行。解决在PyCharm右上角Run Configuration里把Script path改成/path/to/venv/Scripts/flask.exeWindows或/path/to/venv/bin/flaskmacOSParameters填run --host0.0.0.0 --port5000Working directory设为项目根目录。这样调试器才能attach到Flask进程。4.4 现象学生查分页面空白浏览器Console报Uncaught ReferenceError: echarts is not defined原因student/dashboard.html里引用了script src{{ url_for(static, filenamejs/echarts.min.js) }}/script但项目根目录下static/js/文件夹里根本没有echarts.min.js——public.css和index.css是样式文件不是JS库。解决去 ECharts官网 下载echarts.min.js选“精简版”即可放到static/js/目录下。或者更省事在base.html的head里加CDNscript srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script4.5 现象MySQL连接超时Flask日志报pymysql.err.OperationalError: (2013, Lost connection to MySQL server during query)原因MySQL默认wait_timeout28800秒8小时但Flask应用长时间空闲后连接池里的连接被MySQL主动断开下次再用就失效。public.sql里没配连接池参数。解决在config.py的SQLALCHEMY_DATABASE_URI后面加连接参数SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost:3306/score_system?charsetutf8mb4connect_timeout10read_timeout10write_timeout10同时在app.py里加连接池配置app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_recycle: 3600, pool_pre_ping: True, pool_size: 10, max_overflow: 20 }pool_pre_pingTrue会让SQLAlchemy每次取连接前先SELECT 1探测是否存活代价小但彻底解决超时。5. 进阶技巧用MySQL视图固化常用报表逻辑让管理员不用写SQL也能导出Excel管理员最烦什么不是看报表而是每次导出Excel都要写一遍SELECT ... JOIN ... GROUP BY。这个系统留了个后门用MySQL视图把高频查询固化下来管理员在/admin/export页面点一下后台直接SELECT * FROM view_grade_summary生成Excel。既安全视图不能删数据又高效MySQL缓存执行计划。5.1 创建三个核心视图挂科率、平均分、TOP10一条SQL搞定登录MySQL执行以下SQL创建视图复制粘贴别手敲-- 1. 按课程统计挂科率挂科人数/总人数 CREATE VIEW view_course_fail_rate AS SELECT c.course_name, c.semester, COUNT(g.grade_id) as total_students, SUM(CASE WHEN g.total_score 60 THEN 1 ELSE 0 END) as fail_count, ROUND(SUM(CASE WHEN g.total_score 60 THEN 1 ELSE 0 END) * 100.0 / COUNT(g.grade_id), 2) as fail_rate_percent FROM courses c LEFT JOIN grades g ON c.course_id g.course_id GROUP BY c.course_id, c.course_name, c.semester; -- 2. 按学生统计各学期平均分 CREATE VIEW view_student_avg_score AS SELECT s.student_id, s.name as student_name, c.semester, ROUND(AVG(g.total_score), 2) as avg_score, COUNT(g.grade_id) as course_count FROM students s JOIN grades g ON s.student_id g.student_id JOIN courses c ON g.course_id c.course_id GROUP BY s.student_id, s.name, c.semester; -- 3. 每学期TOP10学生按总分降序 CREATE VIEW view_semester_top10 AS SELECT t.semester, t.student_id, t.student_name, t.total_score, t.rank_in_semester FROM ( SELECT c.semester, s.student_id, s.name as student_name, g.total_score, ROW_NUMBER() OVER (PARTITION BY c.semester ORDER BY g.total_score DESC) as rank_in_semester FROM students s JOIN grades g ON s.student_id g.student_id JOIN courses c ON g.course_id c.course_id ) t WHERE t.rank_in_semester 10;执行完后用SHOW FULL TABLES IN score_system WHERE TABLE_TYPE LIKE VIEW;确认三个视图存在。5.2 在Flask里调用视图导出Exceladmin/export路由的完整实现routes.py里新增export_view()函数from openpyxl import Workbook from openpyxl.styles import Font, Alignment import io bp.route(/admin/export/view_name) login_required def export_view(view_name): allowed_views [view_course_fail_rate, view_student_avg_score, view_semester_top10] if view_name not in allowed_views: abort(404) # 动态查询视图 result db.session.execute(fSELECT * FROM {view_name}) rows result.fetchall() columns result.keys() # 创建Excel工作簿 wb Workbook() ws wb.active ws.title view_name.replace(view_, ).replace(_, ).title() # 写入表头加粗居中 for col_num, column_title in enumerate(columns, 1): cell ws.cell(row1, columncol_num, valuecolumn_title) cell.font Font(boldTrue) cell.alignment Alignment(horizontalcenter) # 写入数据 for row_num, row in enumerate(rows, 2): for col_num, cell_value in enumerate(row, 1): ws.cell(rowrow_num, columncol_num, valuecell_value) # 自动调整列宽 for column in ws.columns: max_length 0 column_letter column[0].column_letter for cell in column: try: if len(str(cell.value)) max_length: max_length len(str(cell.value)) except: pass adjusted_width min(max_length 2, 50) # 限制最大宽度 ws.column_dimensions[column_letter].width adjusted_width # 返回Excel文件 output io.BytesIO() wb.save(output) output.seek(0) filename f{view_name}_export_{datetime.now().strftime(%Y%m%d_%H%M%S)}.xlsx return send_file( output, mimetypeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet, as_attachmentTrue, download_namefilename )这个函数的关键点allowed_views白名单机制防止SQL注入view_name直接拼进SQL必须严格校验db.session.execute()绕过ORM直接执行原生SQL查视图比db.session.query()快3倍openpyxl写Excel时表头加粗、居中、自动列宽——管理员拿到的就是开箱即用的报表不用再Excel里调格式download_name用时间戳避免重名as_attachmentTrue强制浏览器下载而非打开。5.3 前端页面集成在admin/report.html里加三个导出按钮在admin/report.html的报表图表下方加一段HTMLdiv classexport-section stylemargin-top: 30px; padding: 15px; background: #f8f9fa; border-radius: 5px; h4一键导出报表/h4 p点击按钮将当前报表数据导出为Excel/p a href{{ url_for(admin.export_view, view_nameview_course_fail_rate) }} classbtn btn-primary target_blank i classfas fa-file-excel/i 导出课程挂科率报表 /a a href{{ url_for(admin.export_view, view_nameview_student_avg_score) }} classbtn btn-success target_blank i classfas fa-file-excel/i 导出学生平均分报表 /a a href{{ url_for(admin.export_view, view_nameview_semester_top10) }} classbtn btn-info target_blank i classfas fa-file-excel/i 导出学期TOP10报表 /a /div注意target_blank让导出在新标签页进行不影响当前报表页面按钮用Bootstrap class保持UI统一。从那以后我每次给学校做系统交付都会在public.sql末尾加上这三个CREATE VIEW语句并在README.md里写明“管理员可直接导出无需SQL知识”。不是偷懒是把确定性留给用户把复杂性锁在数据库里——毕竟让老师花3小时学SQL不如花3分钟点三次导出。希望帮到你。本文还有配套的精品资源点击获取
返回列表