ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL学生成绩管理系统课设实战:从ER图到RBAC权限落地

MySQL学生成绩管理系统课设实战:从ER图到RBAC权限落地

简介:本资源是一份面向高校数据库课程设计实践的完整技术文档,适用于计算机、软件工程等专业学生完成课设任务。系统基于Python Flask框架与MySQL数据库开发,实现管理员、教师、学生三角色权限管理,支持成绩录入、查询、修改、删除及学生在线选课等功能,兼顾实用性与教学规范性。压缩包为单个3.95MB的Word文档(.doc),全面涵盖系统总体设计、需求分析、数据库概念/逻辑/物理设计、前后端实现细节(含界面截图与关键代码片段)、测试用例及课程设计报告全文,目录结构完整,符合高校课设报告标准格式。目前已有5074人学习下载,读者可直接复用该文档撰写课程设计报告,快速掌握B/S架构学生成绩系统的全周期开发流程,包括MySQL建模、Flask接口设计、HTML+CSS+JS前端整合及Postman接口调试方法。

1. 为什么一个“只跑在本地”的MySQL学生成绩管理系统,反而成了数据库课设里通过率最高的选题?

不是因为功能多炫酷,而是它把「数据库设计的完整闭环」压缩进了一个可触摸、可调试、可答辩的最小可行系统里。很多同学一上来就想做“微服务+Vue3+JWT+Redis缓存”,结果卡死在跨域、token刷新、权限拦截链上,最后连登录页都跑不起来;而这个系统用 Python Flask 搭个轻量后端,MySQL 建六张表(student/teacher/course/major/student_course/major_course/admin),前端直接套用静态模板——所有精力都落在数据怎么组织、关系怎么约束、权限怎么落地、边界怎么兜住这四件事上。它不追求高并发,但每一条 SQL 都要经得起 ER 图推演;它没上云部署,但每个 INSERT 都得考虑外键级联、空值约束、密码加密;它甚至没做前端路由守卫,却靠三层用户角色(学生/教师/管理员)把 RBAC 权限模型拆解成可验证的 if-else 分支。这不是“简陋”,是精准聚焦——数据库课设要考的,从来不是你会不会写 fetch,而是你能不能让student_course表里的一条 grade 更新,自动触发课程统计视图重算,且不崩掉major_course的专业开课逻辑。下面这份方案,就是我带过三届数据库课设后,帮 A 同学从初稿被导师打回 4 次,到终稿答辩时被当场要走源码当教学案例的完整复盘。

2. 总体架构设计:Flask + MySQL 单机栈的四层职责划分与数据流向控制

这个系统表面看是“前后端分离”,实则本质是数据驱动型架构:所有页面跳转、按钮响应、表格渲染,最终都归结为对 MySQL 中七张表的增删改查操作。Flask 不是胶水,而是数据流的调度中枢;HTML/CSS/JS 不是界面,而是数据库状态的可视化投影。这种设计让课设答辩时,导师问“学生修改密码后,旧密码还能不能登录”,你不用翻代码找 session 逻辑,直接答:“不能,因为密码字段在 student 表中已更新,且登录校验 SQL 是SELECT * FROM student WHERE student_id = ? AND password = ?,旧哈希值必然不匹配”——这就是数据库课设最想看到的思维落地。

2.1 四层结构定义:从请求入口到数据落盘的严格分界

