ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python Flask + MySQL学生成绩管理系统源码全解析

Python Flask + MySQL学生成绩管理系统源码全解析

简介:面向需要完成数据库课程设计的高校学生,这份MySQL学生成绩管理系统课设报告提供了一套完整的系统设计与实现方案。该系统基于Python Flask与MySQL构建,覆盖管理员、教师、学生三类用户角色,实现选课、成绩录入、修改、删除、查询及统计等核心功能,可有效解决人工统计效率低、易出错的问题。资源为1个doc类型文档,压缩包整体3.95MB,内容依照课程设计报告标准结构展开,依次包含系统总体设计、需求分析、概要设计、数据库概念结构与逻辑结构设计、系统界面与功能代码实现、测试与总结等章节。读者可从中学习Flask+MySQL项目如何做需求梳理、数据表设计、前后端分离接口测试,并可参考其中的功能模块划分和实现代码,直接作为同类课设的撰写蓝本。目前已有5073人学习,适合需要快速搭建学生成绩管理系统并完成课设文档的开发者参考。

1. 三个角色一套系统:这就是你想要的数据库课设完整源码

期末周最折腾人的不是考试,是把几十个班的成绩汇总成表。这套基于 Python + Flask + MySQL 的学生成绩管理系统,就是冲着这个场景来的:管理员管理学生、教师、课程、专业;教师录入和统计所授课程的成绩;学生查成绩、选课信息、改密码。三种角色,三种权限,前后端分离,用 Postman 就能把接口全部测通。它没做服务器部署,单机就能跑,特别适合课程设计、毕业设计做数据库课设参考,或者想快速了解 Flask 怎么组织一个完整 Web 项目的开发者。

系统在 Windows 10 + PyCharm + MySQL 8.0 环境下开发,前端基于 HTML + CSS + JS 页面模板改造,后端接口按模块拆开。下面我从数据库设计、后端接口、前端联调、单机运行一路讲下去,把核心代码和踩坑点都拆开。

2. 数据库设计拆开看:6 张表加一个触发器

整个系统的数据模型围绕三个核心实体展开:学生、教师、管理员,再加上课程、专业、选课关系。数据库设计做得很规矩,从 E-R 图到关系模式再到物理建表,每一步都能对上。

2.1 实体关系与逻辑结构

先看关系模式,这套模式就是你建表的依据:

学生(学号、姓名、性别、出生年份、籍贯、入学年份、专业编号、密码) 教师(教师编号、姓名、性别、出生年份、密码) 课程(课程编号、课程名、开课学年、开课学期、学分、教师编号) 专业(专业编号、专业名) 学生选课(学号、课程编号、成绩) 专业选课(专业编号、课程编号) 管理员(用户名、密码)

学生和教师通过“授课”联系,一个教师教多门课,一门课被多个专业选修;学生通过 student_course 表与课程关联,成绩就挂在关联记录上。管理员是独立的账户体系,只负责维护基础数据。

2.2 核心表结构说明

数据库名grade_manage_system,一共 7 张表,其中 6 张是业务表。下面把最容易搞混的字段类型列出来:

表名关键字段类型与约束说明
studentstudent_idchar(10) 主键学生编号即登录账号
studentpasswordvarchar(128) not null存的是加密后的密码
teacherteacher_idchar(5) 主键教师编号即登录账号
coursecourse_idchar(5) 主键课程编号
coursecredittinyint unsigned 0~10学分范围约束
student_coursestudent_id + course_id联合主键学生选课成绩表
student_coursegradetinyint unsigned 0~100成绩可空,教师后录入
major_coursemajor_id + course_id联合主键专业选课关系

一个容易忽视的细节是char和varchar的选择:编号类字段都用char定长,因为学号、工号长度固定,查询走索引效率更好;姓名、籍贯这类长度不定的用varchar。课时设计里有一个major_course表单独存“专业能选哪些课”,而不是直接让学生选课,这样一来管理员只需配置专业与课程的映射,选课关系就能批量生成。

2.3 建表脚本与触发器:自动派发选课记录

建库建表脚本在项目文档里写得很完整,直接按顺序执行即可。不过其中最有意思的是那个触发器,它在向major_course插入一条专业选课关系后,自动为该专业所有学生生成对应的选课记录:

