☰
Python Flask+MySQL学生成绩管理系统:数据库设计与触发器实践
2026/10/17 21:38:30 网站建设 项目流程

简介:围绕 MySQL 学生成绩管理系统展开的数据库课程设计文档,完整覆盖系统总体设计、需求分析、概要设计、系统实现与测试总结全流程,适合计算机专业学生用于数据库课设、毕业设计或 Flask+MySQL 入门实践。文档以华东交通大学课程设计报告为原型,详细说明了管理员、教师、学生三类角色的权限划分,功能结构,数据库概念结构、逻辑结构与物理设计,以及成绩录入、修改、删除、查询和选课管理等模块的实现思路,能帮助读者快速理解前后端分离开发与接口测试方法。资源包为 1 个 doc 文件,约 3.95MB,是典型的课程设计报告文档,已有 5073 人学习下载。通过阅读该报告,可以对照完善自己项目中的业务逻辑、数据表设计与测试方案,节省从头梳理和撰写文档的时间。

1. 学生成绩管理系统:数据库课设的完整可复现方案

数据库课设最怕的不是功能写不完,而是答辩时被问一句“为什么这个字段这样设”就卡壳。这套学生成绩管理系统用 Python + Flask + MySQL 把三种角色的权限闭环走完了:学生查成绩、教师录成绩、管理员维护七张表,从 E-R 模型到建表 SQL 再到触发器逻辑都是成套写在文档里的,不是那种“跑起来再说”的半成品。源码和设计文档配套,适合做课程设计参考,也适合想低成本搭一套教学管理系统原型的人。它解决的核心问题是:成绩统计不再靠人工表格来回传,选课、录分、分类统计都有一条可验证的数据通路。

2. 需求拆分与数据库建模:为什么是七张表、三套权限

一份数据库课设能不能立住,重头戏在需求分析和表结构。这个项目的边界很清楚:不用做排课、不用做考勤,就是“选课—录成绩—查成绩—统计”这一条主链路。把这条链路拆开,每个模块映射到哪张表、哪种用户角色,都应该是能在文档里讲明白的。

2.1 三种角色的业务边界:谁在什么表上做什么

先把功能说透。系统分管理员、教师、学生三类用户,权限重心完全不同:

  • 学生端:登录、看个人信息、看已选课程与成绩、查任课教师、改密码;
  • 教师端:登录、看个人信息、看自己教的课程、给选课学生录入/修改成绩、看课程分数段统计、改密码;
  • 管理员端:对学生、教师、课程、专业、专业选课做增删改查,登录账号写死在 admin 表里。

这个划分决定了三套接口不能共用一个表来鉴权。学生登录查 student,教师登录查 teacher,管理员登录查 admin,三张表的密码字段结构一致,但业务数据完全不同。很多课设翻车就是因为在数据表里塞了一个统一的 user 表,把角色字段硬凑进去,最后导致学生表、教师表的个性化字段(专业、授课课程)全都无从挂载,还是要拆表回来返工。

数据需求方面,系统涉及六类核心数据:学生基本信息、教师基本信息、课程基本信息、专业信息、管理员账号、选课成绩关系。前五类是静态数据,最后一类是核心业务关系。一类对应一张表的话就是七张,没有多余的,也没有少。学生选课表承载的是“谁选了什么课、考了多少分”这种动态关系,专业选课表承载的是“哪个专业默认要上哪些课”的配置关系,两者必须分开,混在一张表里会导致一个问题:新增一个学生时,他还没选课,成绩表里根本没有他的记录,专业维度的默认开课逻辑就无处安放。

2.2 表结构设计的四个关键选择

回看原始文档的物理设计,有几个点值得专门拆开说。

第一,字段类型用 char 而不是 varchar。学号 char(10)、教师编号 char(5)、课程编号 char(5),这些是定长编码,没有长度波动,定长 char 在 InnoDB 里索引更紧凑,查询走主键时性能比 varchar 更稳。如果后期改成长学号也只需要改字段长度,不影响逻辑。