整个系统按数据处理阶段划分为四层,每层有明确输入输出和不可越界的职责:

  • 表现层(Frontend):纯静态 HTML 页面 + 原生 JS 事件绑定,不包含任何业务逻辑。所有表单提交走<form action="/login" method="POST">,所有按钮点击触发fetch('/api/student/info', {method: 'GET'}),返回 JSON 后用document.getElementById().innerText = data.name渲染。禁止在 JS 里拼接 SQL 或做权限判断。

  • 接口层(Flask Routes):每个@app.route()对应一个原子业务动作,如/login、/student/grade、/admin/add_student。该层只做三件事:① 解析 request.form 或 request.json;② 调用 service 层函数;③ 封装 response(成功返回 JSON 或重定向,失败返回 400/403 状态码)。绝不处理密码明文、不拼接 SQL 字符串、不写 if user.role == 'admin'。

  • 服务层(Service Logic):Python 模块services.py,含auth_service.py、student_service.py、teacher_service.py、admin_service.py。这里才出现真正的业务规则,例如:

    • 学生修改密码:先查原密码哈希是否匹配 → 再用 bcrypt.hashpw() 加密新密码 → 最后执行 UPDATE;
    • 教师录成绩:检查student_id和course_id是否存在于student_course表 → 若不存在则 INSERT 初始化记录 → 再 UPDATE grade;
    • 管理员删课程:先 DELETE FROM course WHERE course_id = ? → 再触发 ON DELETE CASCADE 自动清理student_course和major_course中关联记录。
  • 数据访问层(DAO / Raw SQL):db_utils.py封装 pymysql 连接池,所有 SQL 语句以字符串形式硬编码在sql_queries.py中。例如:

    # sql_queries.py STUDENT_LOGIN = "SELECT student_id, student_name, password FROM student WHERE student_id = %s" TEACHER_COURSE_GRADE_STATS = """ SELECT CASE WHEN grade >= 90 THEN 'A' WHEN grade >= 80 THEN 'B' WHEN grade >= 70 THEN 'C' WHEN grade >= 60 THEN 'D' ELSE 'F' END as grade_level, COUNT(*) as count FROM student_course sc JOIN course c ON sc.course_id = c.course_id WHERE c.teacher_id = %s AND sc.grade IS NOT NULL GROUP BY grade_level ORDER BY FIELD(grade_level, 'A','B','C','D','F') """

    提示:所有 SQL 必须参数化(%s 占位符),严禁 f-string 拼接。这是数据库课设的红线——哪怕只是课设,也要养成防 SQL 注入的肌肉记忆。

2.2 数据流向控制:从登录态建立到成绩查询的全链路追踪

以学生查看自己成绩为例,走一遍真实数据流:

  1. 前端发起请求:学生在/student/home.html点击“查看成绩”按钮,JS 执行:

    fetch('/api/student/grades', { method: 'GET', headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } }) .then(r => r.json()) .then(data => renderGradesTable(data));
  2. 接口层接收并鉴权:Flask 路由/api/student/grades触发:

    @app.route('/api/student/grades', methods=['GET']) def get_student_grades(): student_id = get_current_student_id() # 从 token 解析或 session 获取 if not student_id: return jsonify({'error': '未登录'}), 401 grades = student_service.get_grades_by_student_id(student_id) return jsonify(grades)
  3. 服务层组装查询逻辑:student_service.py中:

    def get_grades_by_student_id(student_id): # 关键:JOIN 多表获取可读信息,而非仅查 student_course sql = """ SELECT c.course_id, c.course_name, c.credit, t.teacher_name, sc.grade, c.semester, c.year FROM student_course sc JOIN course c ON sc.course_id = c.course_id JOIN teacher t ON c.teacher_id = t.teacher_id WHERE sc.student_id = %s AND sc.grade IS NOT NULL ORDER BY c.year DESC, c.semester DESC """ return db_utils.execute_query(sql, (student_id,))
  4. 数据层执行并返回:db_utils.execute_query()使用连接池执行 SQL,自动处理异常、关闭游标,返回字典列表:

    [ {'course_id': 'CS101', 'course_name': '数据结构', 'credit': 4, 'teacher_name': '张老师', 'grade': 87, 'semester': '秋', 'year': '2023'}, {'course_id': 'MATH202', 'course_name': '高等数学II', 'credit': 5, 'teacher_name': '李老师', 'grade': 92, 'semester': '春', 'year': '2023'} ]

注意:这个流程里没有“前端判断用户角色再决定显示哪个按钮”,所有权限控制都在服务层完成。学生调用/api/student/grades可以查,但若他尝试 GET/api/admin/students,接口层直接 403 —— 这才是数据库课设要求的“基于数据的权限隔离”,而不是靠前端藏按钮糊弄过去。

2.3 用户角色与数据隔离策略:RBAC 在单库中的轻量实现

