基于PHP+MVC的学生信息查询系统源码解析与安全实践

发布时间:2026/9/15 15:13:31
基于PHP+MVC的学生信息查询系统源码解析与安全实践 简介基于PHP的MVC架构学生信息查询管理系统源码包面向正在学习PHP Web开发与MVC设计模式的开发者。系统按模型、视图、控制器分层实现涵盖学生信息增删改查、成绩管理、用户登录鉴权、路由分发与错误处理等完整功能是理解MVC思想与PHP实战的典型范例。全部代码共438个文件压缩包约9.1MB以278个PHP源码文件为主辅以数据库SQL脚本、HTML/CSS/JS前端页面、配置文件及使用说明并附有演示录屏目录结构清晰便于对照学习。已有91人浏览学习。下载后可获得整套可部署源码从中能学到数据库表设计、登录会话安全、控制器路由映射、Composer依赖组织、错误处理与日志记录等关键知识点既适合课程设计、毕业设计也可作为项目二次开发的基础框架整体完成度较高。1. 基于PHP的MVC学生信息查询管理系统源码到手先想清这三件事打开一个名为“基于PHP的基于MVC学生信息查询管理系统源码.zip”的压缩包第一件事不是解压后立刻改学校名。几乎所有PHP课设都长得很像但这个标题真正问的是MVC分层在PHP里怎么落学生信息查询用什么方式写才不会把漏洞带进答辩。常见做法是拎出前端控制器、模型和视图三层把MySQL里的学生数据用PDO预处理交到页面再在入口处统一接收查询参数。这篇博文就用一个最小可复现的学生信息查询管理系统源码来拆解这三件事适合手工接过二手项目、准备课程设计答辩、以及从面向过程PHP转MVC的人。2. MVC分层在PHP学生信息查询管理系统里的落地结构2.1 为什么学生信息查询一定要用MVC而不是一个list.php写完直接用原生PHP做学生信息查询最省事的写法是在list.php里写PDO、写table、写过滤条件。代码放在一起十几个查询条件一加文件就到几百行改一个字段得翻半天。MVC的价值不在性能而在把“查询请求怎么进”“学生数据从哪来”“页面怎么画”三件事拆开控制器负责接收参数并调用模型模型只负责和student表打交道视图只负责把数组渲染成表格。这套思想也被Spring MVC、ASP.NET MVC沿用所以答辩时把“MVC设计模式”挂在嘴上的前提是你真把手写的router和model理清了。我一般会把手写MVC项目的目录结构固定成这样student-info-mvc/ ├─ public/ │ └─ index.php ├─ app/ │ ├─ controllers/ │ │ └─ StudentController.php │ ├─ models/ │ │ └─ StudentModel.php │ ├─ views/ │ │ └─ student/ │ │ ├─ list.php │ │ └─ detail.php │ └─ core/ │ └─ Router.php ├─ config/ │ └─ database.php └─ sql/ └─ student.sqlpublic是唯一对外可见的目录浏览器不能直接访问到app下的控制器和视图。每新增一个学生模块的功能就往controllers和models里各加一个文件需要换模版时只动views。这种做法的好处是即使你手上拿到的“源码.zip”目录结构差很多也能按这个基线判断它到底是不是真正的MVC还是只在文件名里写了MVC。2.2 前端控制器与数据库配置的默认参数入口文件只做两件事注册所需类再把请求交给路由。最简单的写法不需要Composer?php // public/index.php 前端控制器 require __DIR__ . /../app/core/Router.php; require __DIR__ . /../app/controllers/StudentController.php; require __DIR__ . /../app/models/StudentModel.php; $router new Router($_GET[route] ?? student/list); $router-add(student/list, StudentController, list); $router-add(student/detail, StudentController, detail); $router-dispatch();关键在$_GET[route]它是整个MVC学生信息查询管理系统的请求调度入口。?? student/list表示没有给route参数时默认跳转列表页避免直接访问根路径时白屏。add方法只做路由表登记真正执行在dispatch里这样后续加student/export导出Excel只需要多一行add和一个控制器方法。数据库连接统一放在config/database.php用PDO而不是mysqli因为预处理在防SQL注入这件事上比手动拼接字符串可靠得多?php // config/database.php return [ host 127.0.0.1, port 3306, dbname student_info, user root, pass , charset utf8mb4 ];连接时把PDO::ATTR_ERRMODE设为异常模式并在表结构上使用utf8mb4后端才能和页面上提交的中文姓名、班级名保持一致。这套配置是这条PHP源码路线里默认的选择也是面试时一说php pdo就能接上的部分。学生信息表至少要覆盖教务处最常见的查询字段CREATE TABLE student ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, class_name VARCHAR(100) NOT NULL DEFAULT COMMENT 班级, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_class_name (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;字段和注释说明student_no加唯一键同一学号重复录入时数据库直接报错这比控制器里先SELECT再INSERT要可靠class_name加普通索引是因为列表页最常出现的筛选条件就是“按班级看学生”gender用TINYINT不用字符串能省存储也能避免“男/女/未知”三套文字不统一。3. 路由、控制器和模型把学生信息查询请求拆成三步3.1 手写Router从URL参数到控制器方法路由层不需要引入第三方库核心就是把route参数和“控制器方法”映射起来。下面是常见做法?php // app/core/Router.php class Router { private array $routes []; public function add(string $route, string $controller, string $method): void { $this-routes[$route] [$controller, $method]; } public function dispatch(): void { $route $_GET[route] ?? student/list; $route trim($route, /); $route preg_replace(/[^a-zA-Z0-9_\/\-]/, , $route); $conf $this-routes[$route] ?? null; if (!$conf) { http_response_code(404); exit(路由不存在); } [$controllerName, $methodName] $conf; $controller new $controllerName(); $controller-$methodName(); } }逻辑说明preg_replace把route里的特殊字符全部滤掉避免有人通过route参数传路径符号去访问预期外的文件路由表查不到对应项时直接返回404。之所以不用把整个控制器类全注册到路由里是为了让普通课设源码能靠四个方法跑起来而不是上来就引入反射和注解。3.2 StudentController的查询动作与输入处理控制器是查询动作的入口它只做三件事接收参数、调用模型、把数据交给视图?php // app/controllers/StudentController.php class StudentController { public function list(): void { $page max(1, (int)($_GET[page] ?? 1)); $keyword trim($_GET[keyword] ?? ); $gender isset($_GET[gender]) ? (int)$_GET[gender] : -1; $perPage 10; $model new StudentModel(); $result $model-search($keyword, $gender, $page, $perPage); $students $result[list]; $pagination $result[pagination]; require __DIR__ . /../views/student/list.php; } public function detail(): void { $id (int)($_GET[id] ?? 0); $model new StudentModel(); $student $model-findById($id); if (!$student) { http_response_code(404); exit(学生不存在); } require __DIR__ . /../views/student/detail.php; } }这里的参数说明要看两个细节page做了max(1, ...)下限保护用户传page0或page-3不会触发SQL错误gender默认值是-1表示“不筛选性别”这样0、1、2这些真实枚举值不会和“未选择”冲突。list和detail两个动作都没有直接拼SQL这是MVC控制器和页面脚本最直观的差别。3.3 StudentModel用PDO预处理把学生数据取出来模型层承载所有SQL学生信息查询系统的模型通常只读不需要写update操作但这不代表可以不处理输入?php // app/models/StudentModel.php class StudentModel { private PDO $pdo; public function __construct() { $config require __DIR__ . /../../config/database.php; $dsn sprintf(mysql:host%s;port%d;dbname%s;charset%s, $config[host], $config[port], $config[dbname], $config[charset]); $this-pdo new PDO($dsn, $config[user], $config[pass], [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); } public function search(string $keyword, int $gender, int $page, int $perPage): array { $where []; $params []; if ($keyword ! ) { $where[] (student_no LIKE :kw1 OR name LIKE :kw2); $params[kw1] % . $keyword . %; $params[kw2] $params[kw1]; } if ($gender 0) { $where[] gender :gender; $params[gender] $gender; } $whereSql $where ? WHERE . implode( AND , $where) : ; $offset ($page - 1) * $perPage; $sql SELECT id, student_no, name, gender, class_name FROM student $whereSql ORDER BY id DESC LIMIT $perPage OFFSET $offset; $stmt $this-pdo-prepare($sql); $stmt-execute($params); $list $stmt-fetchAll(); $countSql SELECT COUNT(*) FROM student $whereSql; $countStmt $this-pdo-prepare($countSql); $countStmt-execute($params); $total (int)$countStmt-fetchColumn(); $totalPages $perPage 0 ? (int)ceil($total / $perPage) : 1; return [ list $list, pagination [ total $total, page $page, perPage $perPage, totalPages $totalPages ], ]; } public function findById(int $id): ?array { $stmt $this-pdo-prepare(SELECT id, student_no, name, gender, class_name FROM student WHERE id ?); $stmt-execute([$id]); $row $stmt-fetch(); return $row false ? null : $row; } }代码逻辑说明$where数组先收集条件再拼接成WHERE段SQL绑定的参数统一存在$params里LIKE条件里把同一个关键字拆成:kw1和:kw2两个命名参数避开了PDO原生预处理对重复命名参数的支持限制。LIMIT $perPage OFFSET $offset两个值都来自int类型和max(1, ...)保护所以可以用变量直接拼接否则就必须改成占位符绑定。注意COUNT(*)语句复用了同一个$whereSql和$params这样关键字搜索和性别筛选的结果总数才和列表一致不会出现列表有数据、分页总数为0的尴尬。4. 学生信息查询的关键动作关键字、分页与SQL注入边界4.1 查询条件怎么拼才安全上一章的search方法已经展示了“条件数组参数绑定”的组合方式。这里要强调的是边界LIKE语句里用户输入的关键字放在%中间是安全的因为整个字符串只作为参数传给预处理语句MySQL不会把它解释成SQL。真正容易出错的是把ORDER BY、表名、字段名也拿用户输入来拼比如ordername还能接受如果允许ordername; DROP TABLE student;预处理也救不了你。学生信息查询系统常见的危险写法是把$_GET[keyword]直接拼到字符串里// 错误直接拼接用户输入 $sql SELECT * FROM student WHERE name LIKE % . $_GET[keyword] . %;即使做了addslashes在关闭magic_quotes的PHP 5.4以后也可能绕过。用PDO prepare execute就是把输入和SQL结构分开这是PHP源码项目里最稳妥的防注入手段。4.2 分页参数设计保持查询状态而不是刷新就丢分页是学生信息查询管理系统里最容易做错的一环。常见做法是分页数字直接用href?routestudent/listpage2结果用户带着keyword张搜索后点第2页关键字丢掉了只能返回无输入结果的列表。要解决这个问题分页链接生成时要把当前查询条件一并透传?php // app/views/student/list.php 分页部分 $query array_filter([ route student/list, keyword $_GET[keyword] ?? , gender $_GET[gender] ?? , ]); $queryString http_build_query($query); for ($i 1; $i $pagination[totalPages]; $i) { echo a href?page . $i . . $queryString . . $i . /a; }http_build_query会自动做URL编码中文关键字转成%E5%BC%A0后放在链接里比手写字符串拼接干净。array_filter会把值为空字符串的gender和keyword去掉但保留非空的查询条件。页面里还需要用htmlspecialchars输出姓名防止学生在姓名里写了script一旦作为td内容输出就变成存储型XSS。4.3 查询性能与索引不是所有表都需要全文索引当学生表超过几万行后LIKE %张%用不上索引这是所有学生信息查询系统的通病。合理的做法是先做前缀匹配张%同时让class_name等完全匹配字段走索引。下面这组索引在数据量逐渐变大时能明显拉开差距ALTER TABLE student ADD KEY idx_name_prefix (name); ALTER TABLE student ADD KEY idx_class_gender (class_name, gender);配合查询时的name LIKE 张%关键字不带前导%才能命中索引。如果产品要求必须搜索姓名中间的字就得考虑冗余拼音字段或者改用MySQL全文索引这对课设源码来说超纲了但答辩时能说出来代表你清楚边界。4.4 查询结果的显示与空状态视图层不要在tr里写PHP业务逻辑只做循环和转义。列表页至少要处理两种状态查询结果为空、分页总数为0。空状态给用户看的是“没有匹配的学生”而不是空白表格。显示项处理方式说明姓名htmlspecialchars($row[name], ENT_QUOTES, UTF-8)防XSS性别[0 未知, 1 男, 2 女][$row[gender]]业务枚举映射学号原样输出但不要加a链接学号是业务主键不是可导航内容分页若总页数1则不渲染分页条减少无效点击5. 用PHP内置服务器跑通源码并验证三个关键点5.1 启动命令与两个入口在项目根目录执行cd student-info-mvc php -S 127.0.0.1:8080 -t public浏览器打开http://127.0.0.1:8080/index.php?routestudent/list如果不带route默认会进student/list。php -S是PHP自带的调试服务器不依赖Nginx和Apache适合先跑通再部署。5.2 用curl验证路由过滤与SQL注入防护curl http://127.0.0.1:8080/index.php?route../../etc/passwd curl http://127.0.0.1:8080/index.php?routestudent/listkeyword%27%20OR%2011%20--%20第一个请求应返回“路由不存在”第二个请求应正常渲染空列表而不是打印SQL语法错误。5.3 三个必查的位置检查PHP版本php -v要7.4以上因为代码里使用了array类型声明。检查PDO扩展php -m | grep pdo有pdo_mysql才算通。检查错误显示部署到正式环境时把display_errors关闭error_log指向可写文件避免PDO异常把数据库连接信息暴露在网页上。最后在StudentModel::search里临时加上error_log($sql)观察实际执行SQL里关键字和性别条件的绑定位置确认无误后删掉这行调试日志。本文还有配套的精品资源点击获取