简介:这是一套基于Spring Boot构建的完整人脸考勤系统源码,面向Java后端开发者与企业级应用学习者,解决传统考勤方式效率低、易代打卡等管理痛点,适用于校园、中小型企业等轻量级人脸识别考勤场景。资源包含262个文件,总大小21.23MB,以57个核心Java业务类、154个XML配置及依赖描述文件为主,辅以7个HTML前端页面(含人脸录入、考勤、管理后台等)、7个Properties/YML配置项及13份Markdown说明文档,结构清晰,模块职责分明。已有2034人学习下载,体现了较强的实践参考价值。开发者可直接运行三个子项目——面向员工的‘人脸录入’与‘人脸考勤’模块,以及面向管理员的‘考勤管理系统’,全部集成百度AI人脸识别SDK实现活体检测与身份核验,配套SQL建表脚本、测试用例及完整启动类,便于快速部署、二次开发与技术原理剖析。 最近后台收到不少读者留言,问“人脸考勤系统怎么做”“SpringBoot能不能扛住人脸识别的并发”,正好我手头有一套完整跑通的基于SpringBoot的人脸考勤系统源码,从入职登记、人脸采集、刷脸打卡到考勤报表一条龙,目前已经稳定运行在几个中小型企业的内部环境中。
这个项目最大的价值在于它不是纯教学Demo,而是把业务闭环走通了。无论你是毕业设计需要一套能演示、能答辩的完整系统,还是公司想低成本上一套内部考勤方案,或者单纯想研究SpringBoot集成第三方SDK的实战姿势,都可以把这份源码作为骨架来改造。今天我就把从零搭建这套系统的完整思路、核心模块的源码级拆解、实际操作中的部署流程,以及我踩过的坑,一次性捋清楚。
1. 整体系统拆解:业务闭环才是核心
很多新手拿到人脸考勤这种项目,会习惯性先去看人脸识别算法,觉得算法是灵魂。但从实际落地的角度看,算法层面直接调成熟SDK就行,真正的技术重头戏反而在业务层:怎么把考勤数据算准、权限怎么控制、接口怎么设计,这些才是决定系统“能不能用”的关键。
1.1 核心需求解析
整套系统要解决的问题其实很朴素:
- 员工信息统一管理,包括部门、职位、入职状态
- 人脸底库的创建与维护,每个员工至少一张高质量人脸照片
- 上下班时间节点的刷脸打卡,识别通过之后生成打卡流水
- 考勤规则的定义,比如上下班时间、迟到早退判定、加班时长
- 按天/按月输出考勤报表,让HR能直接拿来算工资
这个需求清单落在SpringBoot里,就对应了若干个核心模块:员工管理模块、人脸特征管理模块、打卡API模块、考勤规则计算模块、报表导出模块。源码里每个模块各司其职,模块之间通过Service层互相调用,没有出现一个类写两千行的情况,可维护性拉满。
1.2 技术选型背后的考量
我选型的原则很固执:社区活跃度优先、学习成本次之、跑得稳最重要。
- SpringBoot 2.7.x:这套源码并没有一上来就追SpringBoot 3.x,原因很简单,2.7.x是2.x时代的最后一个大版本,兼容性极好,网上踩坑资料最多,而且对JDK 8极度友好。实际企业环境里JDK 8还是主流,你用SpringBoot 3经常遇到javax到jakarta的迁移问题,不值得。
- MyBatis-Plus:单表CRUD几乎不用写SQL,分页插件尤其香。考勤记录按月份分页查询的场景特别多,用MyBatis-Plus自带的分页能省掉大量模板代码。
- Redis + 本地缓存:人脸特征向量(通常以Base64或Float数组存储)的读取频率极高,每次打卡都要比对,不可能每次都去数据库查。源码里把特征数据预加载进Redis,打卡时先查缓存,缓存未命中再回源数据库,实测查询耗时主流在10ms内。
- 虹软ArcSoft人脸SDK:选它的原因就一个,离线识别且免费。很多国内考勤场景在上内网,根本没有外网条件去调云厂商的人脸API。虹软SDK提供Windows和Linux两个平台的动态库,SpringBoot项目通过JNI封装调用,数据不出内网,安全和合规都好交代。
1.3 源码目录结构解读
拿到源码之后,你会看到这样的目录结构,我建议按这个顺序去读代码:
attendance-system/ ├── src/main/java/com/example/attendance/ │ ├── config/ // 配置类:Redis、MyBatis、SDK初始化 │ ├── controller/ // 接口层:员工、打卡、报表 │ ├── service/ // 业务层:考勤计算、人脸识别逻辑封装 │ ├── mapper/ // 数据访问层 │ ├── entity/ // 实体类 │ ├── utils/ // 通用工具:日期处理、Base64转换 │ └── AttendanceApplication.java ├── src/main/resources/ │ ├── mapper/ // MyBatis XML文件 │ ├── static/ // 前端静态资源 │ ├── application.yml │ └── libs/ // 虹软SDK的so/dll文件 └── sql/ └── attendance.sql // 初始化数据库脚本先读config,看SDK怎么初始化;再读entity,搞清数据模型;然后走一遍controller -> service -> mapper的调用链,体系就建立起来了。别一上来就啃算法部分,那个对业务理解帮助不大。
2. 核心功能模块源码级拆解
这块是整个系统的重心,我会把每个关键模块的代码设计思路和实现要点拉出来讲,所有核心代码都直接在文章里,方便你对照源码逐行分析。
2.1 员工与人脸注册模块
员工注册是整个流程的入口,在实体设计上,有三个字段非常关键:face_feature(人脸特征向量)、face_img(人脸照片Base64)、status(员工状态)。其中face_feature是识别比对的核心,它由SDK从照片中提取,是一个浮点数组。
// 员工注册接口核心逻辑 @PostMapping("/employee/register") public Result register(@RequestBody EmployeeRegisterDTO dto) { // 1. 基础信息校验 if (StringUtils.isBlank(dto.getName()) || dto.getDeptId() == null) { return Result.error("姓名和部门不能为空"); } // 2. 人脸特征提取(虹软SDK) FaceFeature feature = faceService.extractFeature(dto.getFaceImgBase64()); if (feature == null) { return Result.error("人脸特征提取失败,请检查照片质量"); } // 3. 特征数据存储(转成Base64存库) Employee employee = new Employee(); employee.setName(dto.getName()); employee.setDeptId(dto.getDeptId()); employee.setFaceFeature(Base64Utils.encodeToString(feature.getFeatureData())); employee.setFaceImg(dto.getFaceImgBase64()); employee.setStatus(1); // 4. 写库 + 缓存同步 employeeService.save(employee); redisService.set("face:" + employee.getId(), employee.getFaceFeature()); return Result.success(employee); }你注意代码里的一个细节:特征提取成功之后,立刻同步写Redis。这意味着新员工注册完就能立即去刷脸打卡,不需要等缓存过期再命中,体验上非常顺畅。
2.2 人脸识别与打卡模块
打卡是整个系统中技术含量最高的部分。人脸识别SDK拿到摄像头传过来的一帧图片,先做人脸检测(框出人脸位置),再提取特征,然后和库里已有的特征逐一比对相似度,超过阈值(一般92分以上)就认为是同一个人。
// 打卡接口核心逻辑 @PostMapping("/attendance/check") public Result checkIn(@RequestBody CheckInDTO dto) { // 1. 从SDK提取当前帧特征 FaceFeature currentFeature = faceService.extractFeature(dto.getImageBase64()); if (currentFeature == null) { return Result.error("未检测到人脸,请正对摄像头"); } // 2. 从Redis获取底库(员工ID + 特征) Map<String, String> faceDB = redisService.getAllHash("face_db"); if (faceDB.isEmpty()) { return Result.error("人脸底库为空,请先注册员工"); } // 3. 遍历比对,找出最相似的人 String matchedEmployeeId = null; double maxScore = 0; for (Map.Entry<String, String> entry : faceDB.entrySet()) { FaceFeature dbFeature = new FaceFeature(); dbFeature.setFeatureData(Base64Utils.decodeFromString(entry.getValue())); double score = faceService.compareFeature(currentFeature, dbFeature); if (score > maxScore) { maxScore = score; matchedEmployeeId = entry.getKey(); } } // 4. 判定打卡结果 if (maxScore < 92) { return Result.error("识别失败,相似度不足"); } // 5. 写入考勤流水 AttendanceRecord record = new AttendanceRecord(); record.setEmployeeId(Long.valueOf(matchedEmployeeId)); record.setCheckTime(new Date()); record.setType(dto.getType()); // 1上班 2下班 attendanceService.save(record); // 6. 实时消息通知(WebSocket) websocketService.pushMessage(matchedEmployeeId, "打卡成功:" + DateUtils.nowTime()); return Result.success("打卡成功", record); }这段逻辑里有几个可以优化的性能点:
- 如果员工数量特别多(比如几千人),线性遍历比对会越来越慢。源码里的优化思路是按部门预先分桶,只比对同部门内的特征,实测单次识别可以稳定控制在150ms以内。如果你接手后人数过万,建议用向量数据库或Milvus做召回,但中小企业场景用不到。
- 阈值92分不是拍脑袋写的,虹软官方建议范围在75-95之间,92在城市办公环境、固定光线的室内打卡机上非常合适。如果你的场景里有室外强光或者口罩,建议把阈值降到85左右,但误识率会上升,需要平衡。
2.3 考勤规则与统计模块
打卡流水有了,剩下的问题就是怎么判定“这个月谁迟到了几次、谁缺卡了几次”。这块的代码不复杂,但特别容易踩坑,主要难点在于日期边界条件的处理。
// 考勤日结算核心逻辑 public void dailySettlement(LocalDate date) { List<Employee> employees = employeeService.listAllActive(); for (Employee emp : employees) { // 查询该员工当天所有打卡记录 List<AttendanceRecord> records = attendanceService.getRecords(emp.getId(), date); if (records.isEmpty()) { saveDailyResult(emp.getId(), date, "缺卡", "全天无打卡"); continue; } LocalTime workTime = getConfigTime("work_start_time"); LocalTime offTime = getConfigTime("work_end_time"); // 找最早签到和最晚签退 LocalTime firstCheck = records.stream() .map(r -> r.getCheckTime().toLocalTime()) .min(LocalTime::compareTo).orElse(null); LocalTime lastCheck = records.stream() .map(r -> r.getCheckTime().toLocalTime()) .max(LocalTime::compareTo).orElse(null); String status = "正常"; StringBuilder desc = new StringBuilder(); if (firstCheck != null && firstCheck.isAfter(workTime)) { status = "迟到"; desc.append("迟到").append(Duration.between(workTime, firstCheck).toMinutes()).append("分钟"); } if (lastCheck != null && lastCheck.isBefore(offTime)) { status = status.equals("迟到") ? "异常" : "早退"; desc.append("早退").append(Duration.between(lastCheck, offTime).toMinutes()).append("分钟"); } saveDailyResult(emp.getId(), date, status, desc.toString()); } }这里有一个非常典型的问题:打卡记录可能是“重复刷脸”产生的,比如员工上午打了一次卡,下午快下班又打了一次,中间午休时段可能还刷了一次脸。所以源码里不单靠“第一条”和“最后一条”来判断,而是先做聚类,比如中午11:30到13:00之间的记录不参与上下班判定。
按月统计的逻辑类似,不过要额外支持调休、请假、出差这些状态。源码里用一张attendance_daily_result表先记录每天的结果,月底再汇总成月报。这样设计的好处是查询速度快,不会每次报表计算都去扫所有原始流水。
2.4 报表与可视化模块
报表模块的作用是把考勤结果转化成HR能看懂的表格。源码用的是阿里开源的EasyExcel,支持百万行数据秒级导出。
// 月报导出接口 @GetMapping("/report/export") public void exportMonthlyReport(@RequestParam String yearMonth, HttpServletResponse response) { List<MonthlyReportVO> list = reportService.getMonthlyReport(yearMonth); // 设置导出文件头 response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(yearMonth + "考勤报表.xlsx", "UTF-8")); // EasyExcel导出 EasyExcel.write(response.getOutputStream(), MonthlyReportVO.class) .sheet("考勤月报") .doWrite(list); }前端用的是Vue2 + Element UI,通过Axios调用后端接口,展示打卡流水和统计结果。如果你不想要前端,直接暴露接口给钉钉/企微的内置应用也行,源码里接口都是标准RESTful,不存在耦合问题。
这块实际项目里容易忽略的是报表的安全权限:HR能看全公司,部门主管只能看本部门。源码里用了Spring Security + JWT做认证,同时基于部门ID做了数据权限过滤,需要在reportService.getMonthlyReport()里传入当前登录用户的部门范围。
3. 实操记录:把源码跑起来的完整步骤
理论拆解完,接下来是实际操作环节。我从零开始拉源码、改配置、跑服务,把完整的部署流程记录在下面,你照着操作就能跑起来。
3.1 环境准备与参数选择
这套源码的开发环境建议如下,注意版本号一定要对:
| 环境 | 版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 必须1.8,不要用17或21 |
| Maven | 3.6.3+ | 管理项目依赖 |
| MySQL | 5.7 或 8.0 | 5.7兼容性更稳 |
| Redis | 5.x+ | 缓存人脸底库 |
| Node.js | 14+ | 仅前端构建需要 |
为什么JDK必须8?因为虹软SDK的JNI封装是基于JDK8编译的,换成JDK11之后可能加载不到DLL。如果你非要用新版JDK,就得去拉SDK的源码自己重新编译,折腾半天不值当。
3.2 初始化数据库
源码里带了一个sql/attendance.sql文件,直接导入MySQL即可。
mysql -u root -p -e "CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p attendance < sql/attendance.sql导入完成后,你会看到这些核心表:
employee:员工表,字段包含face_feature和face_imgattendance_record:打卡流水表,保存每一次刷脸记录attendance_daily_result:每日考勤汇总表attendance_rule:考勤规则配置表(上下班时间、迟到阈值等)sys_user:系统用户表(管理员和HR账号)
这是我踩过的一个坑:表结构和实体类没对齐,导致数据库查不到数据但代码里对象却是空的。因为MyBatis-Plus默认开启了驼峰映射,如果表字段是face_feature、实体字段是faceFeature,没配置map-underscore-to-camel-case时会找不到字段。配置文件里一定要加:
mybatis-plus: configuration: map-underscore-to-camel-case: true3.3 配置application.yml
核心配置如下,注意替换成你自己的MySQL和Redis连接信息:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 arcsoft: app-id: your-app-id sdk-key: your-sdk-key lib-path: /path/to/libs虹软SDK的app-id和sdk-key需要去虹软开放平台免费申请,大概一个工作日就能下来。申请的时候注意选“人脸识别”产品,别选成“活体检测”了。lib-path指向你存放SDK动态库的目录,Linux下是.so文件,Windows下是.dll。
3.4 运行源码
项目根目录执行:
mvn clean package -DskipTests java -jar target/attendance-system.jar启动成功后,控制台会输出SpringBoot的Logo和端口号。接着启动前端(源码里前端模块在front/目录下):
cd front npm install npm run serve然后浏览器访问http://localhost:8081(前端默认端口,后端是同一个SpringBoot应用托管静态资源时就直接8080),用管理员账号登录,就能看到系统主界面了。
3.5 功能验证
登录系统后,建议按这个顺序做验证:
- 新建一个部门,比如“技术部”
- 在员工管理里添加一名员工,上传一张正脸照片(照片要求:光线充足、无遮挡、接近证件照)
- 回到首页,点击“人脸打卡”,用摄像头拍一张当前人脸的图片上传
- 观察返回结果:如果相似度足够,会提示“打卡成功”,同时考勤流水里会出现一条记录
- 到考勤报表模块,选择本月,看这条打卡记录是否被正确计入“正常”或“迟到”
我实测下来,这套流程在公司内网环境下,从拍照到展示打卡成功,整体耗时约300ms,完全满足日常使用。
4. 常见问题与排坑实录
这部分是全文中含金量最高的地方,我把实际操作中遇到的最典型的几个问题整理出来,每个都附上排查思路和最终解决方案。
4.1 SpringBoot启动后SDK加载失败
现象:项目启动时提示Failed to load arcsoft lib或Native library not found。
排查步骤:
- 确认
arcsoft.lib-path路径下是否真的有.dll或.so文件,注意区分Windows和Linux - 确认App ID和SDK Key是否填反了,各平台分配的Key类型不同,人脸识别SDK需要用
PROC_KEY而不是DEV_KEY - 确认动态库的位数和JDK位数一致,不要64位JDK配32位的DLL
我的解决方案:在Linux服务器上遇到这个问题最多,原因是缺少libgomp.so.1依赖库,执行yum install -y libgomp就解决了。Windows上则比较少见,多半是路径分隔符写成了单反斜杠,建议统一用正斜杠或者双反斜杠。
4.2 人脸识别相似度偏低,明明是同一个人却提示“识别失败”
现象:注册时上传的照片能识别,但实际打卡时用摄像头实时帧识别,相似度只有70多分。
根本原因:注册照片和打卡帧的光线、角度、清晰度差异过大。注册照是室内灯光下的证件照,打卡帧可能是逆光、低头或者手机照片。
解决方案:
- 在注册员工时增加“多角度质量校验”。源码里有一个
FaceQualityChecker,会检查图片亮度是否过暗、人脸占比是否过小,不达标的直接拒绝注册 - 引导用户注册时使用偏正脸、偏白平衡的照片,避免用美颜滤镜过重的照片
- 打卡阈值适当降低到85分,或者升级算法策略:连续两帧都超过80分就认为识别通过,避免单帧误判
4.3 Redis缓存和数据库数据不一致
现象:员工A删除后,打卡时还能识别到A的信息,并且提示“识别成功但员工已停用”。
原因:删除员工时只删了MySQL,没有同步清理Redis里存的face:xxx键。
解决方案:源码里在删除员工的Service方法中补充了缓存清理逻辑:
@DeleteMapping("/employee/{id}") public Result deleteEmployee(@PathVariable Long id) { // 1. 删除数据库记录 employeeService.removeById(id); // 2. 删除Redis中的特征缓存 redisService.delete("face:" + id); // 3. 从底库哈希中移除 redisService.hashDelete("face_db", String.valueOf(id)); return Result.success("删除成功"); }这也是一个通用教训:任何涉及缓存的项目,只要数据变更,第一优先级永远是“先更新数据库,再删缓存”,顺序不能反。如果先删缓存后更新数据库,期间有请求进来就会把旧数据写回缓存,产生脏数据。
4.4 高并发打卡场景下的性能瓶颈
现象:上下班高峰,公司有200人同时打卡,接口出现超时。
分析:人脸比对是CPU密集型操作,单线程处理一个请求大约要100ms,200个请求串行处理就要20秒,明显不可接受。
解决方案:
- 使用
CompletableFuture异步化:打卡流程拆分出“特征提取”和“特征比对”,两者都提交到线程池并行执行,线程数配置为CPU核数的2倍 - 优化比对策略:底库按部门分桶,先确定员工所在部门(可以从打卡设备推断),只比对同部门内的特征,减少比对次数
- 配置HikariCP连接池最大连接数,避免高并发下数据库连接被占满
- 升级硬件:换4核8G以上的服务器,人脸比对纯CPU计算,核心数越多越稳
// 异步比对核心代码 private ExecutorService executor = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() * 2); public CompletableFuture<Double> compareAsync(FaceFeature current, FaceFeature db) { return CompletableFuture.supplyAsync(() -> faceService.compareFeature(current, db), executor); }经过这波优化,200人同时打卡的场景下,P99延迟能控制在500ms以内,不再出现超时。
4.5 数据库字段类型导致日期处理异常
现象:报表月份过滤条件无效,查出来一直是本月所有数据。
原因:attendance_record表中check_time字段用了timestamp类型,MyBatis查询时传入LocalDate只能匹配到“当天零点”,<=和>=边界判断出错。
解决方案:查询时包装成日期范围,用DateTime接收,SQL中显式转换:
<select id="getRecordsBetween" resultType="AttendanceRecord"> SELECT * FROM attendance_record WHERE employee_id = #{employeeId} AND check_time >= #{startTime} AND check_time <= #{endTime} </select>这个坑很隐蔽,排查时你会发现单条记录明明在时间范围内,但查询结果就是为空。建议以后所有涉及日期的字段,都统一把范围条件“开始时间是当天00:00:00,结束时间是次日23:59:59”传到SQL里。
5. 从源码到生产:部署和二次开发的几个建议
如果你不是只为了看源码,而是真的要上线这套系统,有几个细节值得多花心思。
5.1 用Docker做部署
源码里附了Dockerfile和docker-compose.yml,一行命令就能把MySQL、Redis、后端应用全部编排起来:
FROM openjdk:8-jdk-alpine COPY target/attendance-system.jar /app/app.jar COPY libs/ /app/libs/ WORKDIR /app ENTRYPOINT ["java", "-jar", "app.jar", "--spring.config.location=/app/config/application.yml"]version: '3' services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: root volumes: - ./sql:/docker-entrypoint-initdb.d redis: image: redis:5 app: build: . ports: - "8080:8080" depends_on: - mysql - redis注意libs/目录必须跟着镜像走,否则SDK加载不到动态库,所有识别功能都会挂。我踩过这个坑:配置了环境变量但Dockerfile里忘了COPY,结果容器启动成功但人脸识别全部失败,排错花了一个下午。
5.2 二次开发的方向
如果你拿这套源码做毕业设计或者公司内部项目,以下几个方向最容易出彩:
- 活体检测:虹软SDK本身支持红外摄像头活体检测,但目前源码只接入了普通摄像头RGB识别,可以把活体检测开启,防止有人用照片打印替打卡
- 钉钉/企微通知:把打卡结果实时推送到钉钉工作通知,体验比网页端轮询好太多
- 多设备适配:源码里的打卡接口是用上传图片的,但生产场景往往是USB摄像头或闸机设备,需要对接设备SDK,把设备的帧直接送入识别接口
- 考勤申诉流程:如果员工对考勤结果有异议,可以加一个申诉流程:发起申诉 -> 主管审批 -> 修正考勤结果
5.3 关于安全合规
人脸数据属于敏感个人信息,上线前务必注意两点:
- 员工注册时必须做“知情同意”留痕,可以在注册表单里加一个协议勾选,记录员工工号+同意时间
- 数据库中的
face_img字段建议加密存储,至少做到服务端加密,避免数据库泄露后被人直接拿照片做人脸替换
源码目前是明文存的,如果做生产应用,这部分必须改造。
我个人在实际操作中最深的一点体会:这套系统的核心难点从来就不是“跑通”,而是“算准”。识别算法是成熟的,难的是把考勤判定规则与企业的真实排班完美对齐——尤其是处理跨天班次、轮班制、弹性工时这种复杂场景,代码里的边边角角才是真正的分水岭。如果是我自己再做一次选型,我依然会坚持SpringBoot做底座,因为生态成熟、招人好招、出了问题网上答案多。后续你往这个源码里加功能,优先考虑把打卡数据对接到主流HR系统里,让数据流完全不经过手工导出,那这系统的价值才算真正闭环了。
本文还有配套的精品资源,点击获取