系统虽小,但 RBAC(基于角色的访问控制)必须真实落地,不能只写在需求文档里。我们用三张物理表 + 一套 SQL 约束实现:

  • 角色存储:不单独建 role 表,而是在student、teacher、admin三张表中用主键天然区分身份。student_id以S开头(如S2021001),teacher_id以T开头(如T00123),admin_name固定为admin。登录时根据账号前缀或表名即可判定角色。

  • 数据可见性控制:所有查询 SQL 显式加 WHERE 过滤。例如教师查自己所教课程的学生名单:

    -- 正确:用 teacher_id 关联 course,再关联 student_course,最后 JOIN student SELECT s.student_id, s.student_name, sc.grade FROM course c JOIN student_course sc ON c.course_id = sc.course_id JOIN student s ON sc.student_id = s.student_id WHERE c.teacher_id = %s;

    错误示范:先查出所有学生,再在 Python 里 for 循环过滤 —— 这违背数据库课设“数据在库内处理”的核心思想。

  • 写操作约束:INSERT/UPDATE 语句强制携带角色上下文。如教师录成绩:

    # teacher_service.py def update_student_grade(teacher_id, student_id, course_id, new_grade): # 先验证该课程是否由当前教师授课 course_check_sql = "SELECT 1 FROM course WHERE course_id = %s AND teacher_id = %s" if not db_utils.execute_query(course_check_sql, (course_id, teacher_id)): raise PermissionError("无权修改非本人授课课程成绩") # 再执行更新 upsert_sql = """ INSERT INTO student_course (student_id, course_id, grade) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE grade = VALUES(grade) """ db_utils.execute_query(upsert_sql, (student_id, course_id, new_grade))

这套设计让“权限”不再是抽象概念,而是可执行、可测试、可截图答辩的 SQL 片段。导师问“怎么保证教师只能改自己课的成绩”,你打开teacher_service.py,指着那两段 SQL 就能讲清楚。

3. 数据库设计落地:从 E-R 图到可运行建表语句的 7 大关键决策

很多同学的课设被毙,不是因为代码写错,而是数据库设计第一关就塌方:ER 图画得漂亮,建表时忘了外键,插入测试数据时主键冲突,查成绩时发现没建索引导致卡顿……这份方案把从概念模型到物理表的每一步决策都摊开,告诉你为什么这么选、不这么选会翻什么车。

3.1 实体关系映射:E-R 图到表结构的 3 处易错转化点

原始 E-R 图中有学生、教师、课程、专业、专业选课、学生选课、管理员 7 个实体/联系。但实际建表时,我们合并了部分实体,做了如下关键转化:

  • 管理员不作为独立实体建表,而是单行配置:admin表只有一条记录(admin_name='admin',password='asdfg13579')。原因:课设场景下管理员唯一且固定,无需增删改查其他管理员,建表只为满足“有登录入口”这一功能点。若强行建多管理员表,反而要额外设计admin_role字段,偏离课设重点。

  • 专业选课(major_course)与学生选课(student_course)分离,但建立派生关系:E-R 图中“专业选课”是专业与课程的多对多联系,“学生选课”是学生与课程的多对多联系。但现实中,学生只能选本专业开设的课程。因此我们在major_course表上建触发器,当新增专业开课时,自动为该专业所有学生初始化student_course记录(grade 为 NULL):

    DELIMITER // CREATE TRIGGER after_insert_major_course 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 ;

    这个触发器是课设亮点:它把“专业培养方案”这一业务规则,固化在数据库层面,而非靠应用层循环插入。答辩时演示“添加计算机专业开《算法导论》”,后台自动为所有计科学生生成待录入成绩记录,导师立刻明白你懂数据库的主动约束力。

  • 教师与课程的“授课”关系,不建独立关系表,而用 course 表的 teacher_id 外键实现:E-R 图中“授课”是学生-教师-课程三元联系,但简化为二元联系更符合实际(一门课由一位教师主讲)。course.teacher_id直接引用teacher.teacher_id,并加ON UPDATE CASCADE,确保教师工号变更时课程自动同步。

