简介:这是一套基于PHP与MySQL的学生成绩查询系统毕业设计资源,面向计算机相关专业毕业生及PHP/MySQL初学者,覆盖学生信息管理、成绩录入与查询、课程设置、用户登录及权限控制等典型模块,可作为课程设计或毕业论文项目的完整参考。资源包共66个文件,压缩后约12.3MB,包含22个PHP源码文件、htm/css前端页面、SQL数据库脚本、doc/txt说明文档、两个EXE操作录屏(管理员模块、学生模块与数据库设计),另含项目说明与录屏备份的RAR资料包,便于按模块对照学习。目前已有307人浏览学习,适合希望快速搭建成绩管理系统或借鉴完整毕设流程的读者。资源中的源码与数据库脚本可直接部署验证,录屏讲解能帮助梳理系统分层与核心业务逻辑,项目摘要文档和说明文件则为论文撰写与二次开发提供了辅助资料,整体学习路径覆盖从设计到实现的各个关键环节。
1. 学生成绩查询系统:一个三天能跑通、五天能交差的 PHP+MySQL 毕设源码
A同学接到任务时有点慌:课程设计要做一个学生成绩管理系统,数据库课只教过 SELECT,前端只会写表格,还要求登录、分角色、成绩统计全都要有。他翻到一个 php+mysql 的毕设源码包,整个项目没有框架依赖,管理员、教师、学生三种角色各管各的功能,从数据库脚本到前端页面一次给齐。这类源码最适合两类人:一类是正在做 PHP+MySQL 毕业设计或课程设计的学生,需要能讲清楚登录鉴权、关联查询、权限隔离这些答辩必问点;另一类是刚学完 PHP 语法、想看看一个完整项目怎么组织文件的新手。系统核心就是一张成绩表关联学生、课程、教师三张基础表,数据链路短、逻辑直白,拿来改改字段和页面就能变成自己的选题。
2. 技术选型与整体架构:原生 PHP + MySQL,不玩框架但五脏俱全
2.1 为什么毕设项目里 PHP+MySQL 是常青树
拿到源码先看技术栈。这套系统用原生 PHP 5.6/7.x 编写,数据库是 MySQL 5.7 或 8.0,前端就是普通的 HTML + CSS + JavaScript,没有引入 Laravel、ThinkPHP 这类重量级框架。有人会觉得“不用框架是不是太原始”,但放在毕业设计场景里,原生 PHP 恰恰是优势:数据库连接、会话管理、SQL 执行全部显式写在代码里,答辩时老师问“你这行是干什么的”,你能从 require 一直讲到 fetch_assoc,不需要绕一层框架的 Route 和 ORM。
从运行角度看,它需要一个 Apache 或 Nginx 能解析 PHP 的环境,数据库单独跑在 MySQL 服务上。本地开发我一般直接装集成环境(常见的 phpStudy 或 XAMPP 一类),把源码丢到 web 根目录,导入 SQL 脚本就能访问。这个套路本身也是 PHP 项目最经典的黑匣子破解方式——不用关心服务怎么编译,只要 PHP 扩展开启、端口不冲突,项目就能转起来。
2.2 目录结构:三种角色各占一个子目录
源码的目录组织非常清晰,基本按“Admin / Teacher / Student”三个端拆开,公共部分单独抽出来。一个典型的目录骨架长这样:
score-system/ ├── index.php // 入口,按登录角色跳转 ├── admin/ // 管理员端 │ ├── login.php // 管理员登录页 │ ├── dashboard.php // 统计仪表盘 │ ├── student.php // 学生账号管理 │ ├── teacher.php // 教师账号管理 │ ├── course.php // 课程管理 │ └── score.php // 全量成绩查看/导出 ├── teacher/ // 教师端 │ ├── login.php │ ├── score_input.php // 录入成绩 │ └── my_course.php // 查看任课班级 ├── student/ // 学生端 │ ├── login.php │ └── my_score.php // 查询个人成绩 ├── common/ │ ├── config.php // 数据库连接配置 │ ├── functions.php // 公共函数,如字段过滤 │ └── auth.php // 登录态检查,每个页面顶部引用 ├── sql/ │ └── init.sql // 建库建表 + 初始账号数据 └── assets/ ├── css/ └── js/这个结构的优点是“页面即模块”:每新增一个功能,就是往对应角色目录里加一个 PHP 文件,再在导航菜单里加一行链接。写毕设文档时,功能模块图也能直接照这个目录画,不用额外编。缺点也有——没有统一路由,所有页面靠文件直出,但小系统这样做反而最不容易出错。
2.3 请求生命周期:一个成绩查询从点击到渲染都经历了什么
我习惯用一个具体操作把整个项目串起来讲。假设学生登录后点击“我的成绩”,这条链路是这样的:
浏览器把登录表单 POST 到 student/login.php,login.php 顶部先 session_start(),然后引入 common/config.php 拿到数据库连接对象。接着用预处理方式查 student 表,把用户输入的密码做 md5 后比对,匹配成功就把 student_id 写进 $_SESSION,然后 header 跳转到 my_score.php。my_score.php 第一行不是业务代码,而是 require auth.php——auth.php 里检查 $_SESSION 里有没有指定字段,没有就直接 redirect 回登录页,这一步是整个系统的权限闸门。
通过鉴权后,my_score.php 执行一条 JOIN 查询,把 score 表、course 表按课程 ID 关联起来,拿当前登录学生的全部成绩记录,最后在循环里拼 HTML 表格输出。整个流程没有 AJAX、没有前后端分离,每次点击都是一次完整的“页面刷新式”请求,逻辑非常符合刚学完 PHP 的认知水平。
2.4 本地跑起来:四步完成环境搭建
本地运行我推荐用集成环境,省去自己编译 PHP 的时间。步骤按这个顺序走:
第一步,安装集成环境并启动 Apache 和 MySQL 服务,检查端口是否被占用,如果 80 或 3306 被占用,在配置里改掉。第二步,把源码包解压到 Web 根目录,比如 phpStudy 的 WWW 目录下,确保 index.php 能通过 http://localhost/score-system/index.php 访问。第三步,用 phpMyAdmin 或命令行执行 sql/init.sql,这一步会创建数据库、六张表以及默认管理员账号。第四步,修改 common/config.php 里的数据库密码,改成你本地 MySQL 的实际密码。
<?php // common/config.php define('DB_HOST', '127.0.0.1'); define('DB_USER', 'root'); define('DB_PASS', 'root'); // 改成你本地 MySQL 密码 define('DB_NAME', 'student_score_db'); $mysqli = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); if ($mysqli->connect_errno) { die('数据库连接失败: ' . $mysqli->connect_error); } $mysqli->set_charset('utf8mb4');config.php 是整个项目的命门,页面加载失败八成是这个文件的地址或密码不对。设置字符集这行千万别省,不然后面中文乱码会一直缠着你。跑通之后默认管理员账号通常是 admin / admin123,登录后第一件事是去密码修改功能里换掉默认密码,这一点在答辩演示时容易被评委抓。
3. 数据库设计:六张表如何撑起三类角色的全部业务
3.1 核心表结构:学生、课程、成绩三张表是绝对主角
打开 sql/init.sql,库名一般是 student_score_db,六张表分别是管理员表、学生表、教师表、班级表、课程表和成绩表。其中成绩表是整个系统的核心,其他表都是为它服务的。学生表保存学号、姓名、密码和班级 ID,教师表保存工号、姓名和所属教研室,班级表只做简单关联,避免在学生表里重复存储班级名称。
学生表的结构很典型,字段不多但每一列都在业务里用得到:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int 自增 | 主键 |
| student_no | varchar(20) 唯一 | 学号,登录账号 |
| name | varchar(50) | 姓名 |
| password | varchar(64) | md5 加密后的密码 |
| class_id | int | 关联班级表 |
| phone | varchar(20) | 选填联系电话 |
成绩表则承担了考试成绩的记录、查询和统计三个职责,字段设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int 自增 | 主键 |
| student_id | int | 学生 ID |
| course_id | int | 课程 ID |
| teacher_id | int | 任课教师 ID |
| score | decimal(5,2) | 成绩,支持一位小数 |
| semester | varchar(20) | 学期,如 2024-2025-1 |
| exam_date | date | 考试日期 |
| remark | varchar(200) | 补考、缓考等备注 |
score 字段用 decimal(5,2) 而不是 float,成绩这种精确数据不允许出现浮点误差,这也是答辩老师爱问的一个细节。
3.2 表间关系与外键策略:成绩表为什么要设联合唯一索引
六张表的关联关系不复杂:班级表是一级,学生表通过 class_id 挂在班级下;课程表独立维护;成绩表通过 student_id、course_id、teacher_id 三个外键分别关联学生、课程和教师。一个学生有多个成绩,一个课程被多个学生选修,所以成绩表本质上是一张多对多的中间表,额外挂了一个教师字段记录是谁教的。
设计上有一个值得注意的细节:成绩表加了一个针对 student_id、course_id、semester 三个字段的联合唯一索引。这个索引有双重作用,第一是查询加速,学生查成绩时 WHERE 条件正好命中这个索引;第二是防止同一个学生、同一门课、同一学期被重复录入两次,数据层面就把脏数据堵死了。实际写 init.sql 时,建议给外键字段都加上普通索引,否则联表 JOIN 的数据量一大,查询会明显变慢。
3.3 初始化脚本:建表语句里藏着三个容易忽略的点
init.sql 里除了 CREATE TABLE,还负责往三张用户表里插入默认账号。学生默认密码一般是学号本身,教师默认密码是工号,管理员是 admin123,这些初始数据用于首次登录测试。一个合格的教学系统必须让学生能第一次登录就进入系统,否则演示时就卡在登录页了。
建表语句里有三个坑值得留意。第一是所有表都要显式指定 DEFAULT CHARSET=utf8mb4,不能沿用 MySQL 默认的 latin1,否则中文姓名直接变问号。第二是字符串类型长度要克制,学号 20 位、姓名 50 位足够,不要随手写 varchar(255),成绩表和班级表这么做会让索引体积变大。第三是 ENGINE 要指定 InnoDB,MyISAM 不支持事务,成绩录入时如果中途出错会出现部分学生有成绩、部分没有的尴尬情况。
CREATE TABLE `score` ( `id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `course_id` int NOT NULL, `teacher_id` int NOT NULL, `score` decimal(5,2) NOT NULL, `semester` varchar(20) NOT NULL, `exam_date` date DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_stu_course_semester` (`student_id`, `course_id`, `semester`), KEY `idx_course_id` (`course_id`), KEY `idx_teacher_id` (`teacher_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;成绩表创建脚本里我保留了三个索引:联合唯一索引防重复录分,course_id 和 teacher_id 的单列索引支持按课程和按教师筛选。如果后续要加“按班级统计平均分”的功能,建议再加一个 class_id 冗余到成绩表,或者在查询时 JOIN 学生表,这属于典型的空间换时间取舍。
4. 核心代码拆解:登录鉴权、联表查询与成绩录入的完整实现
4.1 登录与权限隔离:$_SESSION 三行代码挡住未登录用户
登录是第一个要拆解的核心点。系统没有把登录逻辑写在某个框架的中间件里,而是直接放在 login.php 和 auth.php 两个文件里,逻辑非常直观。登录接收 role 参数决定去查哪张用户表,密码统一 md5 处理,查出来后把用户 ID 写进 SESSION。
<?php // student/login.php 处理部分 session_start(); require_once __DIR__ . '/../common/config.php'; $role = $_POST['role'] ?? 'student'; $account = trim($_POST['account'] ?? ''); $password = md5(trim($_POST['password'] ?? '')); // 角色与表名的映射,必须是白名单,不能直接把 role 拼进 SQL $tableMap = ['student' => 'student', 'teacher' => 'teacher', 'admin' => 'admin']; $table = $tableMap[$role] ?? 'student'; // 用预处理方式查用户 $stmt = $mysqli->prepare("SELECT id, name FROM {$table} WHERE student_no = ? AND password = ?"); $stmt->bind_param('ss', $account, $password); $stmt->execute(); $user = $result_test = $stmt->get_result()->fetch_assoc(); if ($user) { $_SESSION['user_id'] = $user['id']; $_SESSION['real_name'] = $user['name']; $_SESSION['role'] = $table; header('Location: ' . $table . '/index.php'); // 按角色跳转 exit; } else { echo '<script>alert("账号或密码错误");history.back();</script>'; }这段代码里有几个处理值得借鉴。表名用了白名单映射而不是直接把 POST 的 role 拼进 SQL,杜绝了表名注入;密码比对统一走 md5 后再进 SQL,数据库里不会存明文;登录成功后立刻 exit,防止 header 后续被其他输出干扰。参数说明:role 对应三种角色,account 是学号或工号,password 是前端表单传来的明文,进入后端先 md5 再比对。
auth.php 则是每个受保护页面顶部的“门禁”,代码只有几行但作用关键。它会检查 SESSION 里有没有 user_id,没有就跳回登录页,同时按角色判断访问权限——比如 admin 目录下的页面要求 role 必须是 admin,否则直接拦下。这个文件的引用方式是在每个 PHP 页面开头写 require_once,属于手动实现的“路由守卫”。
4.2 学生成绩查询:一条 JOIN 把三张表串成成绩单
学生端最核心的就是 my_score.php 里的查询。按照需求,学生想看的是“我选了哪些课、各考了多少分、学分多少”,这些字段分散在 score 表和 course 表里,必须通过 JOIN 合并。
SELECT c.course_name, c.credit, sc.score, sc.semester, sc.exam_date FROM score sc JOIN course c ON sc.course_id = c.id WHERE sc.student_id = ? ORDER BY sc.semester DESC, c.id ASC;JOIN 的思考过程是这样的:成绩表里有 course_id 但没课程名,课程表里有课程名和学分但没成绩,所以以成绩表为主表,LEFT JOIN 课程表后,每一行成绩都能带上课程名和学分。WHERE 条件里的 student_id 来自 SESSION,不用学号直接查,避免用户在 URL 里改参数看别人的成绩。ORDER BY 先按学期倒序再按课程 ID 排序,确保最新学期的课排在最前面。
在 PHP 里执行这条查询后,用常规的 while 循环遍历结果集,拼装成表格行。这里有一个常见误区:有人会把成绩表和学生表 JOIN 上再取 student_no 当展示字段,其实学生姓名在 SESSION 里已经拿到了,一行代码就能显示在页头,没必要多 JOIN 一张表增加查询负担。如果按学期做筛选,只需要在 WHERE 后面加一个 semester = ? 条件,参数同样用预处理绑定,就能做成下拉框筛选项。
4.3 成绩录入:预处理循环 + 联合索引防重复
教师端录入成绩是最容易出错的模块。它的难点不在 SQL 本身,而在批量逻辑:一个班级几十个学生,每行一个分数,提交时要循环写入,同时还得保证同一个学生同一门课不会被录两次。
<?php // teacher/score_input.php 核心循环 $course_id = (int)$_POST['course_id']; $semester = trim($_POST['semester']); $scoreList = $_POST['score_list']; // 结构: [student_no => score] // 预取所有学号对应的学生 ID,避免循环里一条一条查库 $noToId = []; $res = $mysqli->query("SELECT id, student_no FROM student WHERE class_id = " . (int)$_POST['class_id']); while ($row = $res->fetch_assoc()) { $noToId[$row['student_no']] = $row['id']; } $stmt = $mysqli->prepare( "INSERT INTO score (student_id, course_id, semester, score) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE score = VALUES(score)" ); foreach ($scoreList as $studentNo => $score) { if (!isset($noToId[$studentNo])) continue; // 学号不存在则跳过 $sid = $noToId[$studentNo]; $scoreVal = (float)$score; if ($scoreVal < 0 || $scoreVal > 100) continue; // 范围校验 $stmt->bind_param('iisd', $sid, $course_id, $semester, $scoreVal); $stmt->execute(); }这段代码的核心技巧是先用一次查询把整个班的学号到 ID 映射拉进数组,循环时直接查数组,而不是每录一个学生就执行一次 SELECT。这个优化在大批量录入时效果非常明显。ON DUPLICATE KEY UPDATE 配合第 3 章说的联合唯一索引,实现了“有记录就更新分数,没记录就插入”的幂等逻辑,教师重复提交表单不会产生重复数据。bind_param 的四个参数类型 iisd 分别对应用户 ID(整数)、课程 ID(整数)、学期(字符串)、分数(小数)。
录入完成后通常会做一个统计展示,SQL 用聚合函数一组出结果,给教师看这个班这节课的及格情况:
SELECT COUNT(*) AS total, ROUND(AVG(score), 1) AS avg_score, SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) AS passed, ROUND(SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS pass_rate FROM score WHERE course_id = ? AND semester = ?;这条统计 SQL 在毕设答辩中属于“亮点型”代码:COUNT 统计总人数,AVG 算平均分,SUM(CASE WHEN) 结构在不增加子查询的情况下统计及格人数,再除以总数得到及格率。如果老师追问排名怎么写,可以顺势引出 MySQL 8.0 的窗口函数 RANK(),这一步就能展示你在数据库知识上的深度。
5. 部署避坑指南:PHP 环境、连接、乱码与权限的五个血泪问题
5.1 打开页面直接报错:Call to undefined function mysqli_connect
现象:访问任何页面都出现类似 Fatal error: Uncaught Error: Call to undefined function mysqli_connect() 的报错,页面完全白屏。
原因:PHP 安装时没有开启 mysqli 扩展,或者 php.ini 里对应的 extension 行被注释掉了。这种现象在集成环境里不常见,但如果你用的是系统自带 PHP 或者手工编译的 PHP,非常容易遇到。
解决:打开 PHP 安装目录下的 php.ini,搜索 mysqli,把 ;extension=mysqli 这一行前面的分号删掉,保存后重启 Apache 或 PHP-FPM。如果是集成环境,直接在面板里勾选 PHP 扩展即可。验证方式是在 web 目录放一个临时文件写入 phpinfo(),搜索 mysqli 字段,出现才说明扩展生效。
提示:PHP 7.4 以后,mysqli 扩展已不再默认编译进核心,很多精简版 PHP 包需要单独启用。
5.2 数据库连接失败:Access denied for user,密码明明是对的
现象:config.php 里填的密码和 MySQL 一致,但页面提示 Access denied for user 'root'@'localhost'。
原因:MySQL 8.0 默认使用 caching_sha2_password 认证插件,而较老版本的 PHP mysqli 扩展只支持 mysql_native_password。两者握手失败,MySQL 直接拒绝连接。
解决:登录 MySQL 命令行,执行下面的 SQL 把 root 用户切回旧认证方式:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;另一个容易翻车的点是端口:MySQL 如果改过端口,比如 3307,config.php 里也要同步改,mysqli 构造函数第五个参数传端口号。我见过有人改了数据库端口却忘了改配置,排查了半小时才发现是端口对不上。
5.3 登录成功却跳转失败:Cannot modify header information
现象:登录时提示 Warning: Cannot modify header information - headers already sent,账号密码正确但页面不跳转,或者跳转后停留在空白页。
原因:PHP 在输出任何内容之后就不能再发送 header。最常见的原因是 PHP 文件开头有隐藏的 BOM 字符,或者 require 的文件末尾多了一个空格或换行,导致 session_start() 或 header() 执行前已经有内容输出。
解决:用编辑器将项目下所有 PHP 文件统一转为 UTF-8 无 BOM 编码。在 VS Code 里右下角点击编码,选择“通过编码保存”,选 UTF-8 保存即可。另一个习惯是写代码时不写闭合的 ?> 标签,这样能彻底避免文件末尾的空行隐患。这个坑在 Windows 自带的记事本编辑过源码后特别容易出现,因为是记事本保存时默认加 BOM。
5.4 页面全是问号:数据库和页面双重乱码
现象:学生姓名和课程名显示为??,或者页面正常但数据库里存的是乱码。
原因:乱码可能出现在三个环节:数据库表字符集不是 utf8mb4、PHP 连接没设置字符集、页面本身的 meta 声明缺失。任何一个环节断裂,中文就会断裂。
解决:按三层逐一排查。第一层,页面 HTML 的 head 里必须有<meta charset="UTF-8">;第二层,config.php 里必须调 set_charset('utf8mb4');第三层,建表语句的 DEFAULT CHARSET 必须是 utf8mb4。如果数据库已经建成且存了乱码,先改表字符集再重建数据,改完字符集后旧数据可能仍是乱码,此时只能重新录入,所以我习惯在跑通业务前先把字符集确认好。
注意:utf8 在 MySQL 里是 utf8mb3 的别名,不支持四字节字符,统一用 utf8mb4 最稳妥。
5.5 成绩录不进去,表单提交后没反应
现象:教师在录入页面填完分数点提交,页面刷新后成绩表里还是空的,也没有任何报错信息。
原因:这种情况最隐蔽,通常不是 SQL 语法错,而是业务逻辑拦截了写入。可能的原因有三类:第一,score_list 的字段名和后端 $_POST 取的名字不一致,循环里一个都没匹配到;第二,第 4.3 节的代码里做了范围校验,分数小于 0 或大于 100 被 continue 跳过;第三,学号与预取映射对不上,isset($noToId[$studentNo]) 为 false,全部被过滤掉。
解决:先在 score_input.php 末尾临时加一行输出,打印 $_POST 结构和循环处理计数,确认表单数据是否传到后端。接着把范围校验临时注释掉,看是否能写入。如果还是不行,在 MySQL 里开启通用查询日志,查看实际执行的 SQL 语句,问题通常立刻暴露——SQL 里如果是空参数,就在参数获取环节找问题;如果参数正常但没写入,再查联合唯一索引是不是误伤。排查完记得把调试代码删掉。
6. 二次开发:加字段、做排名、导出 CSV 的三个改造方案
6.1 从“一考定成绩”改成“平时分 + 期末分”综合评定
很多教学场景不只看期末一考,还有平时分、作业分、期末分按比例合成总评。这个改动在系统里属于“加字段 + 改录入”两步走。
先在成绩表追加字段,同时把历史数据迁移一下:
ALTER TABLE score ADD usual_score DECIMAL(5,2) DEFAULT 0, ADD final_score DECIMAL(5,2) DEFAULT 0, ADD total_score DECIMAL(5,2) DEFAULT 0; UPDATE score SET total_score = ROUND(usual_score * 0.3 + final_score * 0.7, 2);然后把录入页面 score_input.php 里的表单改成三个输入框,提交时同时接收 usual_score 和 final_score,后端按比例算 total_score。注意 total_score 的计算不能放 SQL 里用生成列,因为生成列无法用 ON DUPLICATE KEY UPDATE 直接更新,这个坑我踩过,老老实实后端算好再写入最稳。
6.2 班级排名:MySQL 8.0 窗口函数与 5.7 变量法
学生端如果能显示“我这门课在班里排第几”,整个系统的完整度会明显提升。MySQL 8.0 直接用窗口函数一行解决:
SELECT student_id, semester, score, RANK() OVER (PARTITION BY course_id, semester ORDER BY score DESC) AS course_rank FROM score WHERE course_id = ? AND semester = ?;RANK() 按课程和学期分组,组内按分数倒序排名,同分数并列。MySQL 5.7 不支持窗口函数,就得用会话变量的方式模拟,写法稍绕:
SET @prev_course := '', @rank := 0; SELECT student_id, score, course_rank FROM ( SELECT student_id, score, @rank := IF(@prev_course = CONCAT(course_id, semester), @rank + 1, 1) AS course_rank, @prev_course := CONCAT(course_id, semester) AS cur_group FROM score ORDER BY course_id, semester, score DESC ) t;变量法的思路是:按课程和学期分组,每组第一个学生的排名重置为 1,后面每行加 1。要注意 ORDER BY 必须在子查询里先完成,否则分组错乱,排名全废。
6.3 导出成绩单:CSV 加 BOM 才能被 Excel 正常打开
管理员和教师端都适合加一个“导出当前列表为 CSV”的按钮,代码量不大但体验提升明显。关键坑是在输出 CSV 之前必须加 BOM 头,否则 Excel 打开中文会乱码,而 Windows 记事本打开却正常,玄学味道十足。
<?php // admin/score.php 导出逻辑 header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename=score_export.csv'); echo "\xEF\xBB\xBF"; // UTF-8 BOM,让 Excel 正确识别编码 $fp = fopen('php://output', 'w'); fputcsv($fp, ['学号', '姓名', '课程', '学期', '成绩']); while ($row = $result->fetch_assoc()) { fputcsv($fp, [$row['student_no'], $row['name'], $row['course_name'], $row['semester'], $row['score']]); } fclose($fp); exit;这里用 php://output 流式输出,不落盘、不占服务器空间,适合成绩这种量级的数据。导出的文件还能作为答辩时“系统支持数据导出”的功能演示,比截图更有说服力。
在那之后我自己每次部署这类 PHP+MySQL 源码,都会强制走一遍固定清单:先开 mysqli 扩展,再确认数据库编码,再检查文件 BOM,然后测登录、试录入,最后查一遍 SQL 日志。这套流程跑下来,九成以上的环境问题五分钟内都能定位。希望帮到你。
本文还有配套的精品资源,点击获取