做毕设这些年,见过太多人一上来就选“XX管理系统”,最后交上去的功能千篇一律:增删改查、登录注册、导个Excel就完了。但“基于人脸识别的出勤管理系统”这个题目不一样,它天然带着一个技术亮点——人脸识别,做完之后既能讲算法、又能讲业务、还能讲架构,毕业答辩时很能撑场面。这类系统本质上是“Spring Boot 后端 + 人脸识别算法 + 考勤业务规则”三件事的融合,只要把这三件事拆清楚,剩下的无非是表怎么建、接口怎么写、页面怎么调的问题。
这篇文章我会按我自己带项目时的完整思路走一遍:从需求拆解、技术选型、核心功能实现,到环境搭建、常见坑点,最后再聊聊答辩和扩展方向。全程不写虚的,全部是能直接“抄作业”的内容,适合正在做计算机毕业设计、或者想快速搭一套企业内部人脸考勤系统的朋友。
1. 先拆需求:这个项目到底在做什么
1.1 从传统打卡到人脸识别考勤,差别在哪
传统考勤常见的有这么几种:磁卡刷卡、指纹打卡、手机定位打卡,再原始一点的就是纸质签到表。磁卡和指纹最大的问题是“可以代打”或“容易复制”,指纹还会因为手指脱皮、沾水、油污导致识别失败。纸质签到就更不用说了,基本就是形式主义。人脸识别考勤解决的核心问题不是“快”,而是“人证合一”——脸长在你身上,别人替不了。
另外一个隐藏需求是“无接触”。这几年大家对公共设备的卫生问题越来越敏感,指纹打卡机每天几百人按同一个位置,确实不太让人放心。人脸识别只需要站在摄像头前,不需要碰任何东西,体验上更顺,也更符合现代办公场景。
从毕设角度来说,人脸识别还有一个特别大的优势:它同时覆盖了“算法应用”和“业务系统”两个层面。你可以只做本地摄像头调用、特征提取和比对,也可以换成在线人脸识别接口,技术上都有东西可讲。相比之下,普通的CRUD考勤系统很难写出这么深的内容。
1.2 系统模块划分与Spring Boot技术选型
这个项目如果要做得完整,一般拆成这么几个模块:
- 员工管理模块:部门、职位、员工的增删改查,员工状态(在职/离职)管理。
- 人脸库模块:员工人脸注册、人脸照片上传、特征值提取与更新、人脸状态禁用/启用。
- 考勤打卡模块:上班打卡、下班打卡、拍照上传、人脸比对、打卡结果返回。
- 考勤规则模块:上班时间、下班时间、迟到判定、早退判定、缺卡判定。
- 请假补卡模块:请假申请、审批、补卡申请,审批通过后修正考勤记录。
- 统计报表模块:按日、按月统计出勤情况,支持导出Excel。
技术栈方面,我一般建议这么搭配:
| 层次 | 技术选型 | 选择理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7 / 3.x | 生态成熟,上手快,适合快速搭建REST API |
| ORM | Spring Data JPA 或 MyBatis-Plus | 任选一个,JPA写CRUD更快,MyBatis-Plus复杂查询更直观 |
| 数据库 | MySQL 5.7 / 8.0 | 关系型数据,考勤记录、员工信息都很结构化 |
| 缓存 | Redis | 用于防重复打卡、缓存人脸特征库,提升并发能力 |
| 人脸识别 | OpenCV + Dlib 或在线API | 离线部署推荐Dlib特征向量,演示稳定;在线API适合快速开发 |
| 前端 | Vue 3 + Element Plus 或微信小程序 | PC端管理后台用Vue,移动端如果想加分可以做小程序 |
| 上传存储 | 本地磁盘 / MinIO / 云OSS | 毕设阶段本地存储即可,写清楚路径映射 |
这里说下为什么用Spring Boot而不是SSH或者Servlet原生开发。Spring Boot最大的价值是“开箱即用”,内置Tomcat、自动配置、Starter机制,一个人开发时少写大量配置。人脸识别本身已经够折腾了,如果你还把时间花在配置Spring XML、处理事务代理、纠结web.xml上,项目周期会拉得很难受。
人脸识别方案这里要重点说一下。如果你做毕设,我个人优先推荐离线方案,也就是Dlib + OpenCV。Dlib可以把人脸转换成128维的特征向量,比对时算欧氏距离或余弦相似度,效果在小型人脸库(几百人以内)下完全够用。在线API的好处是接入快、识别率更高,但演示时需要联网,而且部分免费接口有调用次数限制,万一现场网络出问题会很尴尬。离线方案无论有没有网都能跑,答辩演示更稳。
2. 核心功能设计与关键实现
2.1 员工人脸注册与特征库管理
人脸注册是整个系统的第一步,也是决定识别效果的关键。很多人第一次做会犯一个错误:直接把上传的照片存到数据库,打卡时再把现场照片和库里的照片做像素级对比。这种方法完全不可取,一是效率低,二是同一张脸在不同光线、角度下的像素差异非常大,准确率惨不忍睹。
正确的做法是:注册时把照片做“人脸检测 + 特征提取”,然后只保存提取出的特征向量。比如Dlib会把人脸映射成一个128维的float数组,你可以把这个数组转成JSON字符串或者用逗号分隔的文本存到数据库的TEXT字段里。打卡时同样提取现场人脸特征,再去库里找“距离最近”的一条,距离小于阈值就认为是同一个人。
注册流程建议做成这样:
- 前端调用摄像头拍照,或者上传一张正面照片。
- 后端收到图片后先做人脸检测,如果图中检测不到人脸,直接返回“未检测到人脸,请重新拍摄”。
- 检测到人脸后提取特征向量。
- 判断该员工是否已经注册过,如果注册过则覆盖更新(一般叫“人脸更新”而不是重复插入)。
- 保存人脸照片路径、特征向量、注册时间。
这里有个细节很多人会忽略:注册照片的质量直接决定识别准确率。建议在注册页面给出拍照指引,比如“正对摄像头、自然光、不要逆光、不要戴帽子口罩”。你可以在前端加一个简单的引导框,也可以在后端做图片清晰度检测。毕设阶段做个引导提示就够了,不用太复杂。
特征向量存储我见过不少写法,有的同学喜欢用BLOB二进制,有的用TEXT存JSON。两种都可以,但要注意:如果用TEXT存JSON,比对时需要反序列化,几百条数据没问题,几千条就会开始卡。建议在Service层做一个特征库缓存,比如启动时把员工编号和特征向量加载到内存,打卡比对时直接从内存里找,性能会好很多。
2.2 打卡识别流程与异常考勤判定
打卡是整个系统的高频操作,流程设计得好不好,直接影响用户体验。标准的打卡流程是这样:
- 用户打开打卡页,摄像头启动并实时预览。
- 点击“打卡”按钮,前端截取当前画面,把图片Base64编码后传给后端。
- 后端接收图片,调用人脸检测提取特征。
- 在特征库中做比对,计算相似度(或距离)。
- 如果匹配成功,查询该员工当天是否已有打卡记录。
- 如果没有记录,插入一条上班打卡记录;如果已有上班记录但无下班记录,则更新为下班打卡记录;如果上下班都打过了,返回“今日已打卡完成”。
- 根据考勤规则自动更新状态:迟到、早退、正常、缺卡。
这里有几个坑要提前排掉。
第一个坑是“打卡方向判定”。有些系统给用户提供一个“上班/下班”的单选按钮,让用户自己选。这样虽然简单,但非常不专业,因为用户有可能选错,而且很low。正确做法是服务端根据时间和当前已有记录自动判断。你只需要记住一个原则:同一员工同一天最多两条记录,第一次是上班,第二次是下班。如果用户早上8点打了卡,中午12点又打了一次,系统应该把第二次当作下班吗?不一定,但我建议你按简单规则处理:早于某个时间(比如12:00)的第二次打卡更新为下班,或者直接按业务规则去定。毕设阶段可以做成可配置参数。
第二个坑是“重复提交”。用户手抖点两下,或者摄像头卡顿导致请求重发,很容易出现同一天产生了多条上班记录。解决办法有两个:数据库层面加唯一索引,比如(employee_id, work_date)唯一,或者更细一点(employee_id, work_date, attend_type);接口层面用Redis的SETNX做幂等处理,同一个员工同一秒只能提交一次。这两个都加上,基本就不会出问题。
第三个坑是“迟到/早退/缺卡”的判定规则。这个最好做成配置表,而不是写死在代码里。比如:
# 考勤规则配置 attendance: work-start-time: "09:00:00" # 上班时间,晚于此时间打卡为迟到 work-end-time: "18:00:00" # 下班时间,早于此时间打卡为早退 late-threshold: 0 # 宽容时间,默认0表示不宽容后端每天可以用定时任务或懒计算方式,把某天的打卡记录和规则对比,自动更新状态。异常考勤判定规则可以参考这个表:
| 情况 | 判定条件 | 结果 |
|---|---|---|
| 迟到 | 第一次打卡时间 > 上班时间 + 宽容时间 | status = 迟到 |
| 早退 | 第二次打卡时间 < 下班时间 - 宽容时间 | status = 早退 |
| 缺卡 | 当天只有一条打卡记录 | status = 缺卡 |
| 正常 | 上下班都打且时间符合 | status = 正常 |
| 请假 | 有审批通过的请假单 | status = 请假 |
请假和打卡之间通常还有一条规则:如果某员工当天请假了,那即使没打卡也不能判定为缺卡。这个在报表统计时要注意。
2.3 请假补卡与考勤统计报表
考勤系统如果只有打卡,那其实只做了一半。真正让系统像个“系统”的,是请假、补卡和统计报表这些业务闭环。
请假模块建议设计成简单的两级审批:员工提交请假申请,填写开始时间、结束时间、请假类型(事假、病假、年假)、请假事由,直属领导审批。审批流程可以用一个简单的状态字段控制:0待审批、1通过、2驳回。后端提供三个接口:
POST /api/leave:提交申请。PUT /api/leave/{id}/approve:审批通过或驳回。GET /api/leave/my:查询本人请假记录。
补卡模块的作用是处理“忘了打卡”的情况。员工提交补卡申请,说明补卡日期和时段(上班还是下班),审批通过后由管理员手动补充一条打卡记录并标记为“补卡”。补卡记录最好和正常打卡记录区分开,用source字段标识是auto还是manual,否则算报表时容易混淆。
报表部分要能做这几个维度:
- 按日汇总:某天有哪些人迟到、早退、缺卡、请假。
- 按月汇总:某员工整月的出勤率、迟到次数、早退次数、请假天数。
- 按部门汇总:部门平均出勤情况。
我用过的实现方式是:attendance表里每天每个员工最多两条记录,再建一个attendance_daily汇总表,每天定时任务把所有员工的当天状态刷进去。查询月汇总时直接GROUP BY employee_id聚合,速度很快。如果不想用定时任务,也可以在查询时实时计算,数据量不大时问题不大。
3. 从零实操:环境搭建到系统跑通
3.1 开发环境与工程初始化
我建议的本地环境是:
- JDK 8 或 17(Spring Boot 2.7用JDK8没问题,3.x需要JDK17+)
- Maven 3.6+
- MySQL 5.7 / 8.0
- Redis 5.x/6.x(做防重缓存用,没有Redis可以先用本地HashMap顶替)
- IDEA 2022/2023
- Node.js 16+(前端选Vue时需要)
新建Spring Boot项目时,依赖我通常只加这几个:spring-boot-starter-web、spring-boot-starter-data-jpa或mybatis-plus-boot-starter、mysql-connector-j、spring-boot-starter-data-redis、lombok。文件上传不需要额外的依赖,Spring MVC本身就支持MultipartFile。
核心配置文件application.yml大概长这样:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/face_attendance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true servlet: multipart: max-file-size: 10MB max-request-size: 20MB redis: host: localhost port: 6379 database: 0 face: threshold: 0.5 # 人脸相似度阈值,大于该值认为匹配 upload-path: ./uploads/ # 人脸照片存储路径 camera-width: 640 # 摄像头采集宽度,用于前端提示face.threshold这个参数要特别注意:它直接决定比对结果的松紧程度。阈值调太高会认不出人,调太低会认错人。Dlib的欧氏距离一般小于0.5认为是同一个人,但不同模型、不同光照条件要实测调整。我常用的办法是让管理员在后台设置一个“调试模式”,显示当前比对的相似度分数,再根据实际调整阈值。
3.2 数据库表设计:员工、人脸特征、考勤记录
这个项目的核心表不用太多,五六张就够了。我最常用的建表脚本如下,你可以直接用:
CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) UNIQUE NOT NULL COMMENT '工号', name VARCHAR(50) NOT NULL, department_id BIGINT NOT NULL, position VARCHAR(50), status TINYINT DEFAULT 1 COMMENT '1在职 0离职', password VARCHAR(100) COMMENT '登录密码', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE face_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT UNIQUE NOT NULL, face_feature TEXT NOT NULL COMMENT '人脸特征向量JSON', face_image_url VARCHAR(255) COMMENT '注册照片路径', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT NOT NULL, work_date DATE NOT NULL, check_in_time DATETIME COMMENT '上班打卡时间', check_out_time DATETIME COMMENT '下班打卡时间', status VARCHAR(20) DEFAULT '正常' COMMENT '正常/迟到/早退/缺卡/请假', source VARCHAR(20) DEFAULT 'auto' COMMENT 'auto自动 manual补卡', face_image_url VARCHAR(255) COMMENT '打卡照片', UNIQUE KEY uk_emp_date (employee_id, work_date) ); CREATE TABLE leave_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, employee_id BIGINT NOT NULL, leave_type VARCHAR(20) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, reason VARCHAR(255), status TINYINT DEFAULT 0 COMMENT '0待审批 1通过 2驳回', approve_remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_key VARCHAR(50) UNIQUE NOT NULL, rule_value VARCHAR(50) NOT NULL, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );员工表和face_info表是一对一的关系,face_feature字段用TEXT存特征向量。有人可能会问为什么不单独建一张“人脸照片表”,把历史照片都存下来?如果你的系统以后要支持“人脸换绑”追溯,那确实应该再建一张face_log表记录历史版本。毕设阶段可以不用,但我在实际项目中会加,因为员工换照片是很常见的需求。
考勤表这里的唯一索引uk_emp_date (employee_id, work_date)非常关键,它是防重复打卡的最后一道防线。注意:每天每个员工最多一条考勤汇总记录,上班和下班时间是两个字段,而不是一天两条记录。这样设计的好处是查月度报表时一个员工一个月就几十条数据,聚合起来很快。
3.3 核心接口实现:注册、打卡、查询
后端接口的设计,我习惯按“Controller薄、Service厚”的原则来写。Controller只负责接参、调Service、返回统一结果,业务逻辑全部下沉到Service层。
人脸注册接口大致是这样:
@RestController @RequestMapping("/api/face") public class FaceController { @Autowired private FaceService faceService; @PostMapping("/register") public Result register(@RequestParam("employeeId") Long employeeId, @RequestParam("image") MultipartFile image) { // 1. 保存上传图片到本地 String imageUrl = fileStorage.save(image); // 2. 调用人脸服务检测并提取特征 FaceDetectResult result = faceService.detectAndExtract(image); if (!result.isDetected()) { return Result.error("未检测到人脸,请重新拍摄"); } // 3. 保存或更新人脸特征 faceService.saveOrUpdateFace(employeeId, result.getFeature(), imageUrl); return Result.ok("注册成功"); } }人脸检测和特征提取的Service层,可以用一个接口把具体实现隔离开:
public interface FaceRecognitionService { // 从图片字节中检测人脸,返回特征向量 FaceDetectResult detectAndExtract(byte[] imageBytes); // 在特征库中查找最相似的人 FaceMatchResult search(byte[] imageBytes, List<FaceFeatureDTO> featureList); }这样设计的好处是,你以后想从Dlib换成在线API,只需要重新实现这个接口,Controller和业务代码不用动。这也是答辩时一个不错的加分点——你可以在“系统设计”环节讲清楚“面向接口编程”的思路。
打卡接口的核心逻辑,我建议这样写:
@PostMapping("/checkin") public Result checkin(@RequestParam("image") MultipartFile image, @RequestParam("empNo") String empNo) { // 1. 防重复提交:Redis里以 employeeId + date 为key做幂等 String key = "attendance:" + empNo + ":" + LocalDate.now(); Boolean first = stringRedisTemplate.opsForValue() .setIfAbsent(key, "1", Duration.ofSeconds(5)); if (Boolean.FALSE.equals(first)) { return Result.error("操作太频繁,请稍后重试"); } // 2. 人脸比对 FaceMatchResult matchResult = faceService.match(image); if (matchResult.getScore() < threshold) { return Result.error("人脸识别失败,请正对摄像头"); } // 3. 查询员工当天考勤记录 Attendance attendance = attendanceRepository .findByEmployeeIdAndWorkDate(matchResult.getEmployeeId(), LocalDate.now()) .orElse(null); // 4. 根据记录情况判断是上班还是下班 if (attendance == null) { attendance = new Attendance(); attendance.setEmployeeId(matchResult.getEmployeeId()); attendance.setWorkDate(LocalDate.now()); attendance.setCheckInTime(LocalDateTime.now()); attendance.setCheckOutTime(null); // 判断是否迟到 if (isLate(attendance.getCheckInTime())) { attendance.setStatus("迟到"); } } else if (attendance.getCheckOutTime() == null) { attendance.setCheckOutTime(LocalDateTime.now()); // 判断是否早退 if (isEarly(attendance.getCheckOutTime())) { attendance.setStatus("早退"); } else if (!"迟到".equals(attendance.getStatus())) { attendance.setStatus("正常"); } } else { return Result.error("今日已打卡完成"); } attendanceRepository.save(attendance); return Result.ok("打卡成功", attendance); }这里有几个细节我想单独说一下。
第一,setIfAbsent是Redis实现幂等的经典姿势,同一个员工5秒内只能发起一次打卡请求。但注意,Redis的key过期时间不要太长,否则用户如果第一次识别失败,第二次要等很久。5秒是一个比较合适的值。
第二,代码中先做人脸比对再查库,还是先查库再比对?我试过两种顺序。先比对可以避免无关请求打到数据库,但比对的耗时通常比查库更久。更合理的顺序是:先拿empNo查到员工ID,再查当天考勤记录是否已满,如果已满直接返回,不调用人脸识别。这样能省很多无用的AI计算。当然,如果员工编号是用户手动输入的,还要考虑造假问题:一个人拿着别人的工号打卡,然后人脸比对不通过,会被拒绝。这个逻辑你要在Service里想清楚。
第三,打卡结果的status是动态更新的。员工早上8:50打卡时状态是“正常”,但下午下班如果早退,就会把状态改成“早退”。所以每天开始时可以用定时任务将所有员工的缺卡状态刷一遍,但最终状态还是以下班时的判断为准。
查询接口就比较简单了:
@GetMapping("/records") public Result records(@RequestParam Long employeeId, @RequestParam String startDate, @RequestParam String endDate) { List<Attendance> list = attendanceRepository .findByEmployeeIdAndWorkDateBetween(employeeId, LocalDate.parse(startDate), LocalDate.parse(endDate)); return Result.ok(list); }前端用Vue写一个简单的表格页,下拉选员工、选日期范围、点查询、表格展示,这个模式几乎所有管理后台系统都一样,不再展开。
3.4 前端页面与联调部署
前端部分我推荐直接做两个端:一个PC后台管理端(Vue3 + Element Plus),负责员工管理、考勤查询、报表统计;一个打卡端(可以是纯粹的HTML页面,也可以做成小程序)。如果时间不够,至少要把打卡端做成一个能调用摄像头的网页。
打开摄像头用浏览器原生的getUserMedia就行,不用任何插件:
const constraints = { video: { width: 640, height: 480 } }; navigator.mediaDevices.getUserMedia(constraints) .then(stream => { video.srcObject = stream; }) .catch(err => { console.error("摄像头调用失败:", err); });拍照时把视频帧画到Canvas上,再转成Base64或Blob发送给后端。注意浏览器安全策略:getUserMedia只在localhost或HTTPS环境下可用。如果部署到服务器,没有HTTPS的话摄像头会无法打开,这个问题经常有人踩。
前端调接口时,文件上传用FormData格式即可:
const formData = new FormData(); formData.append("image", blob, "face.jpg"); formData.append("empNo", this.empNo); axios.post("/api/checkin", formData, { headers: { "Content-Type": "multipart/form-data" } }).then(res => { if (res.data.code === 200) { this.message = res.data.msg; } });部署方面,毕设项目最省事的方案是:后端打成jar包跑在服务器上,前端build后把dist目录交给Nginx托管,再配置一个反向代理把/api转发到后端的8080端口。这样整个系统占用的资源很少,一台2核4G的云服务器完全够用。如果只是本地演示,IDEA直接跑后端,Vue用开发模式启动,浏览器访问localhost就行,完全不用考虑跨域问题。
4. 常见问题与排查接锅实录
4.1 人脸识别率不稳定,优先排查这几个点
人脸识别效果不好,99%不是算法的问题,是输入条件的问题。我排查的顺序是:先看摄像头画面是否清晰,再看光线,再看角度,最后才看算法参数。
最常见的情况是背光。办公室窗户在身后,脸是黑的,摄像头拍出来一片阴影,再好的模型也识别不了。解决办法是在打卡区域设置一个补光灯,或者要求用户稍微侧身避免逆光。如果是笔记本自带摄像头,画质通常比较差,建议外接一个USB摄像头,几十块钱的就能用。
口罩问题也要考虑。如果你的人脸识别用的是“全脸特征”,那口罩会直接把下半张脸特征挡掉。应对思路有两个:一是注册时就要求用户拍“半脸”照片(只露眉眼),打卡时也要求不摘口罩,比对时只用上半脸特征——这个对算法要求比较高;二是取消口罩识别,要求摘口罩打卡。企业场景下我见过两种诉求都有,毕设阶段如果要展示效果,建议提前把“口罩会影响识别”写在说明文档里,别在现场演示时翻车。
阈值调节的话,纯离线方案可以先做一个测试页面,输入两张照片,显示相似度分数,然后根据分数来设定阈值。我的经验是:阈值设在0.4到0.6之间(Dlib欧氏距离算法)比较平衡,0.5最常见。
4.2 照片存储越用越慢,怎么优化
很多同学会直接把打卡照片转成Base64字符串存数据库,这是性能杀手。一张几百KB的图片转成Base64后体积会膨胀约三分之一,存进数据库后,每个月几万条考勤记录,数据库会越来越大。
我的做法是:数据库只存图片路径,图片文件放本地磁盘或对象存储。存储路径按照/uploads/2025/06/01/xxx.jpg这种格式组织,后端通过静态资源映射或Nginx直接访问。如果想减少存储,还可以在上传时做压缩,把图片缩小到320x240左右,人脸识别完全够用,一张照片只有几十KB。
还有一个坑是“打卡查询时顺便返回了图片的Base64”,导致接口响应非常慢。应该把图片URL返回给前端,前端用<img src="/uploads/xxx.jpg">懒加载。后端也要做好静态资源的访问控制,否则任何人都能通过URL遍历查看员工照片,这个属于隐私问题,要注意。
人脸特征库也是一样,如果员工有几万人,每次打卡都全量比对是不现实的。这时候就需要给特征库加索引或者用向量数据库,比如Milvus、Faiss。但毕设场景通常只有几十到几百个员工,全量比对完全没问题,不用过度设计。
4.3 同一张脸重复打卡,如何防重
这个问题我在测试时经常遇到:一个人站在摄像头前多停留几秒,前端可能自动或手动重复触发了好几次请求,结果一天产生了多条打卡记录。防重不能只靠前端按钮置灰,后端必须兜底。
后端防重有两条防线:
- 数据库唯一索引:
(employee_id, work_date)唯一,重复插入会直接报错,这是最后一道防线。 - Redis幂等:打卡请求进来先
SETNX,同一个员工同一秒只能处理一次。这里要注意,如果Redis不可用,系统要降级到本地锁或者直接放行,不能因为缓存挂了导致所有人都没法打卡。
事务问题也要注意。打卡时先查记录、再判断、再更新,这个逻辑在并发下如果不加锁,会出现“两个请求都查到没有记录,然后都插入上班记录”的情况。Redis幂等可以挡住大部分并发,但更稳妥的做法是打卡Service方法上加@Transactional,必要的时候用数据库悲观锁SELECT ... FOR UPDATE。毕设演示时并发量不大,Redis加唯一索引已经足够。
4.4 毕设答辩前,怎么把项目讲出亮点
答辩时不要上来就讲“我用了Spring Boot,做了增删改查”,这个开头毫无亮点。我建议按“业务痛点 -> 解决方案 -> 关键实现 -> 数据验证”这条线来讲。
开场你可以这样说:“现在很多企业还在用指纹或纸质签到考勤,存在代打卡、效率低、数据统计滞后的问题。本系统基于人脸识别技术,结合Spring Boot搭建了一套从员工人脸注册、自动打卡、异常考勤判定到月度报表的完整闭环。” 这就把业务价值和核心技术点都点到了。
接下来重点讲两个地方。第一个是人脸识别的技术选型:为什么用特征向量而不是直接比像素?你在Dlib或其他库的特征提取原理上能说出“128维向量”“欧氏距离相似度”这些术语,答辩老师就会觉得你有深度。第二个是你在防重复打卡上做的设计:Redis幂等、数据库唯一索引、事务控制,把这三层讲出来,老师能看出你考虑了真实生产环境的问题,而不是只写了个Demo。
演示时有一个忠告:提前把测试员工的人脸注册好,别在演示现场现注册。现场光线、网络、摄像头驱动都是不可控因素,一旦注册失败或识别不出来,整个演示节奏就乱了。我习惯准备两个测试账号,一个已注册成功的人脸,一个备用;再准备一张清晰的证件照作为“兜底”,万一摄像头识别多次失败,可以直接选文件上传来演示。
最后再补充一点:考勤系统的扩展空间其实很大。如果你时间充裕,可以考虑把PC端打卡换成微信小程序,或者增加基于定位的外勤打卡、自动排班、加班审批流程。做一个“小程序端人脸打卡 + 管理后台”的完整方案,整体的完整度和工作量明显上一个档次,在答辩时的评价也会更高。
我自己的实际体会是,这种“算法 + 业务”结合的项目,最忌讳的就是把算法和业务耦合得太死。写代码时把人脸识别单独拆成一个API接口,考勤业务完全不知道底层用的是Dlib还是在线API,后期维护、换算法、扩展功能都很舒服。你把这个思路想清楚了,这个项目就真正跑通了。