3.2 表结构设计:6 张核心表的字段取舍与约束依据

以下是最终采用的 6 张表(admin表单列,不计入核心业务表),每张表的字段设计都直指课设考核点:

表名字段类型约束设计说明课设考点
studentstudent_idCHAR(10)PRIMARY KEY学号固定 10 位,如S20210001,避免 INT 自增导致学号泄露年级主键设计合理性
major_idCHAR(3)FOREIGN KEY REFERENCES major(major_id) ON DELETE CASCADE专业编号 3 位,如CS、MA,外键级联删除保障数据一致性外键约束实践
passwordVARCHAR(128)NOT NULLbcrypt 加密后长度约 60 字符,预留 128 安全密码安全存储
teacherteacher_idCHAR(5)PRIMARY KEY教师编号 5 位,如T0012,与 student_id 编码规则区分实体标识规范
coursecourse_idCHAR(5)PRIMARY KEY课程编号 5 位,如CS101,自解释性强业务主键设计
teacher_idCHAR(5)FOREIGN KEY REFERENCES teacher(teacher_id) ON UPDATE CASCADE授课教师变更时自动更新,避免脏数据级联操作应用
majormajor_idCHAR(3)PRIMARY KEY专业编号 3 位,如CS,与 student.major_id 严格一致数据字典统一
major_nameVARCHAR(20)UNIQUE NOT NULL专业名称唯一,防止“计算机科学”和“计算机科学与技术”重复唯一性约束
student_coursestudent_id,course_idCHAR(10),CHAR(5)PRIMARY KEY (student_id, course_id)联合主键,天然杜绝同一学生重复选同一门课复合主键实践
gradeTINYINT UNSIGNEDCHECK (grade BETWEEN 0 AND 100)成绩 0~100 整数,CHECK 约束防异常值数据完整性约束
major_coursemajor_id,course_idCHAR(3),CHAR(5)PRIMARY KEY (major_id, course_id)联合主键,表示“某专业开设某课程”多对多联系实现

注意:student_course.grade允许为 NULL,表示“已选课但未录入成绩”。这是真实业务场景(期末考试前选课,考后录分),也是课设中体现“数据状态管理”的细节。

3.3 索引与性能优化:3 个必加索引及失效场景排查

课设不要求高并发,但必须体现基础性能意识。以下索引在建表后立即创建:

-- 1. 学生查成绩:按 student_id 快速定位所有选课记录 CREATE INDEX idx_student_course_student_id ON student_course(student_id); -- 2. 教师查授课班级:按 teacher_id 关联 course,再查 student_course CREATE INDEX idx_course_teacher_id ON course(teacher_id); -- 3. 管理员模糊查学生:按姓名快速检索(籍贯、入学年份等非高频查询字段不建索引) CREATE INDEX idx_student_name ON student(student_name);

索引失效的典型场景与修复(答辩常问):

  • 现象:SELECT * FROM student WHERE student_name LIKE '%明%'很慢
    原因:%在开头导致索引失效
    解决:课设中改为精确查询(WHERE student_name = '张明'),或告知导师“生产环境会用全文索引或 ES,课设聚焦核心流程”

  • 现象:SELECT * FROM student_course WHERE student_id = 'S20210001' AND grade > 85未走索引
    原因:idx_student_course_student_id是单列索引,对grade条件无效
    解决:建联合索引CREATE INDEX idx_sc_sid_grade ON student_course(student_id, grade),但课设中因数据量小(<1000 行),可暂不加,答辩时说明“数据量小时优化优先级低于功能完整性”

  • 现象:DELETE FROM student WHERE student_id = 'S20210001'卡住
    原因:student_course表未对student_id建索引,级联删除需全表扫描
    解决:必须加idx_student_course_student_id,这是外键级联的硬性要求

这些不是玄学,是课设答辩时你能脱口而出的“我考虑过,且有依据”的证据。

4. 核心功能实现:学生/教师/管理员三大角色的 9 个关键接口与代码实录

代码不是堆砌,而是围绕数据库操作展开的精密 choreography。下面 9 个接口覆盖课设全部得分点,每个都附可直接粘贴运行的代码、SQL 注释、以及我当年带 A 同学时踩过的坑。

