简介:这份资源是面向高校数据库课程设计场景的MySQL学生成绩管理系统完整课程设计报告,适合正在完成数据库课设、需要参考系统设计与实现思路的计算机相关专业学生。报告以Python Flask为后端框架、MySQL为数据库、HTML+CSS+JS为前端技术,采用前后端分离方式开发,围绕管理员、教师、学生三类角色展开权限划分与功能设计。内容涵盖系统总体设计、需求分析、概要设计、系统实现与测试总结五大章节,具体包括功能结构图、数据库概念结构与逻辑结构设计、物理设计、界面设计、主要功能程序代码以及测试用例与总结,能够帮助读者理清从需求梳理到数据库建模、再到接口实现与功能验证的完整课设流程。资源包共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.0,Python 版本建议 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(host='127.0.0.1', port=5000, debug=True),debug=True在开发阶段很有用,改完代码自动重载,报错页也能看到堆栈。但要注意,这个模式下 Flask 会暴露调试器,只适合本机跑,别拿去公网。
提示:如果启动时报
ModuleNotFoundError: No module named 'flask',八成是虚拟环境没激活,或者 PyCharm 的解释器还指向系统 Python。在 PyCharm 右下角切一下解释器就行。
3. 数据库设计拆解:7 张表、外键级联和一个自动选课触发器
3.1 表结构与主外键关系
数据库名grade_manage_system,7 张表的关系可以这样理解: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_course,grade传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 = f"select * 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必须是三个枚举值之一,传错直接 400;username和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_id=m.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;,连接串里加charset='utf8mb4',即pymysql.connect(..., charset='utf8mb4')。三处(库、表、连接)字符集要一致,缺一处都可能出问题。
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(grade>=60)/count(*) as pass_rate。前端把返回的字典渲染成卡片,比纯数字表格直观。
验证改造是否成功,我一般用 Postman 建一个集合,把登录、查列表、改成绩、看统计四条链路串起来,每次改完跑一遍。比手点页面快,也不容易漏。从那以后我每次动数据库相关代码,都强制先把建表脚本在一个空库里重跑一遍,确认触发器、外键、初始数据都能从零建起来——血泪经验,别问我怎么知道的。希望帮到你。
本文还有配套的精品资源,点击获取