简介面向数据库课程设计、学生成绩管理及Web开发入门这是一份基于Python Flask与MySQL的学生成绩管理系统完整设计文档。系统按管理员、教师、学生三类角色划分权限实现了课程管理、成绩录入修改删除查询、学生选课、成绩统计及个人信息维护等功能前端采用HTML/CSS/JS与后端分离开发适合作为计算机专业课程设计或毕业设计参考。文档共1个doc文件压缩包约3.95MB包含系统总体设计、需求分析、概要设计、数据库概念与逻辑结构设计、系统实现及测试总结等章节详细呈现了功能模块划分、数据表结构、主要界面和关键程序代码可直接按步骤理解开发思路并复用设计框架。目前已有5072人学习下载对希望快速完成同类课设、熟悉FlaskMySQL前后端分离项目的读者具有实际借鉴价值。1. MySQL 学生成绩管理系统先搞清楚它到底是课设还是生产系统学期末在群里被问得最多的 JavaWeb 课程设计十个里有六七个是「学生成绩管理系统」。这个名字听起来像是教学 demo实际上一旦把需求摊开——学生信息维护、课程安排、成绩录入、排名统计、班级均分对比——它正好把 MySQL 的核心功能全部覆盖了一遍多表关联、事务提交、存储过程、索引调优、锁与隔离级别。所以不管你是拿它交课设还是想借此把 MySQL 从「会写 SELECT」推到「能设计表、能调慢查询」的层次这个标题都值得认真对待。本文按一条完整的落地路径走先谈表结构为什么这样建再讲 JDBC 怎么连、成绩事务怎么提交最后是排序、存储过程、索引与排错。所有 SQL 都基于 MySQL 8.0 以上版本用的驱动是 8.x 的 mysql-connector-j和你在 mysql 安装教程里装的 8.0/8.4 系列能直接对上。如果你是照着 5.7 的老教程学的文中会标出差异点。2. 成绩系统的三张核心表DDL 设计与约束设置2.1 用三张表还是两张表学生、课程、成绩的关系模型常见做法是建三张表student学生、course课程、score成绩。学生和课程是多对多关系成绩表是中间关系表。有人为了省事把课程字段直接塞进学生表比如加一列course_name、一列score这样做在录入时确实少写几个 JOIN但一旦出现「一个学生选多门课」就立刻翻车要么数据冗余要么只能存最后一门成绩。关系模型在这里的关键不是「规范化」三个字而是后续统计 SQL 是否好写。一张规范的成绩表天然支持按课程分组、按学生汇总、按班级排名不需要改表结构。下面是建表脚本按 MySQL 8.0 语法写-- 学生表 CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 学号, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号(业务编号), stu_name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) DEFAULT NULL COMMENT 班级, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 课程表 CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 成绩表 CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生ID, course_id BIGINT NOT NULL COMMENT 课程ID, score DECIMAL(5,2) NOT NULL COMMENT 成绩, exam_time DATE DEFAULT NULL COMMENT 考试日期, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE RESTRICT, CONSTRAINT chk_score_range CHECK (score 0 AND score 100) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;这段建表逻辑有几个点值得展开。UNIQUE KEY uk_student_course保证同一个学生对同一门课只有一条成绩记录业务上这叫「选课唯一约束」比在 Java 代码里先 SELECT 再 INSERT 靠谱得多。外键的删除策略我用了一混搭学生删除时成绩级联删课程删除时成绩拒绝删。理由很实际——删学生通常意味着整条学籍记录作废删课程如果直接级联删成绩期末复盘时数据就没了。2.2 成绩字段的 DECIMAL 与 CHECK 约束细节成绩用DECIMAL(5,2)而不是INT是因为很多学校成绩计小数点后两位比如 89.5 或 92.25。但要注意Java 端用float或double取值后写回 MySQL 容易出现精度漂移常见做法是在 Java 侧用BigDecimal数据库侧用DECIMAL两头都是精确小数。CHECK (score 0 AND score 100)这个约束有个版本坑MySQL 8.0.16 之前CHECK 约束会被解析但直接忽略。如果你用 5.7 版本上面的chk_score_range等于白写必须在应用层校验。MySQL 8.0.16 之后才是真正强制。所以你在 mysql 安装教程里装的 8.0 或 8.4 不需要担心这个问题。ALTER TABLE 是后续调整结构的常用手段。比如你想给score表加一个「考试类型」字段ALTER TABLE score ADD COLUMN exam_type TINYINT DEFAULT 1 COMMENT 考试类型 1期中 2期末;如果你想把成绩表的唯一索引改成允许补考两次先删原唯一键再建新键ALTER TABLE score DROP INDEX uk_student_course; ALTER TABLE score ADD UNIQUE KEY uk_student_course_examtype (student_id, course_id, exam_type);2.3 初始化数据的 INSERT 与常见纠缠插入学生数据时有两个纠缠几乎每个课设都会遇到。第一个是学号用业务编号stu_no还是自增id。我的建议是两列都留自增id做外键关联stu_no做界面展示和 Excel 导入匹配。不要让stu_no既是主键又是业务编号因为学号经常有前导零比如2024级学号202401001用 INT 存会把前导零丢掉用 VARCHAR 做异地导入导出又容易类型不一致。第二个纠缠是批量插入成绩。课程设计里的成绩录入大多是一次性导入一个班几十人用一条 INSERT 带多组 VALUES 比循环逐条 INSERT 性能好一个量级INSERT INTO score (student_id, course_id, score, exam_time) VALUES ((SELECT id FROM student WHERE stu_no202401001), 1, 88.5, 2024-06-30), ((SELECT id FROM student WHERE stu_no202401002), 1, 76.0, 2024-06-30), ((SELECT id FROM student WHERE stu_no202401003), 1, 93.0, 2024-06-30);这段 SQL 里用了关联子查询把stu_no翻译成student_id是为了导入 Excel 时能直接粘贴学号。如果数据量上千行子查询开销会很明显一般我会先SELECT id, stu_no INTO临时表做映射再 JOIN 插入。3. 用 JDBC 把 JavaWeb 项目连上 MySQLDAO 层与成绩统计3.1 mysql-connector-j 驱动的连接参数配置JavaWeb 项目连 MySQL驱动坐标和连接参数是第一个翻车点。老教程里写com.mysql.jdbc.Driver那是 5.x 驱动的包名换成 MySQL 8.x 的mysql-connector-j之后驱动类名变成com.mysql.cj.jdbc.DriverURL 也要多拼几个参数。下边是一份可以直接用的db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/student_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_passwordserverTimezoneAsia/Shanghai这个参数是 8.x 强制要求的不写会直接报 SQLException 提示时区错误。useSSLfalse是因为本地开发 MySQL 通常没配 SSL 证书开着反而在连接时报ssl connection error。characterEncodingutf8配合建表时的utf8mb4才能保证中文不乱码。对应的 JDBC 连接工具类我一般写成静态块加载配置public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(Database config load failed: e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }如果你是用 Maven 管理依赖mysql-connector-j的典型坐标是com.mysql:mysql-connector-j:8.0.33版本号按你本地安装的 MySQL 8.x 对应选。Spring Boot 项目只要引入spring-boot-starter-jdbc和对应驱动上面的手写工具类可以整体替换成 HikariCP 数据源但底层连接参数是同一套。3.2 PreparedStatement 与成绩录入的最小实现JDBC 操作里最容易出安全问题的是字符串拼接 SQL。成绩录入界面传进来的是用户填的数字和下拉框选的学生、课程一旦用INSERT INTO score VALUES ( id , courseId )这种写法SQL 注入只需一个闭合引号。从第一天就养成PreparedStatement的习惯后边不用返工。public int insertScore(long studentId, long courseId, BigDecimal score) { String sql INSERT INTO score (student_id, course_id, score, exam_time) VALUES (?, ?, ?, NOW()); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, studentId); ps.setLong(2, courseId); ps.setBigDecimal(3, score); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } }这个方法有几点说明。try-with-resources会自动关闭 Connection 和 PreparedStatement不写 finally 也不会漏连接。setLong和setBigDecimal是真正传给 MySQL 的值参数类型和表结构里的BIGINT、DECIMAL一一对应。executeUpdate()返回受影响行数0 表示没有插入成功。这里如果触发了唯一键uk_student_courseMySQL 会抛 SQLIntegrityConstraintViolationException你需要在上层捕获后提示「该学生这门课已有成绩」。3.3 成绩统计的 JOIN 与 GROUP BY 组合查询成绩管理系统的核心查询按频率排序大概是按班级看某门课成绩、按学生看所有成绩、算单科平均分和及格率。这三个查询用同一张三表 JOIN 都能完成区别在分组粒度。下面这段是「某门课程每个班的平均分与人数」也是评教和教学检查最常看的报表SELECT s.class_name, COUNT(*) AS exam_count, ROUND(AVG(sc.score), 2) AS avg_score, SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) / COUNT(*) AS pass_rate FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE c.course_name 数据结构 GROUP BY s.class_name ORDER BY avg_score DESC;CASE WHEN配合SUM的条件计数是计算及格率的惯用写法比先查总数再查及格数两次查询高效。ORDER BY avg_score DESC里的avg_score是 SELECT 子句的别名MySQL 允许在 ORDER BY 里直接用别名但不能在 WHERE 里用别名这是新手查「为什么 WHERE 不认识我起的别名」最常见的原因。ROUND(..., 2)是给平均分做两位小数处理避免输出 72.333333 这种结果。如果只想看单个学生的总成绩和排名用子查询临时表SELECT t.student_id, t.total_score, RANK() OVER (ORDER BY t.total_score DESC) AS rank_no FROM ( SELECT student_id, SUM(score) AS total_score FROM score GROUP BY student_id ) t WHERE t.student_id 12345;4. 存储过程、事务处理与排序让 MySQL 替你把成绩算好4.1 一个期末成绩结算的存储过程示例成绩系统的存储过程最常见的诉求是「期末一次性汇总本学期所有学生总成绩写入一张总评表」。Java 端循环查再循环算也能做但数据量大时网络往返次数太多而且中间状态不好控制。把计算放在 MySQL 内部只需要一次调用。DELIMITER // CREATE PROCEDURE sp_calc_semester_summary(IN p_exam_type TINYINT) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; DELETE FROM semester_summary WHERE exam_type p_exam_type; INSERT INTO semester_summary (student_id, exam_type, total_score, avg_score) SELECT student_id, p_exam_type, SUM(score), ROUND(AVG(score), 2) FROM score WHERE exam_type p_exam_type GROUP BY student_id; COMMIT; END// DELIMITER ;这个存储过程里有三个值得说的设计。DECLARE EXIT HANDLER FOR SQLEXCEPTION是事务的后悔药设置中途任何一步报错自动 ROLLBACK 并把错误重新抛出不会留下一半新数据一半老数据。DELETE先清空再INSERT保证重复调用不会把总评表越插越厚。调用时传p_exam_type参数可以分别结算期中、期末。调用方式CALL sp_calc_semester_summary(2);注意DELIMITER //是 mysql 命令行客户端的语法在 Navicat 这类图形工具里直接创建过程不要写这一行在 JDBC 里执行整段建过程脚本时会报错因为 JDBC 不支持 DELIMITER 关键字。4.2 成绩批量录入的事务边界与控制成绩录入是一个典型的事务处理场景老师一次导入全班成绩中间一条学号填错导致外键失败如果不用事务前 30 条已经写进库后 20 条报错回滚数据就处于残缺状态。用 JDBC 的事务控制关键是把 auto-commit 关掉操作完成后手动提交或回滚。public boolean batchInsertScores(ListScoreEntity scoreList) { String sql INSERT INTO score (student_id, course_id, score, exam_time) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (ScoreEntity sc : scoreList) { ps.setLong(1, sc.getStudentId()); ps.setLong(2, sc.getCourseId()); ps.setBigDecimal(3, sc.getScore()); ps.setDate(4, Date.valueOf(sc.getExamTime())); ps.addBatch(); } ps.executeBatch(); } conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } }事务边界就画在setAutoCommit(false)和conn.commit()之间。addBatch()executeBatch()是批量写入的推荐姿势比循环执行executeUpdate()快很多因为它把多条 INSERT 打包发给 MySQL减少了往返。这里必须强调一个坑不要在同一事务里先做大量 SELECT 再做写操作因为 MySQL 默认隔离级别是 REPEATABLE READ读取会建立一致性快照一旦事务较长快照占用和锁等待时间都会上升。4.3 成绩排序的多种姿势ORDER BY 单列、多列与中文排序排序在成绩系统里看似简单细究起来至少有三种场景。第一种是总成绩排名ORDER BY total_score DESC。第二种是同分情况下按学号升序排让榜单顺序稳定SELECT student_id, SUM(score) AS total_score FROM score GROUP BY student_id ORDER BY total_score DESC, student_id ASC;第三种是课程名按中文排序。MySQL 默认的 utf8mb4 排序规则是utf8mb4_0900_ai_ci对于常见汉字是按 Unicode 编码排不是按拼音排。如果你需要按拼音排序课程名要在建表或查询时指定排序规则SELECT course_name FROM course ORDER BY course_name COLLATE utf8mb4_zh_0900_as_cs;utf8mb4_zh_0900_as_cs是 MySQL 8.0 的简体中文拼音排序规则包含声调与大小写敏感属性。5.7 版本没有这个排序规则常见替代是用CONVERT(course_name USING gbk)排序但不推荐在正式环境这么干因为 GBK 转码会破坏部分字符。5. 安装、乱码、锁与慢查询成绩系统的避坑与排查清单5.1 驱动类名变更与 SSL 连接报错现象项目引入 mysql-connector-j 8.x 后启动报ClassNotFoundException: com.mysql.jdbc.Driver或者连接时抛CommunicationsException。原因MySQL 8.x 的驱动类包名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.DriverJDBC URL 也要增加useSSLfalse和serverTimezone参数。驱动又老又新的混用比如 5.1.49 驱动连 MySQL 8.0 数据库还会触发SSL connection error。解决检查db.properties里的驱动类名是否是com.mysql.cj.jdbc.DriverURL 是否带了serverTimezoneAsia/ShanghaiuseSSLfalse。如果是 Maven 项目用依赖树确认没有同时引入两个版本的 mysql-connectormvn dependency:tree -Dincludesmysql:mysql-connector-java5.2 控制台与数据库的中文乱码现象通过 JDBC 插入中文姓名表里显示正常但命令行查询乱码或者界面传中文进去数据库直接变???。原因三层字符集不一致——数据库表的 charset 是latin1客户端连接字符集是gbkJDBC URL 没带characterEncoding。任何一个环节不对中文要么存不进去要么读出来是乱码。解决建库时统一用 utf8mb4CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC URL 带上characterEncodingutf8。已经建好的库可以修改结构ALTER DATABASE student_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.3 外键约束导致删除学生失败现象删除一条学生记录时报Cannot delete or update a parent row: a foreign key constraint fails但成绩表里明明已经没有该学生的数据。原因成绩表的外键fk_score_student设置了ON DELETE CASCADE但如果另一个地方比如补考表、选课表也引用了 student 表的 id且那边没设级联删除删除主表记录时 MySQL 会先检查所有外键引用。解决先查所有外键引用再决定策略SELECT TABLE_NAME, CONSTRAINT_NAME, DELETE_RULE FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE REFERENCED_TABLE_NAME student;对课程表这类引用我用的是ON DELETE RESTRICT就是有意让它拦住误删。如果是课设答辩场景需要强制删除可临时SET FOREIGN_KEY_CHECKS0但删除后必须改回1否则后续关联操作全部失去保护。5.4 GROUP BY 统计结果与预期不符现象执行成绩统计 SQL返回的COUNT(*)或AVG(score)与在查询工具里手工核对的结果不一致有时还报Expression #1 of SELECT list is not in GROUP BY clause。原因SELECT 子句里出现了既不在 GROUP BY 中、也没有被聚合函数包裹的列比如SELECT s.stu_name, AVG(sc.score) FROM score sc JOIN student s ... GROUP BY s.class_name。MySQL 5.7 之后默认开启ONLY_FULL_GROUP_BY这种 SQL 会直接报错。解决要么把s.stu_name也加进 GROUP BY要么改成聚合写法SELECT s.class_name, GROUP_CONCAT(s.stu_name), ROUND(AVG(sc.score), 2) FROM score sc JOIN student s ON sc.student_id s.id GROUP BY s.class_name;GROUP_CONCAT是成绩报表里常用的列转行函数可以把一个班级所有学生姓名拼到一行后边接平均分。注意它默认拼接长度上限是 1024 字节超过会静默截断。5.5 批量更新成绩时锁等待超时现象两个老师同时在不同班级录入成绩其中一个事务迟迟提交不了最后报Lock wait timeout exceeded; try restarting transaction。原因两个事务以不同顺序更新了同一组学生记录。事务 A 先更新学号 1~10事务 B 先更新学号 10~1相互持有对方需要的行锁死锁检测触发后其中一方被回滚。解决统一批量更新的顺序例如所有事务都按student_id升序更新合理设置锁等待时间在 MySQL 配置里调大一点给业务留缓冲SET GLOBAL innodb_lock_wait_timeout 50;这个参数只影响等待不解决死锁本身。更稳妥的做法是把单次事务拆分小批量比如每 100 条提交一次。6. 给成绩报表加速窗口函数排名、复合索引与 EXPLAIN 验证版本 8.0 之后成绩排名可以彻底告别「用户变量 自连接」的写法。RANK()、DENSE_RANK()、ROW_NUMBER()三个窗口函数覆盖了三种不同排名需求。最直观的区分方式RANK()并列时跳过后续名次比如两个第一名并列第二就不存在直接跳到第三DENSE_RANK()不跳ROW_NUMBER()始终给唯一序号。SELECT s.stu_name, c.course_name, sc.score, RANK() OVER (PARTITION BY c.course_id ORDER BY sc.score DESC) AS course_rank FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id ORDER BY c.course_id, course_rank;PARTITION BY c.course_id是窗口函数的分区意思是在每门课内部各自排名。这比「查出所有成绩后在 Java 里循环分组排序」干净得多也省掉了一次查询。排名类报表一旦加上 WHERE 按班级筛选要注意窗口函数在 WHERE 之后执行所以不能直接在 WHERE 里写course_rank 1要包一层子查询。索引方面成绩表有几个高频查询值得建复合索引。按班级查成绩、按课程查排名、按学生查总分这三个方向对应的索引设计如下ALTER TABLE score ADD INDEX idx_score_course (course_id, score); ALTER TABLE score ADD INDEX idx_score_student_course (student_id, course_id);第一个索引服务WHERE course_id ? ORDER BY score DESC这类查询第二个索引服务「查某学生某门课成绩」或「查某学生所有成绩」。复合索引的列顺序有讲究最左前缀原则决定了(course_id, score)索引可以支持course_id单独查询但不能支持score单独查询。验证索引是否生效用EXPLAIN看执行计划是习惯操作EXPLAIN SELECT s.class_name, AVG(sc.score) FROM score sc JOIN student s ON sc.student_id s.id WHERE sc.course_id 1 GROUP BY s.class_name;重点看type列从ALL变成ref或range说明索引生效了ALL表示全表扫描。如果rows列估算的行数仍然很大考虑是不是 WHERE 条件里的列没被索引覆盖或者查询写法导致 MySQL 优化器放弃索引。最后提一个我在课设答辩现场反复遇见的验证技巧成绩系统交付前往 score 表里灌 10 万条随机数据跑一遍上面所有统计和排名 SQL。不卡死、排名正确、EXPLAIN 里没有意外全表扫描这套系统到生产环境换台机器也能扛得住。我自己的习惯是写一个存储过程做压测数据生成几百行数据看不出索引好坏十万行才见真章。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?