
简介基于Android Studio实现的人脸识别考勤系统APP配套Web管理后台与MySQL数据库并集成高德地图定位适用于高校课堂签到、企业员工考勤等场景可作为毕业设计或课程实践项目参考。系统分为后台管理与APP双端后台涵盖学生、班级、教师、角色权限、数据字典等管理模块APP端区分老师与学生角色支持发布签到、人脸识别签到、签到地图、考勤统计、课表管理、新闻浏览等功能。后台采用Spring Boot、MyBatis Plus、Shiro客户端接入OkHttp与fastJson便于理解双端交互和权限控制流程。资源共64个文件以项目截图51张jpg、9张png、说明文档2个md、数据库脚本1个sql及核心代码压缩包为主整体约75.74MB目录结构清晰。目前已有121人学习下载适合需要完整人脸考勤方案、双端联调思路及后台权限管理设计的开发者可结合源码与数据库脚本直接搭建运行环境。1. AndroidStudio人脸识别考勤系统先想清楚“考勤”和“识别”谁更难得住你把人脸识别和考勤放在一起第一反应通常是“去找一个人脸SDK”。但真实落地里识别准不准只占一半另一半是系统能不能证明“这个人此刻就在公司、而且确实是本人”。用AndroidStudio把APP、管理后台、数据库和高德地图定位串成一整套考勤系统正是为了把这两半都补齐。这套方案适合三类人想在公司或部门里做轻量考勤的Android工程师做课程设计或毕设的学生以及想摸清“前端识别→后端存储→定位防作弊”完整链路的人。下面的内容不绕理论直接按“识别怎么做→数据怎么存→定位怎么防作弊→哪些地方会翻车”的顺序展开每一段都给能照抄的最小实现。2. Android端人脸识别通道怎么打通SDK选型、CameraX预览与特征比对的最小实现2.1 识别引擎怎么选别一上来就自己训练模型人脸识别是这个标题里的核心技术节点而识别引擎的选择直接决定系统八成的工作量。常见做法是接成熟的离线SDK或在线API而不是自己在Android上训练模型。离线SDK方案里我常用的是虹软ArcFace这类引擎理由有三个本地跑完特征提取和比对不依赖公网有免费试用额度集成包相对干净识别阈值、最小人脸像素这些参数可以直接调出问题时可排查的点很少。在线API方案比如把图片传到云端人脸识别接口也能用好处是识别精度高、不用维护模型坏处是每次考勤都要走网络。公司网络一抖动打卡就卡在“识别中”考勤这种每天早高峰要用的东西最忌讳这种不确定。相比之下同样是做人脸识别人脸识别门禁机能用固定机位来保证光照和角度考勤APP的机位是同事的手今天拿高明天拿低离线引擎在这个场景下更稳。有人会问直接用OpenCV加LBPH做人脸识别行不行。做个课程演示可以但距离可用差得很远。LBPH对光线、角度、眼镜帽子这类遮挡非常敏感考勤这种“每天都要用”的场景偶尔翻一次车就足够让同事失去信任。我的选型顺序是离线商用SDK优先在线API次之OpenCV只用来做演示和验证流程。2.2 从CameraX预览到特征比对最小可跑的Java代码Android端的最小链路是CameraX的ImageAnalysis拿到每一帧画面从帧里检测人脸截取人脸区域交给识别引擎生成特征值再和注册库里的特征比对。引擎初始化建议放在Application的onCreate里做避免第一次打卡时经历冷启动。// 初始化人脸识别引擎包名和方法名按你接入的SDK替换 FaceEngine faceEngine new FaceEngine(); // 检测模式考勤场景只需要检测不需要跨帧跟踪关掉跟踪能省CPU int detectMode FaceEngine.DETECT_MODE_IMAGE; // 识别模式注册和比对都用识别模式特征值长度由SDK决定 int recognizeMode FaceEngine.RECOGNIZE_MODE_NORMAL; // 最小人脸像素建议80x80低于这个尺寸识别率会明显下降 int minFaceSize 80; // 检测间隔连续识别场景建议按帧间隔避免每帧都做耗时操作 int interval 200; // 单位毫秒200ms约等于每秒处理5帧 ResultInfo initResult faceEngine.init(context, detectMode, recognizeMode, minFaceSize, interval); if (initResult.getCode() ! ResultInfo.CODE_ACTIVATION_SUCCESS) { Log.e(FaceEngine, 引擎初始化失败错误码: initResult.getCode()); }逻辑说明这里的interval不是相机帧率而是引擎做检测的最小时间间隔。设成0会让每一帧都跑检测CPU占用飙升设成500毫秒又会觉得打卡迟钝。200毫秒是我试下来比较舒服的值。minFaceSize设置得越小远处的小脸越容易被捕捉但误检也会变多。手机上人脸通常占屏幕四分之一以上80像素是一个平衡点。拿到人脸区域后要提取特征并比对这一步决定了“这个人是谁”// 从当前画面截取的人脸Bitmap转成引擎需要的格式 byte[] currentFeature extractFeature(faceBitmap); // 从本地数据库或内存缓存中取出注册特征 byte[] registerFeature getRegisterFeature(employeeId); // compareFeature返回相似度范围0到1 float score faceEngine.compareFeature(currentFeature, registerFeature); // 阈值0.8是我这边的经验值建议按实际测试结果调整 boolean isMatch score 0.8f; if (isMatch) { // 本地比对通过再带着分数、定位和时间戳去请求后台 } else { // 分数在0.6到0.8之间算“存疑”可拍照留给人工复核 }这段逻辑的要点是特征比对在端上完成后台只接收结果不重复算特征。好处是识别过程不依赖网络后台压力也小。注意不同SDK的评分含义不一样有的是相似度、有的是距离值。接新SDK时不要照搬阈值先拿10个人的注册照和现场照各测几十次找到“本人全过、他人全拒”的分界点再写死。2.3 注册环节决定成败多特征录入与识别阈值的实测调法考勤翻车最多的时候不在识别在注册。很多初版系统只在录入员工时拍一张正面照结果换了光线就断。我一般会在注册时连拍3到5张照片覆盖正面、左右偏转30度和戴不戴眼镜的变化分别提取特征后全部存进员工特征表。比对时只要任意一张分数过阈值就算通过这样“早上阳光打半张脸”这类情况会缓解很多。每位员工的特征建议单独建表字段类似employee_id、feature_index、feature_value、created_at。注意不要在打卡时把现场照片也存到数据库做比对那样库会越来越大还涉及隐私合规问题。现场照片只在上报可疑记录时附一张即可。阈值怎么调也是个需要耐心的事。把阈值调到0.9误识别人会变少但本人早上一脸疲惫、侧着站时也容易被拒。调到0.7本人容易过但相似脸或注册库照片质量差的人会互相串。我的做法是分两步先用默认阈值跑一周每天把“存疑记录”导出来人工看一眼如果误拒多就往下降0.05如果误识多就往上升0.05。这个调整过程一定要放在真实办公环境做不要拿实验室光照下的图测。2.4 功耗与体验预览生命周期和检测间隔怎么配合考勤机是固定机位但手机做考勤时同事会拿在手里随意角度举着这对检测框的鲁棒性要求更高。我一般会开一个独立的“打卡页”页面进入时才启动摄像头预览识别成功后立即释放摄像头。CameraX在生命周期回调里做绑定页面不可见时自动关闭预览比旧版Camera API省心很多。如果你们要支持“熄屏后走过来自动打卡”功耗就是另一个量级的问题后台保活、传感器唤醒都会牵扯进来第一版不建议碰。另一个体验细节是识别失败时不要立即弹“识别失败”让同事再试一次而是连续取3帧结果取最高分。很多人举着手机的角度本来就不稳定第一帧没对上很正常。这3帧的时间大约多花1秒但能把早高峰的抱怨声压下去不少。3. 数据库与管理后台考勤流水怎么设计才经得起对账3.1 核心表结构打卡记录拆成流水迟到状态动态计算考勤系统的数据库设计里最容易犯的错是给每个员工一行记录然后用morning_time、evening_time这样的字段存上下班时间。看着直观但一旦出现补签、外勤、一天多次进出整张表就废了。正确做法是全部做成流水表一次打卡一条记录。我的核心表结构如下-- 员工表维护员工基础信息status用于逻辑删除 CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(32) NOT NULL UNIQUE COMMENT 工号, name VARCHAR(64) NOT NULL COMMENT 姓名, department VARCHAR(64) COMMENT 部门, status TINYINT DEFAULT 1 COMMENT 1在职 0离职, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 人脸特征表一个员工多条特征注册时生成 CREATE TABLE face_feature ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(32) NOT NULL, feature_index TINYINT NOT NULL COMMENT 第几张注册照, feature_value BLOB COMMENT 特征数组二进制存储, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_emp_no (emp_no) ); -- 考勤流水表一次成功识别就是一条流水 CREATE TABLE attendance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(32) NOT NULL, record_time DATETIME NOT NULL COMMENT 打卡时间由前端时间戳统一转换, longitude DECIMAL(10, 6) COMMENT 打卡点经度, latitude DECIMAL(10, 6) COMMENT 打卡点纬度, location_text VARCHAR(255) COMMENT 定位描述高德逆地理编码结果, face_score DECIMAL(5, 2) COMMENT 人脸比对得分, valid_flag TINYINT DEFAULT 0 COMMENT 后台校验结果 0待校验 1有效 2存疑, verify_msg VARCHAR(255) COMMENT 校验不通过原因, KEY idx_emp_time (emp_no, record_time), KEY idx_valid (valid_flag) );这里有一个关键选择我为什么不把“是否迟到”存进流水表因为迟到是相对规则算出来的规则一变历史统计就全乱。比如今天临时把上班时间从9点改成9点半之前判好的“迟到”字段就成脏数据了。把迟到逻辑留到后台查询时动态计算才能支持不同部门配不同考勤规则。前端APP只需要负责上报“谁在什么时间什么位置打了个卡”规则判断全部收到管理后台做。3.2 管理后台的上报与校验链路四部分数据如何协作管理后台的作用不只是看报表它还是整个系统的裁判。打卡时APP上传的内容一般包含四部分员工ID、识别分数、当前位置、客户端时间。后台收到后要做三件事第一查考勤规则确认当前时间落在哪个班次窗口第二查这个时间窗口内是否已有有效打卡防止连点刷屏第三拿上传坐标算距离判断是否在打卡范围。如果每一条打卡都要后台把员工特征库拉出来重新比对一遍性能会很差。常见做法是特征比对在端上完成后台只收结果当分数落在“存疑”区间时再让后台把现场照片存下来等管理员人工复核。这个折中方案在几十到几百人的中小团队里够用逻辑上也说得通分数足够高系统直接认分数模糊人来看。后台接口按职责拆成三个/api/attendance/upload接收打卡数据/api/attendance/review处理人工复核/api/report/daily生成日报表。管理后台的前端页面看报表时直接查attendance_record聚合不再经过APP端。提示上传接口一定做幂等。用emp_no record_time做唯一索引或先查后插否则网络重试会造成一人两条打卡记录。3.3 时区、补签与请假月底对账时少吵架的三个约定这里必须提一个真实存在的坑客户端System.currentTimeMillis()拿到的是UTC毫秒后端如果直接拿字符串当时间比较会不知不觉差8个小时。我见过同事排查“明明9点打卡报表显示下午5点”查了一下午最后发现是后台服务时区没设成Asia/Shanghai。统一做法是APP上报时附带毫秒时间戳后台用new Timestamp(timestamp)转成Date再按固定时区格式化前后端都不要传本地时区的日期字符串。补签和请假建议单独建表不要直接改流水时间。补签其实是往流水表插入一条带valid_flag2标记的记录并在verify_msg里写明“管理员补签原打卡时间08:12”请假则在报表聚合时通过leave_record表排除掉。这样月底对账时拿流水表原始数据和门禁日志核对能说清楚每一分钟去哪了。日报查询还有一个容易忽略的性能点attendance_record会随时间膨胀查询时一定要带上record_time的索引下推不要写DATE(record_time) CURDATE()这种对字段做函数的SQL否则全表扫描跑死数据库。这算是考勤系统上线后最常见的慢查询来源。4. 高德地图定位考勤定位SDK接入、打卡距离判定与防作弊边界4.1 高德定位接入混合定位、单次定位和打卡半径高德定位在Android端用的是AMapLocationClient集成时需要在高德开放平台申请Key并在AndroidManifest.xml里配置权限和应用Key。关键点有两个一是定位模式选混合定位也就是GPS、Wi-Fi和基站一起用室内信号差时不至于直接失败二是打卡场景只需要单次定位成功后主动销毁客户端能省不少电。连续定位在打卡场景没必要除非你们做的是外勤轨迹回放。// 高德定位客户端初始化Key在AndroidManifest里配置 AMapLocationClient locationClient new AMapLocationClient(context); AMapLocationClientOption option new AMapLocationClientOption(); // 混合定位模式GPS Wi-Fi 基站室内外兼顾 option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hybrid); // 单次定位拿到结果就停止避免持续耗电 option.setOnceLocation(true); // 定位超时时间室内环境经常要更久给到5秒以上 option.setHttpTimeOut(8000); locationClient.setLocationOption(option); locationClient.setLocationListener(new AMapLocationListener() { Override public void onLocationChanged(AMapLocation location) { if (location ! null location.getErrorCode() 0) { double lat location.getLatitude(); double lng location.getLongitude(); // 拿到坐标后立即销毁客户端 locationClient.onDestroy(); // 这里继续走打卡上报逻辑 } } }); locationClient.startLocation();定位回调拿到坐标后判断打卡位置是否有效是用球面距离算的。高德SDK自带距离工具类也可以用Haversine公式自己算但没必要重复造轮子。打卡半径一般设100米到500米写字楼密集区建议100米园区建议300米以上。设太小同事在公司楼下等电梯时会频繁打不上卡设太大隔壁楼的人也能顺手把卡打了。4.2 后台三重校验分数、时间窗口和距离缺一不可一条打卡记录要被判定为有效我会要求同时满足三个条件识别分数过阈值、时间落在规则窗口内、坐标距离在半径内。这三个条件放后台统一校验因为APP端的逻辑可以被反编译改掉一个条件就能伪造打卡。校验代码不需要复杂常见写法是public boolean checkValid(AttendanceUpload dto, AttendanceRule rule, GeoPoint officePoint, double radius) { // 1. 识别分数不低于阈值这里以0.8为例 if (dto.getFaceScore() 0.8f) { dto.setVerifyMsg(识别分数过低: dto.getFaceScore()); return false; } // 2. 时间在上班时间往前推30分钟到下班时间往后推1小时之间 LocalTime now dto.getRecordTime().toLocalTime(); if (now.isBefore(rule.getWorkStart().minusMinutes(30)) || now.isAfter(rule.getWorkEnd().plusHours(1))) { dto.setVerifyMsg(不在考勤时间窗口内); return false; } // 3. 坐标距离不超过打卡半径 double distance distanceOf(dto.getLatitude(), dto.getLongitude(), officePoint.getLatitude(), officePoint.getLongitude()); if (distance radius) { dto.setVerifyMsg(距离超限: distance 米); return false; } return true; }这里有一个容易被忽略的点如果员工在家想远程打卡距离判断会直接拦掉但如果用模拟定位软件伪造坐标距离判断就失守了。所以我在后台还会记录location_text和定位类型保留原始坐标人工复核时能作为辅助证据。另外verify_msg字段一定要写清楚具体原因不然月底处理存疑记录时管理员只能对着时间戳猜。4.3 防作弊的边界模拟定位识别与人工复核兜底考勤防作弊想做到滴水不漏是不现实的。高德SDK提供的定位设置能识别一部分模拟位置源但Android的模拟定位手段更新很快纯靠SDK判断会有漏网。我的做法是三层组合第一层是坐标距离校验挡掉所有距离不对的第二层是识别分数校验挡掉拿照片顶替的第三层是现场照片留存分数低或定位异常时自动上传一张照片让管理员用肉眼看一眼。第三层兜底实际使用中比前两层更管用。人脸识别再怎么调阈值总有相似脸的边界情况靠管理员人工看一眼是最朴素也最可信的方案。当然照片上传要控制频率只在valid_flag2时传避免每个人每次打卡都传一张否则一个月下来存储成本很高。隐私方面照片只保留30天超过后定期清理这个策略可以写进后台配置里。5. 在做这套考勤系统时最容易踩的5个坑及其排查记录5.1 预览黑屏、权限卡死和模拟器Starting Up的应对现象APP在早高峰时段启动相机有时直接黑屏过一会儿又恢复在AndroidStudio模拟器上测试时经常卡在模拟器Starting Up的状态半天起不来。原因真机上是摄像头权限弹窗还没点“允许”时CameraX已经尝试打开相机打开失败后预览流没有恢复机制模拟器上是宿主机摄像头驱动未必能被虚拟设备正确识别和程序本身没关系。解决启动预览前先检查权限并在onError回调里做一次重试。测试时不要指望模拟器直接拿一台真机。最快的验证方式是先用系统相机拍照确认摄像头本身是好的再排查自己的回调逻辑。模拟器这个东西在做人脸识别时价值很有限跑通UI可以跑识别链路只会浪费时间。5.2 同一个人识别时灵时不灵注册照和光照的坑现象同事早上打卡能过下午换个角度或光线暗一点就被拒识反复几次后开始抱怨系统不行。原因典型的光照和姿态问题。很多SDK在暗光下特征提取质量明显下降注册库如果只有一张正面照下午侧光时对不上。解决注册时收3张以上照片覆盖不同角度识别失败时不要立即拒绝连续取3帧取分数最高的结果第三次才提示失败。我这边调完之后同一批人的首次通过率从六成提到了九成以上。在源头多拍一张照片比调低阈值划算得多调低阈值会引入误识别的风险。5.3 定位坐标飘到隔壁楼室内定位的现场处理现象几个人在同一个会议室打卡有人提示“不在考勤范围”走出会议室两步又好了。原因室内GPS信号弱网络定位在楼内很容易出现几十米甚至上百米的漂移玻璃幕墙和高楼层尤其明显。会议室通常在大楼中间卫星信号被结构遮挡后定位点会在附近几个楼之间乱跳。解决把定位方式改成混合定位并在拿到定位结果后做一次去抖动处理——连续两次定位距离小于20米时才采用。打卡半径在室内办公场景适当放宽到150~200米同时保留现场照片作后台复核证据。这一条属于物理环境问题靠代码完全消除很难只能从判罚策略上兜。5.4 MySQL空闲断连早起第一卡总失败现象早上打卡经常有几个人报“提交失败”后台日志里出现Connection is not available或Communications link failure。原因MySQL默认的wait_timeout是8小时晚间没人打卡时连接池里的空闲连接被服务端断开第二天早上第一条请求却还在用旧连接于是直接失败。这是数据库连接池类应用最常见的晨间故障。解决连接池打开连接前做一次有效性检测并把空闲回收时间设得比服务端超时短。常见做法是把HikariCP的testWhileIdle和testOnBorrow打开idleTimeout设成4小时左右给服务端8小时超时留出一半余量。这个坑不发生在考勤业务逻辑里但会让所有人都觉得是你系统烂。5.5 时区不一致Excel对不上账的元凶现象月底拿后台导出的Excel和考勤机日志核对发现有一批人打卡时间对不上有的人差了8小时有的人差了1分钟。原因客户端、应用服务器、数据库服务器三处时区设置不一致。客户端传了2025-03-01 09:00:00这样带时区歧义的字符串被后台按服务器时区解析后存进数据库再导成Excel时又转了一次时区来回转几道就乱了。解决统一用毫秒时间戳传输显示层才格式化为北京时间数据库连接串里显式指定serverTimezoneAsia/Shanghai数据库连接池和应用时区也统一设为东八区。时区问题平时看不出毛病一到月底对账就让你加班这个亏我吃过一次后就再没犯过。6. 上线前用“模拟一天”验证这套考勤系统打点脚本与对账技巧6.1 用Python脚本模拟20个人打一天卡不上线就没法确认后台逻辑对不对。我一般在联调阶段写一个小脚本模拟数据直接压到后台接口比拿手机一遍遍打快得多。下面这个脚本会循环模拟20个人从8点50到9点10分的打卡部分坐标故意偏移两公里用来验证距离校验import requests import random import time base http://192.168.1.10:8080/api/attendance/upload for i in range(20): emp_no EMP%04d % (1000 i) # 模拟两种坐标大部分在公司附近少数故意偏移 if i % 5 0: # 故意造一个距离公司2公里以上的记录 longitude 116.39 0.03 latitude 39.91 0.02 else: longitude 116.39 random.uniform(-0.005, 0.005) latitude 39.91 random.uniform(-0.005, 0.005) payload { empNo: emp_no, faceScore: random.choice([0.85, 0.82, 0.79, 0.91]), recordTime: int(time.time() * 1000), longitude: longitude, latitude: latitude, locationText: 测试坐标 } requests.post(base, jsonpayload) time.sleep(0.2)这个脚本有两个参数值得注意faceScore列表里混了0.79这种低于阈值的分数用来验证存疑记录是否会被正确打标坐标偏移部分则是验证距离校验。如果跑完后后台存疑记录的类型和预期一致说明校验链路是通的。6.2 用SQL把账对平应到、实到、迟到和异常压测完以后再写一条聚合SQL验证结果能把报表逻辑里隐藏的问题暴露出来SELECT emp_no, COUNT(*) AS total_records, SUM(CASE WHEN valid_flag 1 THEN 1 ELSE 0 END) AS valid_count, SUM(CASE WHEN valid_flag 2 THEN 1 ELSE 0 END) AS suspicious_count FROM attendance_record WHERE record_time BETWEEN 2025-03-01 00:00:00 AND 2025-03-01 23:59:59 GROUP BY emp_no;如果某人的valid_count和预期不符先查verify_msg字段多半能看到“距离超限”或“识别分数过低”。这一步能帮你在真实上线前发现SQL写法问题、索引失效问题和时区转换问题。6.3 我最后的习惯每天凌晨做一次对账任务以上做完以后我养成了一个固定习惯后台加一个每天凌晨的定时任务自动把昨天的流水重新算一遍发现异常记录就推给管理员。上线第一个月里这个任务帮我逮到过两次代理打卡一次是因为定位漂移造成的误判。定位和人脸识别都只是工具真正让考勤系统可信的是后台校验和人工复核闭环。如果以后你自己做类似系统先把对账这块想清楚后面能省掉很多和同事解释“为什么打卡没算我”的沟通成本。希望帮到你。本文还有配套的精品资源点击获取