create database grade_manage_system; use grade_manage_system; create table major( major_id char(3) primary key, major_name varchar(20) not null unique ); create table student( student_id char(10) primary key, student_name varchar(20) not null, sex char(2), birth_year char(4), province varchar(20), enter_year char(4) not null, major_id char(3) not null, password varchar(128) not null ); alter table student add foreign key (major_id) references major (major_id) on delete cascade; create table teacher( teacher_id char(5) primary key, teacher_name varchar(20) not null, sex char(2), birth_year char(4), password varchar(128) not null ); create table course( course_id char(5) primary key, course_name varchar(20) not null, year char(4) not null, semester char(2) not null, teacher_id char(5) not null, credit tinyint unsigned not null ); alter table course add foreign key (teacher_id) references teacher (teacher_id) on delete cascade; create table major_course( major_id char(3), course_id char(5) ); alter table major_course add primary key (major_id, course_id); alter table major_course add foreign key (major_id) references major (major_id) on delete cascade; alter table major_course add foreign key (course_id) references course (course_id) on delete cascade; create table student_course( student_id char(10), course_id char(5), grade tinyint unsigned ); alter table student_course add primary key (student_id, course_id); alter table student_course add foreign key (student_id) references student (student_id) on delete cascade; alter table student_course add foreign key (course_id) references course (course_id) on delete cascade; create table admin( admin_name char(15) primary key, password varchar(128) not null ); delimiter // create trigger trigger_insert_1 after insert on major_course for each row begin insert into student_course (student_id, course_id, grade) select student_id, new.course_id, null from student where major_id = new.major_id; end// delimiter ;

这个触发器的逻辑很聪明:当管理员添加一条“某专业可选某课程”的映射时,MySQL 自动把这个专业下所有学生对应的student_course记录插进去,grade默认是NULL,等教师后续录入成绩。好处是学生不需要手动选课,坏处是如果管理员误删major_course记录,外键级联会把所有关联的成绩也一起删掉,操作时要注意。

执行这段脚本时还有两个细节。一是delimiter的问题,如果你在 Navicat 或命令行里直接粘贴触发器语句,不加delimiter会报语法错误——MySQL 默认以分号作为语句结束符,而触发器内部有分号,必须先把结束符临时改成//。二是外键的删除策略,这里的on delete cascade表示删掉专业表记录,该专业所有学生连带删除,数据关联比较“重”,生产环境建议改成on delete set null或加软删除标记,课程设计用 cascade 反而省事。

3. Flask 后端核心代码:一个登录请求从进入到返回的完整链路

这个项目的后端采用 Flask,前后端分离,接口用 Postman 测试。从源码结构可以看出,它的路由是按用户角色拆的,登录鉴权走 Flask session 机制。下面把最关键的几个接口拆开讲。

3.1 项目目录结构与路由拆分

典型的 Flask 单体项目布局,入口文件加蓝图或直接集中管理路由。常见做法是把每个角色的接口拆成独立的 Python 模块:

grade_manage_system/ ├── app.py # Flask 应用入口 ├── models/ # 数据库连接与查询封装 │ ├── __init__.py # pymysql 连接池初始化 │ └── db.py # 查询通用方法 ├── routes/ │ ├── __init__.py │ ├── auth.py # 登录、登出、修改密码 │ ├── student.py # 学生端接口 │ ├── teacher.py # 教师端接口 │ └── admin.py # 管理员端接口 ├── static/ # css / js / 图片 └── templates/ # html 模板

前端页面模板放在templates目录,静态资源在static,接口返回 JSON 格式数据,前端用 fetch 或 ajax 调用。

3.2 登录接口:Session 鉴权怎么设计

登录是三个角色共用的入口,前端把角色、账号、密码以 JSON 格式 POST 到/api/login,后端根据角色查不同表,校验通过后把用户信息写进 session。

