恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

原生PHP+MySQL在线招生报名系统开发实战:从数据库到准考证打印

  • 首页
  • 资讯中心
  • /
  • 原生PHP+MySQL在线招生报名系统开发实战:从数据库到准考证打印

相关资讯

单片机毕设选题推荐:基于 STM32 或 51 单片机的环境安全监测与风扇联动控制系统设计 基于 STM32 或 51 单片机的蓝牙环境安防监测终端设计与实现(023805) 2026/9/1 12:21:02
叠纸游戏春招笔试全解析:图形学、Unity与算法备考指南 2026/9/1 12:21:02
MATLAB多孔介质LBM模拟:从D2Q9到渗透率计算 2026/9/1 12:21:02

最新资讯

抖音趣味测评小程序源码:从搭建部署到流量主变现全流程解析
Galileo X 具身移动系统:从零构建机器人自主导航全流程
电工电子技术实战:从基尔霍夫定律到交直流电路分析
视觉引导抓取系统封装:从手眼标定到工程实践
用Qt从零实现VisionPro风格卡尺控件:边缘检测与交互设计
基于Electron的桌宠开发:集成Live2D、BongoCat与RPA的桌面自动化助手

今日推荐

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

本周热门

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

原生PHP+MySQL在线招生报名系统开发实战:从数据库到准考证打印

