ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PHP学生成绩查询源码解析:从mysql到PDO的登录安全与分页重构

PHP学生成绩查询源码解析:从mysql到PDO的登录安全与分页重构 简介一款面向PHP初学者和课程设计场景的学生成绩查询系统以源代码加论文的完整形式打包发布。系统围绕学生、课程、成绩三类核心数据展开覆盖登录验证、数据增删改查、成绩录入与查询、权限控制等典型Web开发环节还涉及MVC分层、PDO连接MySQL、AJAX无刷新交互与表单安全过滤等关键知识点适合用来理解PHP项目的完整开发流程。压缩包共63个文件大小仅696KB主要以PHP源码为主另含HTML页面、CSS样式、SQL数据库脚本、DOC论文文档和说明文本结构清晰便于按模块阅读。已有40人学习下载无论是用于毕业设计、课程作业还是PHP入门练习都能从中获得可直接运行的代码和配套文档。论文部分对系统设计思路、数据库建模、安全性处理和响应式布局做了梳理配合源码可快速掌握从需求到实现再到总结的完整链路。1. 从 PHP 学生成绩查询源码包看老项目怎么组织拿到PHP学生成绩查询(源代码论文).zip这类压缩包时很多人会以为它和网上的开源 CMS 一样解压、配库、访问首页就能跑。实际上这套项目的核心 PHP 文件不到二十个而且有大量.mno后缀的临时文件、_notes缓存目录和 Dreamweaver 自动生成的服务器脚本。真正决定业务逻辑的是login.php、jc.php、xueshen.php这几个文件配合xs.sql里的建表语句就构成了一条完整的“登录—增删改—成绩查询”链路。对于刚做完 PHP 语法、想交课程设计的人它比一千行框架代码更容易读对于有五年经验的人拆它的意义在于看自己的重构思路是否仍停留在当年后端渲染的写法。2. 拆文件结构从文件名反推业务模块解压后第一件事不是急着配库而是把_notes、_mmServerScripts、*.mno这类编辑器痕迹删掉。.mno是 Dreamweaver 在保存页面时留下的临时文件本质上就是源文件的副本_notes是用*.txt保存的站点缓存里面经常是绝对路径信息对运行时没有任何作用_mmServerScripts/MMHTTPDB.php是老版 Dreamweaver 用于可视化绑定数据库的辅助脚本一旦脱离开发环境它反而是安全隐患来源。2.1 用一个清理命令还原干净源码在项目根目录执行下面这段 bash可以快速去掉编辑器遗留文件find . -name *.mno -delete find . -name _notes -type d -exec rm -rf {} \; rm -rf ./_mmServerScripts第一条命令匹配所有.mno文件并删除第二条把_notes目录连同内容删掉第三条删掉 Dreamweaver 的服务器端脚本目录。这些文件都不是业务代码清理后项目体积通常能缩水三分之一以上也避免后面用grep检索时被旧内容干扰。注意不要只删顶层find会递归进入子目录这就是比手动在资源管理器里搜索更可靠的原因。2.2 剩余文件与业务动作的对应关系清理后真正需要阅读的 PHP 文件大概能按动作分成四组如下表所示动作常见文件名说明登录与会话login.php、user.php登录入口、当前用户信息展示学生 / 课程基础维护xueshen.php、agai.php、xdel.php学生列表、修改、删除成绩录入和统计jc.php、jcruku.php、jcguanli.php成绩列表、入库、管理查询与页面框架index.php、search.php、head.php、foot.php首页、搜索、公共头部尾部这里只靠文件名推断用途所以不必把它当作精确需求文档。cg1.htm、cg2.htm、cg3.htm多半是成绩表格的静态原型或打印模板而phpmysql.txt、说明.txt通常是配置步骤笔记。真正的目录结构说明还要再看一遍readme.txt特别是数据库账号密码和文件上传位置。2.3 入口文件如何把页面串起来老式 PHP 项目很少用框架路由index.php通常是一个分发器。读这类项目时我会先打开根目录的index.php确认它到底做了什么。常见做法是?php session_start(); require_once config.php; $action isset($_GET[action]) ? $_GET[action] : list; switch ($action) { case login: include login.php; break; case search: include search.php; break; default: include xueshen.php; break; } ?$action决定包含哪个业务文件config.php负责加载数据库连接和公共函数。这样写的好处是小页面可以被独立访问调试时可以直接请求index.php?actionlogin。要注意的是这种include方式会把文件内容内联到当前脚本如果某个业务文件里重复调用session_start()会触发 “Cannot send session cache limiter” 警告。所以统一在config.php里只调用一次session_start()更加稳妥。2.4 相对路径导致的“登录后白屏”这类 zip 源码里include head.php与require config.php经常混用而且没有使用__DIR__。一旦入口文件从index.php换到admin.php相对路径就变成相对当前工作目录于是出现“登录页能打开进去之后白屏”的现象。排查方式是在入口文件第一行临时加两行配置ini_set(display_errors, 1); error_reporting(E_ALL);再把所有裸文件名改成__DIR__ . /config.php。__DIR__指向当前文件所在的目录不受入口切换影响无论从哪个页面require都不会找错位置。3. 用户认证与防注入把登录接口从 mysql 时代迁移到 PDO老式课程设计里的login.php逻辑很简单读取 POST 参数拼一条 SQL 去user表里查记录查到就写 session。这段代码在今天的主机上已经跑不起来了但把它的演进过程搞清楚能解释为什么后来会有 PDO、预处理和密码哈希这些概念。成绩查询系统的访问入口就是登录页这里被攻破后面所有功能都等于暴露。3.1 老登录代码的典型问题如果打开login.php看到的是下面这种写法不用怀疑这就是早年课设模板?php $username $_POST[username]; $password $_POST[password]; $conn mysql_connect(localhost, root, 123456); mysql_select_db(xs, $conn); $sql SELECT * FROM user WHERE username$username AND password$password; $result mysql_query($sql); if (mysql_num_rows($result) 0) { $_SESSION[user] $username; header(Location: index.php); } else { echo 登录失败; } ?这里不是风格问题而是三个致命点。第一$_POST里的值原封不动拼进 SQL用户输入 or 11就能绕过后面的AND password...条件第二mysql_*系列函数在 PHP 7 里已经被移除现在装 PHP 7.0 以上环境这里直接报致命错误第三密码以明文形式存在数据库里一旦数据文件泄露所有账号都会暴露。所以学习这段源码时不要照抄而是把它当作“为什么需要预处理”的反面教材。3.2 用 PDO 预处理重写 login.php现代环境里我会把数据库连接单独放到config.php用 PDO 统一管理?php // config.php $dsn mysql:hostlocalhost;dbnamexs;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]; $pdo new PDO($dsn, root, 123456, $options);ATTR_ERRMODE设为ERRMODE_EXCEPTION让 SQL 执行出错时抛出异常而不是静默失败便于定位查询问题ATTR_DEFAULT_FETCH_MODE用关联数组返回后面取字段名可以直接写$row[username]比数字下标清楚。DSN 中必须写charsetutf8mb4否则从 MySQL 读取中文姓名和课程名时可能变成乱码。对应的登录脚本改成?php session_start(); require_once __DIR__ . /config.php; $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $stmt $pdo-prepare(SELECT * FROM user WHERE username ?); $stmt-execute([$username]); $row $stmt-fetch(); if ($row password_verify($password, $row[password])) { session_regenerate_id(true); $_SESSION[user_id] $row[id]; $_SESSION[role] $row[role]; header(Location: index.php); exit; } $error 用户名或密码错误;最核心的改动是prepare()配合?占位符。用户名作为参数交给 PDO 处理由驱动完成转义从语法层面隔离了拼接注入。password_verify用来核对密码前提是写入用户表时用password_hash($password, PASSWORD_DEFAULT)生成哈希它会在哈希中附带盐值和算法标识比md5($password)安全得多。登录成功后先执行session_regenerate_id(true)再写入$_SESSION可以避免攻击者利用固定会话 ID 冒充已登录用户。最后一行exit很关键保证header(Location: ...)之后不会继续输出后面的页面内容。3.3 角色权限与页面入口保护原始xs.sql里user表是否区分角色要看具体字段。如果存在type或role字段通常就是用它区分管理员、教师和学生。可以按下面的粒度控制访问role 取值允许访问的页面业务范围adminadmin.php、xxgl.php、所有管理页维护学生、课程、用户基础数据teacherjc.php、jcruku.php、jcguanli.php录入成绩、修改成绩、查询统计studentsearch.php、cg*.htm查询本人成绩、查看成绩单判断逻辑不适合在每个文件里复制一遍常见做法是抽一个check_login.php?php session_start(); if (empty($_SESSION[user_id])) { header(Location: login.php); exit; } $allowed [admin, teacher]; if (!in_array($_SESSION[role], $allowed, true)) { exit(当前账号无权访问); }在需要保护的管理页第一行写require_once __DIR__ . /check_login.php;。in_array的第三个参数用了true开启严格类型比较避免admin 这种带空格的脏数据被放过去。如果接口要返回 JSONexit可以改为http_response_code(403);让前端统一接收无权限状态。4. 成绩查询链路从三张表到 AJAX 分页搜索成绩查询的核心不是把score表直接SELECT出来而是要拼出带学生姓名、课程名称、考试时间的可读列表。这套项目的xs.sql通常定义了学生、课程、成绩三张表下面是一套可以替换原项目的精简结构CREATE TABLE student ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, class_name VARCHAR(50) DEFAULT ) ENGINEInnoDB; CREATE TABLE course ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 0 ) ENGINEInnoDB; CREATE TABLE score ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id INT UNSIGNED NOT NULL, course_id INT UNSIGNED NOT NULL, score DECIMAL(5,2) NOT NULL, exam_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY fk_score_student(student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY fk_score_course(course_id) REFERENCES course(id) ON DELETE CASCADE, UNIQUE KEY uk_stu_course(student_id, course_id) ) ENGINEInnoDB;DECIMAL(5,2)表示最多三位整数加两位小数百分制下够用比FLOAT更精确联合唯一键uk_stu_course(student_id, course_id)从数据库层面阻止同一学生同一课程重复录入。两个外键都带ON DELETE CASCADE学生或课程被删除后成绩自动消失避免产生孤儿记录。如果业务上想保留历史成绩可以把CASCADE改成SET NULL但前提是student_id和course_id允许为空。4.1 连表查询与分页参数用户在前端输入学号或姓名按下查询后SQL 需要同时关联三张表SELECT s.student_no, s.name, c.course_name, sc.score, sc.exam_time FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE s.student_no ? OR s.name LIKE ? ORDER BY sc.exam_time DESC, sc.id DESC LIMIT 20 OFFSET 0;student_no用精确匹配name LIKE支持按姓名模糊查找。排序先用考试时间倒序再加sc.id DESC作为第二排序避免同一次考试多条记录时顺序不稳定。分页参数建议由服务端统一计算避免相信前端传过来的偏移量参数含义建议值page当前页码从 1 开始最小 1超过总页数时取最大页pageSize每页条数成绩列表建议 20offset偏移量等于 (page-1)*pageSize服务端计算前端不直接传keyword搜索关键字空字符串表示不过滤PHP 端执行查询时的写法$page max(1, (int)($_GET[page] ?? 1)); $pageSize 20; $offset ($page - 1) * $pageSize; $keyword trim($_GET[keyword] ?? ); $sql SELECT s.student_no, s.name, c.course_name, sc.score, sc.exam_time FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE s.student_no ? OR s.name LIKE ? ORDER BY sc.exam_time DESC, sc.id DESC LIMIT $pageSize OFFSET $offset; $stmt $pdo-prepare($sql); $stmt-execute([$keyword, %$keyword%]); $rows $stmt-fetchAll();这里的LIMIT和OFFSET直接拼接因为$pageSize和$offset都经过(int)强转是纯整数不会带进 SQL 注入。如果坚持所有参数都走占位符MySQL 驱动对LIMIT ?需要额外用bindValue(..., PDO::PARAM_INT)指定类型。$keyword trim(...)用来去掉首尾空格否则用户输入搜索条件时带一个空格就会查不出结果。注意LIMIT参数直接拼接只适合强转后的整数如果参数来自字符串拼接必须改用bindValue(..., PDO::PARAM_INT)。为了显示分页导航还要执行一条COUNT查询$countSql SELECT COUNT(*) FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE s.student_no ? OR s.name LIKE ?; $countStmt $pdo-prepare($countSql); $countStmt-execute([$keyword, %$keyword%]); $total (int)$countStmt-fetchColumn(); $totalPages max(1, (int)ceil($total / $pageSize));fetchColumn()返回第一列的数字强转成int后再用ceil算出总页数。max(1, ...)防止空表时出现第 0 页。这个COUNT查询必须和列表查询使用相同的WHERE条件否则页码数量和实际数据对不上。4.2 用 fetch 做无刷新查询老项目的search.php通常用表单 GET 提交选完条件后整页刷新。要提升体验可以改造为fetchAPI让查询请求异步返回 JSONdocument.querySelector(#searchBtn).addEventListener(click, async () { const keyword document.querySelector(#keyword).value.trim(); const page Number(document.querySelector(#currentPage)?.value) || 1; const resp await fetch(search.php?keyword${encodeURIComponent(keyword)}page${page}pageSize20); if (!resp.ok) throw new Error(HTTP resp.status); const data await resp.json(); renderTable(data); });encodeURIComponent(keyword)是必须的否则输入张三或2024-06这类带中文和连字符的值URL 可能被浏览器截断或解码错位。#currentPage用可选链?.读取取不到时就默认第 1 页。resp.ok用来捕获 403、500 这类非 2xx 状态避免继续解析错误页面。服务端search.php查询成功后这样输出header(Content-Type: application/json; charsetutf-8); echo json_encode([code 0, data $rows, total $total], JSON_UNESCAPED_UNICODE);JSON_UNESCAPED_UNICODE让中文以 UTF-8 原样输出而不是变成\u5f20\u4e09前端调试时可以直接看到内容。返回结构里保留一个code字段后续做错误处理时可以在统一位置区分业务失败和参数错误。前端渲染时用document.createElement拼行注意把score数字保留两位小数输出。5. 论文 源码组队从课设模板到可演示系统的重构技巧答辩前最有效的动作不是重写全部功能而是用十分钟把项目从“旧模板”变成“可演示产品”。第一步是确认本机能跑起来用 PHP 内置服务器验证php -S 127.0.0.1:8080 -t . mysql -uroot -p xs xs.sqlphp -S是 PHP 自带开发服务器-t .指定 web 根目录为当前目录不需要再配 Apache 或 Nginx。导入xs.sql之前先确认数据库已经创建如果还没有可以执行CREATE DATABASE xs CHARACTER SET utf8mb4;。这一步能跑通后续所有页面调试才有基础。5.1 用真实数据准备演示很多课设系统的初始表是空的答辩现场输入学号查不到记录非常尴尬。可以手动插入三条成绩INSERT INTO student (student_no, name) VALUES (2024001, 张三); INSERT INTO course (course_no, course_name) VALUES (C001, PHP程序设计); INSERT INTO score (student_id, course_id, score) VALUES (1, 1, 92.5);数据量不用大但要覆盖“模糊查询能命中”和“分页能翻页”两种场景。让答辩老师看到一次真实的查询结果比空跑系统更有说服力。5.2 论文论述重心把重构过程写进去论文部分最常见的问题是生搬 MVC。这套 zip 里的index.php、xxgl.php都是把 SQL、HTML、PHP 混在一个文件里说它采用 MVC 很容易在答辩时被一句话问穿。更稳妥的表述是原项目采用面向过程的页面脚本组织论文提出的改造方案是将数据访问抽到 model 层视图统一由模板输出Controller 只负责转发请求。配合这个论点可以加一张对照表原实现重构方案解决的问题mysql_connectnew PDOPHP 7 兼容字符串拼接 SQL预处理占位符防 SQL 注入md5 明文密码password_hash/password_verify密码泄露风险每个页面重复检查 sessionrequire check_login.php权限控制遗漏答辩演示时先用普通学生账号访问admin.php看到 403再切换管理员登录进入比只说“系统有权限模块”更有说服力。提交 zip 之前在项目根目录再执行一次find . -name *.mno -delete把_notes和_mmServerScripts清掉避免评审老师打开压缩包时因为垃圾文件失去耐心。本文还有配套的精品资源点击获取
返回列表