1. 项目背景与需求拆解
1.1 实验室考勤的痛点到底在哪
先说结论:实验室考勤和公司上下班打卡完全是两码事。公司打卡考勤,基本就是"人到了、时间对了"就算过关,但实验室的考勤场景要复杂得多——你不仅要记录"谁来了",还要记录"谁什么时候走的""在哪个实验室""做了哪个实验""有没有通过安全准入",甚至还要处理临时请假、设备借用、助教排班这些乱七八糟的联动事项。
我接手这个项目的时候,实验室管理员的原始工作方式是:纸质签到表 + Excel 登记 + 月末手工汇总。听起来很落后,但真实情况就是这样。每逢月底,管理员要翻几十页签到记录,统计每个人的出勤天数、迟到次数、缺勤情况,再算成平时分,工作量大不说,还容易漏统计、算错数。
另一个让管理员头疼的问题是安全管理。很多实验设备是有操作风险的,不是所有学生都能随便进。以前的流程是学生签了到就进,管理员根本没法实时判断这个人有没有经过安全培训。出了安全事故,溯源也困难——纸质签到表上的字迹潦草,日期还可能填错。
所以这个项目的核心需求,说到底就是三件事:
- 把考勤记录从纸质搬到线上,学生用微信小程序扫码或定位签到,后台自动生成记录,月末自动出统计报表。
- 把准入规则和考勤绑定,只有通过了安全考试的学生才能签到进入对应的实验室。
- 给管理员一个清晰的后台,能在电脑上查看实时到岗情况、审批请假、导出统计数据。
1.2 为什么是微信小程序而不是App
这个决策其实没纠结太久。实验室的学生群体几乎人人都有微信,小程序"扫码即用、用完即走"的特性正好匹配高频但轻量的考勤场景。如果做成 App,学生要先下载安装、注册账号,光是这一步就会流失一多半使用者。而小程序只需要在微信里搜一下或者扫个码,整个签到过程不到十秒。
后台管理端选择 Web 页面,用 PHP 框架来写,主要考虑的是部署成本低、服务器要求不高、实验室自己的服务器就能带得动。后面我会详细讲 ThinkPHP 和 Laravel 在选型上的权衡,这里先不展开。
1.3 这个项目适合谁参考
如果你是计算机相关专业的毕业生,正在纠结毕业设计选题;或者你在高校实验室、培训机构做管理工作,想把手动考勤改成线上系统;再或者你只是想看一套完整的"微信小程序 + PHP 后端"实战案例——这篇文章都能给你点实际的东西。我会按真实的开发顺序来讲:从需求整理、技术选型,到数据库设计、接口开发,再到小程序端对接和上线部署,最后附带我自己踩过的坑。
2. 技术选型:ThinkPHP 和 Laravel 的权衡实录
2.1 同一套系统为什么纠结两个框架
标题里同时出现了 ThinkPHP 和 Laravel,可能有人会问:你到底用哪个?实话说,我前期用 Laravel 写了一版后台原型,中期因为部署环境和团队熟悉度的原因,又用 ThinkPHP 重写了核心模块。最后交付的版本是以 ThinkPHP 为主干、参考了 Laravel 的一些设计思路。这个经历本身挺有代表性,值得展开说说。
先说说两个框架在我实际使用中的直观感受。
Laravel 的优势在于工匠精神——Composer 依赖管理规范,Eloquent ORM 非常优雅,写关联查询几乎不用拼 SQL,中间件、事件监听、队列这些基础设施开箱即用。尤其适合从零搭建、结构清晰、后续可能要持续迭代的项目。但它的缺点也明显:对服务器环境要求偏高(至少 PHP 7.4+ 加上一堆扩展),首次加载性能一般,在不做优化的情况下,并发稍高就会出现响应变慢。对学生项目来说,还有一个隐形门槛:Laravel 的学习曲线比 ThinkPHP 陡峭,如果团队里有人只接触过原生 PHP 或者 ThinkPHP 3.x,让他直接上手 Laravel,前期沟通成本会很高。
ThinkPHP 的优劣势正好反过来。它对 PHP 版本要求宽松,部署在普通的 Apache/Nginx + PHP 5.6 环境上都能跑;上手快,文档是中文的,遇到问题搜索引擎能捞到大量现成答案;单库查询性能在同量级下优于 Laravel 默认配置,尤其适合中小型管理系统这种"读多写少、表结构稳定"的场景。缺点就是架构思想上相对传统,比如 ORM 的关联处理、依赖注入、容器这些概念不像 Laravel 那么彻底,写复杂业务时要自己多写一点代码。
2.2 我的选型决策逻辑
选型这件事不能单看框架本身,要结合项目实际情况。我的决策逻辑有三个维度:
- 部署环境:实验室的服务器是一台用了五年多的老机器,操作系统是 CentOS 6,PHP 版本 5.6。装 Laravel 理论上可以,但需要升级 PHP、装扩展、改虚拟主机配置,动到服务器的系统环境就有风险。而 ThinkPHP 5.x 对 PHP 5.6 的兼容性很好,几乎是解压就能跑,部署成本最低。
- 团队成员:参与开发的两位同学一位熟 ThinkPHP,一位半路出家。如果选 Laravel,至少要给他们一周的框架学习缓冲期。选 ThinkPHP,第二天就能开始写业务代码。
- 业务复杂度:实验室考勤系统的核心表大概七八张,关联关系不算深,事务处理也不复杂,ThinkPHP 的模型层够用。Laravel 的强大功能在这个量级下属于"杀鸡用牛刀",反而徒增维护成本。
这里给一个实用建议:如果你做的是毕设,选框架之前先查清楚你们学校实验室或答辩老师普遍熟悉哪个框架。很多老师对 Laravel 的认可度比较高(因为它更"现代化"),但如果你能用 ThinkPHP 把系统做得扎实、逻辑清晰,同样能拿高分。关键是你能讲清楚为什么选它,而不是无脑跟风。
2.3 两套框架混合开发的一个稳妥姿势
如果你也面临"团队有人熟 TP 有人熟 Laravel"的尴尬,我提供一个折中方案:用 ThinkPHP 做主框架,但把代码结构写得"Laravel 化"——控制器尽量薄,业务逻辑抽到 Service 层,数据操作走模型层,配置放到独立文件。这样既享受了 TP 的部署便利,又保留了 Laravel 式分层带来的可维护性。后面重构时,如果非要切到 Laravel,Service 层和模型层的迁移成本会低很多。
3. 系统整体设计与数据库建模
3.1 角色权限模型怎么定
考勤系统涉及三类角色:学生、管理员(实验室老师或助教)、系统维护员(可能就是你自己)。我没有单独建角色表,而是用type字段在用户表里区分,简单直接。原因很简单:角色不多、职能明确,没必要引入 RBAC 那套复杂的权限管理。如果未来要扩展成校级平台、增加多角色多权限,再升级不迟。
用户表的角色字段我设计了四个值:
- 0:学生(默认角色,通过小程序注册后就是这个)
- 1:普通管理员(可以审批请假、查看考勤记录、导出统计)
- 2:超级管理员(在普通管理员基础上,还可以管理用户、管理实验室设备)
- 3:系统维护员(数据库和系统配置层面的操作权限,实际开发调试用)
3.2 数据库表设计详解
整套系统一共设计了 11 张表,核心的几张我列一下:
用户表(user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PK | 用户ID |
| openid | varchar(64) | 微信小程序登录凭证,唯一索引 |
| nickname | varchar(50) | 微信昵称 |
| real_name | varchar(20) | 真实姓名,必须实名登记 |
| student_no | varchar(20) | 学号,考勤统计用 |
| phone | varchar(20) | 手机号,从小程序端获取 |
| avatar | varchar(255) | 头像URL |
| type | tinyint | 角色类型 |
| is_safety_passed | tinyint | 是否通过安全考试 |
| create_time | datetime | 注册时间 |
实验室表(lab)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PK | 实验室ID |
| lab_name | varchar(50) | 实验室名称 |
| lab_code | varchar(20) | 实验室编号,用于签到码 |
| location | varchar(255) | 物理位置描述 |
| latitude | decimal(10,7) | 纬度 |
| longitude | decimal(10,7) | 经度 |
| radius | int(11) | 考勤允许误差半径,单位米 |
| is_requires_safety | tinyint | 进入是否需要安全准入 |
| status | tinyint | 启用状态 |
考勤记录表(attendance)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PK | 自增主键 |
| user_id | int(11) | 学生用户ID |
| lab_id | int(11) | 实验室ID |
| type | tinyint | 1签到 2签退 |
| time | datetime | 打卡时间 |
| source | varchar(20) | 签到方式:扫码/手动 |
| status | tinyint | 1正常 2迟到 3早退 4异常 |
请假表(leave_request)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int(11) PK | 主键 |
| user_id | int(11) | 请假人 |
| lab_id | int(11) | 请假涉及的实验室 |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| reason | varchar(255) | 请假原因 |
| leave_type | tinyint | 1事假 2病假 3其他 |
| status | tinyint | 0待审批 1同意 2拒绝 |
| approve_user_id | int(11) | 审批人 |
| approve_time | datetime | 审批时间 |
| approve_note | varchar(255) | 审批意见 |
设计这 11 张表时我总结了三个经验:
- 每个表必须有主键且为自增 int,不要用 UUID 之类花里胡哨的东西——PHP 框架对自增主键的支持最友好,查询效率也最高。
- 时间字段统一用 datetime,别用 int 存时间戳。虽然 int 省空间,但查出来还得
date()转格式,而且写 SQL 调试时看一串数字完全不知道是哪天。 - 外键不要真的加物理外键约束,逻辑关联用索引维护就好。我之前加过物理外键,结果插入数据时动不动报错,调试排查还麻烦,后来全删了。
3.3 小程序端的功能模块划分
小程序端页面不多,核心是五个页面:
- 首页:展示当前日期、本周出勤统计、待处理事项(比如请假审批待办,仅管理员显示)、快捷签到入口。
- 签到页:核心功能页。进入后自动定位显示所在位置,点击签到或签退按钮,后台校验位置和时间,返回结果。
- 记录页:查看个人历史考勤记录,按月份筛选,迟到/早退用颜色标注。
- 请假页:提交请假申请,填写起止时间、原因、类型;管理员端在这里多一个审批列表。
- 我的页:个人信息展示、安全考试状态、实验室列表(管理员可维护)。
3.4 管理后台页面规划
后台用 Bootstrap 做了一套响应式界面,功能不复杂,但页面不少:
- 仪表盘:今日签到人数、当前实验室在线人数、请假待审数量
- 用户管理:学生列表、启用/禁用账号、重置安全状态
- 实验室管理:增删改查实验室,设置签到半径
- 考勤管理:多维筛选考勤记录,按日期/用户/实验室导出 Excel
- 请假管理:审批列表,一键通过/拒绝,填审批意见
- 统计报表:月度出勤统计表,按班级/个人生成报表
4. 核心功能模块实现与接口设计
4.1 微信登录与手机号获取
小程序端登录流程用的是微信官方推荐的wx.login+code2Session换 openid 的方式。这一步有个细节容易踩坑:openid 必须作为用户的唯一业务标识,不能把用户 id 直接暴露给小程序端。我见过有人把数据库自增 id 当用户标识传给前端,虽然能让代码写得省事,但安全隐患极大——别人可以通过枚举 id 刷别人的数据。
手机号获取这里,微信在 2023 年后把"通过wx.getPhoneNumber获取手机号"规则调整得严格了,必须是企业主体的小程序并且完成认证,个人主体的小程序无法使用这个接口。如果你也是个人开发者做毕设,调试时用"测试号 + 手动绑定手机号"的方案更可行:在小程序端登录后弹窗让用户手动填写手机号,后端做个验证码校验(用阿里云短信或腾讯云 SMS),虽然多一步操作,但能绕开企业认证的限制。
我当时的交互流程是:
- 小程序端
wx.login()拿到 code。 - 后端通过
code2Session换取 openid 和 session_key。 - 先查用户表,如果 openid 不存在,则自动创建一条"未完善信息"的用户记录。
- 前端引导用户填写真实姓名、学号、手机号,提交后更新用户信息。
- 信息完善后,进入首页开始正常使用。
这里要提醒一句:session_key 是敏感信息,绝不能下发给小程序端。它用来解密手机号等敏感数据,如果泄露,别人可以伪造你的身份,后端务必只保存 openid,session_key 用后即弃。
4.2 考勤签到的算法与防作弊
签到功能的两个核心参数是位置和时间。
位置校验用的是经纬度距离计算。小程序端通过wx.getLocation获取当前经纬度,传给后端,后端计算与实验室坐标的球面距离,在设定半径内才能打卡。球面距离公式我用的是 Haversine 公式:
PHP 代码实现如下:
function haversineDistance($lat1, $lng1, $lat2, $lng2) { $earthRadius = 6371000; // 地球半径,单位米 $latFrom = deg2rad($lat1); $latTo = deg2rad($lat2); $deltaLat = deg2rad($lat2 - $lat1); $deltaLng = deg2rad($lng2 - $lng1); $a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($latFrom) * cos($latTo) * sin($deltaLng / 2) * sin($deltaLng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return $earthRadius * $c; }默认签到半径设为 200 米,不能设太小,因为 Wi-Fi 定位和基站定位的误差常常有二三十米;也不能设太大,否则学生在隔壁宿舍都能签上。
时间校验的逻辑是:每个实验室可设置上午、下午、晚上三个考勤时段,迟到判定为晚于开始时间 10 分钟以上。我的实现方式是在实验室表里加一个time_slot_config字段,存 JSON,比如:
{ "morning": {"start": "08:00", "end": "12:00"}, "afternoon": {"start": "14:00", "end": "18:00"}, "evening": {"start": "19:00", "end": "22:00"} }签到时后端取当前时间,判断落在哪个时段内,对比开始时间判定是否迟到。
防作弊这块,我做了三个处理:
- 后端位置校验为主:不能信任前端传过来的经纬度直接入库,必须拿前端坐标去和实验室坐标算距离,防止有人改包或抓包伪造坐标。
- 签到频率限制:同一用户同一实验室同一时段只能签到一次,防止重复打卡。
- 异常签到标记:如果签到时定位距离过远(比如大于 2000 米),依然记录,但标记为"异常",由管理员后续人工确认。
4.3 请假审批的状态机设计
请假流程不复杂,但要处理的状态逻辑还不少。我设计的状态流转是:
待审批(0) → 同意(1) / 拒绝(2) (不可逆)很简单的三态模型,但因为要支持销假操作(学生提前结束请假),我在后面扩展了一个"已销假(3)"状态。实际效果是:学生请假两天,结果半天就处理完了,可以销假回来签到,避免被误判为缺勤。
审批时后端逻辑要注意权限校验:只有 type 为 1 或 2 的用户才能审批,普通学生提交的审批请求必须返回 403。这个权限判断写在 Service 层,而不是控制器层,防止在多个接口里重复编写导致遗漏。
4.4 统计报表的实现方案
考勤统计这块,我吃了不少亏,总结出相对好用的方案:
延迟计算 + 缓存比实时计算可靠得多。刚开始我用了"每次查询都动态汇总统计"的方式,数据库扛不住是一方面,逻辑也容易出 bug——比方说月末汇总时,如果有学生请假被修改状态,统计数字就对不上。后来我改成每天凌晨跑定时任务,生成当天的统计快照存进attendance_daily_summary表,查询时直接读快照,数据一致性和查询速度都有保障。
定时任务用 ThinkPHP 的命令行任务(php think crontab),核心代码如下:
// 伪代码示意 public function dailySummary() { $yesterday = date('Y-m-d', strtotime('-1 day')); $startTime = $yesterday . ' 00:00:00'; $endTime = $yesterday . ' 23:59:59'; // 按用户分组统计 $records = Db::name('attendance') ->where('time', 'between', [$startTime, $endTime]) ->where('type', 1) // 只统计签到 ->select(); // 聚合计算并写入 summary 表 // ... }导出 Excel 我用了 PHPExcel(对,就是已经不算新的那个库)。在 ThinkPHP 中引入很简单,用 Composer 装phpoffice/phpexcel,然后写一个导出方法。注意导出大文件时设置内存上限,否则 PHP 会直接崩掉,报Allowed memory size exhausted,这在处理几百人一个月的记录时真的会发生。
5. 微信小程序端的关键技术细节
5.1 顶部导航栏高度适配
微信小程序的顶部导航栏有一个问题:不同机型、不同系统(iOS/Android)的导航栏高度不一样。如果程序页面需要自定义头部,直接写死padding-top就会导致在部分手机上内容被遮挡。
通用做法是通过wx.getSystemInfoSync()获取状态栏高度,然后动态设置:
const systemInfo = wx.getSystemInfoSync(); const statusBarHeight = systemInfo.statusBarHeight; // 状态栏高度 const navBarHeight = 44; // 导航栏内容高度,iPhone X 以后是 44,其他机型 44 或 48 Page({ data: { statusBarHeight: statusBarHeight, navBarHeight: navBarHeight }, onLoad() { // 动态计算自定义导航栏总高度 const winWidth = wx.getSystemInfoSync().windowWidth; // 胶囊按钮位置(右上角胶囊) const menuButton = wx.getMenuButtonBoundingClientRect(); const navHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; this.setData({ statusBarHeight: statusBarHeight, navHeight: navHeight }); } });用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置,然后反推导航栏高度,这个方法目前我在各机型上测试都比较稳妥。注意在自定义导航栏的页面里,navigationStyle要设置为custom,否则默认导航栏和自定义导航栏会叠加,页面直接变两层。
5.2 微信小程序签到定位的坑
wx.getLocation接口有几点要特别注意:
- 要在
app.json里声明permission配置,否则首次调用会直接被系统拦截:
"permission": { "scope.userLocation": { "desc": "您的位置信息将用于实验室签到考勤" } }- 定位前要先检查用户授权状态,如果用户拒绝过授权,需要引导到设置页打开授权。我这里写了一个工具函数:
function checkAndGetLocation() { return new Promise((resolve, reject) => { wx.getSetting({ success(res) { if (res.authSetting['scope.userLocation']) { // 已授权,直接获取位置 wx.getLocation({ type: 'gcj02', // 国测局坐标,国内使用 success: resolve, fail: reject }); } else { // 未授权,调用授权 wx.authorize({ scope: 'scope.userLocation', success() { wx.getLocation({ type: 'gcj02', success: resolve, fail: reject }); }, fail() { // 引导去设置页 wx.showModal({ title: '提示', content: '需要位置权限才能签到,请在设置中开启', confirmText: '去设置', success(res) { if (res.confirm) { wx.openSetting(); } } }); reject(new Error('DENY')); } }); } }, fail: reject }); }); }- coordType 的选择:国内小程序要用
gcj02(国测局坐标),不要用wgs84(GPS 原生坐标)。因为微信提供的地图和多数国内地图服务都是基于 gcj02 加密偏移的,用 wgs84 会导致坐标偏移几百米。如果你存的是 wgs84,定位在实验室正门口却提示"距离过远",大概率就是坐标偏移问题。
5.3 微信小程序的单选框与表单
考勤记录页的筛选功能里用到了单选框,小程序原生radio-group的行为有点反直觉——它不支持通过value属性直接设置默认选中项,必须用checked标记,或者通过radio-group的bindchange事件动态设置。我写了一个简单方案:
<radio-group class="filter-group" bindchange="onFilterChange"> <label wx:for="{{filters}}" wx:key="value"> <radio value="{{item.value}}" checked="{{item.checked}}" color="#1aad19" /> <text>{{item.label}}</text> </label> </radio-group>data: { filters: [ { label: '全部记录', value: 'all', checked: true }, { label: '正常', value: 'normal', checked: false }, { label: '迟到', value: 'late', checked: false }, { label: '早退', value: 'early', checked: false }, { label: '异常', value: 'abnormal', checked: false } ] }, onFilterChange(e) { const val = e.detail.value; this.setData({ currentFilter: val, filters: this.data.filters.map(item => ({ ...item, checked: item.value === val })) }); // 重新请求记录列表 this.fetchRecords(val); }5.4 订阅消息与请假审批通知
请假审批结果需要通过微信订阅消息推送给学生。设置订阅消息时发现一个坑:一次性订阅模板用户每次都要授权,而长期订阅模板仅对特定场景开放(比如政务、医疗等),普通小程序没有长期订阅权限。
我的方案是:在用户提交请假申请时,前端弹窗请求订阅授权(复用同一个模板),后端在审批通过后发送一次性订阅消息。如果用户当时没授权,就退而求其次——小程序端提供一个"待办通知"列表,学生主动查看审批结果。实际用下来,大部分学生都会在提交请假的瞬间顺手点一下授权,所以效果还行。
发送订阅消息的后端实现(ThinkPHP):
use think\facade\Cache; public function sendLeaveNotice($userId, $result, $startTime) { $user = User::find($userId); $accessToken = Cache::get('wx_access_token'); if (!$accessToken) { $accessToken = $this->getAccessToken(); Cache::set('wx_access_token', $accessToken, 7000); } $url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=' . $accessToken; $data = [ 'touser' => $user['openid'], 'template_id' => '你的模板ID', 'page' => 'pages/index/index', 'data' => [ 'thing1' => ['value' => '实验室请假'], 'phrase2' => ['value' => $result], 'time3' => ['value' => $startTime] ] ]; // 发送 post 请求... }注意access_token一定要缓存起来,微信接口的access_token有效期只有 7200 秒,频繁获取会被微信限流,报45009错误。
6. 常见问题与排查技巧实录
6.1 微信登录偶发失败:code 只能用一次
这是我在调试小程序登录时遇到的第一个问题,现象是用户第一次登录正常,退出再登录就报"无效的 code"。查了半天发现是wx.login返回的 code 只能用一次,用完就作废。我之前误把 code 缓存了起来,第二次请求用的是缓存中的旧 code,所以报错。
正确做法是每次登录都重新调wx.login()获取新的 code,不要尝试复用。如果业务上需要"静默登录",可以封装一个ensureLogin方法,每次校验本地是否有 openid,没有或失效就重新调wx.login()。
6.2 定位偏差导致签到失败
有一个学生反馈"我就在实验室里面,却提示距离太远"。日志发现该学生的上报坐标和实验室坐标差了有 700 米。排查后原因是他手机系统开启了模糊定位,微信拿到的坐标是基站级别的粗精度,而非 GPS 精确定位。这个问题没法在小程序端强制用户关闭,只能在签到页加一个定位精度提示,检测到精度超过 100 米就提示"当前定位精度较低,请到窗边或空旷处重新定位",提高签到成功率。
6.3 小程序段号超过 2MB 无法预览
开发过程中往小程序里塞了不少图片,结果编译时提示代码包超过 2MB。这里我踩过的坑值得分享:别把图片放本地,必须放服务器或 CDN;首屏页面拆分包处理。
具体做法是:
- 大图全部换成压缩后的
webp或jpg格式,控制在 50KB 以内。 - 把非核心页面(实验室详情、历史记录、帮助中心)放进分包
subPackages。 - 公共组件用
lazyCodeLoading: "requiredComponents"配置,按需加载。
还有一个隐藏技巧:小程序开发工具里可以开启"上传时压缩代码",能再省一点空间。如果实在超了太多,检查project.config.cn里的minified设置是否开启。
6.4 并发打卡数据竞态问题
考勤签到接口在上午 8:00 到 8:10 之间会收到密集请求,如果两个人同时提交签到,可能插入两条记录。我的解决方式是在数据库层加了"用户+时段+类型"的唯一索引:
ALTER TABLE attendance ADD UNIQUE INDEX idx_user_time_type (user_id, time, type);同时后端在插入前用select ... for update做行锁,双保险。实测下来并发一百个请求也不会产生重复打卡。
6.5 服务器上传接口的坑:PHP 最大上传大小限制
学生上传头像和请假证明材料时,有时会报"上传失败"。排查后是 PHP 配置的upload_max_filesize默认只有 2M,改成 10M 后正常。如果你也遇到类似问题,修改php.ini后别忘重启 PHP-FPM,否则不生效。
6.6 小程序登录态失效问题
wx.login的 session_key 有效期不固定,如果用户长期不打开小程序,登录态会过期。我在用户表加了一个last_login_time,每次请求时检查session_key的存活时间,超过 48 小时就强制重新登录。方法是在后端的用户鉴权中间件里做判断,过期则返回特定的code: 401,小程序端捕获到这个状态码后,自动触发重新登录流程,用户无感。
7. 工具链与调试技巧
7.1 抓包工具在小程序联调中的妙用
开发小程序接口联调时,查接口返回数据前后端经常对不上,这里我强烈建议直接上抓包工具。PC 端微信小程序可以通过 Charles 或者 Reqable 抓包看到小程序发起的完整请求和响应体,排查"前端传的参数对没对""后端返回的结构是不是预期格式"非常直观。
具体配置不展开,简单说思路:手机和电脑连同一个局域网,手机设置 HTTP 代理到电脑的 IP 端口,安装抓包工具提供的 CA 证书就可以解密 HTTPS 流量。注意微信小程序在调试模式下可以开启开发者工具的网络面板,更方便,但如果要真机调试,抓 HTTPS 包几乎是唯一手段。
7.2 ThinkPHP 调试模式的正确打开方式
ThinkPHP 的调试模式很多人只是当开关用,没善用它的日志。我在config/app.php里把'app_trace' => true打开后,浏览器底部会出现一个调试工具栏,可以看到每次请求的 SQL 语句、执行时间和内存占用。开发阶段排查性能问题全靠它。
线上环境务必把'app_debug' => false,并且关闭 trace,否则暴露敏感信息是小,性能损耗是大。
7.3 定时任务和计划任务的调试
ThinkPHP 的定时任务在 Windows 开发机上没法跑,我用的方案是开发阶段手动触发一个debug参数,比如php think crontab --action=summary --date=2025-06-01,这样可以断点调试计算逻辑。上线后用系统 crontab 每分钟执行一次调度器,核心命令如下:
* * * * * cd /www/wwwroot/your-project && php think crontab >> /var/log/attendance_cron.log 2>&1注意给 PHP 设置足够的内存,统计大表数据时默认 128M 不够用,我设的是 512M。
8. 安全加固与上线部署
8.1 小程序接口防刷与鉴权
接口鉴权我用的是自定义 Token 方案:用户在登录成功后,后端生成一个token(用 JWT 或者简单一点用md5(openid + salt + time)),存缓存并返回给前端。前端每次请求在 header 里带Authorization: Bearer token,后端中间件统一校验。处理逻辑如下:
- 校验 token 是否存在且未过期
- 通过 token 找到对应用户 id 和角色
- 将用户信息挂到请求上下文,后续逻辑直接获取
8.2 后台管理端的登录保护
后台是独立于小程序的 Web 管理页面,我用的是会话登录。这里有个很关键的细节:管理后台的接口必须和小程序端接口分离鉴权,不能复用同一套 token 体系。防止有人从小程序端逆向出接口地址后直接调用管理员接口操作数据。
我的做法:管理后台单独用admin_user表,登录后生成一个独立的admin_token,存到服务端 session 或 Redis,所有后台接口统一校验这个 token,同时检测访问 IP 和客户端 UA。
8.3 服务器环境部署实录
实际部署时我用的环境是 Nginx + PHP-FPM + MySQL 5.7。上传代码到服务器后执行:
composer install --no-dev --optimize-autoloader php think migrate:run chmod -R 755 runtime chown -R www:www runtime/记得把静态资源(图片、上传文件)的目录权限改对,否则学生传不了头像。如果用了 Nginx,配置伪静态,让所有非静态文件请求走index.php入口:
location / { try_files $uri $uri/ /index.php?s=$uri&$args; }8.4 上线前检查清单
我整理了一份上线前必查清单,可以抄作业:
- 微信小程序后台:配置合法域名(request、uploadFile 必填),关闭"不校验合法域名"的调试选项
- 后台管理页面:修改默认管理员密码,删除安装目录或锁定安装文件
- 数据库:备份一次完整数据库,导出所有表结构
- PHP 配置:检查
display_errors关闭,log_errors开启,upload_max_filesize加大 - 定时任务:用
crontab -l确认调度器在跑,手动执行一次统计任务确认数据落表 - 日志清理:runtime 日志目录写个清理脚本,防止长时间运行磁盘满
9. 从开发到交给管理员使用,还有哪些事没做完
到这里项目的核心开发基本完成了。但我必须说,一个系统从"能跑"到"真好用"之间,还有一段很长的路。这段路主要不是代码,而是跟实际使用者之间的磨合。
我给实验室管理员验收的时候,管理员提了三个需求,都是我最初没想到的:
第一个是批量导入学生名单。管理员有全校学生的 Excel 花名册,他不想一个一个在小程序里注册。我后来加了一个"导入名单"按钮,上传 Excel 后按学号匹配,一键生成待激活账号,学生首次登录时绑定 openid 即可。
第二个是迟到分时段统计。管理员需要知道哪些学生经常在迟到边缘(比如 8:09 打卡),他想单独发消息提醒这些学生。我加了一个"迟到预警"报表,按最近一个月迟到次数排序。
第三个是导出格式的定制。管理员想要导出的 Excel 里能直接看到"第几周到第几周"的出勤汇总,还要能打上"学院"和"专业"筛选。这个改动说大不大,说小不小,核心是给用户表加了college和major字段,然后统计 SQL 里加了分组维度。
这些需求让我明白一件事:做系统不是写完代码就结束,而是要在使用中不断迭代。尤其是这种面向管理员的内部系统,最终评价标准是管理员觉得顺不顺手,而不是功能列表有多长。
最后说一个我个人的体会:技术选型上的纠结(ThinkPHP 还是 Laravel)其实远没有想象中重要,真正决定项目成败的是需求调研够不够细、数据模型清晰不清晰、联调和测试到不到位。我在这个项目里因为前期需求没聊透,中途加了好几次功能,返工了不少代码。如果你也在做类似系统,建议把第一步花在跟管理员坐在一起看他怎么工作上,这份时间投入一定物超所值。