PHP+MySQL成绩查询系统毕业设计:从表设计到会话权限的完整实现 简介一份基于PHPSQL的学生成绩查询系统毕业设计完整资料面向计算机专业毕业生及需要快速开发成绩管理系统的初学者可解决毕设选题、系统代码实现、文档撰写与答辩展示的多种需求。压缩包约826KB主要文件类型为PHP项目源码、Word版毕设文档和PPT答辩演示稿覆盖系统运行、理论说明与现场汇报三类场景。资料内实现了学生登录、成绩查询、个人信息修改以及教师登录、成绩录入与修改等核心功能基于MVC架构模块化设计便于理解分层开发思想并作后续扩展。毕设文档涵盖需求分析、概要设计、详细设计、系统测试等章节答辩PPT提炼项目背景、技术路线、功能展示与创新点帮助毕业季学生快速整理汇报材料。目前已有341人学习下载可供正在准备Web开发类毕设的学生参考代码结构、业务流程与文档写作范式也可直接作为课程设计或期末项目的起步模板。1. 成绩查询系统毕业设计不只是做一套增删改查“phpsql成绩查询系统的毕业设计”这个题目几乎每个开题季都能看到。功能单看并不难学生登录、成绩查询、个人信息修改教师登录、成绩录入、成绩管理。但真正动手后你会发现这个题目把所有毕业设计要踩的坑都装进去了——SQL 表设计、PDO 预处理、会话管理、角色权限、批量写入、文档结构、答辩演示一环扣一环。很多人卡在“代码能跑但文档和 PPT 讲不清楚”也有人卡在“用老写法写出来换到新版本 PHP 直接报错”。这个方向适合两类人一类是 PDO、会话、联表查询这些点还没串起来的学生想借一个完整项目把 PHP MySQL 从能用到做明白另一类是时间紧、需要一套能稳定复现且讲得出原理的方案而不是东拼西凑的碎片代码。这篇笔记按我实际做这套系统的顺序来讲从表结构到登录逻辑再到批量录入成绩最后落到“怎么把系统讲清楚”这件事上照着走能少走不少弯路。2. 数据层先立住成绩系统的表设计与初始化 SQL动手写页面之前我会先把表结构定下来。原因很简单这个毕设的全部功能——学生登录、成绩查询、个人信息修改、教师登录、成绩录入——本质上都是对几张表的增删改查。表设计错了后面每个页面都要绕路走表设计对了代码写起来就是顺着字段一路填空文档里画 ER 图也顺理成章。2.1 角色权限模型两类登录还是三类登录标题里只提了学生和教师最常见的做法就是两张用户表student 和 teacher字段各管各的。学生表存学号、姓名、班级、手机号、邮箱、密码教师表存工号、姓名、职称、手机号、密码。登录账号直接复用学号和工号不需要再单独维护一张 user 表逻辑最简单答辩时也好解释学生的业务号就是登录名教师的业务号也是登录名两套身份天然隔离。如果指导老师要求有管理员我会在后面加一张 admin 表字段很少id、account、password、create_time。登录页加一个角色下拉框把 student、teacher、admin 三种角色分发给三张表查询判断逻辑完全一致。不要一开始就把管理员塞进学生表或教师表角色混在一张表里权限判断会越写越乱后期想拆也麻烦。2.2 四张核心表字段、主外键与冗余取舍这个系统最少需要四张表student、teacher、course、score。course 不能省因为教师录入成绩必须有“录哪门课”这个上下文score 也不能只放一个成绩字段至少要留出平时成绩、期末成绩、总评成绩三个位置才能在查询页面显示得像真实系统。表关键字段说明studentid, student_no, name, class_name, phone, email, password_hash, create_timestudent_no 加唯一索引作登录账号teacherid, teacher_no, name, title, phone, password_hash, create_timeteacher_no 加唯一索引作登录账号courseid, course_no, course_name, teacher_id, creditteacher_id 关联 teacher.idscoreid, student_no, course_id, usual_score, exam_score, total_score, term, update_timestudent_no course_id 做联合唯一索引这里有两个容易被忽略的细节。第一个是密码字段不要叫 passwordMySQL 里 password 是保留字建表时容易出语法问题我用 password_hash 命名语义也更清楚。第二个是成绩表用 student_no 关联学生而不是用自增 id原因很实际答辩时评委常会要求打开数据库现场加一条学生记录再刷新成绩查询页面。如果用自增 id 串联你得先查 id 再去关联成绩用学号串联业务语义一眼能看懂演示也顺畅。成绩表里的总评分数我建议直接用 PHP 算好后写进去而不是在 SQL 里表达式现算。录入页提交时同时拿到平时分和期末分按权重算出总评再插入数据落库后查询页面只做展示逻辑清晰也方便后期按分数段统计。2.3 初始化 SQL建库、建表、插入测试数据一条龙我一般把全部建表语句整理成一个 init.sql放到项目的 sql 目录下。不要打开图形界面一句一句执行直接在命令行用 mysql 执行 sql 脚本一次跑完答辩时还能演示“初始化数据库只需一条命令”。-- sql/init.sql -- 成绩查询系统初始化脚本 CREATE DATABASE IF NOT EXISTS score_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE score_system; -- 学生表学号唯一登录账号复用学号 CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, class_name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT , email VARCHAR(100) DEFAULT , password_hash VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 教师表工号唯一登录账号复用工号 CREATE TABLE teacher ( id INT AUTO_INCREMENT PRIMARY KEY, teacher_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, title VARCHAR(20) DEFAULT 讲师, phone VARCHAR(20) DEFAULT , password_hash VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 课程表外键关联教师 CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit TINYINT DEFAULT 2, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINEInnoDB; -- 成绩表一个学生一门课只能有一条记录 CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL, course_id INT NOT NULL, usual_score DECIMAL(5,1) DEFAULT 0, exam_score DECIMAL(5,1) DEFAULT 0, total_score DECIMAL(5,1) DEFAULT 0, term VARCHAR(20) NOT NULL, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_no, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_no) REFERENCES student(student_no), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB;这段脚本里有几个参数值得说清楚。utf8mb4 不是普通的 utf8它支持完整的 Unicode 字符集学生姓名里不管有没有生僻字都能存页面不会因为一个特殊字符直接变问号。DECIMAL(5,1) 表示总长 5 位、小数 1 位最大能存 9999.9成绩场景绰绰有余比 FLOAT 更适合存分数不会出现 84.99999 这种浮点误差。联合唯一索引 uk_student_course 是防止同一条成绩被重复插入的关键教师重复提交时触发的就是这个约束。插入测试数据的语句我也放在同一个文件里方便初始化后直接登录演示。-- 插入测试数据密码统一为 123456 的 password_hash 密文 INSERT INTO student (student_no, name, class_name, password_hash) VALUES (2021001, A同学, 计科2101, $2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx), (2021002, B同学, 计科2101, $2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx); INSERT INTO teacher (teacher_no, name, title, password_hash) VALUES (T1001, B老师, 副教授, $2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx); INSERT INTO course (course_no, course_name, teacher_id, credit) VALUES (C001, 数据库原理, 1, 3), (C002, 网络工程, 1, 2); INSERT INTO score (student_no, course_id, usual_score, exam_score, total_score, term) VALUES (2021001, 1, 85.0, 78.0, 81.0, 2024-2025-1), (2021002, 1, 90.0, 88.0, 89.0, 2024-2025-1);这里的 password_hash 密文不能手写要先用 PHP 的 password_hash 函数生成再贴进来。后面登录章节会写生成方法思路是先用一段临时脚本把明文 123456 转成哈希再复制到 init.sql 里。这样数据库初始化后所有测试账号的密码都是统一的 123456答辩演示时不用记一堆密码。执行方式在命令行里是这样mysql -u root -p sql/init.sql如果习惯用图形客户端也可以在数据库图形管理工具里选择 sql/init.sql 文件整体导入效果一样。两种方式我都试过命令行更稳图形工具偶尔会因为 SQL 文件中途某条语句报错而停在半路需要逐段排查。2.4 查询怎么走联表、去重与统计思路表建好以后查询逻辑基本是固定的。学生查成绩必然要关联课程表拿到课程名和学分教师看成绩列表要关联课程表确认自己教的是哪门课。最核心的就是这条联表查询SELECT s.student_no, s.name, c.course_name, c.credit, sc.usual_score, sc.exam_score, sc.total_score, sc.term FROM score sc JOIN student s ON sc.student_no s.student_no JOIN course c ON sc.course_id c.id WHERE s.student_no 2021001;如果页面要按学期筛选就在 WHERE 后面拼一个 AND sc.term 学期参数。这里注意索引的使用score 表已经有 student_no course_id 的联合唯一索引按学号过滤时能直接走索引数据量不大时完全感觉不到差异但答题时能说出“联合索引覆盖查询条件”这句话比只说“我做了查询”加分得多。统计每门课的平均分、最高分、最低分、选课人数是答辩时最容易展示的冷门功能但题目里没提我建议作为加分项做。SQL 用聚合函数SELECT course_id, COUNT(DISTINCT student_no) AS stu_num, AVG(total_score) AS avg_score, MAX(total_score) AS max_score, MIN(total_score) AS min_score FROM score WHERE course_id 1 GROUP BY course_id;这里用 COUNT(DISTINCT student_no) 而不是 COUNT(*)虽然联合唯一索引下两者结果一样但写出 DISTINCT 能体现你在考虑重复数据的边界情况。慢 SQL 优化在毕设里一般用不上但你要知道这条查询可以用 EXPLAIN 查看执行计划确认它走了 uk_student_course 索引而不是全表扫描。3. 用 PHP 把登录和会话做扎实连接层与角色分发打开老毕设源码最常见的是 mysql_query、mysql_connect 这一套函数PHP 7 勉强能跑PHP 8 直接报 undefined function。新项目我不会再用这些写法全部用 PDO 扩展既能防 SQL 注入也跨数据库。这一章重点说环境结构、连接层和登录流程这些内容在答辩里基本是必问点。3.1 环境准备与目录结构开发环境我推荐 PHP 8 MySQL 8本地用集成面板几分钟起来编辑器用 PHPStorm 这类 IDE语法提示和调试器都省心。项目目录按功能分层不要把几十个 PHP 文件全部丢根目录答辩时目录整洁也是印象分。score_system/ ├── sql/ │ └── init.sql ├── config/ │ └── db.php ├── common/ │ └── functions.php ├── public/ │ ├── login.php │ └── index.php ├── student/ │ ├── score.php │ ├── profile.php │ └── logout.php └── teacher/ ├── score_list.php ├── score_add.php ├── score_edit.php └── logout.php这个结构的原则是配置文件单独放公共函数单独放学生端和教师端页面分目录隔开。后面所有页面开头都 require common/functions.php登录态校验和角色校验集中在函数里而不是每个页面重复写一遍检查代码出问题时只改一处。3.2 PDO 连接为什么放弃旧写法连接配置我写成独立文件 config/db.php全项目共用一份。下面是完整的连接代码?php // config/db.php // 全局 PDO 连接配置所有页面统一引入 $host 127.0.0.1; $port 3306; $dbname score_system; $user root; $pass root; $dsn mysql:host{$host};port{$port};dbname{$dbname};charsetutf8mb4; $options [ // 驱动出错时抛异常方便定位问题 PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, // 默认以关联数组返回查询结果 PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, // 关闭本地预处理模拟使用 MySQL 原生预处理 PDO::ATTR_EMULATE_PREPARES false, ]; try { $pdo new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { exit(数据库连接失败 . $e-getMessage()); }这里三个 options 建议理解后记下来。ERRMODE_EXCEPTION 表示 SQL 执行出错会抛异常这样你能在 try-catch 里看到真实错误信息而不是页面白屏FETCH_ASSOC 让每条记录变成关联数组用 $row[student_no] 而不是 $row[0] 取值代码可读性好很多ATTR_EMULATE_PREPARES 设为 false 是让预处理真正发生在 MySQL 服务端防 SQL 注入更彻底。字符集在 DSN 里直接指定 utf8mb4比执行 SET NAMES 语句早一步生效能少踩不少乱码坑。3.3 学生登录与 session 回跳登录页是整个系统最先写、也最值得写好的页面。核心流程接收表单 → 按角色查对应表 → password_verify 校验密码 → 写入 session → 跳转到对应首页。?php // public/login.php // 统一登录入口支持学生和教师两种角色 session_start(); require dirname(__DIR__) . /config/db.php; // 初始化错误信息避免页面警告变量未定义 $error ; if ($_SERVER[REQUEST_METHOD] POST) { $account trim($_POST[account] ?? ); $password $_POST[password] ?? ; $role $_POST[role] ?? student; if ($role student) { $sql SELECT id, student_no, name, password_hash FROM student WHERE student_no :account LIMIT 1; $home student/score.php; } elseif ($role teacher) { $sql SELECT id, teacher_no, name, password_hash FROM teacher WHERE teacher_no :account LIMIT 1; $home teacher/score_list.php; } else { $error 未知角色; } if (!$error) { $stmt $pdo-prepare($sql); $stmt-execute([:account $account]); $user $stmt-fetch(); // password_verify 校验哈希不比对明文 if ($user password_verify($password, $user[password_hash])) { $_SESSION[user_id] $user[id]; $_SESSION[account] $account; $_SESSION[name] $user[name]; $_SESSION[role] $role; header(Location: . $home); exit; } $error 账号或密码错误; } } ? !-- 页面表单略角色通过隐藏域或下拉框提交 --这段代码的关键点首先SQL 里的条件字段用 :account 占位符执行时绑定参数彻底避开字符串拼接其次SELECT 只取需要的字段不把整行无用数据塞进 sessionsession 里存了 user_id、account、name、role 四样够用且不冗余。header 跳转后必须 exit否则后续代码还可能继续执行。关于密码生成测试账号初始化时用 password_hash(123456, PASSWORD_DEFAULT) 生成登录时只用 password_verify 对比。这个答案在答辩里要能背出来哈希不是加密它是单向摘要数据库泄露后也无法反推明文每次校验时把输入密码做同样的哈希再比对。3.4 教师登录与角色分发教师登录不能和后端写死在一起。一个简单的教师页面入口需要单独判断避免学生登录后手输 teacher/score_add.php 地址绕过权限。做法是每个受保护页面顶部先加载公共函数再校验角色?php // common/functions.php // 登录态与角色校验的公共函数 function check_login(): void { if (empty($_SESSION[account])) { header(Location: ../public/login.php); exit; } } function check_role(string $needle): void { if (($_SESSION[role] ?? ) ! $needle) { exit(无权访问); } }调用方式很直接student/score.php 第一行 require functions.php第二行 check_role(student)teacher/score_add.php 则 check_role(teacher)。check_login 可以和被 check_role 合并但拆开写的好处是有些页面允许登录用户查看但禁止非登录用户比如学生个人信息页这时只调 check_login调 check_role 反而限制死。3.5 公共函数统一下一步跳转角色分发不止登录一次登录后访问各自目录下的首页也需要统一跳转。我会在 functions.php 里加一个 redirect_home()function redirect_home(): void { $home ($_SESSION[role] ?? ) teacher ? ../teacher/score_list.php : ../student/score.php; header(Location: . $home); exit; }这个函数用在登录页、退出页和 session 失效被踢回后的重定向。尤其退出登录正常逻辑是销毁 session 后回到 login.php但很多新手会忽略“销毁 session 文件”这一步只清空数组导致退出后刷新还能进内部页面。完整退出写法要记住?php // student/logout.php session_start(); $_SESSION []; if (ini_get(session.use_cookies)) { $params session_get_cookie_params(); setcookie(session_name(), , time() - 42000, $params[path], $params[domain], $params[secure], $params[httponly]); } session_destroy(); header(Location: ../public/login.php); exit;这段代码把 session 数组清空、cookie 过期、服务端 session 文件销毁三步都做了不留残留。4. 核心功能逐个落地学生查询、教师录入与信息修改登录和会话跑通后系统骨架已经立起来了。接下来把题目要的功能逐个填进去学生查成绩、学生改个人信息、教师录成绩、教师改成绩。四个功能里教师批量录入成绩是工作量最大、也最能体现工程能力的一部分我把重点放在这里。4.1 学生端成绩查询与学期筛选学生登录后进入 student/score.php从 session 拿学号因为 session 里已经有 account不用再查一次学生表。页面支持按学期筛选我一般用一个下拉框列出当前存在的学期。?php // student/score.php require dirname(__DIR__) . /common/functions.php; check_role(student); // 从 session 取学生学号作为查询条件 $account $_SESSION[account]; // 可选的学期筛选参数 $term trim($_GET[term] ?? ); // 基础 SQL 固定拼好学期条件按需追加 $sql SELECT c.course_name, c.credit, sc.usual_score, sc.exam_score, sc.total_score, sc.term FROM score sc JOIN course c ON sc.course_id c.id WHERE sc.student_no :account; $params [:account $account]; if ($term ! ) { $sql . AND sc.term :term; $params[:term] $term; } $sql . ORDER BY sc.term DESC; $stmt $pdo-prepare($sql); $stmt-execute($params); $rows $stmt-fetchAll(); ? !-- 页面用表格输出 $rows这里略 --动态 SQL 的写法很多人会直接拼字符串容易引入注入风险。我这里的做法是基础 SQL 写死条件参数全部用占位符$params 数组统一传递。这样无论学期参数怎么变SQL 结构是固定的预处理始终有效。如果想让系统看起来比题目要求更完整可以加一个排名。排名用相关子查询实现SELECT COUNT(*) 1 AS rank_num FROM score WHERE course_id 1 AND total_score (SELECT total_score FROM score WHERE student_no 2021001 AND course_id 1)这段 SQL 建议写进文档而不是写进页面答辩时把这段讲清楚评委基本不会再质疑你的 SQL 水平。4.2 教师端批量录入多行文本一次提交成绩录入如果设计成“一门课一个学生一提交”教师录完一个班要反复点几十次提交按钮体验很差。常见的做法是教师选课程和学期后在一个文本框里粘贴多行数据每行格式是“学号 平时成绩 期末成绩”后台一次事务批量写入。前端表单form methodpost actionscore_add.php input typehidden namecourse_id value? htmlspecialchars($course_id) ? input typehidden nameterm value? htmlspecialchars($term) ? textarea namerows rows10 cols60 placeholder每行一条学号 平时成绩 期末成绩/textarea button typesubmit批量录入/button /form后台处理是这个系统的技术核心?php // teacher/score_add.php require dirname(__DIR__) . /common/functions.php; check_role(teacher); // 接收并简单清洗参数 $course_id (int)($_POST[course_id] ?? 0); $term trim($_POST[term] ?? ); $rawLines trim($_POST[rows] ?? ); if ($course_id 0 || $term ) { exit(课程或学期不能为空); } // 按行拆分文本框内容去掉空行 $lines array_filter(array_map(trim, explode(\n, $rawLines))); if (!$lines) { exit(没有可录入的成绩数据); } // 先确认这门课属于当前登录教师防止越权操作 $check $pdo-prepare( SELECT id FROM course WHERE id ? AND teacher_id ? ); $check-execute([$course_id, $_SESSION[user_id]]); if (!$check-fetch()) { exit(无权操作该课程); } // 事务批量写入任一失败则全部回滚 $pdo-beginTransaction(); try { $upsert INSERT INTO score (student_no, course_id, usual_score, exam_score, total_score, term) VALUES (:student_no, :course_id, :usual_score, :exam_score, :total_score, :term) ON DUPLICATE KEY UPDATE usual_score VALUES(usual_score), exam_score VALUES(exam_score), total_score VALUES(total_score), term VALUES(term); $stmt $pdo-prepare($upsert); $count 0; foreach ($lines as $line) { // 支持空格或 Tab 分隔 $parts preg_split(/\s/, $line); if (count($parts) 3) { continue; } [$studentNo, $usual, $exam] $parts; // 总评按平时:期末 4:6 计算结果保留一位小数 $total round((float)$usual * 0.4 (float)$exam * 0.6, 1); $stmt-execute([ :student_no $studentNo, :course_id $course_id, :usual_score $usual, :exam_score $exam, :total_score $total, :term $term, ]); $count; } // 全部成功才提交 $pdo-commit(); echo 本次成功录入 . $count . 条成绩; } catch (Exception $e) { // 任一条失败回滚到录入前状态 $pdo-rollBack(); exit(录入失败 . $e-getMessage()); }这段代码有三个问题需要特别注意。第一ON DUPLICATE KEY UPDATE 依赖表的联合唯一索引 uk_student_course没有这个索引重复录入不会触发更新而会直接插入重复记录所以表设计时那条 UNIQUE KEY 不能省略。第二事务 beginTransaction 和 commit/rollBack 是配套的不能用一次连接执行多条 INSERT 就完事成绩数据一旦做到一半失败前面已经插入的记录就会残留在库里答辩时说自己“用了事务保证数据一致”是加分项。第三录入前确认课程 teacer_id 属于当前登录教师这行代码很多人会漏。默认场景下教师工号和课程本来就会对应但 URL 参数可能被改必须服务端再校验一次。分数权重我写的是平时 0.4、期末 0.6不同系统可以自定义建议放在一个常量或者配置数组里不要散落在 SQL 拼接处。学生缺考时可以统一录入负数或者标记字段我一般用 -1 表示缺考但这样统计平均分时会把 -1 算进去要让 AVG 忽略它AVG(CASE WHEN total_score 0 THEN total_score END)这块在 PPT 里可以单独放一页评委对这个“业务规则与统计口径”的考虑很感兴趣。4.3 成绩修改与删除权限确认不能省教师录错成绩时需要编辑入口。成绩修改和删除的表单与录入页类似但关键看两点一是操作前再次校验课程归属二是改完后更新 update_time。因为表里 update_time 有 ON UPDATE CURRENT_TIMESTAMP只要 UPDATE 语句真实改动到行时间会自动刷新不需要手动处理。删除成绩我建议用软删除而不是硬删除。给 score 表加一个 is_deleted 字段默认 0删除时 UPDATE is_deleted 1查询条件统一加 AND is_deleted 0。这样做的好处是误删后还能恢复答辩演示出误删场景时你有后悔药。如果指导老师要求逻辑简单硬 DELETE 也不是不行但你要能回答出“为什么用硬删除”而不是“我没想过”。// teacher/score_edit.php 核心片段 $id (int)($_GET[id] ?? 0); // 先查出这条成绩对应的课程是否属于当前教师 $sql SELECT sc.id FROM score sc JOIN course c ON sc.course_id c.id WHERE sc.id :id AND c.teacher_id :teacher_id; $stmt $pdo-prepare($sql); $stmt-execute([ :id $id, :teacher_id $_SESSION[user_id], ]); if (!$stmt-fetch()) { exit(无权操作); }这里用 JOIN 把 score 表和 course 表连起来一次查询就完成“这条成绩归属校验”。如果先查成绩再单独查课程需要两次数据库访问代码也啰嗦。4.4 个人信息修改读与写分开密码单独处理学生个人信息修改涉及两个操作展示当前信息SELECT和保存新信息UPDATE。展示时从 session 里的 user_id 查整行数据表单回填。保存时只更新允许修改的字段学号不要开放修改它是登录名与关联外键。?php // student/profile.php require dirname(__DIR__) . /common/functions.php; check_role(student); $account $_SESSION[account]; // 读取当前学生信息 $stmt $pdo-prepare( SELECT student_no, name, class_name, phone, email FROM student WHERE student_no :account ); $stmt-execute([:account $account]); $student $stmt-fetch(); if (!$student) { exit(学生不存在); } // 保存个人信息 if ($_SERVER[REQUEST_METHOD] POST) { $name trim($_POST[name] ?? ); $phone trim($_POST[phone] ?? ); $email trim($_POST[email] ?? ); // 前端也能做校验但服务端必须再做一次 if ($name || !preg_match(/^1[3-9]\d{9}$/, $phone)) { exit(姓名或手机号格式不正确); } if ($email ! !filter_var($email, FILTER_VALIDATE_EMAIL)) { exit(邮箱格式不正确); } $update $pdo-prepare( UPDATE student SET name :name, phone :phone, email :email WHERE student_no :account ); $update-execute([ :name $name, :phone $phone, :email $email, :account $account, ]); header(Location: profile.php?updated1); exit; } ?密码修改我单独写一个密码页不和个人信息混在同一个表单里。原因是密码要走 password_hash 重新生成哈希而个人信息只是普通字符串更新两者逻辑不同放一起反而容易出错。密码页必须校验旧密码只有旧密码正确才能改为新密码这算是最基本的账号安全。5. 毕设里的高频踩坑与排查清单这一章把我做这个系统时遇到过的、以及帮同学排查过的问题集中列出来每条按现象、原因、解决的思路讲。很多问题不是逻辑难而是环境、字符集、保留字这些细节没注意。5.1 页面乱码连接、文件、数据库三层字符集现象成绩页面上中文姓名全部显示成问号或者数据库里没乱码但页面上乱。原因字符集问题分三层数据库连接字符集、数据表字符集、PHP 文件本身的编码。如果 init.sql 建库时用了 latin1、连接 DSN 没有指定 charset、或者 PHP 文件是 GBK 保存的三层中任意一层不一致都会乱码。最常见的是 DSN 没写 charsetutf8mb4或者建表时漏了 DEFAULT CHARACTER SET。解决统一到 utf8mb4。init.sql 开头用 CREATE DATABASE ... CHARACTER SET utf8mb4PDO 的 DSN 里写 charsetutf8mb4PHP 文件用 UTF-8 无 BOM 格式保存。如果数据库已经建成 latin1可以改数据库默认字符集后重建表不要只执行一条 SET NAMES 就完事那是临时的。5.2 SQL 注入与万能密码绕过现象登录框输入万能密码后没有账号也能登录成功。典型 payload 是输入 or 11、 or 11 --这类内容原理解释是查询语句被拼接成 WHERE password or 11恒真条件让查询直接返回第一行用户。原因代码用了字符串拼接 SQL 而不是预处理。老教程里最常见的写法是$sql SELECT * FROM user WHERE name $name AND password $pwd用户名里带引号后SQL 语句结构被破坏注释符把后面的条件全部忽略掉。解决全项目使用 PDO 预处理SQL 模板固定占位符绑定参数MySQL 服务端解析时不把参数当作 SQL 指令。另外登录查询还要在代码里判断查询结果是否真返回了用户 or 11这类注入在预处理模式下会被整个当作普通字符串去匹配 student_no匹配不到任何用户。5.3 Session 失效登录后跳转没反应现象登录成功地址栏跳转到了目标页但页面立刻跳回登录页或者刷新一下就掉线。原因最常见的有三个第一session_start() 没在输出内容前调用PHP 提示 headers already sentheader 跳转失败第二PHP 文件开头有 BOM 或空行session 已发送响应头后无法写入 cookie第三登录成功写入 session 后目标页面没有加载同一个 session_start()。解决每个需要 session 的 PHP 文件第一行就写 session_start()不要在 HTML 输出后调用。编辑器保存时选 UTF-8 无 BOM。跳转使用 header(Location: ...) 后加 exit防止后续代码干扰。退出登录要同时销毁 session 文件和 cookie否则会有残留会话。5.4 PHP 8 下老代码大片报错现象打开老教学代码看到 undefined function mysql_connect() 或者 each()、create_function() 直接报致命错误。原因PHP 8 移除了大量旧函数和语法。mysql_* 系列在 PHP 7 时代就已经删除create_function、each 被移除字符串花括号下标$str{0}也废弃了。网上很多旧毕设源码还是十年前写法教学视频也可能用 5.6 版本示范。解决新项目全部用 PDO不用 mysql_* 系列。遇到老代码要迁移先把 mysql_connect 换成 new PDO再把 where 条件改为预处理占位符。PHP 8 里 isset 空字符串不再报错但要注意类型比较更严格比如123abc 123结果可能和预想不同比较前强制类型转换。5.5 慢查询与索引失效现象成绩数据量几百条时候选正常加到几万条后查询明显变慢平均分统计要等好几秒。原因score 表只靠自增主键查询条件里的 student_no、course_id 没有走索引每次查询都全表扫描。虽然毕设数据量小看不出问题但优化思路要能讲出来。解决利用好建表时加的两个索引student_no 上的唯一索引以及 student_no course_id 的联合唯一索引。查询时在 WHERE 里同时用 student_no 和 course_id会走联合索引。可以用 EXPLAIN 看执行计划type 从 ALL 变成 const 或 ref就说明索引生效了。这条经验写进“系统优化”章节比写一大堆缓存方案更实际。5.6 录入成绩后查不到保留字与反引号现象INSERT 语句执行成功但 SELECT 查不到或某些 SQL 在本地正常、部署到服务器报语法错误。原因字段名或表名用了 MySQL 保留字。password 就是典型保留字新建连接账号时经常碰到。另外一个坑是表名大小写问题Windows 下 MySQL 默认不区分大小写Linux 下区分代码里写的表名大小写不一致本地能跑部署就挂。解决字段名避免用保留字密码字段统一叫 password_hash。如果必须用保留字SQL 里用反引号包起来比如SELECT \group FROM ...。表名全部用小写字母代码中保持大小写一致。数据库变量 lower_case_table_names 是 MySQL 实例级配置改它会影响其他项目不建议动。6. 从能跑到能讲把系统、文档和 PPT 串成一场答辩代码完工之后真正的毕业设计才完成一半。题目里明确写了“系统 毕设文档 答辩PPT”所以文档和 PPT 不是附赠品而是答辩成绩的主要来源。我的习惯是按一条演示主线来组织所有材料先以学生身份登录展示成绩查询和学期筛选的稳定性退出后切换教师身份展示成绩批量录入、修改和删除最后回到数据层用 SQL 语句演示一下平均分统计和事务回滚。这条链路能覆盖系统 80% 的功能全程控制在五分钟以内。PPT 不要贴大段代码评委不会现场逐行读。我一般按六个页面走课题背景与研究意义、需求分析、系统设计含用例图和架构图、数据库设计与 E-R 图、功能实现与测试截图、总结与展望。数据库设计页放四张表的关系图功能实现页放两三个人机交互截图重点不是代码数量而是“你设计时考虑了什么问题”。答辩评委最常问的问题就那几个为什么选 PHP 而不是 Java密码为什么不用明文SQL 注入怎么防成绩平均分怎么算并发录入会不会出错。这些问题在文档的“系统设计”和“系统测试”两章里都能找到答案关键是你要用自己的话讲出来不能背稿。事务回滚那段代码、预处理防注入那段代码、联合索引防重复那条 SQL是整场答辩最能体现工程能力的三处地方一定要能独立推导出来。我自己的一个教训是第一次答辩演示时现场网络环境不固定用的还是本地集成面板数据库服务没启动页面直接白屏折腾了几分钟才恢复。后来我把所有演示步骤固定成一套检查单先确认 MySQL 服务、再确认 Apache 或 PHP 内置服务、最后打开浏览器清缓存。这套做下来后两次讲解都顺了很多。做毕设不比做生产系统稳定性差一点没关系但你要知道问题出在哪、怎么定位、怎么向评委解释这比系统本身更能说明你的工程素养。希望这篇笔记能帮你把成绩查询系统这个题目做透。少走一点弯路就多留一点时间给文档和答辩准备也祝你的毕设顺利收尾。本文还有配套的精品资源点击获取