发布时间:2026/9/1 12:21:02
原生PHP+MySQL在线招生报名系统开发实战:从数据库到准考证打印 简介这是一套面向学校招生办、培训机构及中小型考试组织方的PHP在线招生考试报名系统源码解决传统纸质报名效率低、信息易错漏、缴费与准考证发放滞后等痛点。系统含完整前后台前台支持考生自助查询准考证、在线打印报名表后台涵盖报名管理、用户权限控制、考点/考试时间/准考证模板配置、数据统计及分类管理并集成支付宝即时到账接口需自行配置支持一键导出Excel报名表与在线支付闭环。压缩包共1086个文件以257个核心PHP业务逻辑文件为主辅以82个JS交互脚本、72个HTML前台页面、36个CSS样式文件及133个PNG图标资源结构清晰、模块解耦便于二次开发与部署。资源大小5.54MB目录中可见WdatePicker.js.bak、jQuery验证库、CHM帮助文档及多版config配置文件体现工程化设计思路。已有175人学习下载适合PHP中级开发者快速搭建轻量级考试服务平台或作为Web项目实训案例。1. 项目概述与功能拆解1.1 招生考试报名系统到底解决什么问题先说个现象。每年到了招生季学校、培训机构甚至一些社会化考试组织方都会被同一件事折磨报名表收了一堆有手写的有Excel的有在线表单的格式五花八门准考证要么一张张手工填要么一个个打电话通知来领取到了考试当天总有人忘带证件或者把准考证搞丢考完试想统计一下报名数据又得对着纸质表格重新录一遍Excel。这些琐碎工作看起来不难但量一上来光是核对信息就能逼疯一个教务老师。这个基于PHP的在线招生考试报名系统其实就是把这套流程全部搬到线上考生在线填写报名信息并提交系统自动生成报名记录管理员在后台审核考生资格、管理考试场次考生通过姓名和身份证号在线查询准考证直接调出包含照片、考场、座位号、考试时间等信息的准考证页面并打印同时还能随时在线打印报名表存档或用于现场核验。整套系统使用PHP代码实现部署在常规的PHP MySQL环境里代码开源可控很适合学校、培训机构、驾校、职业资格认证机构这类场景直接拿去用或者二次开发。这套系统的核心闭环是这样报名填表 → 后台审核 → 分配考场座位 → 生成准考证 → 考生查询打印 → 现场核验报名表。整个链路都在浏览器里完成考生不用装任何客户端管理人员也只需要会打开网页。1.2 为什么用原生PHP而不是现成的开源系统或框架其实市面上的开源报名系统并不少但很多都存在两个让人头疼的问题。第一是重。很多系统的依赖特别多装个环境就要折腾半天Composer拉一堆依赖包数据库迁移脚本一套一套的对于只想赶紧把报名这件事跑起来的机构来说学习成本和部署成本都不低。第二是定制麻烦。招生考试的流程差异非常大——有的按专业报名有的按场次报名有的还分初试复试每个机构的字段需求都不一样。用现成的系统改起来往往比自己写一个还费劲。所以我在做这套系统的时候刻意选了原生PHP来实现。原生的优势非常明显单文件一个index.php就能入口启动不需要Composer、不需要框架学习成本、数据库用PDO连MySQL国内绝大多数虚拟主机都能直接跑起来放到宝塔面板里十分钟就能上线。对读者来说你拿到这份源码之后可以非常轻松地看清每一行代码在做什么想加字段就加字段想改逻辑就改逻辑不会被框架的封装和路由规则卡住。当然如果是大型项目或者业务特别复杂的场景用ThinkPHP、Laravel这类框架会更合适。但作为一个以“在线报名、准考证查询、在线打印报名表”为核心功能的系统原生PHP已经是最平衡的选择了。它足够轻也足够透。2. 数据库设计与系统架构2.1 核心数据表设计思路在设计数据库之前我先把整个系统的数据流梳理了一遍。系统中存在的核心角色有三个考生、管理员、考试场次。围绕着这三个角色数据关系是一个考生报名某个考试场次报名后产生一条报名记录管理员审核这条记录并分配座位号然后根据这条记录生成准考证。表的设计不需要太复杂四张核心表就够用exam_sessions考试场次表存放每场考试的名称、时间、地点、考场容量等基本信息。students考生基础信息表存放姓名、身份证号、手机号、照片路径等。signups报名记录表存放考生与考试场次的关联以及报名状态、准考证号、座位号等信息。admins管理员账号表用于后台登录。这里我特意把考生基础信息和报名记录分开而不是把所有字段一股脑塞进一张表。原因是同一个考生可能同时报多个项目比如既报笔试又报面试这时候如果字段全部放在报名表里数据就会大量冗余。拆成两张表之后考生的基本信息只存一份报名记录只是引用考生的ID扩展性要好很多。2.2 表结构详细设计与字段说明直接把我实际用的建表SQL贴出来你可以照着建。CREATE DATABASE IF NOT EXISTS exam_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE exam_system; -- 考试场次表 CREATE TABLE exam_sessions ( id int(11) NOT NULL AUTO_INCREMENT, exam_name varchar(100) NOT NULL COMMENT 考试名称如2025年春季招生考试, exam_date date NOT NULL COMMENT 考试日期, exam_time varchar(50) NOT NULL COMMENT 考试时间段如09:00-11:30, exam_location varchar(200) NOT NULL COMMENT 考试地点, capacity int(11) NOT NULL DEFAULT 0 COMMENT 考场容量, signup_deadline datetime NOT NULL COMMENT 报名截止时间, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 场次状态 1开放 0关闭, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试场次表; -- 考生基础信息表 CREATE TABLE students ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 考生姓名, id_card varchar(18) NOT NULL COMMENT 身份证号唯一索引, phone varchar(20) NOT NULL COMMENT 手机号, photo_path varchar(255) DEFAULT NULL COMMENT 证件照路径, address varchar(255) DEFAULT NULL COMMENT 联系地址, remark text COMMENT 备注用于填一些特殊说明, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考生基础信息表; -- 报名记录表 CREATE TABLE signups ( id int(11) NOT NULL AUTO_INCREMENT, student_id int(11) NOT NULL COMMENT 关联考生ID, session_id int(11) NOT NULL COMMENT 关联考试场次ID, ticket_no varchar(32) NOT NULL COMMENT 准考证号唯一, seat_no varchar(20) DEFAULT NULL COMMENT 座位号, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 审核状态 0待审核 1通过 2不通过, payment_status tinyint(1) NOT NULL DEFAULT 0 COMMENT 支付状态预留字段0未支付 1已支付, admission_notes varchar(500) DEFAULT NULL COMMENT 考场注意事项比如需携带2B铅笔等, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_ticket_no (ticket_no), KEY idx_session_id (session_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报名记录表; -- 管理员账号表 CREATE TABLE admins ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password_hash varchar(255) NOT NULL COMMENT 密码使用password_hash函数加密, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员账号表;字段设计上有几个细节你需要特别注意。身份证号在建表的时候我用了char(18)而不是varchar(18)因为身份证号长度固定用定长字段在MySQL底层查询的时候效率更高而且不会有尾部空格的问题。同时加了唯一索引防止同一个考生重复注册——注意不同人身份证号不可能重复这就是天然的候选主键只是索引长度比自增主键长所以还是保留自增主键给身份证号加唯一约束就够了。准考证号ticket_no的生成规则我在下面会细讲这里先提醒一句务必加唯一索引。因为准考证号要作为考生查询的凭证之一如果有重复后面查询就会出问题。2.3 目录结构与路由设计因为是原生PHP不需要复杂的路由配置我采用的目录结构如下exam-system/ ├── index.php # 入口文件也是考生报名首页 ├── config.php # 数据库配置 ├── common.php # 公共函数如生成准考证号、判断登录等 ├── ticket_query.php # 准考证查询页 ├── print_signup.php # 在线打印报名表页 ├── admin/ │ ├── login.php # 管理员登录 │ ├── dashboard.php # 后台首页 │ ├── sessions.php # 场次管理 │ ├── signups.php # 报名审核列表 │ └── export.php # 导出Excel ├── uploads/ # 考生照片上传目录 └── assets/ ├── css/ └── js/入口文件index.php同时承担报名表单展示和提交处理的职责所以里面有个简单的判断如果是GET请求就显示表单如果是POST请求就处理数据。在实际项目中很多页面都是这个模式不一定要用MVC框架那套特别是业务逻辑不复杂的情况下。3. 核心功能实现与代码解析3.1 考生报名功能的完整实现报名是整个系统的入口也是数据质量的关键。这一步如果没做好后面审核和打印全都会出问题。我的报名页面在index.php里实现表单包含姓名、身份证号、手机号、照片上传、报考场次选择等字段。先说前端提交部分。表单用原生HTML加一点简单的JavaScript做校验不依赖任何前端框架页面加载速度很快// index.php 报名表单核心部分 form idsignupForm actionindex.php methodpost enctypemultipart/form-data div classform-group label姓名/label input typetext namename required maxlength50 /div div classform-group label身份证号/label input typetext nameid_card ididCard required maxlength18 placeholder18位身份证号 /div div classform-group label手机号/label input typetext namephone required maxlength20 pattern1[3-9][0-9]{9} /div div classform-group label证件照/label input typefile namephoto acceptimage/* required p classtip支持jpg/png格式大小不超过2MB/p /div div classform-group label报考场次/label select namesession_id required ?php // 从数据库读取当前开放的场次 $stmt $pdo-query(SELECT id, exam_name, exam_date, exam_time, capacity FROM exam_sessions WHERE status 1 AND signup_deadline NOW()); while ($row $stmt-fetch(PDO::FETCH_ASSOC)) { echo option value . $row[id] . . htmlspecialchars($row[exam_name]) . - . $row[exam_date] . . $row[exam_time] . /option; } ? /select /div button typesubmit提交报名/button /form这里有一个关键点场次的下拉框是服务端渲染出来的查询的时候同时过滤了status 1和报名截止时间也就是说已经关闭的场次根本不会显示在页面上。这比前端JS拉数据再渲染要简单可靠得多也是PHP这类服务端脚本语言处理表单场景的传统优势。提交处理的部分我直接写在index.php的顶部if ($_SERVER[REQUEST_METHOD] POST) { try { $name trim($_POST[name]); $id_card trim($_POST[id_card]); $phone trim($_POST[phone]); $session_id intval($_POST[session_id]); // 校验身份证号18位最后一位可能是X if (!preg_match(/^\d{17}[\dX]$/i, $id_card)) { throw new Exception(身份证号格式不正确); } // 先检查是否已存在 $stmt $pdo-prepare(SELECT id FROM students WHERE id_card ?); $stmt-execute([$id_card]); $student $stmt-fetch(); if ($student) { $student_id $student[id]; } else { // 新增考生 $stmt $pdo-prepare(INSERT INTO students (name, id_card, phone) VALUES (?, ?, ?)); $stmt-execute([$name, $id_card, $phone]); $student_id $pdo-lastInsertId(); } // 处理照片上传 $photo_path handlePhotoUpload($_FILES[photo], $student_id); // 生成准考证号并写入报名记录 $ticket_no generateTicketNo($session_id); $stmt $pdo-prepare(INSERT INTO signups (student_id, session_id, ticket_no, seat_no, status) VALUES (?, ?, ?, ?, 0)); $stmt-execute([$student_id, $session_id, $ticket_no, ]); // 发送成功的提示 echo div classalert alert-success报名成功您的准考证号是 . htmlspecialchars($ticket_no) . 请截图保存。审核通过后可在线查询打印准考证。/div; } catch (Exception $e) { echo div classalert alert-danger . htmlspecialchars($e-getMessage()) . /div; } }3.2 准考证号生成规则与防重策略准考证号是整套系统的核心标识规则定了就不能变否则以后扩展和查询都会出问题。我的生成规则参考了实际考试中常见的准考证号格式年份 场次ID 三位流水号。我先说使用场景一个机构一年可能组织几场考试每场考试几百人报名准考证号如果有规律管理员处理起来会很方便考生也觉得正规。function generateTicketNo($sessionId) { $year date(Y); $prefix $year . str_pad($sessionId, 3, 0, STR_PAD_LEFT); $stmt $pdo-prepare(SELECT COUNT(*) FROM signups WHERE ticket_no LIKE ?); $stmt-execute([$prefix . %]); $count $stmt-fetchColumn(); $ticketNo $prefix . str_pad($count 1, 3, 0, STR_PAD_LEFT); // 万一并发下重复就循环加1直到唯一 while (checkTicketNoExists($ticketNo)) { $count; $ticketNo $prefix . str_pad($count 1, 3, 0, STR_PAD_LEFT); } return $ticketNo; }这个方法的逻辑不复杂前缀是年份加场次ID后缀是当天报名人数的流水号。比如2025001001含义是2025年第001号场次的第001号考生。但这里有个隐患——如果两个人同时提交报名COUNT查出来的数字是相同的就会生成同样的准考证号。虽然建表时加了唯一索引兜底真撞了就会报SQL异常但用户体验不好。更稳妥的方式是用数据库的AUTO_INCREMENT拿到ID后再拼接或者更严谨一点直接把signups表的自增ID作为准考证号的一部分。我后来在实际部署中发现并发量其实不大用这个方案加唯一索引保障已经完全够用所以保守起见保持了上面的写法。如果你预期并发量很高建议把准考证号改成$year . str_pad($studentId, 6, 0, STR_PAD_LEFT)这种基于考生ID的方式天然唯一。3.3 准考证查询功能实现准考证查询是考生最常用的功能。我的实现方案非常简单考生输入姓名和身份证号系统在signups表关联查询返回准考证信息和状态。这一步的关键在于查询条件到底用哪个字段。有的系统要求输入准考证号才能查但对考生来说准考证号本身就是要查出来的东西这就不太合理。所以这里我用“姓名 身份证号”作为查询条件身份证号本身具有唯一性再加姓名是双重校验防止别人拿到身份证号就能查。// ticket_query.php 核心代码 if ($_SERVER[REQUEST_METHOD] POST) { $name trim($_POST[name]); $id_card trim($_POST[id_card]); $stmt $pdo-prepare( SELECT s.name, s.id_card, s.photo_path, e.exam_name, e.exam_date, e.exam_time, e.exam_location, u.ticket_no, u.seat_no, u.status, u.admission_notes FROM signups u INNER JOIN students s ON u.student_id s.id INNER JOIN exam_sessions e ON u.session_id e.id WHERE s.name ? AND s.id_card ? AND u.status 1 ); $stmt-execute([$name, $id_card]); $result $stmt-fetch(PDO::FETCH_ASSOC); if ($result) { // 显示准考证 renderTicket($result); } else { echo div classalert alert-warning未查询到已审核通过的准考证信息请核对姓名和身份证号是否填写正确。/div; } }这里有个容易踩的坑status字段如果默认值是0考生提交报名后立刻去查是查不到准考证的。因为管理员还没审核。所以我在查询SQL里把AND u.status 1写死状态为1才显示准考证信息。这样设计有业务上的考量——如果报名还没审核通过就把准考证号显示出来考生可能会直接拿号去考试而实际上考场安排可能还没定容易出乱子。3.4 在线打印报名表实现在线打印报名表这个功能很多同学容易做复杂。实际上它就是两个核心点一个是把报名信息从数据库里取出来按照固定表格格式展示在浏览器里另一个是调用浏览器的打印能力把这段内容输出成纸质文件。后者不需要后端参与完全靠CSS控制。打印页面print_signup.php接受一个ticket_no参数验证后展示报名表// print_signup.php $ticket_no $_GET[ticket_no]; if (empty($ticket_no)) { die(缺少准考证号参数); } $stmt $pdo-prepare( SELECT s.name, s.id_card, s.phone, s.photo_path, s.address, e.exam_name, e.exam_date, e.exam_time, e.exam_location, u.ticket_no, u.seat_no, u.admission_notes, u.created_at FROM signups u INNER JOIN students s ON u.student_id s.id INNER JOIN exam_sessions e ON u.session_id e.id WHERE u.ticket_no ? ); $stmt-execute([$ticket_no]); $data $stmt-fetch(PDO::FETCH_ASSOC);然后HTML部分就是一张标准的信息表格样式上尽量做成A4纸打印友好的。这里关键的是CSSmedia print { body * { visibility: hidden; } .print-area, .print-area * { visibility: visible; } .print-area { position: absolute; left: 0; top: 0; width: 100%; } .no-print { display: none !important; } }这套CSS写法的思路很简单打印的时候隐藏页面上所有元素只显示.print-area区域里的内容。这样页面上就算有导航栏、按钮、提示语打印出来也只有报名表主体内容。media print是浏览器专用于打印的媒体类型规则你不需要额外引入任何库浏览器原生支持。3.5 照片上传与文件管理细节照片上传是报名系统里容易出问题的环节。我见过很多系统在照片上传上栽跟头——要么是路径写错了图片显示的裂图要么是文件没做类型校验被传了病毒要么是文件名是中文导致浏览器打不开。我的实现里核心点有两个严格校验文件类型和大小把文件名改成随机字符串保存。function handlePhotoUpload($file, $studentId) { $allowed [image/jpeg jpg, image/png png]; $finfo finfo_open(FILEINFO_MIME_TYPE); $mime finfo_file($finfo, $file[tmp_name]); if (!isset($allowed[$mime])) { throw new Exception(只支持JPG和PNG格式的图片); } if ($file[size] 2 * 1024 * 1024) { throw new Exception(图片大小不能超过2MB); } $ext $allowed[$mime]; $filename stu_ . $studentId . _ . time() . . . $ext; $targetDir __DIR__ . /uploads/; if (!is_dir($targetDir)) { mkdir($targetDir, 0755, true); } $targetPath $targetDir . $filename; if (!move_uploaded_file($file[tmp_name], $targetPath)) { throw new Exception(照片保存失败请检查目录写入权限); } return uploads/ . $filename; }光看文件名stu_12_1739264234.jpg这就有两重信息在里面12是考生ID1739264234是时间戳。好处是不用查数据库就可以知道这个文件属于谁排查问题很方便。还有个细节照片路径存的是相对路径uploads/xxx.jpg而不是/uploads/xxx.jpg。这样系统部署在子目录也能正常显示不用改配置。4. 后台审核与管理功能4.1 管理员登录与会话管理后台管理功能我单独放在admin/目录下第一件事就是登录认证。用PHP原生的session来控制登录状态密码用password_hash函数加密存储彻底告别明文密码的问题。// admin/login.php session_start(); if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username]); $password $_POST[password]; $stmt $pdo-prepare(SELECT id, username, password_hash FROM admins WHERE username ?); $stmt-execute([$username]); $admin $stmt-fetch(); if ($admin password_verify($password, $admin[password_hash])) { $_SESSION[admin_id] $admin[id]; $_SESSION[admin_name] $admin[username]; header(Location: dashboard.php); exit; } else { $error 用户名或密码错误; } }每个后台页面顶部都要做登录判断我封装了一个函数require_login(); function require_login() { session_start(); if (empty($_SESSION[admin_id])) { header(Location: login.php); exit; } }一个容易被忽略的点生产环境里session的有效期和安全性很重要。建议在php.ini里把session.cookie_httponly设为1这样可以防止JavaScript脚本读取session cookie减少XSS攻击拿到会话的风险。另外后台页面在HTTPS环境下部署的话session.cookie_secure也建议开启。4.2 报名审核与考场分配管理员登录后台后主要的工作就是审核报名记录、给通过审核的考生分配座位号。这个功能集中体现在admin/signups.php页面上。我实际做的列表页长这样一张表格每一行是一条报名记录显示考生的姓名、身份证号、报考场次、照片预览、状态右侧是操作按钮——通过、不通过、分配座位号。审核通过的时候同时分配座位号。座位号的分配策略不复杂每个场次有固定的容量我按审核通过的顺序从1号开始排。程序里需要实时查询某个场次已分配的最大座位号然后加1。// 分配座位号 $sessionId $_POST[session_id]; $stmt $pdo-prepare(SELECT MAX(seat_no) FROM signups WHERE session_id ? AND seat_no ! AND status 1); $stmt-execute([$sessionId]); $maxSeat $stmt-fetchColumn(); $newSeat intval($maxSeat) 1;如果某场考试要设置人数上限你还需要在查询里判断COUNT(*)是否小于capacity避免超额。我在代码里加了这层校验$capacityStmt $pdo-prepare( SELECT e.capacity, COUNT(u.id) as signed_up FROM exam_sessions e LEFT JOIN signups u ON e.id u.session_id AND u.status 1 WHERE e.id ? GROUP BY e.id ); $capacityStmt-execute([$sessionId]); $row $capacityStmt-fetch(); if ($row[signed_up] $row[capacity]) { die(该场次名额已满); }4.3 报名数据导出Excel导出功能虽然不直接影响考生体验但绝对是管理员的刚需。机构报完名往往需要把数据交给财务、教务、监考老师等不同角色没有导出功能就只能后台截图或者手工抄表。我没用PHPExcel或者PhpSpreadsheet这类重库而是用PHP生成CSV文件来解决。CSV可以直接用Excel打开兼容性完全没问题而且代码量少得多// admin/export.php require_once ../common.php; require_login(); $filename 报名数据_ . date(Ymd_His) . .csv; header(Content-Type: text/csv; charsetutf-8); header(Content-Disposition: attachment; filename . $filename . ); $output fopen(php://output, w); // 写入表头 fputcsv($output, [姓名, 身份证号, 手机号, 报考场次, 考试日期, 准考证号, 座位号, 审核状态]); $stmt $pdo-query( SELECT s.name, s.id_card, s.phone, e.exam_name, e.exam_date, u.ticket_no, u.seat_no, u.status FROM signups u INNER JOIN students s ON u.student_id s.id INNER JOIN exam_sessions e ON u.session_id e.id ORDER BY e.exam_date, u.id ); while ($row $stmt-fetch(PDO::FETCH_ASSOC)) { // 把状态码转换为文字 $statusText [待审核, 已通过, 不通过][$row[status]]; fputcsv($output, [$row[name], $row[id_card], $row[phone], $row[exam_name], $row[exam_date], $row[ticket_no], $row[seat_no], $statusText]); } fclose($output);这里有坑CSV导出中文名会乱码。原因是CSV标准其实是ASCII文本Excel解析的时候默认用系统本地编码而Linux服务器的默认locale一般是UTF-8。解决办法是输出前加BOM头。上面代码里我加了output之前实际上需要这样处理echo \xEF\xBB\xBF; // UTF-8 BOM让Excel正确识别编码这个不起眼的操作能让你省去不少“导出Excel中文乱码”的搜索时间。5. 部署配置与常见问题排查5.1 环境要求与一键部署步骤这套系统的运行环境要求很低传统PHP装机三件套就能跑组件版本要求说明PHP7.0推荐7.4或8.0PDO及pdo_mysql扩展必须开启MySQL5.6推荐MySQL 8.0或MariaDB 10.xWeb服务器Apache/NginxApache需要开启mod_rewriteNginx需要配置PHP-FPM部署流程也很简单用宝塔面板或者手动创建站点PHP版本选择7.0以上。把源码上传到站点根目录。创建数据库把上面建表SQL语句直接导入。修改config.php里的数据库连接配置。手动插入管理员账号INSERT INTO admins (username, password_hash) VALUES (admin, $2y$10$...)这里的hash值用password_hash(你想要的密码, PASSWORD_DEFAULT)生成或者写一个install.php初始化脚本来自动创建。如果是本地调试用phpStudy这类集成环境更快解压源码放进去就能跑。整个部署流程大概也就是十分钟的事。5.2 照片上传失败的排查思路照片上传失败是我在实际使用中遇到最多的一个问题而且后台报错信息往往不直观。大部分情况排查方向就这几条。第一看uploads目录是否存在、是否有写入权限。Apache/Nginx运行用户一般是www或www-data必须对该目录有写权限。很多虚拟主机不用命令行就通过FTP把uploads目录权限设为755不行就775再不行就777后两者主要用于测试环境。第二PHP上传配置限制。默认的upload_max_filesize通常是2MB如果照片稍微大一点就会被PHP拒绝却在页面上直接报了500错误。建议在php.ini里改成upload_max_filesize 5M post_max_size 6M这两个配置配套用只改前者不改后者文件超过post_max_size一样报错。第三move_uploaded_file失败。这个情况最常见的原因不是目录权限而是tmp_name目录不可写或者文件名里的路径拼接出错了。一个实用的排查技巧是在move_uploaded_file之前先打印$file[error]对应错误码能直接告诉你问题出在哪。5.3 打印样式错乱的处理在线打印时最常见的毛病是浏览器预览的时候页面很好看一点打印就多出一张空白页或者表格被切得七零八落。这基本都和CSS打印样式没有写好有关。我建议的排查顺序是第一查看页面里是否有position: fixed的导航栏或按钮打印时这些元素会固定在每一页纸上。用了我前面说的visibility: hidden方案可以解决。第二注意表格是否在页面中间被截断。给表格的每一行加page-break-inside: avoid可以防止浏览器在行中间断页。media print { .print-area table tr { page-break-inside: avoid; } }第三A4纸张的宽度通常是210mm如果页面宽度设成了100%的网页宽度打印出来就会被压缩到左边。设置media print { .print-area { width: 190mm; margin: 0 auto; } }5.4 常见问题速查表问题现象排查方向解决方案页面显示数据库连接错误数据库名、密码配置错误检查config.php确认数据库账号有权限访问对应库报名提交后刷新出现重复提交页面没有做防重复处理提交成功后可跳转到成功页/使用SESSION标记本次提交准考证查询查不到数据状态不是1待审核先确认后台审核是否已通过再检查姓名身份证是否匹配导出CSV中文乱码缺少UTF-8 BOM头输出前添加echo \xEF\xBB\xBF;照片上传500错误上传限制过小或目录无权限调整upload_max_filesize和uploads目录写权限部署后样式不显示静态资源路径绝对检查assets目录是否被整体复制有时删除掉了CSS/JS文件打印预览出现重复页眉浏览器自带页眉页脚在打印对话框中关闭“页眉和页脚”选项或CSS设置page的margin这里还要提一个安全原则始终用PDO的预处理语句绑定参数来操作数据库绝对不要用字符串拼接SQL。尤其在这个系统里有大量用户输入会进数据库如果直接$pdo-query(SELECT * FROM students WHERE id_card $id_card)这样拼接一个恶意的身份证号输入就能拖走整个表。预处理语句不复杂就是多写一行却是这个系统最值得保住的安全性底线。6. 二次开发与功能扩展建议6.1 根据实际需求定制字段招生考试在不同场景下的需求差异很大。比如考研培训机构需要采集考生的本科院校和毕业专业驾校需要采集报考车型职业资格认证机构需要采集工作年限。这些个性化字段在原系统里并不存在但二次开发时只需要按下面的步骤来在students表里加字段比如ALTER TABLE students ADD COLUMN school varchar(100) DEFAULT NULL COMMENT 毕业院校;在报名表单中添加对应的输入框。在index.php的POST处理逻辑中把新字段插入数据库INSERT语句里加上新字段名VALUES里加上对应参数。在后台列表页、导出函数中按需展示。整个过程不涉及框架层级的改动只要SQL语句和HTML标签对应上就行。我实际改过最复杂的一次是给一个考试机构加了“考生考点选择”功能——同一个考试在三个校区举行考生报名时就要选择校区。添加一个exam_location_id字段然后改一下考场分配逻辑分配座位的时候按校区分别计数。这个改动前后不超过一小时原生PHP的可维护性在这里体现得淋漓尽致。6.2 增加在线支付功能的思路有些报名需要在线缴费很多读者都会问能不能在这个系统里接支付。答案是可以的而且不复杂。流程是这样在signups表里我已经预留了payment_status字段0是未支付1是已支付。前端在报名完成后显示一个“去支付”按钮后端生成支付二维码的时候带上一个订单号就用ticket_no作为关联。支付回调的接口里查询订单号把对应记录的payment_status更新为1。等支付成功之后管理员审核时就能参考支付状态了。需要注意的点支付回调的签名校验一定要认真实现不要信任任何外部传入的参数。具体到微信支付需要根据你的商户号、API密钥等生成签名用官方SDK能省很多事。我自己的建议是先用沙箱环境跑通再切到正式环境。6.3 多场次管理与批量导入导出如果机构一年组织几十场考试光靠后台手动一个个填场次信息效率太低。这时候可以做一个非常实用的功能——批量导入场次。用CSV或者JSON格式一次批量录入几十个场次的信息。原理就是解析上传的文件循环执行INSERT语句加个事务保证原子性中间遇到错误回滚。导出的方向也可以再加一层支持按场次导出准考证名单Excel带照片的那种。这个用PHPExcel或者更轻的PhpSpreadsheet库来实现给后台加一个“导出本场次考生名单”把照片路径转为图片写入单元格。这个功能在考场确认身份时很方便监考老师拿到一份带照片的名单就不用对着身份证一个个核对了。6.4 发送短信通知的接入线上报名还有一个痛点考生提交报名后不知道自己有没有报上也容易忘考试时间。我后面给这套系统加过短信通知功能接入一个短信服务商的API在几个关键节点触发报名成功后给考生发一条短信提醒告知“报名已提交请等待审核”。审核通过后告知考生可以登录查询并打印准考证。考试前一天提醒考试时间和地点附上注意事项。接入的原理不复杂——就是调用第三方API发送POST请求把内容提交过去。一般主流的服务商都有PHP SDK封装好了签名和发送逻辑。只要在代码里预留一个钩子函数在审核通过的逻辑中加一行调用即可。这一步对提升机构正规性感知非常明显考生体验完全不同。7. 安全加固与上线注意事项7.1 文件上传安全照片上传是整个系统无法绕开的功能同时也是风险最高的入口。如果你允许用户上传图片你相当于把服务器的一个目录开放给了外部输入。我建议用以下几个手段把风险压到最低第一用服务端判断文件类型用finfo函数读取文件的真实MIME类型而不是信任浏览器传上来的$_FILES[photo][type]。这个字段用户可以伪造。第二文件名必须重写。保留用户上传的原始文件名是我见过最大的安全坑——如果用户传一个shell.php系统即使拦截了后缀其他场景也可能出大问题。像我前面代码那样用student_id 时间戳重命名新文件后缀使用白名单里的扩展名基本可以杜绝这类问题。第三上传目录执行权限要关掉。如果你用Nginx PHP-FPM在uploads目录的location配置里加上php_admin_value engine off或者让Nginx对uploads目录直接返回403而不转发给PHP处理location ~* ^/uploads/.*\.(php|php5|phtml)$ { deny all; }这一点我在实际项目里见过太多因为配置疏漏导致的webshell上传事件了。7.2 SQL注入与XSS防护这两个词听起来像教科书术语但在实际业务里就是实实在在的攻击手段。先说SQL注入——攻击者在输入框里填了 OR 11 --这种内容如果代码是字符串拼接SQL就会导致本应查询单条记录的逻辑变成查询全部记录数据泄露的风险非常大。防御的方式是全文统一使用PDO预处理语句绑定参数不给用户输入直接进入SQL的机会。XSS的防御也很简单——所有输出到页面的动态内容一律用htmlspecialchars()转义。我在上面的打印准考证代码里已经展示过htmlspecialchars($data[name])。如果少了这层转义攻击者可以在姓名里填一段JavaScript代码管理员打开后台列表时脚本就会执行轻则弹窗骚扰重则窃取管理员的session。7.3 上线检查清单根据我部署多个项目的经验上线前最好按这个清单过一遍数据库连接用户名不要用root建议创建一个权限最小化的专用账号。.php文件头部不要出现display_errors设为On的配置生产环境必须关闭错误显示改为记录日志。后台路径不要用admin这种默认字典里的路径改成admin_xxxx这种随机字符串大幅减少被自动扫描工具扫描到的概率。定期备份数据库。最简单的可以用crontab定时任务执行mysqldump0 2 * * * mysqldump -u exam_user -p密码 exam_system /backup/exam_$(date \%Y\%m\%d).sql照片上传目录设置一个容量上限防止被当图床磁盘满了导致系统挂掉。8. 写在最后的实操心得我开发这套系统的初衷是做一套看得见、摸得着、改得动的PHP实战项目。没有框架的黑盒封装没有复杂到看不懂的抽象层一个有一定PHP基础的人拿到源码逐行读下去就能明白整个需求从数据库到页面渲染的完整链路。在实际使用中这套系统后来被部署到过好几个机构最容易被忽略但直接影响体验的其实是打印样式和查询路径。打印样式决定了你给考生的第一印象查询路径决定了考生最后一天是否能顺利找到自己的准考证。很多人把精力花在登录、权限这些技术噱头上却忘了对考生来说整个系统就两个页面报名页和打印准考证页。把这两件小事做到极致远比堆功能有意义。如果你打算基于这套源码做毕业设计或者商业项目我的建议是先完整跑通一遍“报名 → 审核 → 查询 → 打印”的流程理解每个环节的字段流向再动手改代码。等到第二遍你可以试着加上在线支付、短信通知、批量导入这些扩展功能每次都只围绕一个核心改动观察数据库和页面的变化这样的成长路径会非常扎实。最后再分享一个小技巧我每次上线前都会用一个完全不存在的身份证号、一个重复的身份证号、一个带引号的姓名、一张改名为.php的图片分别测试一遍。只有这四个测试用例全过了我才放心把系统交给客户用。这个习惯帮我拦截过至少十次低级事故今天也分享给你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号