from flask import Blueprint, request, session, jsonify from models.db import query_one import hashlib auth_bp = Blueprint('auth', __name__) def encrypt_password(raw: str) -> str: """对密码做 sha256 哈希,返回十六进制字符串""" sha = hashlib.sha256() sha.update(raw.encode('utf-8')) return sha.hexdigest() @auth_bp.route('/api/login', methods=['POST']) def login(): data = request.get_json() role = data.get('role') # student | teacher | admin username = data.get('username').strip() password = data.get('password') table_map = { 'student': 'student', 'teacher': 'teacher', 'admin': 'admin' } id_field_map = { 'student': 'student_id', 'teacher': 'teacher_id', 'admin': 'admin_name' } table = table_map[role] id_field = id_field_map[role] # 统一查用户表,密码字段存的是哈希值 sql = f"SELECT * FROM {table} WHERE {id_field} = %s" user = query_one(sql, (username,)) if not user: return jsonify({'code': 1, 'msg': '账号不存在'}) if user['password'] != encrypt_password(password): return jsonify({'code': 1, 'msg': '密码错误'}) # 登录成功,写入 session session['user_id'] = user[id_field] session['role'] = role session['user_name'] = user.get('student_name') or user.get('teacher_name') or username return jsonify({'code': 0, 'msg': '登录成功', 'data': { 'role': role, 'user_id': user[id_field], 'user_name': session['user_name'] }})

这段代码有几点值得学习。密码不是明文存储,而是用 sha256 哈希后存入varchar(128)字段,这也是建表时密码字段长度设成 128 的原因——哈希后的十六进制串长度固定。登录时先按角色确定去哪个表查、用哪个字段做匹配,再比对哈希值。比对时用了%s参数占位而不是拼接字符串,避免 SQL 注入。

session 里只放user_id、role、user_name三个字段,后续接口判断权限时直接读 session。这里要注意 Flask 的 session 默认是客户端 cookie 存储的,密钥要设置一个足够复杂的app.secret_key,否则存在被解密的隐患。如果部署到公网,建议改用服务端 session 方案。

3.3 学生查成绩接口:多表联查的完整链路

学生登录后查看成绩,前端请求/api/student/grade,后端从 session 拿当前登录的学生学号,连表查出课程名、学分、成绩等信息:

from flask import Blueprint, request, session, jsonify from models.db import query_all student_bp = Blueprint('student', __name__) @student_bp.route('/api/student/grade', methods=['GET']) def get_grade(): # 鉴权:确认当前登录的是学生角色 if session.get('role') != 'student': return jsonify({'code': 403, 'msg': '无权限'}) student_id = session.get('user_id') sql = """ SELECT c.course_id, c.course_name, c.credit, c.year, c.semester, t.teacher_name, sc.grade FROM student_course sc JOIN course c ON sc.course_id = c.course_id JOIN teacher t ON c.teacher_id = t.teacher_id WHERE sc.student_id = %s ORDER BY c.year DESC, c.semester """ result = query_all(sql, (student_id,)) return jsonify({'code': 0, 'data': result})

接口通过session.get('role')做角色校验,非学生身份直接返回 403。查询语句把学生选课表、课程表、教师表三表联查,一次拿全课程信息、授课教师、成绩排序按学年和学期降序,前端拿到 JSON 后直接渲染表格即可。

学生端的“查看课程对应教师”和“修改密码”也是类似的写法:修改密码需要先验证旧密码,再比对两次新密码是否一致,最后更新哈希值。改完不强制重新登录,因为用户没变,只更新了密码字段。

3.4 教师录成绩接口:事务与参数边界

教师端最有代表性的是成绩录入接口。教师只能操作自己担任授课教师的课程,这个约束要在 SQL 里卡住:

@teacher_bp.route('/api/teacher/grade', methods=['POST']) def update_grade(): if session.get('role') != 'teacher': return jsonify({'code': 403, 'msg': '无权限'}) teacher_id = session.get('user_id') data = request.get_json() course_id = data.get('course_id') student_id = data.get('student_id') grade = data.get('grade') # 校验成绩范围:0 ~ 100 if grade is None or int(grade) < 0 or int(grade) > 100: return jsonify({'code': 1, 'msg': '成绩必须在 0~100 之间'}) # 先确认这门课确实是该教师教授的 check_sql = """ SELECT course_id FROM course WHERE course_id = %s AND teacher_id = %s """ course = query_one(check_sql, (course_id, teacher_id)) if not course: return jsonify({'code': 1, 'msg': '只能操作自己教授的课程'}) update_sql = """ UPDATE student_course SET grade = %s WHERE student_id = %s AND course_id = %s """ execute(update_sql, (grade, student_id, course_id)) return jsonify({'code': 0, 'msg': '成绩更新成功'})

