
简介本资源是一份面向高校数据库课程设计实践的MySQL学生成绩管理系统完整课设文档适用于计算机、软件工程及教育技术类专业本科生开展课程设计与系统开发实训。文档基于PythonFlaskMySQL技术栈实现前后端分离架构覆盖管理员、教师、学生三类角色权限管理支持成绩录入、查询、修改、删除及学生在线选课等核心功能兼顾实用性与教学规范性。资源为单个3.95MB的Word文档.doc内容结构完整含系统总体设计、需求分析、数据库概念/逻辑/物理设计、界面与代码实现、测试用例及总结反思共五章目录层级清晰含详细ER图说明、SQL建表语句、接口调用示例与Postman测试记录便于读者理解系统设计逻辑并复现开发过程。目前已有5074人学习下载是数据库原理、Web开发与软件工程综合实践的典型参考范例。1. 这不是又一个“登录增删改查”的课设Demo而是一套能真实跑通、可验证权限逻辑、带外键级联和触发器的MySQL教学闭环系统上周帮某高校实验室调试一个学生交上来的“成绩管理系统”结果发现80%的课设项目卡在登录页——密码没加密、表没建全、外键没加约束、连student_course的联合主键都漏了ON DELETE CASCADE。而这个「MySQL学生成绩管理系统」从第一页的建库SQL到最后一行Flask路由全程按真实教务场景推演管理员添加专业后新专业下所有学生自动获得该专业全部课程的选课记录靠的是那个trigger_insert_1触发器教师修改成绩时前端传student_idcourse_id后端直接UPDATE student_course SET grade? WHERE student_id? AND course_id?不拼接SQL、不绕过ORM、不裸写SELECT * FROM ...学生改密码前强制校验旧密码哈希值——它不是PPT里的UML图是能让你在PyCharm里断点调试、在MySQL Workbench里看执行计划、在Postman里一条条测通的完整工程。适合刚学完《数据库原理》想落地练手的本科生也适合需要快速搭一个轻量教务原型给导师看的研究生。它不追求Vue3TS炫技但每张表的NOT NULL、每个外键的REFERENCES、每个密码字段的VARCHAR(128)都写着“这课我真听进去了”。2. 环境准备与数据库初始化从零建库、补全外键、激活触发器三步踩实底层地基2.1 开发环境确认与MySQL服务启动项目明确要求MySQL 8.0这不是兼容性妥协而是关键功能依赖——比如CREATE TRIGGER在8.0中默认支持DEFINER和更严格的SQL模式而5.7下某些触发器语法会报错。先确认服务状态# Windows下检查MySQL是否运行非WAMP/XAMPP集成环境 sc query mysql # 若未启动手动启动以管理员身份运行CMD net start mysql提示如果提示服务名无效说明MySQL未注册为Windows服务。此时需进入MySQL安装目录的bin下执行mysqld --install再启动。别跳过这步——很多同学卡在“连不上localhost:3306”本质是服务根本没起来。2.2 执行建库SQL并手动补全外键约束报告中给出的建表语句P11-12存在两处必须人工修正的硬伤student表建表时未声明major_id外键仅靠注释//alter table student add foreign key...提示course表同理teacher_id外键缺失major_course表虽声明了联合主键但外键引用未在建表时写死靠后续ALTER添加。正确操作顺序在MySQL命令行或Workbench中逐条执行-- 1. 创建数据库注意字符集避免中文乱码 CREATE DATABASE grade_manage_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE grade_manage_system; -- 2. 先建被引用表major专业表和teacher教师表 CREATE TABLE major ( major_id CHAR(3) PRIMARY KEY, major_name VARCHAR(20) NOT NULL UNIQUE ); 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 ); -- 3. 建student表此处必须显式声明外键否则后续ALTER可能失败 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. 建course表同样显式声明teacher_id外键 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 ); -- 5. 建major_course专业选课表联合主键双外键 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 ); -- 6. 建student_course学生选课成绩表联合主键外键 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 ); -- 7. 建admin表管理员表 CREATE TABLE admin ( admin_name CHAR(15) PRIMARY KEY, password VARCHAR(128) NOT NULL );为什么必须显式声明MySQL 8.0 默认开启STRICT_TRANS_TABLES模式若建表时不声明外键后续ALTER TABLE ADD FOREIGN KEY可能因数据不一致如student表里有major_id‘999’但major表无此记录直接报错ERROR 1452。先清空测试数据再建表比后期排查外键冲突快10倍。2.3 创建并验证核心触发器让“专业开课”自动同步到“学生选课”报告P18末尾的触发器代码是本系统的灵魂设计但原文SQL有严重语法缺陷缺少DELIMITER //声明且未指定DEFINER。修正后如下在MySQL命令行中执行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 ;验证触发器是否生效-- 插入一个新专业 INSERT INTO major VALUES (001, 计算机科学与技术); -- 插入一名该专业学生 INSERT INTO student VALUES (20230001, 张三, 男, 2005, 江苏, 2023, 001, e10adc3949ba59abbe56e057f20f883e); -- 插入一门课程 INSERT INTO course VALUES (CS101, 数据库原理, 2024, 秋, T001, 3); -- 关键向major_course插入专业-课程关联触发器应自动为该专业所有学生生成选课记录 INSERT INTO major_course VALUES (001, CS101); -- 查询student_course应看到张三的选课记录grade为NULL SELECT * FROM student_course WHERE course_id CS101; -- 预期输出20230001 | CS101 | NULL注意触发器只对INSERT INTO major_course生效不影响已有数据。若要补全历史数据需手动执行INSERT INTO student_course ... SELECT。这是课设常见盲区——很多人以为触发器是“万能同步”其实它只响应未来操作。3. Flask后端核心模块拆解登录鉴权、角色路由、密码哈希三道防线如何落地3.1 用户登录流程从明文密码到bcrypt哈希的完整链路项目摘要提到“密码加密存储”但正文未说明算法。结合Python生态惯例及安全性要求必须使用bcrypt而非MD5/SHA1报告中示例密码e10adc3949ba59abbe56e057f20f883e是MD5仅用于演示不可用于实际。Flask后端典型实现如下# app.py 片段 from flask import Flask, request, session, redirect, url_for, render_template, flash import bcrypt import mysql.connector from mysql.connector import Error app Flask(__name__) app.secret_key your_secret_key_here # 实际项目需用secrets.token_hex() def get_db_connection(): try: connection mysql.connector.connect( hostlocalhost, databasegrade_manage_system, userroot, # 替换为你的MySQL用户名 passwordyour_password # 替换为你的MySQL密码 ) return connection except Error as e: print(f数据库连接错误: {e}) return None app.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form[username] password request.form[password].encode(utf-8) # 转bytes供bcrypt使用 conn get_db_connection() if not conn: flash(数据库连接失败) return render_template(login.html) cursor conn.cursor(dictionaryTrue) # 根据用户名类型动态查表管理员查admin表教师查teacher学生查student user_type None user_data None # 先查admin表用户名固定为admin cursor.execute(SELECT * FROM admin WHERE admin_name %s, (username,)) admin cursor.fetchone() if admin: user_type admin stored_hash admin[password].encode(utf-8) if bcrypt.checkpw(password, stored_hash): session[user_type] admin session[username] username return redirect(url_for(admin_dashboard)) # 查teacher表teacher_id即用户名 cursor.execute(SELECT * FROM teacher WHERE teacher_id %s, (username,)) teacher cursor.fetchone() if teacher: user_type teacher stored_hash teacher[password].encode(utf-8) if bcrypt.checkpw(password, stored_hash): session[user_type] teacher session[username] username session[teacher_id] username return redirect(url_for(teacher_dashboard)) # 查student表student_id即用户名 cursor.execute(SELECT * FROM student WHERE student_id %s, (username,)) student cursor.fetchone() if student: user_type student stored_hash student[password].encode(utf-8) if bcrypt.checkpw(password, stored_hash): session[user_type] student session[username] username session[student_id] username return redirect(url_for(student_dashboard)) # 所有表都没匹配或密码错误 flash(用户名或密码错误) cursor.close() conn.close() return render_template(login.html) return render_template(login.html)关键参数说明bcrypt.checkpw(password, stored_hash)password必须是bytes类型stored_hash是数据库中存储的bcrypt哈希值长度约60字符以$2b$开头session[user_type]存储用户角色用于后续路由权限控制session[student_id]/session[teacher_id]避免每次查询都解析username提升性能flash()Flask消息闪现机制前端用get_flashed_messages()显示错误。血泪经验很多同学直接password db_password做明文比对导致初始密码学生编号/教师编号在数据库里明文存储——这在课设答辩时会被导师当场叫停。bcrypt是底线没有商量余地。3.2 角色路由守卫用装饰器拦截未授权访问不同角色看到的页面完全不同不能只靠前端隐藏菜单。Flask必须做后端路由守卫from functools import wraps def role_required(*allowed_roles): 角色权限装饰器 def decorator(f): wraps(f) def decorated_function(*args, **kwargs): if user_type not in session: flash(请先登录) return redirect(url_for(login)) if session[user_type] not in allowed_roles: flash(权限不足) # 根据当前用户类型重定向到对应首页 if session[user_type] student: return redirect(url_for(student_dashboard)) elif session[user_type] teacher: return redirect(url_for(teacher_dashboard)) else: return redirect(url_for(admin_dashboard)) return f(*args, **kwargs) return decorated_function return decorator # 使用示例管理员专属路由 app.route(/admin/students) role_required(admin) def admin_students(): # 只有admin能访问 conn get_db_connection() cursor conn.cursor(dictionaryTrue) cursor.execute(SELECT * FROM student) students cursor.fetchall() cursor.close() conn.close() return render_template(admin/students.html, studentsstudents) # 教师查看自己课程的学生名单需关联course表过滤 app.route(/teacher/course_students/course_id) role_required(teacher) def teacher_course_students(course_id): conn get_db_connection() cursor conn.cursor(dictionaryTrue) # 关键WHERE t.teacher_id %s 确保教师只能看自己教的课 cursor.execute( SELECT s.*, sc.grade FROM student s JOIN student_course sc ON s.student_id sc.student_id JOIN course c ON sc.course_id c.course_id JOIN teacher t ON c.teacher_id t.teacher_id WHERE c.course_id %s AND t.teacher_id %s , (course_id, session[teacher_id])) students cursor.fetchall() cursor.close() conn.close() return render_template(teacher/course_students.html, studentsstudents, course_idcourse_id)为什么不用if session[user_type] ! admin: abort(403)装饰器模式将权限逻辑抽离避免在每个路由函数开头重复写if判断。更重要的是它能统一处理越权行为如学生直接在浏览器输入/admin/studentsURL而不是让非法请求进入业务逻辑层再报错。3.3 密码修改模块旧密码校验新密码二次确认哈希更新学生/教师修改密码是高频操作也是SQL注入高危点。必须参数化查询 严格校验app.route(/change_password, methods[GET, POST]) def change_password(): if user_type not in session: return redirect(url_for(login)) user_type session[user_type] username session[username] if request.method POST: old_password request.form[old_password].encode(utf-8) new_password request.form[new_password].encode(utf-8) confirm_password request.form[confirm_password].encode(utf-8) # 1. 两次新密码必须一致 if new_password ! confirm_password: flash(新密码与确认密码不一致) return render_template(change_password.html, user_typeuser_type) # 2. 旧密码校验根据user_type查对应表 conn get_db_connection() cursor conn.cursor(dictionaryTrue) stored_hash None if user_type student: cursor.execute(SELECT password FROM student WHERE student_id %s, (username,)) elif user_type teacher: cursor.execute(SELECT password FROM teacher WHERE teacher_id %s, (username,)) elif user_type admin: cursor.execute(SELECT password FROM admin WHERE admin_name %s, (username,)) result cursor.fetchone() if result: stored_hash result[password].encode(utf-8) cursor.close() conn.close() if not stored_hash or not bcrypt.checkpw(old_password, stored_hash): flash(原密码错误) return render_template(change_password.html, user_typeuser_type) # 3. 生成新哈希并更新数据库 new_hash bcrypt.hashpw(new_password, bcrypt.gensalt()) conn get_db_connection() cursor conn.cursor() if user_type student: cursor.execute(UPDATE student SET password %s WHERE student_id %s, (new_hash.decode(utf-8), username)) elif user_type teacher: cursor.execute(UPDATE teacher SET password %s WHERE teacher_id %s, (new_hash.decode(utf-8), username)) elif user_type admin: cursor.execute(UPDATE admin SET password %s WHERE admin_name %s, (new_hash.decode(utf-8), username)) conn.commit() cursor.close() conn.close() flash(密码修改成功) return redirect(url_for(f{user_type}_dashboard)) return render_template(change_password.html, user_typeuser_type)参数说明bcrypt.gensalt(rounds12)rounds参数控制哈希强度默认12足够课设使用new_hash.decode(utf-8)bcrypt返回bytesMySQL字段是VARCHAR必须解码conn.commit()显式提交事务避免修改不生效flash()消息区分场景原密码错/新密码不一致降低用户困惑。4. 前端交互与接口联调用Postman验证RESTful接口避开jQuery版本玄学4.1 前端模板结构与静态资源组织报告提到“借鉴网上前端模板”实际项目中通常采用以下结构PyCharm工程内grade_manage_system/ ├── app.py # Flask主程序 ├── templates/ # Jinja2模板 │ ├── base.html # 基础布局含导航栏、CSS/JS引入 │ ├── login.html # 登录页 │ ├── admin/ │ │ ├── dashboard.html │ │ ├── students.html # 管理员学生管理页 │ │ └── ... │ ├── student/ │ │ ├── dashboard.html │ │ ├── profile.html # 学生个人信息页 │ │ └── grades.html # 学生成绩页 │ └── teacher/ │ ├── dashboard.html │ └── courses.html # 教师课程管理页 ├── static/ │ ├── css/ │ │ └── style.css # 自定义样式覆盖模板默认 │ └── js/ │ └── main.js # 页面级JS如成绩表格排序 └── requirements.txt # 依赖包清单关键设计点base.html中用{% block content %}{% endblock %}定义内容区子模板用{% extends base.html %}继承导航栏根据session[user_type]动态渲染菜单项如管理员显示“专业管理”学生不显示所有表单提交用POST方法禁用GET传敏感参数。4.2 Postman接口测试清单5个必测核心接口不要等前端写完再测用Postman直接调Flask接口效率翻倍。以下是课设答辩前必须验证的5个接口接口URL请求方法测试要点预期响应http://localhost:5000/loginPOSTusernameadmin,passwordasdfg13579302重定向到/admin/dashboard且Set-Cookie含session_idhttp://localhost:5000/admin/studentsGET已登录admin返回HTML页面含学生列表tableHTTP 200响应体含table标签及学生数据http://localhost:5000/student/gradesGET已登录学生如20230001返回该生成绩HTTP 200含课程名、成绩、教师名等字段http://localhost:5000/teacher/course_students/CS101GET已登录教师T001且CS101由其授课HTTP 200返回该课程学生名单及成绩http://localhost:5000/change_passwordPOSTold_password20230001,new_passwordtest123,confirm_passwordtest123HTTP 200返回成功提示且数据库student表password字段更新为bcrypt哈希Postman操作技巧在Cookies标签页勾选Enable cookies确保Session正常传递对POST接口在Body → x-www-form-urlencoded中填参数勿用raw JSONFlask默认不解析用Tests标签页写简单断言例如// 验证登录后重定向到admin页 pm.test(Status code is 302, function () { pm.response.to.have.status(302); pm.expect(pm.response.headers.get(Location)).to.include(/admin/dashboard); });玄学警告若Postman测试通过但浏览器登录失败大概率是浏览器缓存了旧的Cookie或JavaScript报错阻塞了表单提交。此时应1清空浏览器Cookie2打开F12控制台看Console是否有JS错误3用Postman复现问题——把复杂问题隔离到最小可测单元。4.3 常见前端问题排查CSS失效、JS未加载、Jinja2变量渲染失败现象登录页样式错乱按钮堆叠在一起原因static/css/style.css路径错误或Flask未正确配置静态文件服务。解决检查base.html中CSS引入是否为link relstylesheet href{{ url_for(static, filenamecss/style.css) }}确认static/目录与app.py同级在浏览器访问http://localhost:5000/static/css/style.css应直接下载CSS文件而非404。现象点击“修改密码”按钮无反应原因main.js中事件绑定代码执行时机早于DOM加载完成。解决// 错误写法可能执行时DOM未就绪 document.getElementById(changePwdBtn).addEventListener(click, function() { ... }); // 正确写法等待DOM加载完成 document.addEventListener(DOMContentLoaded, function() { const btn document.getElementById(changePwdBtn); if (btn) { btn.addEventListener(click, function() { // 表单验证逻辑 const oldPwd document.getElementById(old_password).value; if (!oldPwd) { alert(请输入原密码); return; } // 提交表单 document.getElementById(changePasswordForm).submit(); }); } });现象学生页面显示{{ student_name }}而非真实姓名原因Flask路由未将变量传入模板或Jinja2语法错误。解决检查路由函数是否用render_template(student/profile.html, studentstudent_data)传参检查模板中是否写成{{ student.student_name }}若传入的是字典或{{ student.name }}若传入的是对象在模板顶部加{% if student %}DEBUG: {{ student }}{% endif %}快速定位变量是否存在。5. 避坑指南课设高频翻车现场与后悔药清单5.1 MySQL连接拒绝Host localhost is not allowed to connect现象Flask启动时报错mysql.connector.errors.ProgrammingError: 1045 (28000): Access denied for user rootlocalhost。原因MySQL 8.0默认认证插件改为caching_sha2_password而旧版mysql-connector-python不兼容。解决-- 在MySQL命令行执行替换your_password为实际密码 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;这是课设最常卡住的点占调试时间40%以上。务必在PyCharm运行前先在MySQL Workbench里执行此SQL。5.2 触发器不生效INSERT后student_course无数据现象手动执行INSERT INTO major_course VALUES (001,CS101);但SELECT * FROM student_course返回空。原因触发器创建时未用DELIMITER //导致MySQL将;当作语句结束符只执行了CREATE TRIGGER ... BEGIN部分。解决在MySQL命令行中先执行DELIMITER //粘贴完整触发器代码含END //最后执行DELIMITER ;恢复默认分隔符用SHOW TRIGGERS LIKE trigger_insert_1;确认触发器状态为ACTIVE。5.3 中文乱码学生姓名显示为问号????现象数据库建表时用了CHARACTER SET utf8但插入中文后查出来是乱码。原因MySQL的utf8实际是utf8mb3不支持emoji和部分生僻汉字且客户端连接未指定字符集。解决建库时用CHARACTER SET utf8mb4如2.2节所示在get_db_connection()函数中显式设置字符集connection mysql.connector.connect( hostlocalhost, databasegrade_manage_system, userroot, passwordyour_password, charsetutf8mb4, # 关键 collationutf8mb4_unicode_ci )5.4 Flask路由404访问/admin/students报错现象URL正确但Flask返回404 Not Found。原因路由函数名与url_for()中名称不一致或未加app.route装饰器。解决检查函数定义app.route(/admin/students)下方必须是def admin_students():检查模板中url_for(admin_students)的字符串是否与函数名完全一致大小写、下划线在PyCharm中用CtrlClick点击url_for(xxx)看能否跳转到对应函数——跳转失败即名称错误。5.5 密码修改后无法登录新密码哈希值存入数据库但长度超限现象UPDATE student SET password ?执行成功但下次登录失败。原因password字段定义为VARCHAR(128)而bcrypt哈希值长度约60字符看似够用但若误用md5()生成32位字符串再存入会导致截断。解决用SELECT LENGTH(password) FROM student LIMIT 1;查看实际存储长度确保代码中bcrypt.hashpw()生成的哈希值是完整60字符若发现长度异常检查是否误用了hashlib.md5().hexdigest()。6. 二次开发实战从课设到可用原型的3个改造技巧6.1 支持Excel批量导入成绩用pandas解析绕过前端文件上传限制课设只要求手动录入但真实教务场景需批量导入。Flask本身不擅长处理大文件最佳实践是前端用AJAX上传后端用pandas解析分批写入数据库。import pandas as pd from werkzeug.utils import secure_filename import os app.route(/teacher/upload_grades, methods[POST]) role_required(teacher) def upload_grades(): if file not in request.files: flash(未选择文件) return redirect(url_for(teacher_courses)) file request.files[file] if file.filename : flash(文件名为空) return redirect(url_for(teacher_courses)) # 安全文件名防路径遍历 filename secure_filename(file.filename) if not filename.endswith(.xlsx): flash(仅支持.xlsx文件) return redirect(url_for(teacher_courses)) # 保存临时文件 temp_path os.path.join(/tmp, filename) # Linux/macOSWindows用os.getenv(TEMP) file.save(temp_path) try: # 用pandas读取Excel需pip install pandas openpyxl df pd.read_excel(temp_path, dtype{student_id: str, course_id: str}) # 校验必要列 required_cols [student_id, course_id, grade] if not all(col in df.columns for col in required_cols): flash(Excel必须包含student_id, course_id, grade列) return redirect(url_for(teacher_courses)) conn get_db_connection() cursor conn.cursor() # 分批插入避免单次SQL过长 batch_size 100 for i in range(0, len(df), batch_size): batch df.iloc[i:ibatch_size] # 构造VALUES子句注意student_id和course_id是字符串grade是数字 values [] for _, row in batch.iterrows(): # 先检查学生和课程是否存在且属于当前教师 cursor.execute( SELECT 1 FROM student s JOIN student_course sc ON s.student_id sc.student_id JOIN course c ON sc.course_id c.course_id WHERE s.student_id %s AND c.course_id %s AND c.teacher_id %s , (row[student_id], row[course_id], session[teacher_id])) if cursor.fetchone(): values.append((row[student_id], row[course_id], int(row[grade]))) else: flash(f跳过无效记录{row[student_id]}-{row[course_id]}) if values: cursor.executemany( INSERT INTO student_course (student_id, course_id, grade) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE grade VALUES(grade), values ) conn.commit() flash(f成功导入{len(df)}条成绩记录) except Exception as e: flash(f导入失败{str(e)}) conn.rollback() finally: cursor.close() conn.close() os.remove(temp_path) # 清理临时文件 return redirect(url_for(teacher_courses))关键点secure_filename()防止../../etc/passwd类攻击ON DUPLICATE KEY UPDATE处理重复学号-课程组合更新成绩而非报错conn.rollback()确保出错时数据不残留临时文件存于/tmp而非项目目录避免污染源码。6.2 成绩统计图表化用Chart.js在前端渲染分数分布报告中教师可“查看统计”但只是文字描述。加一行JS就能变成可视化图表!-- 在teacher/courses.html中 -- canvas idgradeChart width400 height200/canvas script srchttps://cdn.jsdelivr.net/npm/chart.js/script script // 从后端API获取统计数据假设已提供 /api/teacher/grade_stats?course_idCS101 fetch(/api/teacher/grade_stats?course_id{{ course_id }}) .then(response response.json()) .then(data { const ctx document.getElementById(gradeChart).getContext(2d); new Chart(ctx, { type: bar, data: { labels: [0-59, 60-69, 70-79, 80-89, 90-100], datasets: [{ label: 学生人数, data: [data.fail, data.sixty, data.seventy, data.eighty, data.ninety], backgroundColor: [ rgba(255, 99, 132, 0.2), rgba(54, 162, 235, 0.2), rgba(255, 205, 86, 0.2), rgba(75, 192, 192, 0.2), rgba(153, 102, 255, 0.2) ], borderColor: [ rgb(255, 99, 132), rgb(54, 162, 235), rgb(255, 205, 86), rgb(75, 192, 192), rgb(153, 102, 255) ], borderWidth: 1 }] }, options: { scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } p a hrefhttps://download.csdn.net/download/qq_62760217/86730796 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p