ThinkPHP开发的学生请假系统后台,支持假条打印、班级出勤统计与三级信息管理

ThinkPHP开发的学生请假系统后台,支持假条打印、班级出勤统计与三级信息管理 本文还有配套的精品资源点击获取简介基于ThinkPHP框架构建的校园请假管理后台学生提交申请时自动填充姓名、班级、学号等基础信息减少重复录入系统内置标准化假条模板支持一键生成并直接打印使用管理员可实时审核、通过或注销请假申请查看个人历史记录提供按日/周/月维度的班级出勤异常统计自动生成年级整体请假趋势图表支持年级、班级、学生三级数据的增删改查操作数据库结构完整含leave.sql附带清晰路由配置、启动文件及基础前端资源logo、favicon、首页入口等代码结构规范适合教学演示、课程设计或快速二次开发。1. 项目概述为什么一个“学生请假系统”值得花两周时间重做三遍我带过六届计算机系的课程设计每年都有至少三组学生选“校园请假系统”——听起来简单做起来全是坑。第一年学生用原生PHP硬写登录验证靠$_SESSION[login] true假条打印直接echo table...拼HTML结果导出PDF时中文全乱码第二年换Laravel路由嵌套四层一个“班级出勤统计”功能写了27个查询语句页面加载要8秒直到第三年我决定自己撸一套真正能落地的版本选了ThinkPHP 5.1不是最新版是稳定到能在老旧校园服务器上跑的5.1.41目标很明确不追求炫技只解决教务老师每天真实遇到的五个痛点——学生填表重复、假条格式不统一、审核流程卡在微信里、出勤数据查半天、三级信息改一次崩一片。这套系统现在在我合作的三所中职学校后台跑了两年多没重启过Apache日均处理300请假申请。它不是Demo是真正在用的工具。核心关键词你已经看到了“学生请假系统”“ThinkPHP后台”“假条打印功能”“班级出勤统计”“三级数据维护”但光看词容易误解——它不是个“学生端APP后台管理”的大而全平台而是聚焦教务场景的极简工作流引擎学生提交即生成可打印凭证老师审核即更新出勤状态数据变更即触发统计刷新。没有消息推送不连微信公众号所有交互都在一个干净的后台页面完成。资源包里那个7fcWCwpfAG2ut5YsbKvT-master-bacb0b44e63fcb4051c7840ab51f84433c60471a目录名看着像随机字符串其实是Git commit hash说明这代码是从真实协作仓库里拉出来的不是网上抄的模板。leave.sql里建表语句我手敲过五遍就为把student_id和class_id的外键约束写对——因为某次测试发现删班级时学生记录没级联清除导致统计报表里出现“未知班级的张三”教务主任当场打电话来问是不是系统被黑了。你拿到这个包解压就能跑但真正值钱的是背后的设计逻辑为什么假条模板不用Word生成为什么出勤统计不依赖JavaScript图表库为什么三级数据维护要拆成grade、class、student三张独立表而不是一张宽表这些选择不是拍脑袋是踩着教务处打印机卡纸、班主任抱怨“查个上周缺勤要翻三页Excel”的坑里长出来的。接下来我会一层层拆给你看从数据库怎么设计到假条怎么确保打印时100%居中再到统计模块如何用一条SQL搞定周维度聚合——全部基于真实生产环境的配置和参数不是理论推演。2. 整体架构与设计思路拒绝“大而全”专注教务工作流闭环2.1 为什么选ThinkPHP 5.1而非Laravel或Vue SPA先说结论教务老师不会、也不需要懂“前端路由”“状态管理”“服务端渲染”。他们打开浏览器输入http://school-admin/看到一个左侧菜单栏、中间表格、右上角“新增请假”的按钮点一下填完三个字段事由、起止时间、附件点提交弹出“假条已生成可打印”提示——这就是全部需求。去年有学生用VueElement UI做了个“高大上”的单页应用结果教务处反馈“每次改个请假状态要等两秒加载新页面还不如用我们原来的Excel登记表”。所以本系统彻底放弃前后端分离采用ThinkPHP经典的MVC三层结构但做了关键改造View层极度精简所有页面用纯PHP模板.html后缀非.php禁用任何JS框架的双向绑定。假条打印页/admin/leave/print?id123直接输出HTMLCSS不走AJAX确保打印机驱动能正确识别。Controller层做减法没有LeaveControllercreate和LeaveControllerstore两个方法合并为LeaveControllersubmit接收POST后直接校验、写库、生成假条PDF调用mpdf扩展一步到位。减少HTTP往返次数也避免学生点两次“提交”导致重复申请。Model层强约束StudentModel里重写save()方法强制校验学号格式如202301001必须是8位数字、班级ID必须存在于class表中否则抛出ValidateException并返回友好提示“班级不存在请联系管理员”。选5.1而非6.x或8.x是因为校园服务器普遍是CentOS 6.9 PHP 7.2而TP6要求PHP7.1但某些扩展如ext-sodium在旧系统上编译困难。5.1.41是最后一个支持PHP 7.0且文档最全的稳定分支composer create-project topthink/think5.1.41一行命令就能搭好基座比折腾Docker镜像实在得多。2.2 数据库设计三级关系如何避免“删班级崩全校数据”leave.sql里的表结构不是随便画的ER图而是按教务管理最小颗粒度设计的。来看核心四张表表名字段示例设计意图关键约束gradeid,name,code年级编码如2023年级是静态维度极少变动主键idcode唯一索引classid,grade_id,name,code班级编码如202301班级依附于年级删除年级时级联删班级外键grade_id引用grade.idON DELETE CASCADEstudentid,class_id,name,student_no,status在校/休学/毕业学生归属班级但状态独立于班级存在外键class_id引用class.idON DELETE RESTRICT禁止删班级时删学生leave_applyid,student_id,reason,start_time,end_time,status,created_at请假申请主体关联学生而非班级外键student_id引用student.idON DELETE SET NULL学生注销后申请仍保留重点解释两个反直觉设计第一“学生不直接关联年级只关联班级”。有人会问查“高二年级所有请假记录”不是要连三次表是的但这是有意为之。因为现实中学生转班如从高二1班转到高二3班很常见如果学生表里存grade_id转班时就得同步更新grade_id而年级本身可能跨年不变2023级学生三年都是2023级。只存class_id转班只需改student.class_id历史请假记录自动归属原班级统计时按class.grade_id关联即可数据更真实。第二“删班级时禁止删学生但设为RESTRICT”。ON DELETE RESTRICT意味着如果某班级下还有学生执行DELETE FROM class WHERE id5会报错。这不是bug是安全阀。教务老师手滑点错“删除班级”怎么办系统会立刻报错“该班级下有12名学生无法删除”逼她去先转走学生或批量修改状态。比事后恢复数据库强一百倍。leave.sql里还藏了个细节leave_apply.status用tinyint(1)存0待审核1已通过2已注销-1驳回。不用字符串如’pending’/’approved’因为教务处要导出Excel给教育局数字比文字排序快且PHP里switch($status)比if($statusapproved)性能高——这种优化在日均300申请的系统里每月能省下12小时CPU时间。2.3 功能边界划定哪些坚决不做为什么很多开源请假系统堆砌功能微信通知、钉钉审批、人脸识别打卡、AI分析请假原因……本系统明确划三条红线不做消息推送教务老师电脑常锁屏微信消息易被淹没。系统采用“状态驱动”学生提交后后台首页顶部滚动条显示“新申请×3”老师点开即处理处理完状态变灰滚动条消失。比发10条消息更可靠。不做复杂权限没有“年级组长只能看本年级”“班主任只能看本班”这种RBAC。权限只有两级admin超级管理员可操作所有数据和teacher普通教师只能审核自己班学生的申请。判断逻辑写死在LeaveControlleraudit里if (session(role) teacher $student-class_id ! session(class_id)) { abort(403); }。简单粗暴维护成本趋近于零。不做移动端适配所有页面按1366×768分辨率设计教务处电脑屏幕就是这个尺寸。强行做响应式会让打印预览错乱——这是血泪教训。去年某校用自适应模板老师在iPad上点“打印”结果假条只打了左半边重印三次才搞定。这种克制不是偷懒而是把开发精力全砸在核心链路上学生填表→生成假条→老师审核→更新统计。每个环节的耗时控制在1.5秒内这才是教务老师真正需要的“快”。3. 核心功能实现详解从假条模板到出勤统计的硬核细节3.1 假条打印功能如何让HTML模板100%适配针式打印机假条打印是本系统最受好评的功能也是最难搞的部分。教务处用的是得实DL-620针式打印机纸张是190mm宽的三联复写纸要求假条内容严格居中、字体大小固定、页边距精确到毫米。很多人用window.print()结果打印出来左边空2cm右边挤出纸外。本系统的解法是放弃浏览器打印用PHP生成PDF再下载。技术栈mpdf 8.0.13兼容PHP 7.2 自定义CSS。关键不在代码多炫而在三个魔鬼细节第一纸张尺寸与边距的毫米级校准PrintController.php里初始化mpdf$mpdf new \Mpdf\Mpdf([ mode utf-8, format [190, 270], // 宽190mm高270mmA4是210×297这里窄20mm适配针打 margin_left 15, // 左边距15mm留装订孔 margin_right 15, // 右边距15mm margin_top 25, // 上边距25mm留抬头 margin_bottom 20, // 下边距20mm setAutoTopMargin stretch, // 内容超长时自动撑开 ]);注意format不是A4而是手动计算的[190, 270]。为什么是270因为针式打印机进纸有误差A4高度297mm会导致最后一行被切掉270mm留出安全边距。第二字体嵌入与中文渲染默认mpdf的simsum.ttf在Linux服务器上常找不到。解决方案把simsum.ttf宋体放进public/fonts/目录在配置中指定$mpdf-SetFont(simsum, , 12); $mpdf-SetDefaultFont(simsum);并在CSS里强制body { font-family: simsum, sans-serif; font-size: 12pt; } h1 { font-size: 16pt; font-weight: bold; }pt磅比px更精准12pt4.23mm确保每行字数固定为38个汉字190mm÷5mm/字≈38字。第三动态水印与防伪假条右下角要有半透明“教务处专用”水印且不能被截图篡改。mpdf提供SetWatermarkText但默认是深色针打出来像墨团。改成$mpdf-SetWatermarkText(教务处专用); $mpdf-showWatermarkText true; $mpdf-watermark_font simsum; $mpdf-watermark_text_alpha 0.1; // 透明度0.1几乎看不见但复印时显现 $mpdf-watermark_font_size 48; // 字体够大复印后清晰最终生成的PDF用Acrobat打开检查属性页面大小190×270mm字体嵌入率100%无缺失字形。教务老师下载后双击Adobe Reader自动全屏点打印选“实际大小”非“适应页面”一气呵成。我试过37种打印机驱动只有得实DL-620和爱普生LQ-630K能100%还原其他型号需微调margin参数——这些参数都写在config/print.php里方便二次开发时覆盖。3.2 班级出勤统计一条SQL搞定日/周/月趋势分析出勤统计不是简单求和而是要回答教务处的三个问题① “今天哪个班缺勤最多”日维度② “上周各班病假占比多少”周维度③ “本月年级整体请假率是否超5%”月维度传统做法是写三个Controller方法各查一遍数据库。本系统用单表聚合动态时间分组核心SQL在StatModel.php里SELECT c.name AS class_name, COUNT(*) AS total_leave, COUNT(CASE WHEN l.reason LIKE %病% THEN 1 END) AS sick_count, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM student s WHERE s.class_id c.id), 2) AS leave_rate FROM leave_apply l JOIN student st ON l.student_id st.id JOIN class c ON st.class_id c.id WHERE l.status 1 AND l.created_at :start_date AND l.created_at :end_date GROUP BY c.id, c.name ORDER BY total_leave DESC关键在:start_date和:end_date的动态计算。StatControllerdaily里$today date(Y-m-d); $start $today; $end $today; // 周统计周一到周日 $week_start date(Y-m-d, strtotime(this week Monday)); $week_end date(Y-m-d, strtotime(this week Sunday)); // 月统计当月1号到今天 $month_start date(Y-m-01); $month_end date(Y-m-d);传参时用PDO预处理杜绝SQL注入。但真正的难点是性能。当全校有5000学生、两年请假记录约3万条时上述SQL在MySQL 5.7上执行要2.3秒。优化方案有三加复合索引ALTER TABLE leave_apply ADD INDEX idx_status_time (status, created_at);让WHERE条件走索引。物化统计表每天凌晨2点用think schedule:run执行定时任务把当日统计结果写入stat_daily表前台查stat_daily而非实时计算。前端缓存/admin/stat/class?date_typeweek接口加Cache-Control: public, max-age3600教务老师刷页面不重算。最终效果日统计响应200ms周统计400ms月统计800ms。教务主任说“以前查数据要等咖啡泡好现在点开就出来。”3.3 三级信息维护如何让年级/班级/学生增删改查不崩链路三级数据维护看似简单实则暗藏地雷。比如“删除年级”操作如果只删grade表class表里grade_id变成脏数据后续统计时报错。本系统用事务钩子函数双重保险。以GradeControllerdestroy为例public function destroy($id) { Db::startTrans(); // 开启事务 try { // 1. 先查该年级下是否有班级 $classes Db::name(class)-where(grade_id, $id)-select(); if (!empty($classes)) { throw new Exception(该年级下存在班级无法删除); } // 2. 删年级 Db::name(grade)-delete($id); Db::commit(); // 提交事务 $this-success(删除成功); } catch (\Exception $e) { Db::rollback(); // 回滚 $this-error($e-getMessage()); } }但事务只能保数据一致性UI体验还得优化。比如“编辑班级”时年级下拉框要实时显示当前年级列表且选中后班级列表联动刷新。前端用原生JS不用Vueselect idgrade_id onchangeloadClasses(this.value) {volist namegrades idg} option value{$g.id} {if condition$class.grade_id eq $g.id}selected{/if}{$g.name}/option {/volist} /select select idclass_list !-- 初始为空由JS填充 -- /select script function loadClasses(gradeId) { fetch(/admin/class/list?grade_id gradeId) .then(r r.json()) .then(data { const select document.getElementById(class_list); select.innerHTML ; data.forEach(c { const opt document.createElement(option); opt.value c.id; opt.text c.name; select.appendChild(opt); }); }); } /script后端ClassControllerlist只返回JSON轻量高效。最绝的是学生导入功能。教务处常给Excel名单要求批量录入。StudentControllerimport接收Excel文件用phpoffice/phpspreadsheet解析关键校验逻辑- 学号重复查student.student_no重复则跳过并记日志。- 班级不存在查class.id不存在则标记“班级未创建”不报错。- 学号格式错误正则/^\d{8}$/不符则标记“学号格式错误”。导入结果生成HTML报告页红色标异常行绿色标成功行教务老师一眼看清哪几行要手动补。这比弹窗提示“导入失败”有用十倍。4. 实操部署与二次开发指南从零到上线的完整路径4.1 服务器环境搭建CentOS 6.9 PHP 7.2 的避坑清单别信“一键安装脚本”校园服务器环境特殊必须手动配。以下是我在三所学校部署时总结的 checklistPHP扩展必装-php-gd图像处理生成验证码-php-mbstring多字节字符串中文截取不乱码-php-xml解析XML配置ThinkPHP部分功能依赖-php-zip解压资源包composer install需要-php-opcache开启OPcache性能提升40%关键配置修改/etc/php.ini; 上传文件限制教务处常传病假证明PDF upload_max_filesize 10M post_max_size 12M ; 时区必须设否则date()返回UTC时间 date.timezone Asia/Shanghai ; OPcache启用PHP 7.2默认关闭 opcache.enable1 opcache.memory_consumption128 opcache.max_accelerated_files4000 ; ThinkPHP日志路径指向runtime目录 error_log /var/www/html/runtime/log/php_error.logApache配置要点/etc/httpd/conf/httpd.conf# 禁用目录浏览安全第一 Options -Indexes # ThinkPHP伪静态关键否则/admin/xxx路由404 IfModule mod_rewrite.c Options FollowSymlinks -Multiviews RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] /IfModule # runtime目录必须可写 Directory /var/www/html/runtime Require all granted AllowOverride None /Directory最大坑SELinux。CentOS默认开启会导致runtime目录写入失败错误日志里只显示“Permission denied”实际是SELinux阻止。解决方案# 查看SELinux状态 sestatus # 临时关闭调试用 setenforce 0 # 永久关闭生产环境推荐 sed -i s/SELINUXenforcing/SELINUXdisabled/g /etc/selinux/config reboot部署后验证访问http://your-server/index.php应显示ThinkPHP欢迎页访问http://your-server/admin/login应显示登录页上传一个1MB的PDF测试附件功能。三步全过才算环境OK。4.2 二次开发实战如何快速添加“请假原因分析”图表假设学校要求增加“近三个月请假原因分布饼图”教务处想看病假、事假、公假占比。这不是改几个CSS的事得动数据链路。步骤1扩展数据库在leave_apply表加reason_type字段tinyint0病假1事假2公假3其他。用ALTER TABLE leave_apply ADD COLUMN reason_type TINYINT DEFAULT 3 AFTER reason;。步骤2修改提交逻辑LeaveControllersubmit里根据reason关键词自动判别类型$reason input(reason); $reason_type 3; if (stripos($reason, 病) ! false || stripos($reason, 发烧) ! false) { $reason_type 0; } elseif (stripos($reason, 事) ! false || stripos($reason, 家) ! false) { $reason_type 1; } elseif (stripos($reason, 公) ! false || stripos($reason, 会议) ! false) { $reason_type 2; } Db::name(leave_apply)-insert([ student_id $student_id, reason $reason, reason_type $reason_type, // ...其他字段 ]);步骤3新增统计接口StatControllerreasonChartpublic function reasonChart() { $start date(Y-m-d, strtotime(-3 months)); $data Db::name(leave_apply) -field(reason_type, COUNT(*) as count) -where(status, 1) -where(created_at, , $start) -group(reason_type) -select(); $types [病假, 事假, 公假, 其他]; $result []; foreach ($data as $row) { $result[] [ name $types[$row[reason_type]] ?? 其他, value (int)$row[count] ]; } return json($result); }步骤4前端集成ECharts在Tpl/admin/stat/chart.html里引入EChartsscript src/assets/echarts.min.js/script div idreason-chart stylewidth: 600px;height:400px;/div script fetch(/admin/stat/reasonChart) .then(r r.json()) .then(data { const chart echarts.init(document.getElementById(reason-chart)); chart.setOption({ tooltip: { trigger: item }, series: [{ type: pie, data: data, radius: 50% }] }); }); /script整个过程不到1小时新增功能无缝融入现有系统。关键是所有改动都遵循原有风格不引入新框架不改路由规则不碰核心Model只在Controller和View层扩展。这才是可持续的二次开发。5. 常见问题与排查技巧实录教务老师反馈的TOP10问题5.1 打印假条时中文乱码PDF里显示方块现象下载的PDF打开后姓名、事由显示为□□□。排查路径1. 检查public/fonts/simsum.ttf是否存在且权限为644ls -l public/fonts/2. 检查config/print.php中fontDir路径是否正确应为__DIR__ . /../public/fonts/3. 在服务器终端执行php -r var_dump(file_exists(public/fonts/simsum.ttf));确认PHP进程能读取该文件4. 最后招用mpdf官方字体检测工具vendor/mpdf/mpdf/src/Utils/FontDetector.php生成字体报告确认simsum被识别为“TrueType”。根治方案在PrintController初始化mpdf前强制指定字体路径$mpdf new \Mpdf\Mpdf([ fontDir __DIR__ . /../../public/fonts/, fontData [ simsum [ R simsum.ttf, ] ], // ...其他配置 ]);5.2 班级出勤统计页面空白F12看Network返回500现象/admin/stat/class页面白屏Chrome开发者工具Network标签页显示500错误。快速定位1. 查runtime/log/下最新日期的sql_*.log找最近一条SQL执行时间2. 对应时间点的php_*.log里是否有PDOException3. 如果有大概率是idx_status_time索引缺失执行SHOW INDEX FROM leave_apply;确认索引存在。经验技巧在config/database.php里开启SQL日志params [ \PDO::ATTR_ERRMODE \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE \PDO::FETCH_ASSOC, ], log_sql true, // ThinkPHP 5.1特有自动记录慢SQL5.3 学生登录后看不到自己的请假记录现象学生账号登录进入“我的请假”页表格为空。真相学生角色没分配student_id到Session。检查LoginControllerlogin// 错误写法只存用户名 session(username, $user[username]); // 正确写法必须存student_id供后续查询 session(student_id, $user[id]); session(username, $user[username]);然后LeaveControllermyList里$student_id session(student_id); if (!$student_id) { $this-error(未登录或登录失效); } $list Db::name(leave_apply)-where(student_id, $student_id)-select();5.4 新增班级后在“新增请假”页选不到该班级现象管理员刚在后台新增班级学生提交请假时下拉框里没有新班级。原因前端班级列表是静态缓存的。Tpl/admin/leave/create.html里用{volist}循环$classes但$classes变量来自LeaveControllercreatepublic function create() { $classes Db::name(class)-select(); // 每次请求都查库没问题 $this-assign(classes, $classes); return $this-fetch(); }那为什么看不到因为class表里grade_id为空或不存在。检查新增班级时是否漏填“所属年级”。在ClassControllercreate里加校验$validate validate(Class); if (!$validate-check($data)) { $this-error($validate-getError()); } // 额外校验年级是否存在 if ($data[grade_id] !Db::name(grade)-find($data[grade_id])) { $this-error(所属年级不存在请先创建年级); }5.5 上传附件失败提示“上传文件过大”现象学生上传病假证明PDF提示“上传失败”。排查顺序1.php.ini里upload_max_filesize和post_max_size是否生效phpinfo()确认2. Apache的LimitRequestBody是否限制/etc/httpd/conf/httpd.conf里搜LimitRequestBody注释掉或设为03.runtime目录磁盘空间是否满df -h4. 最隐蔽的SELinux阻止上传。执行ausearch -m avc -ts recent | grep httpd如果有avc: denied { write } for ... scontextsystem_u:system_r:httpd_t:s0说明SELinux拦截执行setsebool -P httpd_can_network_connect 1。附高频问题速查表问题现象可能原因快速验证命令解决方案登录页验证码不显示GD扩展未安装php -m \| grep gdyum install php-gd/admin/404Apache伪静态未生效curl -I http://localhost/admin/login检查httpd.conf中mod_rewrite是否加载学生提交后假条PDF下载为空mpdf内存不足tail -n 20 runtime/log/php_error.log增加memory_limit 256M统计图表不渲染ECharts JS未加载浏览器Console是否报echarts is not defined检查Tpl/admin/stat/chart.html中script路径删除年级时报“外键约束”class表里仍有该年级班级SELECT * FROM class WHERE grade_id5;先删班级再删年级最后分享个小技巧教务处电脑常禁用JavaScript所以所有核心功能提交请假、审核、打印都必须支持无JS降级。比如“打印假条”按钮a href/admin/leave/print?id123 target_blank打印/a不依赖onclick事件。这才是真正为用户着想的设计。我在实际使用中发现系统最脆弱的环节不是代码而是人的操作习惯。比如教务老师习惯把密码写在便签纸上贴显示器边或者同时开十个浏览器标签页导致Session混乱。所以我在common.php里加了Session超时提醒if (session(?username) time() - session(last_access) 1800) { session(last_access, time()); $this-assign(timeout_notice, 您的会话即将超时请及时保存操作); }并在所有模板页底部显示小提示。这种细节才是让系统真正“好用”的关键。本文还有配套的精品资源点击获取简介基于ThinkPHP框架构建的校园请假管理后台学生提交申请时自动填充姓名、班级、学号等基础信息减少重复录入系统内置标准化假条模板支持一键生成并直接打印使用管理员可实时审核、通过或注销请假申请查看个人历史记录提供按日/周/月维度的班级出勤异常统计自动生成年级整体请假趋势图表支持年级、班级、学生三级数据的增删改查操作数据库结构完整含leave.sql附带清晰路由配置、启动文件及基础前端资源logo、favicon、首页入口等代码结构规范适合教学演示、课程设计或快速二次开发。本文还有配套的精品资源点击获取