4.1 学生模块:3 个接口实现“查信息、看成绩、改密码”的闭环

4.1.1 学生登录认证:从明文密码到哈希比对的完整链路
# auth_service.py import bcrypt def verify_student_login(student_id, plain_password): """ 学生登录验证 :param student_id: 学号,如 'S20210001' :param plain_password: 用户输入的明文密码(初始为学号本身) :return: dict with keys 'success', 'student_info', 'error' """ # 1. 从数据库查学生记录(含加密密码) sql = "SELECT student_id, student_name, password FROM student WHERE student_id = %s" result = db_utils.execute_query(sql, (student_id,)) if not result: return {'success': False, 'error': '学号不存在'} stored_hash = result[0]['password'].encode('utf-8') # 数据库存的是字符串,需转 bytes # 2. 用 bcrypt.checkpw 验证(注意:plain_password 也要 encode) if bcrypt.checkpw(plain_password.encode('utf-8'), stored_hash): return { 'success': True, 'student_info': { 'id': result[0]['student_id'], 'name': result[0]['student_name'] } } else: return {'success': False, 'error': '密码错误'} # Flask 路由 @app.route('/login/student', methods=['POST']) def student_login(): student_id = request.form.get('student_id') password = request.form.get('password') result = auth_service.verify_student_login(student_id, password) if result['success']: # 生成简单 token(课设不需 JWT,用 session 或 localStorage 存 student_id 即可) session['current_student'] = result['student_info']['id'] return redirect('/student/home.html') else: return render_template('login_fail.html', message=result['error'])

设计说明:

  • bcrypt.checkpw()是标准做法,比 MD5/SHA256 更安全,且自带盐值;
  • stored_hash从 DB 取出后.encode('utf-8')是关键!很多同学忘记这步,报TypeError: Unicode-objects must be encoded before hashing;
  • 初始密码为学号,但登录成功后必须引导修改,这是课设“安全性”得分点。
4.1.2 学生成绩查询:JOIN 多表生成可读成绩单
# student_service.py def get_student_grades(student_id): """ 获取学生所有已录入成绩的详细信息 返回字段:课程编号、课程名、学分、教师、成绩、学期、学年 """ sql = """ SELECT c.course_id, c.course_name, c.credit, t.teacher_name, sc.grade, c.semester, c.year FROM student_course sc JOIN course c ON sc.course_id = c.course_id JOIN teacher t ON c.teacher_id = t.teacher_id WHERE sc.student_id = %s AND sc.grade IS NOT NULL ORDER BY c.year DESC, c.semester DESC, c.course_id """ return db_utils.execute_query(sql, (student_id,)) # Flask 路由(返回 JSON,供前端 JS 渲染) @app.route('/api/student/grades', methods=['GET']) def api_student_grades(): student_id = session.get('current_student') if not student_id: return jsonify({'error': '未登录'}), 401 grades = student_service.get_student_grades(student_id) return jsonify(grades)

参数说明:

  • ORDER BY c.year DESC, c.semester DESC确保最新学期成绩排在最前,符合学生查看习惯;
  • sc.grade IS NOT NULL过滤掉未录入成绩的记录,避免前端显示 “NULL”;
  • 所有字段名用英文下划线(course_name),与数据库一致,减少映射错误。
4.1.3 学生修改密码:哈希更新与旧密码验证双保险
# student_service.py def change_student_password(student_id, old_password, new_password, confirm_password): """ 学生修改密码 :return: dict with 'success', 'message' """ # 1. 验证旧密码正确性(复用登录验证逻辑) if not auth_service.verify_student_login(student_id, old_password)['success']: return {'success': False, 'message': '原密码错误'} # 2. 验证两次新密码一致 if new_password != confirm_password: return {'success': False, 'message': '两次输入的新密码不一致'} # 3. 用 bcrypt 重新哈希新密码 hashed = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()) # 4. 更新数据库 sql = "UPDATE student SET password = %s WHERE student_id = %s" try: db_utils.execute_update(sql, (hashed.decode('utf-8'), student_id)) # 注意:hashpw 返回 bytes,存 DB 需 decode return {'success': True, 'message': '密码修改成功'} except Exception as e: return {'success': False, 'message': f'数据库更新失败: {str(e)}'} # Flask 路由 @app.route('/student/change_password', methods=['POST']) def change_student_pwd(): student_id = session.get('current_student') if not student_id: return jsonify({'error': '未登录'}), 401 old_pwd = request.form.get('old_password') new_pwd = request.form.get('new_password') confirm_pwd = request.form.get('confirm_password') result = student_service.change_student_password(student_id, old_pwd, new_pwd, confirm_pwd) return jsonify(result)

