
简介这份资源是面向高校数据库课程设计场景的MySQL学生成绩管理系统完整课程设计报告适合正在完成数据库课设、需要参考系统设计与实现思路的计算机相关专业学生。报告以Python Flask为后端框架、MySQL为数据库、HTMLCSSJS为前端技术采用前后端分离方式开发围绕管理员、教师、学生三类角色展开权限划分与功能设计。内容涵盖系统总体设计、需求分析、概要设计、系统实现与测试总结五大章节具体包括功能结构图、数据库概念结构与逻辑结构设计、物理设计、界面设计、主要功能程序代码以及测试用例与总结能够帮助读者理清从需求梳理到数据库建模、再到接口实现与功能验证的完整课设流程。资源包共1个doc文档大小约3.95MB结构完整、章节清晰便于按模块查阅与借鉴。目前已有5074人学习下载适合作为数据库课设撰写与系统搭建的参考材料。1. 从一份课程设计报告说起这个 Flask MySQL 成绩管理系统到底能跑出什么期末周最让人头皮发麻的不是考试本身而是考完之后那一周几百份成绩要录入、核对、按分数段统计、再分发给不同角色查看。人工做这件事费时费力还容易出错一个数字填错就得从头翻。这份基于 Python Flask MySQL 的学生成绩管理系统就是冲着这个场景来的——它把管理员、教师、学生三类角色的权限拆开把成绩录入、查询、修改、统计这些高频操作收进一个前后端分离的 Web 应用里单机就能跑起来。如果你正在找一份能直接上手改的数据库课设项目或者想拿它当 Flask 入门练手这套代码的完整度是够的建表脚本、触发器、接口、页面模板都在改起来有抓手。2. 技术栈与目录结构Flask 后端 原生前端 MySQL 8.0 的分层怎么落2.1 技术选型拆解为什么是 Flask 而不是 Django这个项目的定位很明确——课程设计级别、单机运行、不涉及服务器部署。在这个前提下Flask 是比 Django 更合适的选择。Django 自带 ORM、Admin、认证体系功能全但重一个成绩管理系统用不上那么多Flask 轻路由和视图函数一一对应学生看代码时能顺着app.route一路读到底不用先理解一堆框架约定。后端核心依赖就三样flask负责 Web 服务和路由flask-cors处理前后端分离下的跨域请求pymysql或mysql-connector-python负责和 MySQL 8.0 通信。前端没有上 Vue/React用的是 HTML CSS 原生 JS页面模板借鉴了现成的后台管理模板再改所以你会看到一堆静态资源和 jQuery 式的 DOM 操作——这对课设来说是合理的降低理解成本也方便直接改页面。数据库选 MySQL 8.0字符集建议统一用utf8mb4因为学生姓名、籍贯这些字段有中文用utf8在某些排序和比较场景下会翻车。整个系统共 7 张表student、teacher、course、major、admin、student_course、major_course关系不算复杂但外键和触发器用得挺到位后面会细讲。2.2 目录结构一个典型的 Flask 课设工程长什么样项目正文没有给出完整目录树但按这个技术栈和功能规模合格开发一般会组织成下面这种结构。你拿到源码后可以对照着找文件grade_manage_system/ ├── app.py # Flask 入口注册路由、启动服务 ├── config.py # 数据库连接配置host/user/password/db ├── requirements.txt # 依赖清单 ├── db/ │ └── init.sql # 建库建表 触发器 初始数据 ├── routes/ │ ├── auth.py # 登录、登出、改密码 │ ├── student.py # 学生端接口 │ ├── teacher.py # 教师端接口 │ └── admin.py # 管理员端接口 ├── utils/ │ └── db.py # 连接池 / 统一查询封装 ├── static/ │ ├── css/ │ ├── js/ │ └── img/ └── templates/ ├── login.html ├── student/ ├── teacher/ └── admin/这个分层的关键在于routes按角色拆文件。三类用户的接口权限边界天然不同拆开之后每个文件只关心自己角色的 SQL改起来不容易误伤。utils/db.py里通常封装一个get_conn()和query(sql, args)避免每个视图函数都重复写连接代码——这是我在类似项目里一定会做的第一件事否则后面改数据库密码要改几十处。2.3 运行环境与启动步骤开发环境是 Windows 10 PyCharm MySQL 8.0Python 版本建议 3.8 以上。跑起来的顺序不能乱先库后服务# 1. 建虚拟环境并装依赖 python -m venv venv venv\Scripts\activate pip install flask flask-cors pymysql # 2. 初始化数据库在 MySQL 客户端里执行 mysql -u root -p db/init.sql # 3. 改 config.py 里的数据库账号密码 # 4. 启动 Flask python app.pyapp.py里一般会写app.run(host127.0.0.1, port5000, debugTrue)debugTrue在开发阶段很有用改完代码自动重载报错页也能看到堆栈。但要注意这个模式下 Flask 会暴露调试器只适合本机跑别拿去公网。提示如果启动时报ModuleNotFoundError: No module named flask八成是虚拟环境没激活或者 PyCharm 的解释器还指向系统 Python。在 PyCharm 右下角切一下解释器就行。3. 数据库设计拆解7 张表、外键级联和一个自动选课触发器3.1 表结构与主外键关系数据库名grade_manage_system7 张表的关系可以这样理解major是专业student通过major_id挂到专业下teacher独立存在course通过teacher_id挂到教师下student_course是学生和课程的多对多中间表带grade字段major_course是专业和课程的多对多中间表决定某个专业默认要修哪些课admin单独存管理员账号。建表时几个字段类型的选择值得注意。学号用char(10)而不是int因为学号可能有前导零用整型会丢成绩用tinyint unsigned范围 0~255实际业务限制在 0~100用无符号小整型省空间密码字段varchar(128)这是给哈希后的密码留的位置虽然课设里可能直接存明文但字段长度先留够后面想加哈希不用改表。-- 学生表major_id 外键指向专业级联删除 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 student_course( student_id char(10), course_id char(5), grade tinyint unsigned ); alter table student_course add primary key (student_id, course_id);on delete cascade是这里的一个设计点删掉一个专业该专业下的学生记录会被连带删除。课设里这么写省事但真实业务里要慎重——学生数据是核心资产级联删除等于没有后悔药。我一般会把学生表的外键改成on delete restrict删专业前先检查有没有学生挂着让业务层显式处理。3.2 触发器专业选课自动同步到学生选课这是整个数据库设计里最有意思的一段。major_course表记录“某专业要修某课程”但学生实际选课记录在student_course。当管理员给某个专业新增一门课时该专业下所有学生都应该自动出现在这门课的选课名单里成绩先置空。项目用了一个after insert触发器来完成这件事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 ;逐段看after insert on major_course表示在专业选课表插入新行之后触发for each row是行级触发器每插一行执行一次new.course_id和new.major_id引用刚插入的那行数据内部的insert ... select把该专业下所有学生的学号和新课程号批量写进student_coursegrade传null表示还没录入成绩。这个设计的好处是数据一致性由数据库保证应用层不用记着“加专业课的时候顺便给学生选课”。但坑也在这里如果某个学生已经手动选过这门课触发器再插一次会撞联合主键报错。常见做法是在insert前加where not exists判断或者用insert ignore。另外MySQL 里创建触发器前要改delimiter否则分号会被客户端提前截断这是新手最容易翻车的地方之一。3.3 初始数据与登录规则系统预置了三类账号。管理员固定用户名admin、密码asdfg13579不可修改教师用教师编号登录初始密码等于教师编号学生用学号登录初始密码也等于学号。这个规则在课设里很常见好处是初始化数据简单坏处是安全性基本为零——学号是公开信息等于默认密码公开。如果你要拿这个项目做二次开发第一件该改的就是登录逻辑初始密码可以保留但首次登录强制改密密码字段存werkzeug.security.generate_password_hash()的结果校验时用check_password_hash()。字段长度varchar(128)刚好够放 PBKDF2 的哈希串不用改表。4. 后端接口与权限控制三类角色怎么在 Flask 里各走各的路4.1 登录与角色分流登录接口是整个系统的入口它要解决两件事验证账号密码、告诉前端当前用户是什么角色。三类用户的账号存在不同表里所以登录时通常让前端传一个role字段后端按角色去对应表查from flask import Flask, request, jsonify, session import pymysql app Flask(__name__) app.secret_key change-this-in-production app.route(/api/login, methods[POST]) def login(): data request.get_json() role data.get(role) # admin / teacher / student username data.get(username) password data.get(password) table_map { admin: (admin, admin_name), teacher: (teacher, teacher_id), student: (student, student_id), } table, id_col table_map.get(role, (None, None)) if not table: return jsonify({code: 400, msg: 角色不合法}) conn get_conn() with conn.cursor(pymysql.cursors.DictCursor) as cur: sql fselect * from {table} where {id_col}%s and password%s cur.execute(sql, (username, password)) user cur.fetchone() conn.close() if not user: return jsonify({code: 401, msg: 账号或密码错误}) session[role] role session[uid] username return jsonify({code: 200, role: role, name: user.get(student_name) or user.get(teacher_name) or username})这段代码的关键在table_map把角色到表名、主键列名的映射集中管理避免写三个几乎一样的if/elif分支。DictCursor让查询结果返回字典而不是元组前端拿字段名直接取值不用记下标。登录成功后把role和uid写进session后续接口靠它判断权限。参数说明role必须是三个枚举值之一传错直接 400username和password走 JSON body前端fetch时记得设Content-Type: application/json。这里用 f-string 拼表名是因为表名来自白名单映射不是用户输入不存在注入风险但username和password必须用%s占位符这是铁律。4.2 权限装饰器别让每个接口都手写一遍判断登录之后学生不能调管理员的增删改接口教师不能改别的老师的课。最省事的做法是写一个装饰器在进入视图函数前检查session里的角色from functools import wraps def require_role(*allowed): def decorator(fn): wraps(fn) def wrapper(*args, **kwargs): role session.get(role) if role not in allowed: return jsonify({code: 403, msg: 无权限}), 403 return fn(*args, **kwargs) return wrapper return decorator app.route(/api/admin/students, methods[GET]) require_role(admin) def list_students(): conn get_conn() with conn.cursor(pymysql.cursors.DictCursor) as cur: cur.execute(select s.*, m.major_name from student s left join major m on s.major_idm.major_id) rows cur.fetchall() conn.close() return jsonify({code: 200, data: rows})require_role(admin)表示只有管理员能进。装饰器用*allowed接收可变参数一个接口允许多个角色时写require_role(admin, teacher)即可。wraps(fn)保留原函数名调试时堆栈信息不会乱。这个模式比在每个函数开头写if session.get(role) ! admin: return ...干净得多新增接口时也不容易漏。4.3 教师端成绩录入与分数段统计教师端有两个高频操作录入/修改成绩、按分数段统计选课人数。成绩更新本质是一条update统计则是一条带case when的聚合查询app.route(/api/teacher/grade, methods[POST]) require_role(teacher) def update_grade(): data request.get_json() sid, cid, grade data[student_id], data[course_id], data[grade] if not (0 grade 100): return jsonify({code: 400, msg: 成绩需在 0~100 之间}) conn get_conn() with conn.cursor() as cur: cur.execute( update student_course set grade%s where student_id%s and course_id%s, (grade, sid, cid) ) conn.commit() conn.close() return jsonify({code: 200, msg: 更新成功}) app.route(/api/teacher/stat/course_id, methods[GET]) require_role(teacher) def grade_stat(course_id): conn get_conn() with conn.cursor(pymysql.cursors.DictCursor) as cur: cur.execute( select sum(grade 90) as excellent, sum(grade between 80 and 89) as good, sum(grade between 70 and 79) as medium, sum(grade between 60 and 69) as pass, sum(grade 60) as fail from student_course where course_id%s and grade is not null , (course_id,)) stat cur.fetchone() conn.close() return jsonify({code: 200, data: stat})成绩更新前先做范围校验0 grade 100不满足直接返回 400别指望数据库的tinyint unsigned兜底——它只保证非负不保证不超过 100。统计查询里sum(条件)是 MySQL 的常用技巧条件为真返回 1求和就是人数grade is not null排除掉触发器插入但还没录成绩的记录否则统计口径会偏大。注意conn.commit()只对写操作需要读操作不用。如果忘了 commit更新看起来“成功”了但刷新页面数据没变这是新手排查半天的问题。5. 避坑与常见问题跑这个项目时最容易卡住的 5 个地方5.1 触发器创建报 1064 语法错误现象在 Navicat 或命令行里执行create trigger语句报You have an error in your SQL syntax。原因触发器体内有分号客户端把第一个分号当成语句结束后面的end就成了孤儿。解决创建前执行delimiter //创建完再delimiter ;改回来。如果用的是某些图形化工具它可能自己处理了 delimiter那就直接整段执行别手动加。5.2 中文姓名显示成问号现象学生姓名、籍贯插进去变成???。原因数据库或表的字符集是latin1或者连接时没指定字符集。解决建库时写create database grade_manage_system default charset utf8mb4;连接串里加charsetutf8mb4即pymysql.connect(..., charsetutf8mb4)。三处库、表、连接字符集要一致缺一处都可能出问题。5.3 前端请求被 CORS 拦下现象浏览器控制台报has been blocked by CORS policy接口明明能返回但页面拿不到数据。原因前后端分离前端页面和 Flask 服务端口不同浏览器同源策略拦截。解决pip install flask-cors在app.py里from flask_cors import CORS; CORS(app)。开发阶段可以全放开上线前要收窄到具体域名。5.4 外键约束导致删数据失败现象管理员删一个专业报Cannot delete or update a parent row。原因student表有外键指向major且没配级联或者配了restrict。解决先删该专业下的学生或者把外键改成on delete cascade。但改之前想清楚——级联删除是不可逆的课设里图省事可以真实系统里我宁愿多写一步确认。5.5 密码明文存储被一眼看穿现象登录能过但打开数据库一看密码全是明文答辩时被问“安全性怎么保证”。原因课设为了简单直接存明文。解决至少用werkzeug.security的generate_password_hash和check_password_hash登录时比对哈希而不是明文。改动量不大但能让整个项目的完成度高一个档次。6. 二次开发改造从课设到能用的三个具体切入点拿到这套代码如果只是跑通交作业上面几章够了。但如果你想把它改成能真正用起来的东西我建议从三个地方下手改动量可控效果立竿见影。第一个是密码体系。把student、teacher、admin三张表的密码字段全部改成哈希存储登录接口里把password%s的直接比对换成先查用户再check_password_hash。同时加一个“首次登录强制改密”的标记字段比如student表加is_first_login tinyint default 1登录成功后如果该字段为 1前端跳转到改密页。这个改动涉及登录和改密两个接口半天能完成但安全性提升是质变。第二个是成绩录入的批量能力。现在教师端大概率是一条一条改一个班五十人就是五十次请求。可以加一个批量接口接收[{student_id, course_id, grade}, ...]数组用executemany一次性提交def batch_update_grade(items): conn get_conn() with conn.cursor() as cur: cur.executemany( update student_course set grade%s where student_id%s and course_id%s, [(i[grade], i[student_id], i[course_id]) for i in items] ) conn.commit() conn.close()executemany比循环单条execute快一个数量级因为减少了网络往返。前端配一个表格批量编辑教师录完成绩一次提交体验完全不同。第三个是统计口径的扩展。现在的分数段统计是写死的五档实际教学里经常要看平均分、最高分、最低分、及格率。在原来的聚合查询里加几个字段就行avg(grade) as avg_score, max(grade) as max_score, min(grade) as min_score, sum(grade60)/count(*) as pass_rate。前端把返回的字典渲染成卡片比纯数字表格直观。验证改造是否成功我一般用 Postman 建一个集合把登录、查列表、改成绩、看统计四条链路串起来每次改完跑一遍。比手点页面快也不容易漏。从那以后我每次动数据库相关代码都强制先把建表脚本在一个空库里重跑一遍确认触发器、外键、初始数据都能从零建起来——血泪经验别问我怎么知道的。希望帮到你。本文还有配套的精品资源点击获取