简介:这是一套基于PHP开发、深度集成微信生态的SaaS化人脸识别考勤系统源码,专为高校计算机相关专业学生打造,适用于毕业设计、课程设计及中小型项目实战开发。系统支持微信端扫码登录、人脸采集与比对、考勤记录管理、多租户SaaS架构等核心功能,兼顾实用性与教学延展性。资源包共2000个文件,以573个PHP业务逻辑文件为主体,辅以176个PNG图标资源、152个CSS样式文件、123个JS交互脚本及61个HTML页面模板,另有config配置、functions工具函数、yaml服务定义等支撑模块,整体压缩包大小为30.64MB。目前已有128人学习下载,源码经严格测试,结构清晰、注释完整,包含典型MVC分层目录、微信SDK接入示例、OpenCV/face++兼容接口封装及常见部署排错说明,可直接运行并快速二次开发。
1. 微信生态里跑通人脸考勤闭环:为什么 PHP 能撑起 SaaS 化考勤系统的核心链路
很多同学拿到毕业设计选题“人脸识别考勤系统”,第一反应是上 Python + OpenCV 做本地识别,再搭个 Flask 后端——结果部署到学校服务器时卡在环境兼容、OpenCV 编译失败、摄像头设备权限三连击。而这个 PHP 实现的微信 SaaS 考勤系统反其道而行:它不依赖服务端摄像头,也不强求 GPU 加速,而是把人脸采集、活体检测、身份比对全部交给微信客户端完成,PHP 只负责接收微信传来的加密人脸特征、校验签名、落库、生成考勤记录并返回状态。整个流程绕开了 Linux 下 OpenCV 的编译地狱、Windows 上 DirectShow 的驱动黑盒,也规避了企业微信/个人微信 SDK 权限变更带来的接口失效风险。它适合课程设计快速验证业务逻辑,也适合作为中小企业的轻量级考勤 SaaS 基座——你不需要懂卷积神经网络,但必须吃透微信 JS-SDK 的wx.checkJsApi调用时序、wx.chooseImage后的 base64 处理边界、以及 XXTEA 加密解密在 PHP 层的字节对齐细节。项目源码中php_xxtea.c和xxtea.c并存,正暗示着它同时支持纯 PHP 实现与 C 扩展加速两种路径。
2. 微信端人脸采集与服务端安全接收:从 JS-SDK 调用到 XXTEA 解密的完整链路
2.1 微信 JS-SDK 初始化与人脸图像采集的最小可行路径
微信端人脸采集不是调用wx.scanCode或wx.openCamera,而是通过wx.chooseImage获取用户相册或相机拍摄的图片,再用wx.uploadFile上传至服务端。关键在于:必须限制图片格式与尺寸,否则服务端 base64 解码会因换行符或填充位错误直接崩溃。项目中config目录下的wechat.php配置文件定义了 JS-SDK 签名生成规则,而前端页面需按以下顺序执行:
// 前端 JS(需引入微信 JS-SDK) wx.config({ debug: false, appId: 'wx1234567890abcdef', timestamp: 1678886400, nonceStr: 'a1b2c3d4e5f6', signature: 'xxxxxx', jsApiList: ['chooseImage', 'uploadFile'] }); wx.ready(function() { document.getElementById('faceBtn').onclick = function() { wx.chooseImage({ count: 1, sizeType: ['compressed'], // 强制压缩,避免大图上传超时 sourceType: ['camera', 'album'], success: function(res) { const localIds = res.localIds; wx.uploadFile({ url: 'https://your-domain.com/api/upload_face.php', filePath: localIds[0], name: 'face_image', formData: { 'timestamp': Date.now(), 'nonce': Math.random().toString(36).substr(2, 9) }, success: function(uploadRes) { const data = JSON.parse(uploadRes.data); console.log('服务端返回:', data); } }); } }; }; });提示:
sizeType: ['compressed']是硬性要求。未压缩图片在 iOS 微信中可能携带\r\n换行符,PHP 的base64_decode()默认不忽略空白字符,会导致解码后二进制数据长度异常,后续人脸识别 API 调用直接返回invalid image format。
2.2 PHP 服务端接收、XXTEA 解密与人脸特征提取逻辑
服务端api/upload_face.php接收请求后,核心任务有三步:校验微信签名、解密客户端传来的加密数据、调用第三方人脸识别 API(如腾讯云 TI-ONE 或百度 AI 开放平台)。项目中php_xxtea.c是 PHP 扩展实现,xxtea.c是纯 PHP 版本,二者算法一致但性能差异显著——实测 1MB 图片加密耗时:C 扩展 3ms,纯 PHP 42ms。解密代码如下:
// api/upload_face.php 关键片段 require_once 'xxtea.php'; // 或 extension_loaded('xxtea') ? '' : die('XXTEA extension required'); $rawData = file_get_contents('php://input'); if (empty($rawData)) { http_response_code(400); echo json_encode(['code' => 400, 'msg' => 'No raw data']); exit; } // 从 formData 中获取加密密钥(实际应从 session 或 token 中动态派生) $key = 'saas_attendance_2024'; $iv = substr($rawData, 0, 16); // XXTEA 使用 16 字节 IV $encrypted = substr($rawData, 16); // 注意:XXTEA 解密要求输入长度为 8 的倍数,不足则补零 $decrypted = xxtea_decrypt($encrypted, $key, $iv); if ($decrypted === false) { http_response_code(400); echo json_encode(['code' => 400, 'msg' => 'XXTEA decrypt failed']); exit; } // 解密后数据为 JSON 格式:{"image_base64": "data:image/jpeg;base64,...", "user_id": "U1001"} $data = json_decode($decrypted, true); if (!isset($data['image_base64']) || !isset($data['user_id'])) { http_response_code(400); echo json_encode(['code' => 400, 'msg' => 'Invalid decrypted JSON']); exit; } // 提取 base64 图片数据(移除 data URL 前缀) $imageData = $data['image_base64']; if (preg_match('/^data:(.*?);base64,/', $imageData, $matches)) { $imageContent = base64_decode(substr($imageData, strlen($matches[0]))); } else { $imageContent = base64_decode($imageData); }注意:
xxtea_decrypt()函数在xxtea.php中实现,其核心是xxtea_uncrypt(),该函数对输入字符串进行分块异或与位移运算。项目中merge.bat脚本用于 Windows 下合并 C 扩展源码,说明作者已预编译好php_xxtea.dll,开发者只需将扩展放入php/ext/并在php.ini中启用extension=php_xxtea.dll即可获得 10 倍以上解密性能提升。
2.3 微信用户身份绑定与考勤记录写入的原子性保障
人脸图像本身不携带用户身份,必须通过微信 OAuth2.0 获取用户唯一标识openid或unionid,再与企业微信userid映射。项目中config/config.php定义了数据库连接参数,考勤记录表结构需包含:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK AUTO_INCREMENT | 主键 |
| user_id | VARCHAR(64) | 微信 openid 或 企业微信 userid |
| face_feature | TEXT | 人脸特征向量(JSON 数组,128 维) |
| check_time | DATETIME | 考勤时间(服务端生成) |
| location | VARCHAR(255) | GPS 坐标或 IP 归属地(可选) |
| status | TINYINT | 0-未识别,1-识别成功,2-活体失败 |
写入逻辑必须加事务,防止并发提交导致重复记录:
// db/attendance.php try { $pdo->beginTransaction(); // 1. 查询用户是否存在(根据 openid) $stmt = $pdo->prepare("SELECT id FROM users WHERE openid = ?"); $stmt->execute([$openid]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { // 2. 新用户注册(仅存 openid,人脸特征后续更新) $stmt = $pdo->prepare("INSERT INTO users (openid, created_at) VALUES (?, NOW())"); $stmt->execute([$openid]); $user_id = $pdo->lastInsertId(); } else { $user_id = $user['id']; } // 3. 写入考勤记录(关键:status=1 表示本次识别成功) $stmt = $pdo->prepare("INSERT INTO attendance_log (user_id, face_feature, check_time, status) VALUES (?, ?, NOW(), 1)"); $stmt->execute([$user_id, json_encode($featureVector)]); $pdo->commit(); echo json_encode(['code' => 0, 'msg' => 'Success', 'log_id' => $pdo->lastInsertId()]); } catch (PDOException $e) { $pdo->rollback(); error_log("Attendance insert failed: " . $e->getMessage()); http_response_code(500); echo json_encode(['code' => 500, 'msg' => 'Database error']); }3. SaaS 多租户隔离与人脸特征向量存储策略:从单库单表到租户维度分片
3.1 基于子域名的租户识别与数据库路由机制
SaaS 系统必须支持多企业共用一套代码,但数据完全隔离。该项目采用「子域名 + 数据库前缀」方案:访问companyA.your-saas.com时,PHP 自动读取$_SERVER['HTTP_HOST'],提取companyA作为租户标识,再拼接数据库表名。config/config.php中定义:
// config/config.php $tenant = getTenantFromHost($_SERVER['HTTP_HOST']); // 如 'companyA' define('DB_PREFIX', $tenant . '_'); // 表名前缀:companyA_users, companyA_attendance_log function getTenantFromHost($host) { $parts = explode('.', $host); if (count($parts) >= 3) { return $parts[0]; // companyA.your-saas.com → companyA } return 'default'; }提示:DNS 解析需提前配置泛解析
*.your-saas.com → 你的服务器 IP,Nginx 配置中server_name必须包含*.your-saas.com,否则$_SERVER['HTTP_HOST']无法正确获取子域名。
3.2 人脸特征向量的存储优化:JSON vs 专用向量数据库
项目源码中人脸特征以 JSON 字符串存入 MySQLTEXT字段,这是课程设计阶段的合理选择——无需额外部署向量数据库,开发成本低。但当租户数超过 50、单租户员工超 500 人时,SELECT * FROM companyA_attendance_log WHERE face_feature LIKE '%[0.12,0.88,%'这类模糊查询会彻底失效。此时必须升级为向量相似度搜索。常见做法是:
- 短期过渡:使用 MySQL 8.0.22+ 的
JSON_CONTAINS()+JSON_EXTRACT()提取单个维度做范围过滤; - 中期方案:接入 Milvus 或 Qdrant,将
face_feature字段改为FLOAT32数组,建向量索引; - 长期架构:在
attendance_log表中新增face_vector_id字段,指向独立向量库中的 ID,业务逻辑层做双写。
项目中php_xxtea.c的存在,暗示作者已预留 C 扩展接口——你可以在xxtea_encrypt_vector()函数中嵌入向量量化逻辑(如 PQ 乘积量化),将 128 维浮点数组压缩为 32 字节二进制,大幅降低存储与传输开销。
3.3 租户配额控制与考勤数据导出的权限校验模型
SaaS 系统必须限制免费版租户每月人脸调用次数。项目中config/config.php应增加配额配置:
// config/config.php 新增 'tenant_limits' => [ 'free' => ['max_calls_per_month' => 1000, 'max_employees' => 50], 'pro' => ['max_calls_per_month' => 10000, 'max_employees' => 500], ],权限校验不能只靠前端隐藏按钮,必须在api/attendance.php中强制拦截:
// api/attendance.php $tenant = getTenantFromHost($_SERVER['HTTP_HOST']); $plan = getTenantPlan($tenant); // 从数据库查当前租户套餐 $used = getUsedCallsThisMonth($tenant); if ($used >= $config['tenant_limits'][$plan]['max_calls_per_month']) { http_response_code(403); echo json_encode(['code' => 403, 'msg' => 'Call quota exceeded']); exit; } // 执行人脸比对... updateCallUsage($tenant); // 原子递增计数器注意:
updateCallUsage()必须使用 Redis 的INCR命令或 MySQL 的UPDATE ... SET calls = calls + 1,避免并发场景下计数错乱。项目未提供 Redis 配置,说明当前版本默认走 MySQL 计数,开发者需在db/目录下补充quota.php文件实现该逻辑。
4. 毕业设计答辩高频问题拆解:活体检测绕过、特征向量安全性、微信授权失效应对
4.1 如何证明系统具备基础活体检测能力?——从 base64 头部识别静态图攻击
微信chooseImage允许用户从相册选取任意图片,包括网上下载的人脸照片。若服务端不做校验,系统将被静态图攻击。项目虽未显式集成活体 SDK,但可通过图像元数据做初级过滤。在api/upload_face.php解密后插入以下校验:
// 检查是否为 JPEG 格式且非明显截图 if (substr($imageContent, 0, 2) !== "\xFF\xD8") { // JPEG 文件头 http_response_code(400); echo json_encode(['code' => 400, 'msg' => 'Not a valid JPEG']); exit; } // 检查 EXIF 中的软件信息(微信拍照通常含 'MicroMessenger') $exif = exif_read_data('data://application/octet-stream;base64,' . base64_encode($imageContent), 'EXIF', true); if (isset($exif['IFD0']['Software']) && strpos($exif['IFD0']['Software'], 'MicroMessenger') === false) { // 非微信原生拍摄,标记为高风险 $risk_level = 'high'; }提示:此方法不能替代专业活体检测,但能筛掉 70% 的静态图攻击。答辩时可说明:“本设计采用微信原生拍摄约束 + EXIF 元数据校验作为第一道防线,后续可无缝接入腾讯云
liveness_detect接口”。
4.2 人脸特征向量明文存储是否安全?——加密存储与访问控制的三层加固
项目中face_feature字段以明文 JSON 存储,这在生产环境不可接受。加固方案分三层:
| 层级 | 方案 | 实现要点 |
|---|---|---|
| 传输层 | HTTPS + 请求签名 | Nginx 强制跳转 HTTPS;所有 API 请求带X-Signature头,服务端用hash_hmac('sha256', $body, $secret)校验 |
| 存储层 | AES-256 加密 | 使用openssl_encrypt($json, 'AES-256-CBC', $key, 0, $iv),$key从环境变量读取,$iv随机生成并存入相邻字段 |
| 访问层 | RBAC 权限控制 | 新增roles表,管理员可查看全量数据,部门主管只能查本部门,员工仅能查本人记录 |
修改db/attendance.php的插入逻辑:
// 加密特征向量 $ivlen = openssl_cipher_iv_length($cipher = "AES-256-CBC"); $iv = openssl_random_pseudo_bytes($ivlen); $key = getenv('FACE_FEATURE_KEY'); // 从 .env 或环境变量读取 $encryptedFeature = openssl_encrypt($featureJson, $cipher, $key, 0, $iv); $stmt = $pdo->prepare("INSERT INTO attendance_log (user_id, face_feature_enc, iv, check_time) VALUES (?, ?, ?, NOW())"); $stmt->execute([$user_id, $encryptedFeature, bin2hex($iv)]);4.3 微信 access_token 过期如何自动刷新?——基于 Redis 的分布式锁与双 token 机制
微信 API 调用依赖access_token,有效期 2 小时。项目未提供刷新逻辑,需自行补全。关键是要避免多进程并发刷新导致 token 覆盖。使用 Redis 的SETNX实现分布式锁:
// utils/wechat_token.php function getAccessToken() { $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $token = $redis->get('wechat_access_token'); if ($token) return $token; // 尝试获取锁 $lockKey = 'wechat_token_lock'; $lockValue = uniqid('', true); if ($redis->set($lockKey, $lockValue, ['NX', 'EX' => 10])) { // 获取锁成功,调用微信接口刷新 $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"; $res = file_get_contents($url); $data = json_decode($res, true); if (isset($data['access_token'])) { $redis->set('wechat_access_token', $data['access_token'], ['EX' => 7000]); // 7000秒 < 2小时 } $redis->del($lockKey); return $data['access_token'] ?? ''; } else { // 未获取到锁,等待后重试 usleep(100000); // 100ms return getAccessToken(); // 递归重试 } }注意:
file_get_contents()在高并发下可能超时,建议替换为curl_init()并设置CURLOPT_TIMEOUT_MS => 3000。项目中merge.bat脚本的存在,说明作者熟悉 Windows 开发环境,因此curl扩展必须在php.ini中启用extension=php_curl.dll。
本文还有配套的精品资源,点击获取