第二,grade 用 tinyint unsigned。成绩取值 0-100,tinyint unsigned 的最大值是 255,存 100 分完全够用,每个值只占 1 个字节。有人用 INT 存成绩也没错,但没必要。unsigned 是为了彻底排除负数,数据库层面就挡住非法数据。注意这里不能用 varchar,否则“95”和“9 5”这种历史遗留问题是排不完的。

第三,密码字段用 varchar(128)。原始文档用的是“加密后的密码”这个描述,这个 128 是在给哈希算法留空间。常见的做法是拿 werkzeug 的 generate_password_hash 生成加密密码,它默认的 pbkdf2:sha256 格式输出是 100 字节左右的字符串;如果哪天换成 bcrypt,长度还在 128 以内。硬编码成 32 位只够存 MD5,很快就后悔了。

第四,选课关系用联合主键。student_course 表的主键是 (student_id, course_id),major_course 表的主键是 (major_id, course_id)。联合主键的意义不是省一条自增 id,而是从数据库层面保证“同一个学生选同一门课只有一条记录”,不靠应用层去查重。自增 id 做不到这个约束,这也是课设答辩里老师最喜欢问的点。

2.3 建表 SQL 顺序:为什么不能乱跑

原文档里的建表 SQL 分布在各章节里,我把它按可执行顺序整理成了一份完整脚本,直接跑就能把七张表全部落在 MySQL 8.0 里。