这里做了两层校验:第一层是成绩值必须在 0 到 100 之间,防止脏数据入库;第二层是通过teacher_id关联课程表确认归属权,避免教师 A 篡改教师 B 的课程成绩。参数说明:course_id是课程表中 5 位字符编号,student_id是学生学号,grade是整数。实际项目里前端传过来的可能是字符串,代码里的int(grade)转换如果失败,建议再包一层 try-except 返回友好提示。

管理员端接口是 CRUD 的标准套路,五个实体对应五套增删改查路由,逻辑集中在admin.py里。每个接口都带着session['role'] == 'admin'的角色校验,这是整个项目权限控制的骨架——业务层依赖 session,路由层做拦截,前后端分离下算是最简单实用的方案。

4. 前端页面与联调:模板工程四个模块工作区

前端这块,系统沿用了一套现成的 HTML 模板改出的页面,核心是 HTML + CSS + JS,和 Flask 后端通过 JSON 接口通信。这套页面我建议按模块理解:一个登录页、三个角色工作区。

4.1 页面文件体系

文件路径对应模块主要功能
templates/login.html登录页角色选择、账号密码提交、错误提示
templates/student.html学生工作区个人信息、成绩列表、修改密码三个 Tab
templates/teacher.html教师工作区个人信息、课程列表、选课学生详情、分数段统计
templates/admin.html管理员工作区五个子模块的增删改查
static/js/各页面的 fetch 请求汇总接口调用、渲染逻辑

4.2 登录页接口对接方式

前端通过 fetch 把表单数据转成 JSON 发给/api/login,拿到返回后按角色跳转到对应工作区页面:

async function handleLogin() { const role = document.getElementById('role').value; const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value; const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role, username, password }) }); const result = await response.json(); if (result.code === 0) { // 登录成功,按角色跳转 const pageMap = { student: '/student.html', teacher: '/teacher.html', admin: '/admin.html' }; window.location.href = pageMap[result.data.role]; } else { // 显示错误提示 document.getElementById('errorMsg').textContent = result.msg; } }

参数说明:role是登录身份,可选值student、teacher、admin;username是学号或教师编号或 admin;password是明文密码,传输走 POST body JSON。这里前端没有对密码做任何处理,是明文传给后端的,单机运行无所谓,真要部署上线就得加 HTTPS 和前端加密了。

4.3 Postman 测试接口的方法

文档里明确提到用 Postman 做接口测试,这套系统的接口设计是标准的 REST 风格:POST /api/login登录、GET /api/student/grade查成绩、POST /api/teacher/grade录成绩、POST /api/admin/student添加学生。测试时先要在 Postman 里设置请求头Content-Type: application/json,然后填 JSON 体。

有个细节:Flask 的 session 依赖 cookie,Postman 第一次登录后要在界面下方“Cookies”处确认 session 值已经写入。如果后续请求报 403 无权限,多半是 Postman 没有正确携带 cookie,而不是代码逻辑问题。在 Postman 的 Tests 标签里可以加一段自动断言pm.response.code === 200,方便批量回归。

单机联调时,Flask 默认跑在127.0.0.1:5000,前端页面用相对路径请求接口,所以不存在跨域问题。如果你把前端页面用 VS Code Live Server 打开再手动请求 Flask 接口,就会遇到跨域报错,因为浏览器端口变了——这正是为什么项目要求直接由 Flask 渲染页面,前后端代码在同一端口下服务。

5. 单机运行避坑记录:数据库课设最容易翻车的 4 个写法

这套系统我实际跑过,也帮别人排查过。下面四个问题出现频率最高,每一条都是真实的翻车现场。

5.1 MySQL 8.0 客户端连接报错

  • 现象:Flask 启动后访问接口,报Access denied for user 'root'@'localhost'或Authentication plugin 'caching_sha2_password' cannot be loaded。
  • 原因:MySQL 8.0 默认认证插件是caching_sha2_password,而项目常见做法是用较早版本的mysql_native_password创建用户,连接参数不一致。
  • 解决:建库脚本之外,执行一句 SQL 把用户认证方式改掉:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

或者在 Flask 连接配置里显式指定autocommit=True和charset='utf8mb4',避免中文乱码隐患。这个报错的时间成本很高,因为 Flask 启动时不会报错,第一次请求数据库才暴露。

5.2 触发器创建失败或反复触发

  • 现象:执行建表脚本时报You have an error in your SQL syntax,定位到触发器那段。
  • 原因:没有改delimiter,MySQL 在读到触发器内部的第一个分号时就把语句截断了。
  • 解决:按我前面写的脚本,先delimiter //再创建触发器,结束后delimiter ;恢复。注意 Navicat 里如果选中语句直接执行,它有自己处理流程,命令行和脚本执行时必须手动控制。

5.3 管理员误删专业,学生成绩连坐消失

  • 现象:管理员在页面删掉一条专业记录,结果该专业所有学生的成绩记录全没了,数据无法恢复。
  • 原因:major_course的外键设置了on delete cascade,删专业会级联删专业选课,再级联删学生选课。
  • 解决:简单方案是脚本里去掉major_course对major的外键级联,改成on delete restrict,让误删直接报错而不是静默清数据。课程设计阶段保留 cascade 拿到数据里查时注意顺序即可:先删学生选课,再删专业选课,最后删专业。

5.4 登录状态丢失,接口一直 403

  • 现象:Postman 登录成功,但调用其他接口时看到403 无权限。
  • 原因:Flask 的secret_key不稳定或每次启动随机生成,导致 cookie 中的 session 签名失效;也可能 Postman 没有保存 cookie。
  • 解决:在app.py里固定一个随机字符串作为app.secret_key,例如app.secret_key = 'grade-system-demo-key'。代码里不要每次启动os.urandom(24),否则开发者模式下关闭重开 session 全部过期,排查时要先看浏览器 Application 一栏的 Cookie 值是否存在。

6. 拿到源码后怎么改:给教师端加一个“导出成绩 Excel”的思路

先说一个最简单的入口级改造:项目跑通后,把教师端“查看统计”那个分数段数据导成 Excel 文件。这需求在实际课设答辩时很加分的。

实现思路不复杂。后端加一个接口/api/teacher/grade/export,参数是course_id,内部查出课程所有选课学生和成绩,用openpyxl写成一个xlsx文件返回:

from flask import send_file import openpyxl @teacher_bp.route('/api/teacher/grade/export', methods=['GET']) def export_grade(): course_id = request.args.get('course_id') # 查出学生成绩数据(省略查询逻辑) rows = [ {'student_id': '2021001001', 'student_name': '张三', 'grade': 88}, # ... ] wb = openpyxl.Workbook() ws = wb.active ws.title = '成绩表' ws.append(['学号', '姓名', '成绩']) for r in rows: ws.append([r['student_id'], r['student_name'], r['grade']]) file_path = f'./temp_{course_id}.xlsx' wb.save(file_path) return send_file(file_path, as_attachment=True, download_name=f'course_{course_id}_grade.xlsx')

前端教师页面加一个“导出”按钮,fetch 这个接口时用window.location.href直接触发浏览器下载。改造时注意send_file返回后记得清理或覆盖临时文件,或者用io.BytesIO直接内存中生成文件再返回,避免服务器磁盘堆积垃圾文件。

另一个常见的课程设计向改造点是给成绩加“班级排名”和“专业平均分”。做法是在get_grade查询里用一个窗口函数:AVG(grade) OVER (PARTITION BY c.course_id) AS avg_grade,MySQL 8.0 原生支持窗口函数,不需要额外装插件,对学生端展示“你高于平均分多少”这个功能来说足够惊艳。

这套系统整体的扩展点其实都藏在数据库设计里——因为三张基础表(student、teacher、course)都留了外键关系,加一个“选课申请”或“加分项”字段,只需要在对应表增加列,前端加一个输入框。我自己的习惯是:每次拿到这类课设项目,第一步永远是先跑数据库脚本,第二步用 Postman 把登录、查询、增删改的核心流程打一遍,第三步才会去读页面代码。数据库表结构和接口返回格式都对上了,前端渲染基本不会出大问题。这个流程我现在跑任何 Flask 项目都强制走一遍,能省掉后面大半的排查时间。希望这一篇能帮你在课设答辩前少踩几个坑,顺利跑起来。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进