简介:这份资源是面向高校数据库课程设计场景的MySQL学生成绩管理系统完整课程设计报告,适合计算机相关专业学生、任课教师及教务管理人员参考,用于解决成绩录入、统计与查询效率低、人工统计易出错的问题。压缩包内共1个doc文档,约3.95MB,内容为华东交通大学课程设计报告,按系统总体设计、需求分析、概要设计、系统实现、测试与总结五章展开,涵盖项目背景与意义、管理员/教师/学生三类角色的权限划分、数据库概念结构与逻辑结构设计、界面设计与主要功能程序代码,以及功能测试与总结反思。系统采用Python+Flask+MySQL实现,前端基于HTML、CSS、JavaScript并借鉴模板改良,前后端分离开发,借助Postman完成接口测试,可在单机环境运行。已有5074人学习下载,读者可据此了解课设报告的写作框架、需求分析与数据库设计思路,并参考成绩增删改查、选课与权限管理等模块的实现方式。
1. 从一份数据库课设说起:这个 Flask + MySQL 成绩管理系统能跑出什么
期末周最让人头大的不是考试本身,而是教务那边催着要的成绩统计表。一个专业几百号人、七八门课,用 Excel 手工汇总,改一个分数就得重算一遍,错一行还查不出来。这份 MySQL 学生成绩管理系统就是冲着这个场景做的:Python + Flask 做后端,MySQL 8.0 存数据,前端用 HTML + CSS + JS 拼页面,前后端分离,单机就能跑起来。它把管理员、教师、学生三类角色的权限拆得清清楚楚,选课、录成绩、分数段统计、改密码这些教务高频操作都有对应页面。对正在做数据库课设、想找一个结构完整又能二次开发的教育管理类项目的同学来说,这套代码的参考价值在于它的表设计和触发器写法,而不是界面有多花哨。
2. 技术栈与目录结构:Flask 后端怎么和 MySQL 对上话
2.1 技术选型拆解:为什么是 Flask 而不是 Django
这个项目选 Flask 是有现实考虑的。课设场景下,Django 自带的 ORM、Admin、认证体系虽然省事,但会把数据库层包得太厚,反而不利于展示「表结构怎么设计、SQL 怎么写」这个课设核心。Flask 轻,路由和视图函数一一对应,学生看代码时能直接看到每个接口背后执行了哪条 SQL,这对数据库课程设计来说是加分项。
后端大致是这么组织的:app.py作为入口创建 Flask 应用并注册蓝图,config.py里放数据库连接参数,models/或直接在视图里用pymysql/mysql-connector-python执行 SQL,routes/下按角色拆成admin.py、teacher.py、student.py、auth.py几个蓝图。前端是独立的静态页面,放在static/和templates/下,通过fetch或axios调后端 JSON 接口。这种前后端分离的写法在课设里不算常见,但好处是接口能用 Postman 单独测,排查问题时能快速定位是前端传参错了还是 SQL 写错了。
数据库连接这块,常见做法是用连接池而不是每次请求都新建连接。下面是一个典型的配置和获取连接的写法:
# config.py DB_CONFIG = { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "your_password", "database": "grade_manage_system", "charset": "utf8mb4", "cursorclass": pymysql.cursors.DictCursor # 返回字典,方便转 JSON } # db.py import pymysql from dbutils.pooled_db import PooledDB from config import DB_CONFIG pool = PooledDB( creator=pymysql, maxconnections=10, # 最大连接数,课设单机够用 mincached=2, # 启动时预建 2 个连接 blocking=True, # 连接耗尽时阻塞等待而不是报错 **DB_CONFIG ) def get_conn(): return pool.connection()DictCursor这个参数值得单独说一句:默认游标返回的是元组,前端拿到[["2021001", "张三", 88]]还得自己映射字段名;换成字典游标后直接是[{"student_id": "2021001", "student_name": "张三", "grade": 88}],jsonify一下就能返回,省掉一层转换。maxconnections设 10 是单机课设的保守值,真要多用户并发再往上调,但 MySQL 默认max_connections是 151,别设得比它还大。
2.2 目录结构:七个表撑起三类角色的全部操作
项目的目录大致长这样,我按实际开发习惯补全了文件职责说明:
grade_manage_system/ ├── app.py # Flask 入口,注册蓝图、启动服务 ├── config.py # 数据库连接配置 ├── db.py # 连接池封装 ├── routes/ │ ├── auth.py # 登录、登出、改密码 │ ├── admin.py # 学生/教师/课程/专业/选课的增删改查 │ ├── teacher.py # 课程管理、成绩录入、分数段统计 │ └── student.py # 个人信息、成绩查询 ├── static/ │ ├── css/ # 页面样式 │ └── js/ # fetch 调用接口、表单校验 ├── templates/ │ ├── login.html │ ├── admin/ # 管理员各功能页 │ ├── teacher/ │ └── student/ └── sql/ └── init.sql # 建库建表 + 触发器 + 初始数据数据库一共七张表:student、teacher、course、major、admin、student_course、major_course。这个表数量在课设里算克制,但关系设计得比较完整——student.major_id外键指向major,course.teacher_id外键指向teacher,student_course是学生和课程的多对多中间表并带grade字段,major_course是专业和课程的多对多中间表。三类角色的权限差异本质上就是「能操作哪几张表」:管理员全表增删改查,教师只能改student_course.grade和查自己teacher_id下的课程,学生只能读自己的student行和student_course行。
提示:
major_course和student_course都是联合主键,建表时别忘了primary key (major_id, course_id)和primary key (student_id, course_id),否则同一学生同一课程能插进多条成绩记录,统计时直接翻倍。
3. 数据库设计落地:建表、外键与那个自动选课的触发器
3.1 七张表的建表顺序与级联删除
建表有严格的顺序依赖:被引用的表必须先建。major和teacher要先于student和course,course又要先于major_course和student_course。项目里的建表脚本按这个顺序走,但外键是后来用alter table补的,这种写法在课设里很常见——先建表结构,再统一加约束,方便调试时单独开关。
-- 1. 专业表(被 student 和 major_course 引用,最先建) create table major( major_id char(3) primary key, major_name varchar(20) not null unique ); -- 2. 学生表 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; -- 3. 教师表 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 ); -- 4. 课程表 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; -- 5. 专业选课表(联合主键) create table major_course( major_id char(3), course_id char(5), 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; -- 6. 学生选课成绩表(联合主键) create table student_course( student_id char(10), course_id char(5), grade tinyint unsigned, 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; -- 7. 管理员表 create table admin( admin_name char(15) primary key, password varchar(128) not null );on delete cascade是这套表设计里最值得注意的一处。删掉一个专业,该专业下所有学生和major_course记录会跟着删;删掉一门课,所有选课成绩也会跟着删。这在课设演示时很方便,但真实教务系统里通常不敢这么干——成绩是审计数据,删课程不该连带删成绩。二次开发时如果要做「软删除」,把cascade换成restrict,然后在应用层加is_deleted标记字段,是更稳妥的做法。
grade字段用tinyint unsigned存 0~100,比int省空间,但要注意unsigned的范围是 0~255,插入 101 不会报错而是直接存进去。应用层必须做校验,不能指望数据库拦。
3.2 触发器实现自动选课:一行 insert 带出全班记录
项目里最有意思的一段 SQL 是major_course上的after insert触发器。管理员给某个专业添加一门课时,触发器自动把这个专业下所有学生都插入student_course,成绩先置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 s.student_id, new.course_id, null from student s where s.major_id = new.major_id; end// delimiter ;逐段拆一下:after insert表示在major_course插入成功后触发,for each row是行级触发器,new.course_id和new.major_id引用刚插入的那行数据。insert ... select把student表里major_id匹配的所有学生一次性写进student_course,grade给null表示「已选课未出分」。
这里有个坑:如果某个学生之前已经通过其他途径选过这门课,student_course的联合主键会冲突,整个触发器执行失败,major_course的插入也会回滚。稳妥的写法是加insert ignore或者on duplicate key update grade = grade,让重复记录静默跳过。另外delimiter //不能省,否则 MySQL 客户端会把触发器体内的第一个分号当成语句结束。
注意:触发器逻辑藏在数据库里,应用层代码完全看不到。新人接手时如果不知道有这个触发器,排查「为什么加了一门课学生成绩表突然多出几百行」会非常痛苦。建议在
init.sql里写清楚注释,或者在 README 里单独列一节说明。
4. 后端接口实现:登录鉴权、成绩录入与分数段统计
4.1 登录与角色分流:一个接口怎么区分三类用户
登录接口的设计有个取舍:是三个角色各一个登录接口,还是一个接口带role参数。项目用的是后者,前端登录页有个下拉框选身份,提交时带上role,后端根据角色去不同的表查密码。
# routes/auth.py from flask import Blueprint, request, jsonify, session import hashlib from db import get_conn auth_bp = Blueprint("auth", __name__) TABLE_MAP = { "admin": ("admin", "admin_name"), "teacher": ("teacher", "teacher_id"), "student": ("student", "student_id"), } @auth_bp.route("/api/login", methods=["POST"]) def login(): data = request.get_json() role = data.get("role") username = data.get("username", "").strip() password = data.get("password", "") if role not in TABLE_MAP: return jsonify({"code": 400, "msg": "非法角色"}), 400 table, id_col = TABLE_MAP[role] conn = get_conn() try: with conn.cursor() as cur: sql = f"select password from {table} where {id_col} = %s" cur.execute(sql, (username,)) row = cur.fetchone() finally: conn.close() if not row: return jsonify({"code": 401, "msg": "账号不存在"}), 401 # 密码用 sha256 存储,比对时同样哈希 if hashlib.sha256(password.encode()).hexdigest() != row["password"]: return jsonify({"code": 401, "msg": "密码错误"}), 401 session["role"] = role session["uid"] = username return jsonify({"code": 0, "msg": "ok", "role": role})TABLE_MAP把角色映射到表名和主键列名,避免写三份几乎一样的查询。f-string拼表名这里看着像 SQL 注入,但table和id_col都来自白名单字典,用户输入只进%s占位符,是安全的。密码比对用sha256,课设里够用,真实项目该上bcrypt加盐。session存角色和用户 ID,后续接口用装饰器校验权限,比如教师接口先检查session.get("role") == "teacher"。
初始密码规则是学生和教师的密码默认等于自己的编号,管理员固定admin/asdfg13579。这个设计在演示时方便,但上线前必须强制改密,否则任何人知道学号就能登录。
4.2 教师录成绩与分数段统计:一条 update 加一条 group by
教师的核心操作是给student_course里的grade赋值,以及按分数段统计人数。录成绩接口要校验这门课确实属于当前登录教师,防止教师改别人课的成绩。
# routes/teacher.py @teacher_bp.route("/api/grade", methods=["POST"]) def update_grade(): if session.get("role") != "teacher": return jsonify({"code": 403, "msg": "无权限"}), 403 data = request.get_json() course_id = data["course_id"] student_id = data["student_id"] grade = int(data["grade"]) if not 0 <= grade <= 100: return jsonify({"code": 400, "msg": "成绩需在 0~100"}), 400 conn = get_conn() try: with conn.cursor() as cur: # 先确认这门课归当前教师 cur.execute( "select 1 from course where course_id=%s and teacher_id=%s", (course_id, session["uid"]) ) if not cur.fetchone(): return jsonify({"code": 403, "msg": "非本人课程"}), 403 cur.execute( "update student_course set grade=%s " "where student_id=%s and course_id=%s", (grade, student_id, course_id) ) conn.commit() finally: conn.close() return jsonify({"code": 0, "msg": "已保存"})分数段统计用一条case when聚合就能出结果,不用在 Python 里循环:
select sum(case when grade >= 90 then 1 else 0 end) as excellent, sum(case when grade >= 80 and grade < 90 then 1 else 0 end) as good, sum(case when grade >= 70 and grade < 80 then 1 else 0 end) as medium, sum(case when grade >= 60 and grade < 70 then 1 else 0 end) as pass, sum(case when grade < 60 then 1 else 0 end) as fail, count(*) as total from student_course where course_id = %s and grade is not null;grade is not null这个条件不能漏,否则未录成绩的学生会被算进fail段,统计结果直接失真。sum(case when ...)的写法比多次count(*) where少扫几遍表,课设数据量下差别不大,但习惯养成了对以后有好处。
4.3 管理员增删改查:外键约束下的删除顺序
管理员的删除操作最容易翻车。因为外键带cascade,删major会连带删student和major_course,删course会连带删student_course。但student和course之间没有直接外键,删学生不会自动清student_course——等等,student_course.student_id是有外键指向student的,所以删学生会级联删成绩。真正要注意的是删除顺序和事务:一次删多个关联实体时,包在事务里,失败能回滚。
@admin_bp.route("/api/student/<sid>", methods=["DELETE"]) def delete_student(sid): if session.get("role") != "admin": return jsonify({"code": 403}), 403 conn = get_conn() try: with conn.cursor() as cur: cur.execute("delete from student where student_id=%s", (sid,)) affected = cur.rowcount conn.commit() except Exception as e: conn.rollback() return jsonify({"code": 500, "msg": str(e)}), 500 finally: conn.close() return jsonify({"code": 0, "deleted": affected})rowcount返回实际删除行数,前端据此提示「删除成功」还是「记录不存在」。rollback在异常分支里必须调,否则连接归还池子时可能带着未提交事务,下一个请求拿到这条连接会看到脏数据。
5. 避坑与排查:跑这个项目最容易卡住的五个地方
5.1 现象:启动报Access denied for user 'root'@'localhost'
原因基本是config.py里的密码和本机 MySQL 实际密码不一致,或者 MySQL 8.0 的认证插件是caching_sha2_password而驱动版本太老不认。解决分两步:先用命令行mysql -u root -p确认能登进去,能登说明密码对;再检查pymysql版本,低于 0.10 的升级一下,或者把用户认证方式改成mysql_native_password。改完记得重启 Flask,配置是启动时读的。
5.2 现象:触发器建好了,但加专业选课没生成学生成绩记录
先查触发器在不在:show triggers from grade_manage_system;。如果不在,多半是建触发器时delimiter没生效,客户端把触发器体截断了。如果触发器在但没触发,检查major_course插入的major_id在student表里有没有对应学生——专业下没学生,insert ... select自然插 0 行。还有一种情况是联合主键冲突导致整个触发器静默失败,看 MySQL 错误日志能找到Duplicate entry。
5.3 现象:前端调接口一直 401,Postman 里却正常
这是跨域和 session 的经典组合问题。前后端分离时,前端页面和 Flask 不在同一个源,浏览器默认不带 cookie,session就丢了。解决要么在 Flask 里配CORS(supports_credentials=True)且前端fetch加credentials: "include",要么课设阶段干脆把前端页面用 Flask 的render_template托管,同源就没这问题。项目原文说「未做服务器部署,单机测试」,那用后者更省事。
5.4 现象:成绩统计里「不及格」人数比实际多
九成是 SQL 里漏了grade is not null。未录成绩的记录grade是null,null < 60在 SQL 里结果是null不是true,但sum(case when grade < 60 then 1 else 0 end)里null走else分支算 0——等等,这里要仔细:case when null < 60条件为null,不满足when,走else 0,所以不会多算。真正多算的场景是用count(*)减各段之和,或者用where grade < 60单独查时把null排除了导致总数对不上。排查方法:先select count(*) from student_course where course_id=? and grade is null看有多少未录,再和各段之和加总核对。
5.5 现象:删除专业后学生还在,但登录报错
student.major_id外键带cascade,删专业应该连带删学生。如果学生还在,说明建表时外键没加上——项目里外键是alter table补的,如果执行顺序错了或者中途报错被忽略,约束就不存在。用show create table student;看输出里有没有CONSTRAINT ... FOREIGN KEY,没有就手动补上。补之前先清理孤儿数据,否则加约束会失败。
6. 二次开发改造:从课设 Demo 到能用的成绩工具
这套代码跑通之后,最值得动手改的是三处,按投入产出比排序。
第一处是密码存储。现在用sha256无盐哈希,学号当初始密码,等于裸奔。改成bcrypt只要动auth.py两行:登录比对用bcrypt.checkpw,改密用bcrypt.hashpw,再在student和teacher表加个pwd_changed标记,首次登录强制跳改密页。这个改动小,但演示时能讲出「安全设计」的加分点。
第二处是触发器改成应用层事务。触发器逻辑藏在数据库里,调试和迁移都麻烦。把major_course插入后的批量选课挪到admin.py里,用同一个事务包住「插专业选课 + 批量插学生选课」,代码可读性和可控性都更好。改造时注意保留insert ignore语义,避免重复选课报错。
第三处是加一个「成绩导出」接口。教务最终要的是 Excel,用openpyxl把student_course联查student、course的结果写成.xlsx,前端一个下载按钮就完事。这个功能不在原项目里,但它是这类系统真实落地时被问得最多的需求,加上去之后项目完整度明显不一样。
| 改造点 | 涉及文件 | 工作量 | 演示价值 |
|---|---|---|---|
| bcrypt 密码 | auth.py、建表脚本 | 半天 | 高 |
| 触发器转应用层 | admin.py、init.sql | 一天 | 中 |
| 成绩导出 Excel | 新增 export.py | 半天 | 高 |
验证改造是否成功,我一般走一遍固定链路:管理员建专业 → 加专业选课(看学生成绩表是否自动生成)→ 教师登录录一个 95 分和一个 55 分 → 看统计里优秀和不及格各 1 人 → 学生登录查成绩 → 导出 Excel 核对行数。这条链路覆盖了三类角色和触发器、聚合、导出三个关键逻辑,任何一环断了都能立刻定位。
血泪经验是:每次改完 SQL 相关代码,先把init.sql在干净库上重跑一遍再测,别在旧数据上验证。我有次在已有数据的库上测级联删除,结果把演示数据删了一半,临时补数据补到半夜。从那以后我每次动表结构或触发器,都强制在全新数据库上从init.sql走一遍完整链路,确认无误再碰开发库。希望帮到你。
本文还有配套的精品资源,点击获取