1. 为什么要自己动手做一套人脸识别考勤系统
先说个真实的痛点。某小型团队之前用的是那种指纹打卡机,一到冬天干燥季节,手指脱皮、静电干扰,一堆人打不上卡;后来换成刷卡,又有人忘带卡、代刷,月底对考勤记录的时候,行政那边对着Excel恨不得摔键盘。后来我帮他们做了一套基于人脸识别和MySQL的考勤管理系统,刷脸即打卡,识别时间基本控制在1秒内,月底导报表也很快。整个过程做下来,发现这类系统的技术难度其实没有想象中高,主要还是几个关键点要把握到位:人脸识别方案的选型、数据库表结构设计、识别结果如何准确落库、以及后期如何排查那些“莫名其妙”考勤异常的问题。
这篇文章就是把这套系统的完整实现过程拆给你看,从一个实际开发者的角度,把要踩的坑、要注意的细节、可以“抄作业”的代码都给到。适合的人群包括:想做课程设计的同学、公司内需要做小型考勤工具的开发者,以及想了解人脸识别如何落地到实际业务场景的产品和技术朋友。
先说结论:这套系统的核心链路并不复杂,本质上就是“摄像头抓拍人脸 → 人脸特征提取 → 与数据库里存的员工人脸特征做比对 → 返回匹配结果 → 写入考勤记录表”。但把这条链路做稳定、做准、做得能应对真实环境的复杂光线和角度变化,才是真正花时间的地方。
2. 整体设计方案:从需求到架构的思考过程
2.1 核心需求拆解:不只是“打个卡”
很多人一听到“考勤系统”,第一反应就是“识别到人脸就记录一下时间”,但实际需求远比这个复杂。我习惯先把需求拆成几个维度来梳理:
| 需求维度 | 具体说明 | 对应技术环节 |
|---|---|---|
| 人员管理 | 新员工录入人脸,老员工离职删除,信息修改 | 员工表的增删改查 + 人脸特征库维护 |
| 考勤打卡 | 上下班两个时间点识别,识别成功即记录 | 人脸识别 + 考勤记录写入 |
| 考勤计算 | 根据上班时间和下班时间判断迟到、早退、缺卡 | SQL查询 + 逻辑规则计算 |
| 报表统计 | 按月/按日查看每个人的出勤情况 | 多表联查 + 数据聚合 |
| 异常处理 | 没识别上、替打卡、照片不清晰等情况 | 日志记录 + 补卡流程 |
把需求拆开以后,你才会发现,人脸识别其实只占了整个系统的一小部分工作量,更多的活在后端逻辑和数据库设计上。这里给个真实的比例参考:整个开发周期里,人脸识别模块可能只占40%的时间,剩下的60%几乎都花在了业务逻辑、异常处理和数据统计上。
2.2 为什么选择“本地人脸特征比对 + MySQL”方案
人脸识别方案其实有很多种,有纯离线的库(比如OpenCV的人脸检测 + 深度学习模型提取特征),有在线API方案(比如各类云服务的人脸识别接口)。我当时的选择逻辑很直接:
第一,考虑到是内部考勤系统,数据要留在本地,员工的生物特征信息不适合传到第三方平台,这是合规和数据安全的底线。第二,在线API按调用量收费,考勤是高频场景,每天几百上千次调用,长期下来成本不低。第三,本地方案配合GPU或者即使是纯CPU,在考勤这种“单个人脸比对”场景下,性能已经完全够用。
所以最后的方案是:用开源的计算机视觉库做人脸检测和对齐,用预训练好的深度学习模型提取人脸特征向量(比如128维或512维的特征),存到MySQL里。打卡的时候,提取当前摄像头拍到的人脸特征,去数据库里比对,计算余弦相似度,超过阈值就认为是同一个人,然后写入考勤记录。整个链路完全本地化,干干净净。
2.3 系统架构:模块怎么划分
这套系统的架构我按功能拆成了四个模块,各干各的,互不纠缠:
人脸采集模块(录入员工人脸照片,提取特征入库) ↓ 人脸识别模块(实时摄像头抓拍、人脸检测、特征提取、比对) ↓ 考勤业务模块(判断打卡类型、写入记录、处理重复打卡) ↓ 数据管理模块(员工管理、考勤查询、报表统计、异常处理)采集和识别可以共用一个摄像头工具类,但逻辑上最好分开。因为采集的时候是“主动配合”的,会要求员工正对摄像头、调整姿势;而识别的时候是“被动抓拍”的,要能应对各种角度、光线、表情变化,这两者的处理逻辑差别很大。
另外,数据库连接这块,我建议直接用一个连接池来管,不然每来一个打卡请求就建立一次数据库连接,高并发的时候很容易把数据库拖垮。
3. MySQL数据库设计:考勤系统的地基
3.1 表结构设计:不只是两张表那么简单
数据库是这套系统的地基。很多人一上来就建一张员工表、一张考勤记录表,但真正跑起来以后,你会发现还需要人脸特征表、考勤规则表、补卡申请表等。我最终的库表结构是这样的:
-- 员工信息表 CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID', emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(50) NOT NULL COMMENT '姓名', department VARCHAR(50) COMMENT '部门', position VARCHAR(50) COMMENT '职位', status TINYINT DEFAULT 1 COMMENT '状态:1在职 2离职', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表'; -- 人脸特征表(一个员工可能有多张人脸照片的特征) CREATE TABLE face_feature ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL COMMENT '员工ID', feature_vector BLOB NOT NULL COMMENT '人脸特征向量(二进制)', image_path VARCHAR(255) COMMENT '人脸照片保存路径', feature_version VARCHAR(20) COMMENT '特征提取模型版本', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (emp_id) REFERENCES employee(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人脸特征表'; -- 考勤记录表 CREATE TABLE attendance_record ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL COMMENT '员工ID', check_time DATETIME NOT NULL COMMENT '打卡时间', check_type TINYINT COMMENT '类型:1上班 2下班', check_result TINYINT COMMENT '结果:1正常 2迟到 3早退 4缺卡', similarity DECIMAL(5,4) COMMENT '人脸比对相似度', image_path VARCHAR(255) COMMENT '打卡抓拍照片', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_emp_time (emp_id, check_time), FOREIGN KEY (emp_id) REFERENCES employee(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤记录表'; -- 考勤规则表(上下班时间、迟到阈值等) CREATE TABLE attendance_rule ( id INT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) COMMENT '规则名称', work_start_time TIME NOT NULL COMMENT '上班时间', work_end_time TIME NOT NULL COMMENT '下班时间', late_threshold_minutes INT DEFAULT 0 COMMENT '迟到宽容分钟数', is_active TINYINT DEFAULT 1 COMMENT '是否启用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤规则表';关于特征向量的存储格式,这里要强调一个细节。很多初学者会把特征向量转成字符串(比如用逗号拼接的文本),再存进VARCHAR字段,这样其实很不靠谱。特征向量通常是浮点数数组(128维或者512维),转成字符串会浪费大量空间,而且查询的时候没法直接比对。正确的做法是把它序列化成二进制,存到BLOB字段里,或者干脆用专门管理特征向量的工具。这里为了纯粹依赖MySQL实现,存BLOB是最实际的选择。
3.2 为什么特征向量要单独建表
这里多讲一句,为什么人脸特征不直接存在employee表里。实际业务中,一个员工可能录入多张人脸照片(正面照、侧面照、不同光线条件下的照片),目的是提升后期比对的准确率。如果把特征向量直接存在员工表里,设计上就限制了“一个人只能有一条识别特征”,这个约束不合理。
另外,人脸识别模型是会升级的。可能上个月用的模型提取的特征是128维,下个月换了个更优的模型变成512维。如果特征单独建表,加一个feature_version字段,升级的时候只需要重新生成新版本的特征,旧特征还可以保留用来做兜底比对。这是我实际开发中踩过坑以后才加上的设计,前期图省事没做,后来升级模型时差点把整个库都重建了。
3.3 考勤记录表为什么用DATETIME而不是分开存日期和时间
关于考勤记录表,日期和时间字段的设计也有讲究。有些人习惯把日期(2025-01-10)和时间(09:00:00)分开两个字段存,美其名曰“查询方便”。但实际上,用DATETIME一个字段,配合MySQL自带的日期函数(DATE()、TIME()、DATE_FORMAT()),完全可以做到同样的查询效果,而且数据更紧凑、排序更自然。如果分开存,查“某员工某月每天的上下班记录”时反而要多一层拼接逻辑,没有必要。
4. 人脸识别流程:从采集到比对的完整实现
4.1 人脸采集入库:这一步偷懒,后面全是坑
人脸采集是整个系统里最容易被低估的环节。很多开发者随便拿张照片就往库里塞,结果识别的时候发现,要么识别不上,要么频繁误识别。根据我的实操经验,人脸照片采集必须满足几个硬性要求:
第一,照片里人脸区域要足够大。人脸区域至少占整张照片面积的30%以上,摄像头距离人脸0.5~1.2米之间。第二,光线均匀,不能在脸上形成明显的阴影或高光。第三,表情自然,不要夸张大笑或紧闭双眼。第四,尽量是正面照,左右偏转角度不要超过15度。
为了控制采集质量,我在录入环节加了一个简单的校验逻辑:先检测照片里有没有人脸,然后检查人脸框的尺寸占整张图的比例,低于阈值就直接拒绝录入,并提示重新拍摄。这个小逻辑看着不起眼,但大幅提升了后期识别的准确率。
录入时,建议给每个员工拍2~3张不同角度的照片(正面、左右轻微侧转、稍远稍近各一张),分别提取特征存到face_feature表里。比对的时候,只要任意一张特征匹配成功,就认为是同一个人。这招对真实场景特别管用,因为员工每天打卡时的站位和角度不可能完全一样。
4.2 特征提取与比对:核心代码实现
整个核心逻辑可以封装成一个服务类。这里给出关键代码,用Python实现,配合常见的人脸识别开源库:
import pickle import numpy as np import mysql.connector from mysql.connector import pooling class FaceRecognitionService: def __init__(self, db_config): # 使用连接池管理MySQL连接 self.pool = mysql.connector.pooling.MySQLConnectionPool( pool_name="attendance_pool", pool_size=10, **db_config ) self.threshold = 0.6 # 余弦相似度阈值,经验值,需根据实际调试 self._load_model() def _load_model(self): # 加载预训练的人脸识别模型 # 这里使用常见的深度学习人脸识别模型,输出512维特征向量 self.face_detector = ... # 人脸检测器 self.face_encoder = ... # 人脸特征编码器 def extract_feature(self, image): """提取一张照片中最大人脸的特征向量""" # 检测人脸 faces = self.face_detector.detect(image) if len(faces) == 0: return None # 取面积最大的人脸 largest_face = max(faces, key=lambda f: f.area()) # 对齐并裁剪人脸区域 aligned_face = self._align_face(image, largest_face) # 提取特征向量 feature = self.face_encoder.encode(aligned_face) return feature def recognize(self, image): """识别摄像头抓拍的图像中的人脸""" feature = self.extract_feature(image) if feature is None: return None, 0.0 # 获取数据库里所有已注册的员工特征 conn = self.pool.get_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT emp_id, feature_vector FROM face_feature") rows = cursor.fetchall() cursor.close() conn.close() best_match = None best_similarity = 0.0 for row in rows: db_feature = pickle.loads(row['feature_vector']) sim = self._cosine_similarity(feature, db_feature) if sim > best_similarity: best_similarity = sim best_match = row['emp_id'] if best_similarity >= self.threshold: return best_match, best_similarity return None, best_similarity def enroll_employee(self, emp_id, image): """录入员工人脸特征""" feature = self.extract_feature(image) if feature is None: raise ValueError("未检测到人脸,请重新拍摄") feature_bytes = pickle.dumps(feature.astype(np.float32)) conn = self.pool.get_connection() cursor = conn.cursor() cursor.execute( "INSERT INTO face_feature (emp_id, feature_vector) VALUES (%s, %s)", (emp_id, feature_bytes) ) conn.commit() cursor.close() conn.close() @staticmethod def _cosine_similarity(f1, f2): """计算两个特征向量的余弦相似度""" a = np.array(f1, dtype=np.float32).flatten() b = np.array(f2, dtype=np.float32).flatten() norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) if norm_a == 0 or norm_b == 0: return 0.0 return float(np.dot(a, b) / (norm_a * norm_b))这段代码有几个关键点要解释一下。
第一,为什么用连接池而不是每次单独建立连接。考勤打卡的调用频率虽然不算特别高,但人脸识别本身耗CPU,数据库操作又要走网络,如果每个请求都新建连接,系统在高并发场景下(比如早上8点50分一大批人同时涌到闸机口)容易出现连接数打满、响应变慢的情况。连接池复用连接,是稳定性的基本保障。
第二,为什么用pickle序列化特征向量而不是numpy的save。numpy的save会附带一些额外的元数据,占用空间大;pickle直接序列化numpy数组,存取速度更快,而且读出来直接用np.array还原就行。
第三,这个阈值0.6是怎么定的。这不是拍脑袋定的,而是我用一批真实采集的样本测试出来的。具体做法是,收集了约2000张不同员工在不同光线/角度下的人脸照片,两两计算相似度,画出同一人的相似度分布和不同人的相似度分布,取两者的分界点作为阈值。如果你的场景里员工的肤色、年龄、外貌差异不大(比如都是年轻人),阈值可以适当调高到0.65;如果人员结构复杂,0.55~0.6比较稳。这个后面“问题排查”部分还会详细说。
4.3 打卡逻辑:什么时候写入记录,什么时候不写
打卡逻辑看着简单,实际里面藏着很多细节。我当时写了一个考勤服务类,核心逻辑是:
class AttendanceService: def __init__(self, pool, face_service, rules): self.pool = pool self.face_service = face_service self.rules = rules # 考勤规则字典 def check_in(self, emp_id, image, check_time): """ 打卡入口 返回: (success, message, record_id) """ # 1. 先做面部识别 recognized_id, similarity = self.face_service.recognize(image) if recognized_id is None: return False, "未识别到匹配的人脸,请重试", None # 2. 识别结果和请求的emp_id不一致 if recognized_id != emp_id: return False, "人脸与员工信息不匹配", None conn = self.pool.get_connection() cursor = conn.cursor(dictionary=True) try: # 3. 判断当前时间属于上班打卡还是下班打卡 now_time = check_time.time() if now_time < self.rules['work_start_time'] or \ (self.rules['work_start_time'] <= now_time <= self.rules['latency_deadline']): check_type = 1 # 上班 elif now_time >= self.rules['work_end_time']: check_type = 2 # 下班 else: # 中午时间不允许打卡 return False, "当前时段不是打卡时段", None # 4. 防重复打卡:上班打过卡,短时间内再打不重复记录 # 这里判断当前时段是否已有打卡记录 cursor.execute( "SELECT id, check_type FROM attendance_record \ WHERE emp_id=%s AND DATE(check_time)=%s AND check_type=%s", (emp_id, check_time.date(), check_type) ) existing = cursor.fetchone() if existing: return False, "已经打过卡了", None # 5. 写入考勤记录 cursor.execute( "INSERT INTO attendance_record \ (emp_id, check_time, check_type, check_result, similarity, image_path) \ VALUES (%s, %s, %s, %s, %s, %s)", (emp_id, check_time, check_type, self._judge_result(check_time, check_type), similarity, ...) ) conn.commit() return True, "打卡成功", cursor.lastrowid finally: cursor.close() conn.close() def _judge_result(self, check_time, check_type): """判断打卡结果是正常、迟到还是早退""" now_time = check_time.time() if check_type == 1: # 上班 if now_time <= self.rules['work_start_time'] + timedelta(minutes=self.rules['late_threshold_minutes']): return 1 # 正常 else: return 2 # 迟到 else: # 下班 if now_time >= self.rules['work_end_time']: return 1 # 正常 else: return 3 # 早退这个逻辑里最值得说的是“时段判断”。很多人写打卡逻辑时,直接按时间硬编码:9点前上班打卡,18点后下班打卡。但真实场景里会有各种边界情况:早上7:30就来了是上班打卡还是加班打卡?下午16:00离开算早退还是正常下班?中午12:00走出办公室应该记录什么?
我的做法是,先通过考勤规则表配置“上班时间”“下班时间”和“午休区间”,然后根据当前时间落在哪个区间来判定是哪种类型的打卡。不在任何打卡时段内的请求直接拒绝,前端提示“当前不在打卡时段”。这样既灵活,又能避免中午出去吃饭误打卡的问题。
4.4 考勤计算:迟到早退怎么算才合理
考勤结果的计算不只是在打卡那一刻判断一个结果就完了,月底还得有汇总统计。我的方案是,每天凌晨用定时任务把前一天的考勤数据做一次“日结算”,对每个人的上下班打卡记录进行匹配,处理缺卡的情况,并生成每日考勤汇总。
这里容易踩坑的是“跨天排班”的问题。比如夜班员工,晚上22:00上班,第二天凌晨6:00下班。如果简单地按自然日来匹配上下班记录,就会出现问题:凌晨的打卡记录算到昨天还是今天?我的处理办法是,在考勤规则表里加一个“换班日偏移”字段,支持把凌晨的打卡归属到前一个工作日。这个细节一开始没注意,后来有夜班员工月初对账时发现了问题,才补上去的。
5. 实操环节:部署这套系统的关键步骤与避坑指南
5.1 环境准备:硬件和软件选型建议
先说一下硬件需求。我测试用的环境是:
- CPU:老款高性能处理器或者主流i5以上都可以,纯CPU推理单次人脸识别约300~500ms,打卡场景完全够用。
- 内存:8GB起步,16GB更稳。
- 摄像头:普通USB摄像头,分辨率建议不低于720P,最好带自动白平衡和自动对焦。不要买那种特别便宜的鱼眼摄像头,画面畸变会影响人脸检测准确率。
- 操作系统:Windows和Linux都行,但建议生产环境用Linux,长期挂机更稳定。
软件层面,Python版本建议3.8+。核心依赖库包括人脸检测工具库、深度学习框架(CPU版就行)、图像处理库、MySQL驱动、以及Web后端框架(如果要做界面)。如果是纯后端API,建议搭配一个简单的Nginx + WSGI服务;如果有前端界面,前后端分离的话可以加一个Nginx做静态文件服务。
5.2 部署步骤:从零到可以打卡
整个部署流程,我总结成了六个步骤:
创建数据库和表结构。用上面提供的SQL语句,按顺序执行即可。注意字符集要选utf8mb4,因为员工姓名里可能有生僻字,utf8mb4支持更全。
准备一个目录存放打卡抓拍的照片。考勤记录要留痕,每打卡一次,就把当次抓拍的原始图像保存到本地或对象存储,同时把文件路径存到考勤记录表里。这很重要,后期有员工质疑“我没打过卡”的时候,调出照片就是铁证。
安装人脸识别相关依赖库。这里建议先把OpenCV装上,然后装人脸识别用的深度学习库。安装过程中常见的坑是依赖库版本冲突,我的建议是用一个独立的虚拟环境,别和系统全局环境混在一起。
启动一个人脸注册脚本,把现有员工的照片批量导入,提取特征后写入face_feature表。注册时给每个员工拍2~3张不同角度和光照条件的照片,这样识别更稳。
启动考勤服务。建议先跑一段测试模式,用本机摄像头对着自己的脸试打几次卡,确认识别正常、数据库写入正常。
用Web服务把查询和报表功能跑起来。前端页面不需要花太多精力,核心功能是:按部门按日期查询考勤记录,按月汇总迟到早退次数,导出Excel报表。这部分用现成的Web框架几天就能搞定。
5.3 参数调优:阈值和识别速度的平衡
部署完以后,最花时间的其实是调参。两个核心参数:人脸检测的最小尺寸阈值、相似度阈值。
关于最小检出尺寸,默认值可能要求人脸至少占图像的一定比例。如果摄像头安装距离远,人脸很小,就调低这个阈值;如果距离近,可以适当调高,避免误检到背景里的海报人脸。根据我的经验,人脸宽度在图像里至少要有80像素,识别率才比较稳。
相似度阈值这个参数,我在前面已经说过,必须结合实际场景测试。有一个快速调试的思路:找10个员工,每人拍10张不同的照片,任意两张同一人的相似度一般应该在0.65以上;不同人之间的相似度一般应该在0.45以下。如果你的测试结果不是这个数量级,说明模型选得不对,或者照片质量太差。这个数据可以当个基准参考,具体阈值还是要按你实际采集的数据分布来定。
还有一个容易被忽视的点:识别服务的性能监控。每次识别用了多少毫秒,相似度分数是多少,最好都打日志。否则后期系统卡顿或者识别率下降的时候,你会发现根本没有数据可以分析。
6. 常见问题与排查技巧实录
6.1 识别失败率高的几个常见原因
实际跑了一个月以后,你会收到一堆“打不上卡”的投诉。根据我的经验,90%的问题都出在以下几个环节:
| 现象 | 可能的根因 | 排查方法 |
|---|---|---|
| 某人一直识别失败 | 录入的底照质量太差;员工发型/眼镜变化较大 | 重新录入多张照片;检查face_feature表的特征版本是否一致 |
| 某人经常识别成别人 | 相似度阈值太低 | 调高阈值;查看log里的相似度分布 |
| 上午正常下午识别失败 | 摄像头逆光,人脸区域过暗 | 调整摄像头位置或补光 |
| 打卡速度太慢 | 数据库里特征数量多但没有做分区或索引 | 看看是不是全表扫描了 |
这里重点说一下“眼镜”这个变量。很多员工上班戴眼镜、下班摘眼镜,人脸特征会有较大变化。解决办法是在录入照片时,让他们把戴眼镜和不戴眼镜的照片各拍一张,分别提取特征入库。同理,刘海变化、浓妆等情况,也建议多录几张特征照片做冗余。
6.2 数据库写入失败导致打卡无记录
有一次,系统跑着跑着突然没人能打卡了。查了日志发现人脸识别正常,但数据库连接超时。原因是MySQL的max_connections被默认值限制住了,而连接池的初始连接数设置过大,加上有些连接被异常占用后没有释放,导致连接池被占满。
排查思路是这样的:先看MySQL的错误日志,确认是不是连接数满了;然后检查后端代码里有没有连接泄漏,比如try了SQL操作但finally里忘了关闭cursor。这里给一个实用建议:所有数据库操作必须在finally块里释放连接,或者用上下文管理器来自动释放。我当时就是漏了几处,高峰期连接池被打满,重启服务才好。
6.3 打卡记录重复/缺失的数据问题
还有一个常见问题:同一员工5分钟内打了多次卡,生成了多条记录。正常应该只保留第一条,但实际会出现重复。排查下来有两种情况:一是前后端重复请求,前端按钮点了两次;二是数据库判断重复时,用的时间范围太窄,比如只检查了“同一秒是否有记录”,忽略了跨秒的点击。
解决方法是把重复判断的时间窗口放宽到3~5分钟,同时在前端做按钮防抖。对于已经产生的脏数据,跑一个去重SQL先清一下:
DELETE r1 FROM attendance_record r1 INNER JOIN attendance_record r2 WHERE r1.id > r2.id AND r1.emp_id = r2.emp_id AND r1.check_type = r2.check_type AND ABS(TIMESTAMPDIFF(SECOND, r1.check_time, r2.check_time)) < 300;注意执行前先备份数据。
6.4 摄像头画面模糊的处理
摄像头画面模糊,这个坑也踩过。USB摄像头的自动对焦有时候会失效,特别是光线暗的时候,画面一直处于“拉风箱”的状态。解决办法是买支持手动固定焦距的摄像头,部署好后调好角度,把自动对焦关掉,直接在代码里固定焦距。这样反而更稳定,不会在识别时突然对焦导致画面抖动。
6.5 月度报表生成太慢的优化
月度报表在最开始的时候,是用一条SQL把所有的打卡记录查出来,然后在代码里逐条计算状态。这个方案在数据量小的时候没问题,但跑到3个月后,记录数上了几万条,报表接口的响应时间变得不可接受。
优化思路有两个:一是建立每日考勤汇总表,每天定时任务生成汇总,月底直接查汇总表而不是扫描明细表;二是给考勤记录表的查询字段建立合适的复合索引。我当时两条都做了,效果明显,月度报表从原来的几十秒降到了几秒钟。
-- 复合索引:emp_id + check_time + check_type CREATE INDEX idx_emp_check_time ON attendance_record (emp_id, check_time, check_type); -- 每日考勤汇总表 CREATE TABLE daily_attendance_summary ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, work_date DATE NOT NULL, first_check_in TIME COMMENT '首次上班打卡时间', last_check_out TIME COMMENT '最后下班打卡时间', late_count INT DEFAULT 0, early_leave_count INT DEFAULT 0, missing_count INT DEFAULT 0, UNIQUE KEY uk_emp_date (emp_id, work_date) ) ENGINE=InnoDB;6.6 换模型时特征不兼容的问题
最后分享一个升级过程中遇到的问题。前期用的模型提取128维特征,后来换了提取512维特征的新模型,结果老的face_feature记录和新模型算出的相似度怎么都比不高,因为特征空间的维度都不同了。解决办法是,每次模型升级,都重新对所有员工照片提取一次特征,并更新feature_version标记。同时,在识别逻辑里,只和同版本的特征做比对,避免跨版本比较。这个经验也验证了前面设计“特征版本字段”的重要性。
7. 最后几点补充建议
这套系统前后跑了几个迭代版本,我个人感觉最有价值的不是人脸识别算法本身,而是业务逻辑的细节完整度。算法识别准了,只是一切的开始;真正让系统好用、能落地、能坚持跑下去的,是防重复打卡、异常留痕、数据汇总这些“看不见的功夫”。
几个方向如果后续要扩展,可以考虑:第一,对接企业微信或钉钉,把考勤异常通知自动推送到员工手机;第二,加入异常补卡申请流程,员工可以在线提交说明,由管理员审批后修正记录;第三,把识别服务做成独立的微服务,通过HTTP接口给多个终端共用,这样如果以后要多台考勤机,就不用重复部署。我目前正在做的是第三项,把整个识别模块从主系统里拆出去,头疼了一阵子,但好处是后续扩展确实方便。
如果你也在做类似的系统,我的建议是,从一开始就别只盯着“人脸识别”这个技术点,把数据库设计和业务逻辑的坑提前想清楚,比自己瞎折腾然后返工要省太多时间。希望这篇文章能帮你在动手之前就先绕开几个大坑。