ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP成绩管理系统源码实战:从表设计到统计部署

PHP成绩管理系统源码实战:从表设计到统计部署 简介这套PHP成绩管理系统源码面向学校、教育机构及PHP学习者解决学生分数录入、统计分析与按条件查询的需求。系统基于PHP与MySQL实现涵盖学生信息、课程、成绩等数据表设计提供管理员、教师、学生三级权限管理包含成绩批量录入、平均分/及格率统计、多条件检索等功能并兼顾SQL注入与XSS防护可作为毕业设计或业务系统二次开发的基础。前端由HTML页面、JavaScript脚本和CSS样式构成支持按学号、姓名、课程等关键词快速检索统计结果可细化到班级或个人。压缩包共2276个文件约8.03MB以PHP后端代码、HTML页面、JavaScript脚本、CSS样式为主辅以配置文件、Markdown文档、图片与字体资源目录结构清晰便于按模块定位代码。目前已有640人学习下载项目还附带部署运行配置适合想通过完整项目掌握PHP开发流程的初中级开发者。1. 成绩管理系统的源码里核心不是页面而是成绩流转每到月考结束班主任和教务老师最忙的不是改卷而是把分数从成绩册搬到 Excel再按班级算平均分、按学科算及格率。这份“PHP成绩管理系统源码”就是为这类重复劳动准备的解压、配好数据库学生信息、课程列表、分数录入做完后统计查询就变成了几个页面点击。系统基于 PHP MySQL保持传统但稳定的三层结构前端不需要框架服务器上有宝塔或 LNMP 就能跑起来。对刚开始读 PHP 源码的人来说它比图书管理系统更贴近教学业务能在一两百行核心代码里看到录入、更新、聚合、输出这整套动作。一个常见误判是这只是一个“增删改查演示”真正用起来才会发现坑都藏在数据重复和数据一致性上。2. 成绩管理系统的表结构与源码目录规划2.1 用三张表支撑成绩录入、统计和查询成绩系统的数据模型非常稳定学生表存“人”课程表存“考哪科”成绩表存“谁在哪个考试中考了多少分”。如果只建一张表把姓名、班级、科目、分数全塞进去后面要按课程筛选、按考试区分、再按班级聚合SQL 会频繁带 JOIN 和 DISTINCT效率低且容易统计错。常见做法是拆成三张业务表再加一张管理员表用于登录表名作用成绩录入时影响students学生学号、姓名、班级提供下拉列表和学生维度拆分courses课程名称、满分值决定统计及格线scores学生ID、课程ID、考试名称、分数插入或更新的目标表admin_users后台管理员账号控制登录和操作权限建表语句可以按下面这段 SQL 执行。成绩系统中的学号、班级和考试名称都是高频筛选条件所以除了主键还要在成绩表上建联合唯一索引和组合索引。CREATE TABLE admin_users ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(30) NOT NULL, password varchar(255) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE students ( id int(11) NOT NULL AUTO_INCREMENT, student_no varchar(20) NOT NULL COMMENT 学号, name varchar(50) NOT NULL, class_name varchar(50) NOT NULL COMMENT 班级名如高三(1)班, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE courses ( id int(11) NOT NULL AUTO_INCREMENT, course_name varchar(50) NOT NULL, full_score decimal(5,1) NOT NULL DEFAULT 100.0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE scores ( id int(11) NOT NULL AUTO_INCREMENT, student_id int(11) NOT NULL, course_id int(11) NOT NULL, exam_name varchar(50) NOT NULL COMMENT 如2025期末考试, score decimal(5,2) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_course_exam (student_id,course_id,exam_name), KEY idx_course_exam (course_id,exam_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;score 使用 decimal(5,2) 而不是 float是为了避免浮点误差。很多 PHP 新手在这里会踩坑用 float 保存 89.9统计平均分时得到 89.899999。虽然页面显示时会四舍五入但在排位次时小数误差会造成并列和错位。联合唯一键的作用也在这里同一学生、同一课程、同一考试只能存在一条成绩记录重复录入会触发 UPDATE而不是反复 INSERT 生成多行。2.2 源码目录怎么放命名与入口文件源码 zip 解压后的实际目录结构常见的组织方式是score-system/ ├── admin/ # 登录后才能访问的页面 │ ├── student_list.php # 学生管理 │ ├── course_list.php # 课程管理 │ └── score_list.php # 成绩列表和删除入口 ├── includes/ │ ├── db.php # 数据库连接 │ ├── functions.php # 全局函数 │ └── auth.php # 登录校验 ├── public/ # Web 可访问入口 │ ├── index.php # 查询入口 │ └── score_insert.php # 成绩录入分页 ├── templates/ │ └── layout.php # 公共头尾 ├── uploads/ # 临时 CSV 存储目录 ├── backup/ # SQL 备份目录 ├── config.php # 数据库配置 └── database.sql # 初始化数据脚本如果一个源码包把所有 php 文件全放在根目录连数据库连接都挂在每个文件顶部也不是不能用但后面加权限时会很痛苦。系统上线后我会将 admin 目录用密码保护或在 auth.php 里做 session 判断。auth.php 中的逻辑一般是启动 session检查 $_SESSION[admin_id] 是否存在不存在就 header 跳转到 login.php。2.3 PHP 版本选择与初始化步骤这套系统部署在 PHP 7.4 或 8.x 上都行。需要确保扩展里有 pdo_mysql这决定 $PDO 是否能正常建立连接。初始化建议流程在 MySQL 中创建数据库后执行 database.sqlmysql -u root -p score_db database.sql解压 zip 并把代码放到站点的 public 目录修改 config.php。这里要注意如果环境是 phpStudy 或宝塔PHP 8.0 之后默认禁用了一些老函数遇到 undefined function mysql_connect 不要急着改数据库先查代码里是否混用了 mysql_* 函数有就直接把整条连接逻辑换成 PDO。还可以检查 PHP 代码有没有依赖 parse_str 解析查询字符串后拿来查询这种方式容易产生联合查询注入建议统一使用预处理绑定参数。3. 成绩录入从表单到数据库的完整链路3.1 单条成绩录入表单与后端处理成绩录入是日常使用频次最高的操作。页面设计可以简单后端必须严谨。录入时最基础的字段是学生、课程、考试名称、分数。如果使用一条 INSERT 直接写入 scores 表代码短但缺了唯一键约束重复点击提交按钮会重复插入记录统计时平均分被拉低。下面代码使用 ON DUPLICATE KEY UPDATE重复提交自然变成更新。form methodpost actionsave_score.php label学生/label select namestudent_id required option value选择学生/option ?php foreach ($students as $stu): ? option value? $stu[id] ? ? htmlspecialchars($stu[student_no] . . $stu[name]) ? /option ?php endforeach; ? /select label课程/label select namecourse_id required option value选择课程/option ?php foreach ($courses as $c): ? option value? $c[id] ?? $c[course_name] ?/option ?php endforeach; ? /select label考试名称/label input typetext nameexam_name value2025期末 required label分数/label input typenumber namescore min0 max150 step0.5 required button typesubmit录入成绩/button /form在后端不要信任 HTML 的 max 和 min那只是浏览器提示不能拦截恶意请求。?php require __DIR__ . /includes/db.php; require __DIR__ . /includes/auth.php; if ($_SERVER[REQUEST_METHOD] ! POST) { exit(非法请求); } $studentId (int)($_POST[student_id] ?? 0); $courseId (int)($_POST[course_id] ?? 0); $examName trim($_POST[exam_name] ?? ); $scoreValue trim($_POST[score] ?? ); if (!$studentId || !$courseId || $examName ) { exit(缺少必要参数); } $fullScore $pdo-prepare(SELECT full_score FROM courses WHERE id ?); $fullScore-execute([$courseId]); $fullScore (float)$fullScore-fetchColumn(); if (!is_numeric($scoreValue) || $scoreValue 0 || $scoreValue $fullScore) { exit(分数必须是 0 到 {$fullScore} 之间的数字); } $stmt $pdo-prepare( INSERT INTO scores (student_id, course_id, exam_name, score) VALUES (:sid, :cid, :exam, :score) ON DUPLICATE KEY UPDATE score VALUES(score) ); $stmt-execute([ :sid $studentId, :cid $courseId, :exam $examName, :score $scoreValue, ]); header(Location: score_list.php?msgsuccess); exit;上面的校验里做了两件事用课程表里配置的满分值校验前端传回的 score。比如满分是150的化学传入200就应该被拦。对考试名称做了 trim避免用户在输入框里多敲空格产生两份考试记录。连接数据库的统一入口是 includes/db.php里面建立 PDO 对象建议把 PDO 的错误模式设置为异常$pdo new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]);3.2 批量录入与更新成绩的幂等处理单条录入适合平时补录个别学生期中期末大批量成绩应使用批量表单。将 input 的 name 写成数组形式例如 namescore[学生ID]后端就可以循环。代码类似$courseId (int)($_POST[course_id] ?? 0); $examName trim($_POST[exam_name] ?? ); $scores $_POST[score] ?? []; if (!$courseId || $examName || !is_array($scores)) { exit(参数不完整); } $pdo-beginTransaction(); try { $upsert $pdo-prepare( INSERT INTO scores (student_id, course_id, exam_name, score) VALUES (:sid, :cid, :exam, :score) ON DUPLICATE KEY UPDATE score VALUES(score) ); foreach ($scores as $studentId $score) { if (!is_numeric($score)) { continue; } $upsert-execute([ :sid (int)$studentId, :cid $courseId, :exam $examName, :score $score, ]); } $pdo-commit(); } catch (Throwable $e) { $pdo-rollBack(); error_log($e-getMessage()); exit(批量录入失败); }批量提交建议开启事务并在循环外层 prepare 一次、循环内多次 execute。这样能减少 PHP 和 MySQL 之间重复解析 SQL 的时间。幂等性由唯一的 (student_id, course_id, exam_name) 保证一旦某一批提交中途报错回滚后重新提交不会因为部分成功而让成绩表里出现“部分已更新、部分重复”的脏状态。值得注意MySQL 8.0.20 开始不推荐 VALUES() 语法不过部署在老版本 MySQL/MariaDB 上仍常见。如果运行在 MySQL 8.0可以用INSERT INTO scores (student_id, course_id, exam_name, score) VALUES (:sid, :cid, :exam, :score) AS new_score ON DUPLICATE KEY UPDATE score new_score.score;3.3 CSV 导入成绩的编码与字段映射用 Excel 整理好的成绩表另存为 CSV 后编码通常是 GBK/GB2312。PHP 用 fgetcsv 读取时默认按 UTF-8 处理中文班级名和姓名都会乱码。第一步就是把读取到的每一行按 GBK 转 UTF-8$fileName $_FILES[csv_file][tmp_name] ?? ; if (!is_uploaded_file($fileName)) { exit(请选择文件); } $handle fopen($fileName, r); $isHeader true; while (($row fgetcsv($handle, 1024, ,)) ! false) { $row array_map(function ($value) { return mb_convert_encoding(trim($value), UTF-8, GBK, GB2312, UTF-8); }, $row); if ($isHeader) { $isHeader false; continue; } [$studentNo, $scoreValue] $row; if (!is_numeric($scoreValue)) { continue; } $studentQuery $pdo-prepare(SELECT id FROM students WHERE student_no ?); $studentQuery-execute([$studentNo]); $student $studentQuery-fetch(); if (!$student) { // 可以在这里记录日志但不中断导入 continue; } // 继续执行 UPSERT 操作重复学号自动更新 } fclose($handle);这里有个关键的映射逻辑CSV 第一列是学号第二列是分数中间没有课程信息因为上传页已经预选了课程和考试名称。如果一间学校经常导入半期、期末、月考多场考试成绩CSV 的表头最好固定为“学号, 分数”避免在文件里再写课程字段。课程和考试名称由页面带入能避免每次写入都为字符串错误做清洗。导入前后还可以给成绩表做一个统计对比比如导入前 MAX(id)、导入后 MAX(id)如果差异太大说明数据有问题导入完成后将临时文件 unlink 删除并记录操作日志。这将直接回应标题里的“成绩录入”不只是写入一行还包括数据源清洗。4. 成绩统计与查询聚合 SQL 与筛选页面4.1 按班级和课程计算平均分、及格率与优秀率成绩统计的第一步是确定“统计口径”。“及格”对应课程 full_score 的 60%“优秀”可以按 90% 来算也可以由学校自定义。将全部门槛和及格线写死在 PHP 代码里之后调整很麻烦因此可以做成一个统计口径表或配置数组指标默认算法可调参数平均分该课程所有成绩的 AVG无及格率成绩 / 满分 60% 的人数占比及格线百分比优秀率成绩 / 满分 90% 的人数占比优秀线百分比最高/最低分MAX / MIN无统计模块的 SQL 可写成这样$examName $_GET[exam_name] ?? 2025期末; $courseId (int)($_GET[course_id] ?? 0); $sql SELECT stu.class_name, COUNT(*) AS total_count, ROUND(AVG(s.score), 1) AS avg_score, ROUND(100 * SUM(CASE WHEN s.score c.full_score * 0.6 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pass_rate, ROUND(100 * SUM(CASE WHEN s.score c.full_score * 0.9 THEN 1 ELSE 0 END) / COUNT(*), 1) AS excellent_rate, MAX(s.score) AS max_score, MIN(s.score) AS min_score FROM scores s JOIN students stu ON stu.id s.student_id JOIN courses c ON c.id s.course_id WHERE s.exam_name :exam_name AND s.course_id :course_id GROUP BY stu.class_name ORDER BY avg_score DESC; $stmt $pdo-prepare($sql); $stmt-execute([:exam_name $examName, :course_id $courseId]); $rows $stmt-fetchAll();这里把满分值 c.full_score 用进及格率计算而不是硬编码 60、90避免英语满分 120 时计算错误。多套用 SUM(CASE WHEN) 来统计符合条件的行数比先查出全部成绩再在 PHP 里循环判断要节省内存。如果只用 WHERE 过滤及格成绩再分别统计总数你能得到两个数字但需要两次查询所以用 CASE WHEN 更合理。4.2 成绩查询页的条件组合与防注入成绩查询功能是老师使用频率最高的入口。常见的搜索条件有姓名、班级、分数区间。关键点在于过滤条件是动态拼出来的但拼的一定是条件片段而不是直接用 $_GET 拼进 SQL。$conditions []; $params []; if (!empty($_GET[keyword])) { $conditions[] (stu.name LIKE :kw OR stu.student_no LIKE :kw); $params[:kw] % . trim($_GET[keyword]) . %; } if (!empty($_GET[class_name])) { $conditions[] stu.class_name :class; $params[:class] $_GET[class_name]; } $minScore $_GET[min_score] ?? ; $maxScore $_GET[max_score] ?? ; if (is_numeric($minScore) $minScore ! ) { $conditions[] s.score :min_score; $params[:min_score] $minScore; } if (is_numeric($maxScore) $maxScore ! ) { $conditions[] s.score :max_score; $params[:max_score] $maxScore; } $sql SELECT stu.student_no, stu.name, stu.class_name, c.course_name, s.score FROM scores s JOIN students stu ON stu.id s.student_id JOIN courses c ON c.id s.course_id . ($conditions ? WHERE . implode( AND , $conditions) : ) . ORDER BY s.score DESC, stu.class_name ASC LIMIT 0, 100; $stmt $pdo-prepare($sql); $stmt-execute($params); $list $stmt-fetchAll();LIMIT 0, 100 用于控制查询结果数量但是 $pdo-execute($params) 会把整型参数当字符串传递。对应 LIMIT 的位置预处理语句无法通过 execute 数组绑定整数需要在拼接 SQL 前将分页参数强制转成 int再拼进去。比如 $limit (int)$page * $size; $limit max(0, $limit)。4.3 排名与统计结果的分页输出统计结果显示成一张张表之后还需要把名次列出来。MySQL 8.0 用窗口函数 RANK() 最直接但常见老源码跑在 MySQL 5.7 上也可以用用户变量模拟$sql SELECT stu.name, s.score, rank_no : IF(current_course s.course_id, rank_no 1, 1) AS rank_no, current_course : s.course_id AS course_flag FROM scores s JOIN students stu ON stu.id s.student_id CROSS JOIN (SELECT rank_no : 0, current_course : 0) r ORDER BY s.course_id, s.score DESC;ORDER BY course_id 确保了在课程切换时 rank_no 被重置。如果不加这个排序所有课程的学生会混在一起从 1 数到几千。用户变量的赋值顺序是从左到右所以先算 rank_no 再更新 current_course这样才能让每条记录拿到正确的名次。如果想做更细的统计比如每个班级、每个课程的中位数在 PHP 里取全部成绩再排序显然不如用 SQL 的 GROUP BY 简单。常见做法是先用成绩查询页把数据控制在某个考试范围内再做内存统计。把查询结果放进多维数组以 course_id class_name 为 key第二次遍历即可得到各分组的总分与人数。5. 部署与防坑把 PHP 成绩系统跑稳的几个细节5.1 宝塔环境下让源码正常跑起来的检查顺序在宝塔面板里部署这套系统建议按顺序检查PHP 版本是否满足是否启用 pdo_mysql、fileinfo、zip 扩展伪静态是否配置。如果站点出现“No input file specified”先检查站点根目录是否指向 public/index.php 或 index.php 的位置。打开 PHP 设置确认 upload_max_filesize 和 post_max_size 足够大。成绩系统的 CSV 导入文件通常只有几百 KBupload_max_filesize 默认 2MB 可能不够建议改成 20MBpost_max_size 要大于 upload_max_filesize。如果要显示错误而不是白屏可以在开发阶段把 display_errors 打开生产环境关闭并记录日志。5.2 MySQL 8 的时报错与 VALUES 函数更新老源码中常见 VALUES(score) 的语法在 MySQL 8.0.20 之后会报 deprecated 警告虽然不会影响运行但在 PHP error_log 里会堆满 warning。要把所有 ON DUPLICATE KEY UPDATE score VALUES(score) 改成AS new_score ON DUPLICATE KEY UPDATE score new_score.score。对于 MySQL 5.7 或 MariaDB 环境老语法可继续使用但同一套源码要部署到不同版本时建议直接使用新语法以避免上线后不一致。5.3 验证成绩系统闭环的实用技巧验证系统的健壮性不一定靠大量测试数据可以做一个“同分同比对”的小脚本。提交一次重复成绩再查SELECT COUNT(*) FROM scores WHERE student_id... AND course_id... AND exam_name...结果应为 1。之后再执行统计 SQL和 Excel 透视表对比分数应一致。如果统计结果不一致优先排查 join 条件是否漏掉了 course_id 或 exam_name 的过滤条件。最后的建议导出成绩单时CSV 的编码要处理Excel 打开乱码通常不是 PHP 的问题而是缺少 BOM。可以在输出前加echo \xEF\xBB\xBF;。成绩系统的核心安全策略是所有入口都先检查 session所有 SQL 都走预处理所有文件上传都不可执行。做完这三个细节系统就能从“能显示成绩”进化到“能正式交给教务用”。本文还有配套的精品资源点击获取
返回列表