资讯动态

PHP+MySQL学生成绩查询系统:从数据库设计到部署避坑全流程解析

发布时间:2026/10/20 6:27:40 来源:尧图企业网站定制
简介基于PHP与MySQL开发的毕业设计项目——学生成绩查询系统面向计算机专业学生、PHP初学者及需要课程设计或毕业设计参考的开发者。资源包共66个文件压缩后约12.3MB以PHP源码、MySQL数据库脚本、HTML页面、CSS样式为主另含Word文档、文本说明、SQL文件以及管理员模块和学生模块的操作录屏目录规划清晰便于按功能索引。已有三百余人学习下载适合对照源码理解登录验证、学生信息管理、成绩录入与查询、修改删除、奖惩管理等模块的编码方式和数据库表设计是典型的动态网站项目可帮助掌握PHP连接MySQL、会话处理、增删改查等关键技术。文档中的摘要总结与数据库设计配合录屏演示可帮助读者从零搭建环境、复现系统流程并在此基础上进行二次开发或用于答辩展示。整个资源以真实毕设项目为蓝本代码注释和文档结构规范适合作为后续项目改造的基础。1. 学生成绩查询系统毕设里最容易被轻视的“全流程小项目”学生成绩查询系统这个题目乍看像是数据库课设的入门款。等我实际把一套PHPMySQL源码拆完、重跑、再整理成可复现步骤后才发现它覆盖了答辩中最常被追问的几块登录鉴权、多角色权限、成绩的增删改查、统计报表以及部署演示。适合两类人一是正在找毕设源码但不想只交“能跑就行”的PPT的在校生二是想借这个题目把PHP后端和MySQL表设计一次理顺的开发者。这篇按我实际复现的顺序写哪几步容易翻车会单独拎出来讲。2. 需求梳理与技术选型为什么是PHPMySQL这套经典组合2.1 角色权限与用例先画出谁在用什么动手写代码之前我习惯先把角色用例画出来。这套系统的角色非常清晰一共三类学生登录后只能查询自己的成绩、查看个人平均分、按课程或学期筛选。教师登录后可以录入成绩、修改成绩、查看所授课程的全部学生成绩以及最高分、最低分、平均分等统计。管理员维护学生信息、课程信息、教师账号必要时重置密码。角色和功能的对应关系直接决定了后面表怎么建。我一般会先做成一张对照表写代码时就不会把学生和教师的权限搞混。角色核心功能数据权限范围学生查询成绩、查看统计仅本人教师录入/修改成绩、查看成绩统计所授课程管理员维护基础信息、重置密码全部这里最关键的一点是权限判断不能只靠前端按钮隐藏。很多参考代码只在前端把“录入成绩”按钮藏起来学生直接拼URL就能访问教师页面。正确做法是后端在处理每个请求时都校验session中的角色字段这个点我在第5章会展开讲。从用例反推数据表能得到一个很重要的结论学生和课程是多对多关系成绩表是它们的关联表。登录账号不要塞进学生表或教师表单独拆一张用户表这样后面加教师角色、加管理员都不需要动已有表结构。我见过不少参考代码把密码直接放进student表后来要加角色就非常痛苦后悔药只能是一开始就拆表。2.2 技术栈边界PHP版本、MySQL版本与运行环境选PHPMySQL而不是Java或Python对毕设来说有一个很实际的原因部署简单、参考资料多、答辩时老师大概率也熟悉这套。但这不意味着随便找个源码就能跑版本坑是第一个门槛。PHP 5.6时代的老代码在PHP 7.4以上会直接报“Call to undefined function mysql_connect()”因为mysql扩展从PHP 7.0起就被移除了。我建议在本地用集成环境装PHP 7.4 MySQL 5.7这个组合和大多数参考源码写作时的环境最接近踩坑最少。要是你的电脑已经装了PHP 8.x也不用换版本把老代码里的mysql_函数批量改成mysqli或PDO就行。为什么不推荐直接上ThinkPHP或Laravel不是框架不好而是毕设演示时老师更可能问“这条SQL为什么用LEFT JOIN”“成绩校验在哪里做的”原生PHP的代码路径更短你能讲清楚的概率更高。这套系统用原生PHP写按我后文的步骤落地业务代码量不大改起来也快。开发环境我用WAMP这类集成包三个步骤就能启动Apache和MySQL。不建议在Windows上裸装PHP和MySQL再手工配置环境变量、扩展开启、服务启动每一步都有可能在演示前卡住这种和业务无关的玄学问题最消耗时间。3. 数据库设计成绩表结构决定了后面好不好改3.1 四张核心表与关联关系这套系统的数据规模不大但表结构决定了后面业务代码好不好写。我推荐拆成四张表用户表、学生表、课程表、成绩表。教师信息可以并入用户表再用role区分或者单独拆一张教师表。如果教师还需要维护“所授课程”建议独立一张教师表避免user表里塞太多业务字段。表名作用核心字段tb_user登录账号与角色user_id, username, password, role, ref_idtb_student学生基本信息student_id, name, class_name, major, enroll_yeartb_course课程基本信息course_id, course_name, credit, teacher_idtb_score学生选课成绩score_id, student_id, course_id, score, exam_date表之间的关联关系是成绩表的student_id指向学生表的student_idcourse_id指向课程表的course_id。这里我建议加物理外键吗不建议。毕设项目用InnoDB物理外键在导入SQL时如果顺序不对容易报错而且演示数据量小完全可以在应用层用JOIN保证一致性。把外键约束的检查放到PHP代码里比数据库层报错更直观。字段类型有几个容易被忽略的点。score字段用DECIMAL(5,1)而不是TINYINT因为要支持59.5这种带小数的成绩TINYINT只能存整数。student_id用VARCHAR(20)而不是自增INT因为学号可能带字母而且学生在系统里习惯用学号登录。course_id用自增INT课程编号一般是纯数字用INT在JOIN时性能更好也不容易出现前导零消失的坑。3.2 初始化SQL脚本建库建表与测试数据种子拿到源码后第一步是建库。下面这段SQL是我整理的初始化脚本包含建库、建表和测试数据整套系统跑起来就靠它。CREATE DATABASE IF NOT EXISTS grade_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE grade_system; CREATE TABLE tb_user ( user_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(30) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(255) NOT NULL COMMENT 密码支持password_hash或MD5兼容, role ENUM(student,teacher,admin) NOT NULL DEFAULT student COMMENT 角色, ref_id INT UNSIGNED NOT NULL COMMENT 关联学生表或教师表ID ) ENGINEInnoDB COMMENT登录用户表; CREATE TABLE tb_student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) NOT NULL COMMENT 班级, major VARCHAR(50) NOT NULL COMMENT 专业, enroll_year INT NOT NULL COMMENT 入学年份 ) ENGINEInnoDB COMMENT学生信息表; CREATE TABLE tb_course ( course_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 课程ID, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT 学分, teacher_id INT UNSIGNED NOT NULL COMMENT 授课教师ID ) ENGINEInnoDB COMMENT课程表; CREATE TABLE tb_score ( score_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 成绩ID, student_id VARCHAR(20) NOT NULL COMMENT 学号, course_id INT UNSIGNED NOT NULL COMMENT 课程ID, score DECIMAL(5,1) NOT NULL COMMENT 成绩0-100, exam_date DATE NOT NULL COMMENT 考试日期, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB COMMENT成绩表;逻辑说明建库语句里指定utf8mb4字符集这是避免中文乱码的第一个保证。tb_user的ref_id字段用于关联学生表或教师表登录后通过它拿到对应的学生或教师业务数据。成绩表加了UNIQUE KEY uk_student_course保证同一个学生同一门课只有一条成绩记录这是防止重复录入的数据库层兜底。参数说明DECIMAL(5,1)表示总共5位数字其中1位小数最大支持999.9成绩场景完全够用。ENUM类型在MySQL里对取值有限制role字段只会是student、teacher、admin三者之一比用VARCHAR多一层约束但后面要加角色时需要ALTER TABLE需要注意。测试数据种子如下包含三个学生、两门课、五条成绩记录INSERT INTO tb_student (student_id, name, class_name, major, enroll_year) VALUES (2023001, A同学, 计算机2301, 计算机科学与技术, 2023), (2023002, B同学, 计算机2301, 计算机科学与技术, 2023), (2023003, C同学, 软件2302, 软件工程, 2023); INSERT INTO tb_course (course_id, course_name, credit, teacher_id) VALUES (1, 数据库原理, 3.0, 1), (2, Web开发技术, 2.5, 2); INSERT INTO tb_score (student_id, course_id, score, exam_date) VALUES (2023001, 1, 85.5, 2024-06-20), (2023001, 2, 92.0, 2024-06-28), (2023002, 1, 58.0, 2024-06-20), (2023002, 2, 76.5, 2024-06-28), (2023003, 1, 90.0, 2024-06-20);测试数据故意放了B同学数据库原理58分这个不及格记录后面验收统计SQL时能同时验证“最高分、最低分、不及格人数”三类指标答辩演示不需要另外造数据。3.3 成绩聚合查询的核心SQLJOIN与分组统计学生端“查看我的成绩”和教师端“查看课程统计”本质都是JOIN查询区别只在筛选条件。先看学生个人成绩查询SELECT c.course_name, c.credit, s.score, s.exam_date FROM tb_score s JOIN tb_course c ON s.course_id c.course_id WHERE s.student_id 2023001 ORDER BY s.exam_date DESC;逻辑说明FROM tb_score s起别名sJOIN tb_course c关联课程表取课程名和学分。WHERE用student_id过滤出当前学生。ORDER BY exam_date DESC让最近考的成绩排在最前面这个排序在页面展示时体验更好。参数说明这里的2023001在实际代码中应该从session取当前登录学生的学号而不是从URL参数读否则学生可以改成别人的学号查看他人成绩。再展示一条答辩时非常好用的统计SQL能一次性算出某课程的最高分、最低分、平均分和及格率SELECT c.course_name, ROUND(MAX(s.score), 1) AS max_score, ROUND(MIN(s.score), 1) AS min_score, ROUND(AVG(s.score), 1) AS avg_score, CONCAT(ROUND(SUM(s.score 60) / COUNT(*) * 100, 1), %) AS pass_rate FROM tb_course c LEFT JOIN tb_score s ON c.course_id s.course_id GROUP BY c.course_id, c.course_name;逻辑说明GROUP BY把多条成绩按课程聚合MAX、MIN、AVG分别取出最大值、最小值和平均值。及格率用SUM(s.score 60)计算大于等于60分的记录数MySQL中布尔表达式返回1或0SUM就是及格人数除以COUNT(*)总人数再乘100得到百分比。参数说明ROUND(..., 1)保留一位小数避免答辩时老师看到一长串小数。LEFT JOIN保证即使某门课还没有任何成绩记录课程名也能出现在结果里不会丢行。4. 后端PHP代码落地登录鉴权、成绩查询与成绩录入4.1 公共连接层与请求入口PDO预处理整套系统的数据库连接我建议单独放在一个文件里每个页面统一引入不要每个页面重复写连接代码。选PDO而不是mysqli主要原因是PDO的预处理语句在防SQL注入上更省心而且后面如果换成PostgreSQL或SQLite改驱动即可不必动业务代码。?php // config/db.php 数据库连接配置 $host 127.0.0.1; $port 3306; $dbname grade_system; $username root; $password 123456; try { $pdo new PDO( mysql:host$host;port$port;dbname$dbname;charsetutf8mb4, $username, $password, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false ] ); } catch (PDOException $e) { exit(数据库连接失败 . $e-getMessage()); }逻辑说明连接串里明确写了charsetutf8mb4这是解决中文乱码的关键一步很多老代码漏掉这一行就出现问号。ATTR_ERRMODE设为EXCEPTIONSQL出错时会抛异常方便定位问题。ATTR_EMULATE_PREPARES设为false让MySQL服务端执行真正的预处理而不是在客户端模拟。参数说明root对应本地集成环境的默认账号密码123456也是本地默认值。上传到服务器后这两项必须改尤其是密码。如果环境是PHP 5.6以下PDO也可以用但推荐至少用PHP 7.4。4.2 登录与角色路由session判断加后端校验登录是整个系统的入口处理不当后面全是问题。下面这段登录代码同时兼容password_hash和MD5两种密码存储方式?php session_start(); require_once __DIR__ . /config/db.php; $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; if ($username || $password ) { exit(账号和密码不能为空); } $stmt $pdo-prepare(SELECT user_id, username, password, role, ref_id FROM tb_user WHERE username ?); $stmt-execute([$username]); $user $stmt-fetch(); // 兼容新老两种密码存储老代码常用MD5新代码建议password_hash if ($user (password_verify($password, $user[password]) || md5($password) $user[password])) { $_SESSION[user_id] $user[user_id]; $_SESSION[role] $user[role]; $_SESSION[ref_id] $user[ref_id]; header(Location: . ($user[role] student ? student_dashboard.php : teacher_dashboard.php)); exit; } $error 账号或密码错误;逻辑说明session_start()必须放在任何HTML输出之前否则报“headers already sent”。先通过用户名查到用户记录再用password_verify校验。兼容MD5的判断逻辑是要么新密码哈希验证通过要么MD5比对通过两者任一成立都放行。参数说明header(Location: ...)之后必须加exit否则脚本会继续执行后面的代码可能输出多余内容导致跳转失效。这里按角色路由到不同入口页学生进学生面板教师进教师面板管理员可单独再写一个入口。登录后的每个业务页面顶部都要做角色校验?php session_start(); if (empty($_SESSION[user_id]) || $_SESSION[role] ! student) { header(Location: login.php); exit; }逻辑说明这段代码防止学生通过修改URL直接访问教师端的PHP文件。session里的role在登录时写入页面之间无法伪造所以它是唯一可信的角色来源。很多参考代码只在前端隐藏按钮这道后端校验是必须补上的。4.3 学生成绩查询与教师成绩录入两条核心流程学生端查询成绩逻辑比想象中简单但有一个坑需要注意学号不能从前端传必须从session绑定的ref_id推导出来。?php session_start(); require_once __DIR__ . /config/db.php; if (empty($_SESSION[user_id]) || $_SESSION[role] ! student) { header(Location: login.php); exit; } $stmt $pdo-prepare(SELECT student_id, name, class_name, major FROM tb_student WHERE student_id ?); $stmt-execute([$_SESSION[ref_id]]); $student $stmt-fetch(); $sql SELECT c.course_name, c.credit, s.score, s.exam_date FROM tb_score s JOIN tb_course c ON s.course_id c.course_id WHERE s.student_id ? ORDER BY s.exam_date DESC; $stmt $pdo-prepare($sql); $stmt-execute([$student[student_id]]); $scoreList $stmt-fetchAll();逻辑说明第一段查询根据session里的ref_id从学生表取出学号。这里ref_id在登录时写入存的就是学生表的主键student_id所以可以直接当学号用。第二段查询和3.3节的SQL一致只是把学号换成占位符?由PDO参数绑定传入。参数说明如果ref_id存的是学生表的自增内部ID而不是学号那第一段查询的WHERE条件要改成student_id ?且参数传内部ID然后用查出来的学号字段去做后续关联。两种设计都行关键是全系统统一不要一会儿用学号关联一会儿用内部ID关联这种不一致是最隐蔽的翻车点。教师端成绩录入核心是“先查重再插入或更新”我用事务包住整个操作保证要么全成功要么全失败?php require_once __DIR__ . /config/db.php; // 教师身份校验省略假设已通过 $student_id trim($_POST[student_id] ?? ); $course_id (int)($_POST[course_id] ?? 0); $score (float)($_POST[score] ?? -1); if ($score 0 || $score 100) { exit(成绩必须在0到100之间); } try { $pdo-beginTransaction(); $check $pdo-prepare(SELECT score_id FROM tb_score WHERE student_id ? AND course_id ?); $check-execute([$student_id, $course_id]); if ($check-fetch()) { $sql UPDATE tb_score SET score :score, exam_date :exam_date WHERE student_id :student_id AND course_id :course_id; } else { $sql INSERT INTO tb_score (student_id, course_id, score, exam_date) VALUES (:student_id, :course_id, :score, :exam_date); } $stmt $pdo-prepare($sql); $stmt-execute([ :student_id $student_id, :course_id $course_id, :score $score, :exam_date date(Y-m-d) ]); $pdo-commit(); echo 成绩保存成功; } catch (Throwable $e) { $pdo-rollBack(); exit(保存失败 . $e-getMessage()); }逻辑说明先检查成绩记录是否存在存在就走UPDATE不存在走INSERT这就是“防止重复记录”的常用做法。数据库层的UNIQUE KEY作为第二道保险即使两条请求同时打过来也不会插入重复数据。事务保证了检查、写入或更新作为一个整体提交任一步失败就回滚到操作前状态。参数说明score设置为(float)浮点数转换可以自动处理“85.5”这样的输入。考试日期用date(Y-m-d)取服务器当天时间如果你想支持教师指定考试日期可以增加一个日期输入框代码改成读取对应的POST字段。成绩上下限的校验放在后端前端校验只能提升体验不能作为安全边界。5. PHPMySQL联调避坑五类常见翻车现场与处理方法5.1 环境与老代码兼容两处最容易卡住的报错现象一页面白屏浏览器显示“Fatal error: Call to undefined function mysql_connect()”。原因参考代码是PHP 5时代写的函数用的是mysql_connect、mysql_query那一套这些函数从PHP 7.0起被完全移除PHP 8.x更是直接报未定义。解决把代码里的mysql_connect改为mysqli_connectmysql_query改为mysqli_query。如果代码里这类函数出现几十处逐行替换太慢我一般是写一个兼容层文件用自定义函数封装老函数调用再在入口统一引入。但这种兼容层只适合应急复现不建议作为正式交付代码。最干脆的做法是把连接层直接换成第4章说的PDO业务代码一起改干净利落。现象二导入SQL后页面中文全是问号“”。原因建库时没指定utf8mb4字符集表默认用了latin1或者连接数据库时没设置字符集导致PHP和MySQL之间传输中文时按错误编码解读。解决建库语句加DEFAULT CHARACTER SET utf8mb4连接字符串里加charsetutf8mb4三个地方保持一致分别是数据库、连接、页面文件编码。页面文件本身必须另存为UTF-8不要用记事本默认的ANSI。我习惯复现一个项目时先建库再导入导入后立即查一条含中文的记录确认不是问号再写业务代码。5.2 业务逻辑与数据不一致三个隐蔽坑现象三学生登录后能看到其他人的成绩或者查询结果为空。原因学生端查询条件用了前端URL传入的student_id攻击者改一下URL参数就能查任意人的成绩。反过来如果页面代码用$_SESSION[ref_id]但ref_id存的是用户表的user_id而不是学号和成绩表关联时查不到数据结果就是空列表。解决后端强制从session取角色和关联ID不信任任何URL或表单传入的学生ID。如果从user表取出来的ref_id是内部编号就先通过内部编号查出学号再用学号去成绩表查询。关键在于整个系统统一口径我习惯在学生表查询和成绩表查询中都使用同一个学号字段减少中间转换层。现象四密码明明正确password_verify却一直返回false。原因旧源码的数据库里存的是MD5加密的密码串而代码用password_hash写入、password_verify校验。password_hash生成的哈希格式和MD5完全不同自然校验不过。解决登录时做兼容判断用password_verify($password, $user[password])或md5($password) $user[password]两者任一为真就通过。更稳妥的做法是登录成功后用password_hash重新生成哈希更新到数据库下次登录就走新校验一次迁移完成。这个兼容逻辑我已经写在4.2节的代码里直接照用即可。现象五重复录入成绩时报主键冲突或同一学生同一课程出现多条记录。原因score表没有唯一约束代码里又没做“先查再更新”的判断教师点了两次提交就插入了两条。解决建表时加UNIQUE KEY uk_student_course (student_id, course_id)这是数据库层的最后一道防线。业务代码也按第4章的方式先SELECT判断记录是否存在存在则UPDATE不存在则INSERT。两层都做才能既保证正常操作不报错也避免异常情况下数据重复。6. 部署上线与一个验收技巧从本地跑通到演示答辩6.1 本地部署三步走用WAMP这类集成环境部署顺序是固定的先把项目目录放到WWW根目录下目录名建议用grade_system避免中文路径然后打开phpMyAdmin导入初始化SQL脚本最后修改config/db.php里的数据库账号密码启动Apache和MySQL服务浏览器访问http://localhost/grade_system/login.php就能看到登录页。这里有一个容易被忽略的细节如果你是先启动的服务后导入SQL导入失败时MySQL可能已经建了部分表。重复导入脚本时会报“表已存在”。我的习惯是导入前先删除旧的grade_system数据库或者在SQL脚本开头加上DROP DATABASE IF EXISTS grade_system保证每次导入都是干净状态。6.2 用一条SQL核对演示数据答辩前一晚我最常干的一件事是跑一遍全库数据核对确保演示时点开页面不会出现空数据或数据不一致。下面这条SQL专门用来核对课程维度的统计结果SELECT c.course_name, ROUND(MAX(s.score), 1) AS max_score, ROUND(MIN(s.score), 1) AS min_score, ROUND(AVG(s.score), 1) AS avg_score, CONCAT(ROUND(SUM(s.score 60) / COUNT(*) * 100, 1), %) AS pass_rate FROM tb_course c LEFT JOIN tb_score s ON c.course_id s.course_id GROUP BY c.course_id, c.course_name;逻辑说明这条SQL把“数据库原理”和“Web开发技术”两门课的最高分、最低分、平均分、及格率一次算出来和页面上的教师端统计功能对应。如果页面显示的数字和这条SQL查出来的不一致说明页面统计逻辑写错了而不是SQL写错了。答辩被问到“及格率怎么算”时就用第3章的SUM(s.score 60)口径回答先说清楚分母是选课总人数再解释0到59.9算不及格口径统一就不会被追问卡住。参数说明如果演示数据里及格率恰好是100%看不出区分度可以临时把某条成绩改成50分左右再刷新页面展示及格率变化这样比静态页面更有说服力。部署时还有一个小习惯值得养成演示前把config/db.php的显示错误打开万一现场环境有问题能直接看到报错。等演示结束再改回关闭状态避免暴露数据库连接信息。我从复现这套系统之后每次做PHPMySQL的演示项目都强制自己先跑一遍这条核对SQL再看页面数字两边的值对上了才敢上台。这套流程帮我避开了好几次数据对不上的尴尬希望帮到你。整套源码和SQL脚本按上面的顺序跑半天就能打通需要在资源下载页自取即可。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