血泪经验:

  • bcrypt.hashpw()返回bytes,存入VARCHAR字段前必须.decode('utf-8'),否则报TypeError: not all arguments converted during string formatting;
  • bcrypt.gensalt()的rounds参数课设默认即可(12),无需调优;
  • 错误信息要具体(“原密码错误”而非“操作失败”),方便调试。

4.2 教师模块:3 个接口支撑“管课、录分、统计”的教学闭环

4.2.1 教师登录与课程管理:按教师 ID 查所授课程
# teacher_service.py def get_teacher_courses(teacher_id): """ 获取教师所授所有课程基本信息 """ sql = """ SELECT c.course_id, c.course_name, c.credit, c.year, c.semester, COUNT(sc.student_id) as student_count FROM course c LEFT JOIN student_course sc ON c.course_id = sc.course_id WHERE c.teacher_id = %s GROUP BY c.course_id, c.course_name, c.credit, c.year, c.semester ORDER BY c.year DESC, c.semester DESC """ return db_utils.execute_query(sql, (teacher_id,)) # Flask 路由 @app.route('/teacher/courses', methods=['GET']) def teacher_courses(): teacher_id = session.get('current_teacher') if not teacher_id: return redirect('/login/teacher') courses = teacher_service.get_teacher_courses(teacher_id) return render_template('teacher_courses.html', courses=courses)

设计说明:

  • LEFT JOIN确保即使某门课还没学生选,也显示student_count=0;
  • GROUP BY后COUNT()统计选课人数,是教师最关心的数据;
  • ORDER BY c.year DESC让最新开课排前面,符合教学管理逻辑。
4.2.2 教师录入学生成绩:UPSERT 实现“有则更新、无则插入”
# teacher_service.py def upsert_student_grade(teacher_id, student_id, course_id, grade): """ 录入或更新学生成绩 :param teacher_id: 当前登录教师 ID,用于权限校验 :param student_id: 学生学号 :param course_id: 课程编号 :param grade: 成绩(0-100 整数) :return: dict with 'success', 'message' """ # 1. 权限校验:确认该课程是否由当前教师授课 check_sql = "SELECT 1 FROM course WHERE course_id = %s AND teacher_id = %s" if not db_utils.execute_query(check_sql, (course_id, teacher_id)): return {'success': False, 'message': '无权录入非本人授课课程成绩'} # 2. 执行 UPSERT(MySQL 用 INSERT ... ON DUPLICATE KEY UPDATE) upsert_sql = """ INSERT INTO student_course (student_id, course_id, grade) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE grade = VALUES(grade) """ try: db_utils.execute_update(upsert_sql, (student_id, course_id, grade)) return {'success': True, 'message': '成绩录入成功'} except Exception as e: return {'success': False, 'message': f'录入失败: {str(e)}'} # Flask 路由(接收 POST 表单) @app.route('/teacher/grade', methods=['POST']) def teacher_grade(): teacher_id = session.get('current_teacher') if not teacher_id: return jsonify({'error': '未登录'}), 401 student_id = request.form.get('student_id') course_id = request.form.get('course_id') try: grade = int(request.form.get('grade')) except (ValueError, TypeError): return jsonify({'error': '成绩必须为数字'}), 400 result = teacher_service.upsert_student_grade(teacher_id, student_id, course_id, grade) return jsonify(result)