-- 1. 创建数据库,字符集用 utf8mb4,避免中文乱码 CREATE DATABASE IF NOT EXISTS grade_manage_system DEFAULT CHARACTER SET utf8mb4; USE grade_manage_system; -- 2. 专业表:先建无外键依赖的基础表 CREATE TABLE major ( major_id char(3) PRIMARY KEY, major_name varchar(20) NOT NULL UNIQUE ); -- 3. 学生表:依赖专业表,专业删除时级联删除学生 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, FOREIGN KEY (major_id) REFERENCES major(major_id) ON DELETE CASCADE ); -- 4. 教师表:与专业表无关联,可独立创建 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 ); -- 5. 课程表:先有教师,课程才能挂到教师名下 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, FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON DELETE CASCADE ); -- 6. 专业选课表:专业与课程的多对多关系 CREATE TABLE major_course ( major_id char(3), course_id char(5), PRIMARY KEY (major_id, course_id), FOREIGN KEY (major_id) REFERENCES major(major_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE ); -- 7. 学生选课成绩表:核心业务表,承载选课记录和成绩 CREATE TABLE student_course ( student_id char(10), course_id char(5), grade tinyint UNSIGNED, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE ); -- 8. 管理员表:独立一张表,只存用户名和密码 CREATE TABLE admin ( admin_name char(15) PRIMARY KEY, password varchar(128) NOT NULL );

执行顺序的讲究:先建没有外键依赖的基础表(major、teacher),再建依赖它们的表(student、course),最后建关系表(major_course、student_course)。反过来跑必然报“外键不存在”错。admin 独立在最后建,因为它的外键一个都没有。

再说两个容易忽略的参数。数据库字符集指定 utf8mb4,不要用默认的 latin1,不然插入中文姓名就直接警告或者乱码,这是 MySQL 8.0 下最基础的一步。major_course 表原文只给了字段定义,没有在 CREATE TABLE 里直接加约束,我把它改成建表时直接声明联合主键,这样执行完表结构就完整了,不需要再补 ALTER TABLE。如果你拿到的文档保留的是“先建表后 ALTER”的写法,也没问题,但改成一个 SQL 全落的版本,课设报告里更好讲。

逻辑结构设计可以简化为七条关系模式:学生(学号、姓名、性别、出生年份、籍贯、入学年份、专业编号、密码),教师(教师编号、姓名、性别、出生年份、密码),课程(课程编号、课程名、开课学年、开课学期、学分、教师编号),专业(专业编号、专业名),学生选课(学号、课程编号、成绩),专业选课(专业编号、课程编号),管理员(用户名、密码)。这条模式设计与上面的建表 SQL 完全对应,每张表都有主键,每个外键都有指向,ER 模型里的一对多关系在表结构里就是外键列。答辩时如果被问到“概念设计怎么转逻辑设计”,把这段对照关系讲清楚就够了。

3. Flask 后端骨架:登录鉴权、角色路由与成绩接口怎么落

数据库建好只是地基,后端才是让三套角色真正动起来的引擎。这个项目用的是 Flask 提供 REST 接口,前端页面用 HTML + CSS + JS 做展示,前后端分离,中间用 Postman 调接口。我按这个结构把模块拆了一遍,重点关注登录鉴权、角色路由和成绩相关接口的落法。

3.1 项目结构与 CORS 配置

一个适合课设的目录结构是这样的:项目根目录下放 app.py 作为 Flask 入口,config.py 放数据库连接信息,models.py 放公共的数据库查询方法,blueprints 目录下挂三套角色的路由文件,static 里放前端模板改出来的页面和样式。

需要特别注意的是跨域。前后端分离之后,前端页面如果和后端服务不在同一个端口,浏览器会拦跨域请求。常见做法是在 Flask 里装 flask-cors,然后初始化:

from flask_cors import CORS from flask import Flask app = Flask(__name__) app.secret_key = 'grade-manage-system-demo-secret' CORS(app, supports_credentials=True)

supports_credentials 必须开,因为登录依赖 session,而 session 依赖 cookie。如果不开这个选项,前端就算登录成功了,后续请求也带不上鉴权标识,接口会一直返回 401。这个细节很容易漏,漏了就是“登录明明成功但查不到数据”的玄学问题。

3.2 登录鉴权:密码怎么存、会话怎么建、角色怎么验

这个系统的账号体系有三个特征:管理员账号固定、教师和学生初始密码都是编号、密码允许修改。这些需求落到实现上要注意三点。

第一,密码不能明文落库。建表时密码字段是 varchar(128),对应的就是哈希串。常见做法是用 werkzeug 的 generate_password_hash,登录时用 check_password_hash 校验:

from werkzeug.security import generate_password_hash, check_password_hash def verify_login(role, login_id, raw_password): table_map = { 'student': ('student', 'student_id'), 'teacher': ('teacher', 'teacher_id'), 'admin': ('admin', 'admin_name'), } if role not in table_map: return False table, id_col = table_map[role] sql = f"SELECT password FROM {table} WHERE {id_col} = %s" row = query_one(sql, login_id) if row is None: return False return check_password_hash(row['password'], raw_password)

这里用 table_map 是安全的做法,角色是固定枚举,表名和列名都是写死的,不参与用户输入拼接,不会引出入库风险。登录成功后把 role 和 user_id 写进 session,后续接口通过 session 判断登录态和角色,这是单机演示场景下的最短路径,不需要上 JWT。

第二,初始密码存库之前必须加盐哈希。原始设计里“学生初始密码为学生编号”,这是逻辑层的默认值,不等于数据库明文。执行添加学生时直接 generate_password_hash(学号) 存入 password 字段,后续学生登录时校验的是哈希结果,而不是拿明文学号和库里的值直接比。

3.3 核心路由:学生查成绩、教师录成绩、管理员维护数据

学生端最核心的接口是查成绩,一个联表查询就能跨四张表拿到完整信息:

from flask import Blueprint, jsonify, session student_api = Blueprint('student_api', __name__) @student_api.route('/api/student/grades', methods=['GET']) def student_grades(): if session.get('role') != 'student': return jsonify(code=401, msg='未登录或角色不匹配') student_id = session['user_id'] sql = """ SELECT c.course_name, c.year, c.semester, t.teacher_name, sc.grade FROM student_course sc JOIN course c ON c.course_id = sc.course_id JOIN teacher t ON t.teacher_id = c.teacher_id WHERE sc.student_id = %s ORDER BY c.year DESC, c.semester DESC """ rows = fetch_all(sql, student_id) return jsonify(code=0, data=rows)

这里其实只 JOIN 了两张表,因为 student_course 和 course 已经关联上了,course 表里又预存了 teacher_id,所以教师姓名通过 course 到 teacher 再查一次就能拿到。查询参数用 %s 占位符传值,代码里不拼字符串,这是数据库操作里最基本的安全习惯。

教师端录成绩的接口需要注意权限校验和边界检查:

@teacher_api.route('/api/teacher/course/<course_id>/grades', methods=['POST']) def update_grade(course_id): if session.get('role') != 'teacher': return jsonify(code=401, msg='未登录或角色不匹配') payload = request.get_json() student_id = payload.get('student_id') grade = payload.get('grade') if not isinstance(grade, int) or grade < 0 or grade > 100: return jsonify(code=1, msg='成绩必须在0到100之间') sql = """ UPDATE student_course SET grade = %s WHERE course_id = %s AND student_id = %s """ affected = execute(sql, grade, course_id, student_id) if affected == 0: return jsonify(code=1, msg='选课记录不存在或无权修改') return jsonify(code=0, msg='成绩已保存')

三层防护分别是:登录角色校验、数值范围校验、UPDATE 影响行数校验。最后这个 affected == 0 的判断很实用,如果学生压根没选这门课,UPDATE 会静默成功但不影响任何行,接口必须把这个情况暴露出来,否则前端会出现“保存成功但成绩没变”的假象。

管理员端就是标准增删改查,以添加学生为例:

@admin_api.route('/api/admin/student', methods=['POST']) def add_student(): if session.get('role') != 'admin': return jsonify(code=401, msg='无权限') p = request.get_json() sql = """ INSERT INTO student (student_id, student_name, sex, birth_year, province, enter_year, major_id, password) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """ hashed = generate_password_hash(p['student_id']) execute(sql, p['student_id'], p['student_name'], p.get('sex'), p.get('birth_year'), p.get('province'), p['enter_year'], p['major_id'], hashed) return jsonify(code=0, msg='添加成功')

注意这里密码默认取的是学号,而不是前端传进来的什么字段。这是业务规则而不是技术实现:管理员添加的学生不需要先设置临时密码,它自然就是学号,后续学生登录后自己改。前端页面里不需要出现密码输入框,否则就破坏了“初始密码默认学号”的业务约束。

4. 选课触发器与分数段统计:两个值得照抄的设计细节

这个项目最耐看的不是 CRUD,而是两个容易被当成“魔法”的部分:专业选课后自动生成学生选课记录,以及教师端分数段统计。前者靠触发器,后者靠一条 CASE WHEN 聚合 SQL。我把两个都拆开讲清楚,知道原理之后你自己也能改。

4.1 专业选课触发器: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 ;

这段逻辑说人话就是:管理员把一个专业和一门课绑定时表示“这个专业的学生都要上这门课”,MySQL 自动把该专业下所有学生都写进 student_course,成绩初始为 NULL。省掉了应用层的循环插入,也省了手动一条条插入的体力活。

几个要点需要理解:

  • AFTER INSERT 保证主事件先落库,如果触发器内的 INSERT 失败,MySQL 会自动回滚整个主事件,不会出现“专业选课配置已加但学生选课没生成”的半截状态。
  • NEW.major_id 是新插入那行的字段值,不依赖外部变量,触发器内部直接取。
  • 成绩用 NULL 而不是 0,语义是“已选未考”,0 是真实分数,不能混。如果初始成绩用 0,分数段统计里会把没考的人算进 60 分以下档位,整个统计就失真了。

这个触发器命名 trigger_insert_1 比较随意,但不影响功能。如果交课设文档,建议把名字改成 trigger_major_course_auto_enroll,答辩时一眼能看出作用,也显得命名有规范意识。

4.2 成绩分数段统计:一条 SQL 出六段人数

教师端“查看统计”在界面里展示的是不同分数段的人数。最笨的做法是分六次 COUNT,每次带一个 WHERE 条件;但更常见、也是课设里更容易讲出彩的做法是用 SUM(CASE WHEN):

SELECT COUNT(*) AS total_students, SUM(CASE WHEN grade IS NULL THEN 1 ELSE 0 END) AS not_tested, SUM(CASE WHEN grade >= 90 THEN 1 ELSE 0 END) AS level_a, SUM(CASE WHEN grade >= 80 AND grade < 90 THEN 1 ELSE 0 END) AS level_b, SUM(CASE WHEN grade >= 70 AND grade < 80 THEN 1 ELSE 0 END) AS level_c, SUM(CASE WHEN grade >= 60 AND grade < 70 THEN 1 ELSE 0 END) AS level_d, SUM(CASE WHEN grade < 60 THEN 1 ELSE 0 END) AS level_f FROM student_course WHERE course_id = %s;

这段 SQL 在成绩明细不变的情况下,把六个人数一次性查出来,前端拿到就是一个 JSON 对象,直接渲染柱状图或表格。注意 CASE WHEN 的先后顺序:先判断 IS NULL,再判断分数段,否则 NULL 会落到最后一档 level_f,把未考人数和不及格人数混在一起。

如果想更精练,MySQL 8.0 里也可以用 BETWEEN 表达式配合 SUM:

SUM(grade BETWEEN 80 AND 89) AS level_b

MySQL 里布尔表达式求和会自动转 1/0,效果等价。两种写法都行,看课设报告里想不想展示新函数。不过 BETWEEN 写法在可读性上不如 CASE WHEN 直观,我还是更推荐前者。

4.3 为什么选触发器而不是后端显式插入

一个大家常纠结的点:专业选课逻辑放数据库还是放后端?这个项目选择了数据库侧触发器,我认为在课设场景是对的。

第一,原子性。后端显式插入是先插 major_course 再插 student_course,如果第二步失败,数据库里会残留半条数据,需要额外补事务;触发器是 MySQL 自带的事务语义,主事件和触发事件绑定在一起,要么都成功,要么都回滚。

第二,无法绕过。不管后端哪个接口、或者管理员直接用 SQL 工具操作,只要插了 major_course,选课必然生成。应用层写逻辑的话,换一个入口就可能漏调用,比如有开发者图省事直接用命令行 INSERT 一条专业选课,后端逻辑就完全没被触发。

第三,缺点是排错成本高。触发器是隐式的,出问题只能看数据变化推断,表现上像个“黑匣子”。所以要在文档里明确写清楚触发器的触发时机:AFTER INSERT ON major_course,FOR EACH ROW,执行体是向 student_course 插入该专业所有学生的选课记录。调试接口时发现数据多出来了,第一时间去查是不是这个触发器干的,能省不少时间。

这种取舍在课设答辩时也能讲出层次:不是所有逻辑都适合放触发器,但“关联配置自动生成关系明细”这种场景非常适合,属于触发器的最佳使用区间。

5. 排查与避坑:跑这套课设最容易踩的五个坑

代码能跑起来是下限,真正的差距在排错能力。下面五个坑是我拆这个项目时真实遇到过的场景,按“现象 → 原因 → 解决”写出来,照着排查就行。

5.1 建表报“外键不存在”,脚本执行中断

现象:把七张表的 CREATE TABLE 全部贴进 MySQL 一次性执行,跑到中途某一张表报错,后续所有语句停止。

原因:执行顺序问题。course 表依赖 teacher,student 表依赖 major,major_course 依赖 course 和 major,student_course 依赖 student 和 course。被依赖的表还没建好,外键无从挂载。

解决:按依赖顺序逐段执行:major → teacher → student → course → major_course → student_course → admin。不要全选一次性粘贴,MySQL 客户端对多语句执行的中断策略不同,报错后容易留下半套表结构。

5.2 插入中文姓名后显示乱码

现象:添加学生时,student_name 写入数据库后变成“? ?”或者一堆乱码,英文数字正常。

原因:创建数据库时没指定字符集,MySQL 8.0 默认用了 latin1,中文无法正确编码。

解决:建库语句里带 DEFAULT CHARACTER SET utf8mb4,这是根治。如果库已经建好,用 ALTER DATABASE grade_manage_system CHARACTER SET utf8mb4; 兜底。改完库字符集后,已存在的表还需要 ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4; 否则旧表字段元数据还是 latin1。

5.3 密码永远校验失败,前端登录不进去

现象:学生用初始密码学号登录,接口始终返回“密码错误”,检查数据库里密码字段是一串纯数字。

原因:添加用户时把明文密码存进了库,登录时用哈希校验去比对,两边格式不匹配。反过来也一样:库内存了哈希串,登录时却拿明文直接比较。

解决:统一用 generate_password_hash 存、check_password_hash 校验。确认数据库里 password 列字段值以 pbkdf2: 开头,凡是看到纯数字的一定是明文,重新执行一次 UPDATE 把密码刷成哈希。注意这条规则三张表通用:student、teacher、admin 的密码字段必须是哈希后的结果。

5.4 删一个专业,连带学生和选课记录一起没了

现象:管理员在界面上删除某个专业,结果该专业下的学生信息消失,这些学生的选课成绩记录也没了。

原因:student 表外键用了 ON DELETE CASCADE。专业删除时级联删除该专业下的学生;学生删除时又级联删除其选课记录。看起来只删了一个专业,实际触发了一个链式删除。

解决:课设里尽量不用级联删除。把外键行为改成 ON DELETE RESTRICT,或者直接在应用层限制:有学生的专业不允许直接删除,必须先转移或删除学生再删专业。外键约束保留,但级联行为要慎重设计。这个坑很值钱,很多人在课设报告里写“外键都加了 CASCADE”,答辩时真的会被追问。

5.5 Postman 请求接口返回 404,但路由明明写了

现象:代码里写了/api/student/grades,Postman 请求时返回 404,Flask 启动日志里也没有对应路由记录。

原因:Blueprint 没注册到 Flask app,或者注册时加了 url_prefix 导致路径前缀不对。student_api 里写的路由是相对路径,实际对外是拼接后的完整路径,两处不一致就会 404。

解决:统一约定。在 app.py 里注册时固定前缀:

from blueprints.student_api import student_api app.register_blueprint(student_api, url_prefix='/api/student')

这样路由就变成了/api/student/grades。检查一遍最近改过注册路径的地方,如果路径和前端 fetch 的不一致,404 就跑不掉。同理,admin_api 对应/api/admin,teacher_api 对应/api/teacher,三套前缀别混。

再补一条:前端登录后请求接口报 CORS 错误,先检查 flask-cors 的 supports_credentials 是否开启,不开就是第三章节写到的那个玄学问题。

6. 全链路冒烟测试:从建库到成绩闭环的验证清单

数据库课设能不能交,不是看界面点一遍能不能通,而是看数据链路上有没有断点。下面这个冒烟清单是我每次拿到新项目源码都会强制走一遍的流程,分享出来供参考。

步骤操作预期结果验证点
1管理员建专业major 表多一条记录基础数据可写
2管理员加学生、加教师、加课程三张表各多一条记录外键正确挂载
3管理员设专业选课major_course 多一条记录,student_course 自动生成 N 条成绩 NULL 记录触发器生效
4教师登录,录成绩student_course 的 grade 被更新权限校验和 UPDATE 逻辑
5学生登录,查成绩页面显示课程、教师、分数四表联查
6教师点查看统计分数段人数与成绩明细一致聚合 SQL

这个顺序的关键点在第 3 步:配置专业选课后先别急着录成绩,先去 student_course 表里查一下,看触发器是否真的把该专业所有学生都插进去了。业务上必须先建学生、再配专业选课,顺序反了的话,新加的学生不会自动带出选课记录,因为触发器只处理 INSERT major_course 时已存在的学生。所以管理员操作流程要固定:专业 → 学生 → 教师 → 课程 → 专业选课。

最后再跑三条 SQL 做数据一致性核对:

-- 1. 有选课记录但成绩为空的,说明还没录分 SELECT COUNT(*) AS pending FROM student_course WHERE grade IS NULL; -- 2. 统计每门课程的平均分、最高分、最低分 SELECT course_id, ROUND(AVG(grade), 2) AS avg_grade, MAX(grade) AS max_grade, MIN(grade) AS min_grade FROM student_course WHERE grade IS NOT NULL GROUP BY course_id; -- 3. 查某个分数段分布,和前端页面比对 SELECT SUM(CASE WHEN grade >= 90 THEN 1 ELSE 0 END) AS level_a, SUM(CASE WHEN grade >= 60 AND grade < 70 THEN 1 ELSE 0 END) AS level_d FROM student_course WHERE course_id = %s AND grade IS NOT NULL;

我会拿这些查询结果和前端页面截图摆在一起对照,数字一致才算这条链路通过。从那以后,每次拿到新的课设代码,我第一件事不是打开界面点按钮,而是先跑这几条 COUNT 和 SUM,把数据链路验证完再碰界面,比瞎点按钮高效得多。希望这个冒烟清单能帮到你,让你少走点弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询