避坑点:

  • ON DUPLICATE KEY UPDATE依赖student_course的联合主键(student_id, course_id),若建表时没设主键,此语句会报错;
  • int(request.form.get('grade'))的异常捕获必须做,否则用户输字母直接 500 错误;
  • 返回 JSON 时用jsonify(),别用return str(result),否则前端 fetch 拿不到 JSON 对象。
4.2.3 教师课程成绩统计:按分数段分组聚合
# teacher_service.py def get_course_grade_stats(teacher_id, course_id): """ 获取某门课程的成绩分布统计(A/B/C/D/F) """ sql = """ SELECT CASE WHEN sc.grade >= 90 THEN 'A' WHEN sc.grade >= 80 THEN 'B' WHEN sc.grade >= 70 THEN 'C' WHEN sc.grade >= 60 THEN 'D' ELSE 'F' END as grade_level, COUNT(*) as count FROM student_course sc JOIN course c ON sc.course_id = c.course_id WHERE c.teacher_id = %s AND c.course_id = %s AND sc.grade IS NOT NULL GROUP BY grade_level ORDER BY FIELD(grade_level, 'A','B','C','D','F') """ return db_utils.execute_query(sql, (teacher_id, course_id)) # Flask 路由 @app.route('/api/teacher/course/stats', methods=['GET']) def api_teacher_course_stats(): teacher_id = session.get('current_teacher') course_id = request.args.get('course_id') if not teacher_id or not course_id: return jsonify({'error': '参数缺失'}), 400 stats = teacher_service.get_course_grade_stats(teacher_id, course_id) return jsonify(stats)

参数说明:

  • FIELD()函数确保返回顺序为 A→B→C→D→F,而非默认字母序(F 在 A 前);
  • sc.grade IS NOT NULL过滤未录入成绩,避免统计进“F”档;
  • 此 SQL 是课设“复杂查询”得分项,必须手写,不能靠 Python 循环计算。

4.3 管理员模块:3 个接口覆盖“增删改查”的全量数据治理

4.3.1 管理员登录:固定账号密码的极简实现
# auth_service.py def verify_admin_login(username, password): """管理员登录验证(固定账号 admin/asdfg13579)""" if username == 'admin' and password == 'asdfg13579': return {'success': True} return {'success': False, 'error': '用户名或密码错误'} # Flask 路由 @app.route('/login/admin', methods=['POST']) def admin_login(): username = request.form.get('username') password = request.form.get('password') result = auth_service.verify_admin_login(username, password) if result['success']: session['current_admin'] = 'admin' return redirect('/admin/home.html') else: return render_template('login_fail.html', message=result['error'])

设计说明:课设不需复杂权限系统,固定密码体现“管理员唯一性”,且asdfg13579是键盘左下角连按,易记不易输错。

4.3.2 管理员添加学生:外键约束下的完整字段校验
# admin_service.py def add_student(student_data): """ 添加学生 :param student_data: dict, keys: student_id, student_name, sex, birth_year, province, enter_year, major_id, password :return: dict with 'success', 'message' """ # 1. 必填字段校验 required_fields = ['student_id', 'student_name', 'enter_year', 'major_id'] for field in required_fields: if not student_data.get(field): return {'success': False, 'message': f'{field} 为必填项'} # 2. 专业编号存在性校验(外键约束,但应用层提前提示更友好) major_check_sql = "SELECT 1 FROM major WHERE major_id = %s" if not db_utils.execute_query(major_check_sql, (student_data['major_id'],)): return {'success': False, 'message': '专业编号不存在,请先添加专业'} # 3. 学号唯一性校验(避免主键冲突) id_check_sql = "SELECT 1 FROM student WHERE student_id = %s" if db_utils.execute_query(id_check_sql, (student_data['student_id'],)): return {'success': False, 'message': '学号已存在'} # 4. 密码加密 plain_pwd = student_data.get('password', student_data['student_id']) # 默认用学号作密码 hashed_pwd = bcrypt.hashpw(plain_pwd.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') # 5. 插入学生表 insert_sql = """ INSERT INTO student (student_id, student_name, sex, birth <p> <a href="https://download.csdn.net/download/qq_62760217/86730796" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进