news 2026/9/14 2:49:46

Python+OpenCV人脸识别签到系统:客户端服务端架构与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+OpenCV人脸识别签到系统:客户端服务端架构与工程实践

简介:一套基于Python与OpenCV的人脸识别签到管理系统完整源码,面向毕业设计、期末大作业及课设实践,适合需要快速搭建人脸考勤项目并学习客户端与服务端双重架构的开发者。系统功能覆盖人脸注册、实时检测、身份识别、签到记录管理,界面简单直观,可灵活调整以适配不同场景。资源包共97个文件、约14.67MB,主要包含Python源码(26个py及39个pyc)、2个演示视频、课程设计报告(docx+pptx)、16个zbak备份、数据库与配置文件(db/ini/xlsx/pkl)以及Haar人脸检测分类器xml,目录清晰,便于二次开发。目前已有40人学习下载。通过这套源码可掌握OpenCV人脸检测与识别应用、签到系统的客户端与服务端交互流程、界面搭建及数据持久化方案;配套设计报告和演示视频辅助理解整体架构,可直接作为课程报告或答辩资料参考,是一份完整实用的毕业设计项目。

1. 人脸识别签到系统,真正的门槛不在识别而在工程

“Python + OpenCV 人脸识别签到系统”这类项目,在 CSDN、GitHub 上并不少见,但你翻开源码会发现一个规律:大多数都把精力耗在了算法层——怎么检测人脸、怎么提取特征、怎么算相似度。真正走到生产环境就会意识到,识别准确率只是入场券,签到系统能不能落地,取决于两件事:一是客户端和服务端怎么拆分,二是签到记录怎么保证不丢、不重、可追溯。

这也正是本文要讲清楚的。标题里的“客户端和服务端”不是摆设——摄像头采集、人脸检测、特征比对需要在客户端本地完成,而签到记录、人员库、考勤统计则需要一个服务端来统一管理。单机版的人脸签到 Demo 很好写,摄像头一开、人脸一对、写入 Excel 就算完事。但要把它做成一个“管理系统”,客户端负责识别、服务端负责数据,才是一个从业者会交付的架构。

本文沿着“客户端识别 + 服务端管理”这条主线,把整个链路拆开讲:人脸注册和签到怎么设计、客户端与服务端之间怎么通信、识别参数怎么调、签到记录怎么防重,最后落在一个日结统计的技巧上。无论你是拿这个项目做毕业设计,还是想在现有考勤系统里接入人脸签到,都能直接照着做。

2. 客户端与服务端分离:人脸签到系统的骨架设计

2.1 为什么客户端必须本地跑 OpenCV,而不是把图片传到服务端识别

很多人第一次设计人脸签到系统时,直觉是:客户端负责拍照,把人脸图片传给服务端,由服务端用 OpenCV 做识别。这在局域网内看似可行,但存在三个问题:

  • 摄像头帧率瓶颈:OpenCV 的VideoCapture.read()读取一帧约 10-30ms,加上人脸检测的耗时,本地处理一帧大约 50-100ms。如果走网络传输,光是一张 JPEG 编码 + 上传就要 30-50ms,还不算服务端排队时间,整个识别链路会退化到 2-3 秒一帧。
  • 服务端状态爆炸:人脸签到本质是持续的视频帧流,如果每一帧都上传,服务端要同时维护所有客户端的连接状态,还要防止并发过高导致的人脸库比对排队。
  • 离线可用性:客户端如果断网,连签到都做不了,这在考勤场景里是致命的。

所以,客户端负责识别,服务端负责数据,这个边界必须清晰。识别链路中,人脸检测、特征提取、特征比对全部在客户端本地完成;只有签到成功后的“签到记录”才通过网络传到服务端。这样一来,网络只在签到那一瞬间产生一次请求,压力极小,即使摄像头一直开着,也不会对服务端造成负担。

2.2 双端通信协议:用 JSON over HTTP 还是自定义 TCP

客户端和服务端之间传什么数据,是架构设计的第一步。最常见的做法是 HTTP + JSON,因为实现简单、调试方便,而且跟 Web 管理端天然兼容。

客户端需要向服务端发送两类请求:

  1. 人员库同步:客户端启动或定时向服务端拉取最新的人脸特征库。注意这里拉取的“特征”,不是原始图片,而是 OpenCV 人脸识别器(如 LBPH)训练出的特征模型文件,或者每张人脸的特征向量。
  2. 签到上报:客户端识别成功后,把员工 ID、签到时间、现场照片路径上报给服务端。

为什么不用自定义 TCP 协议?因为人脸签到系统的通信频率极低——每人每天只有几次签到请求,用 HTTP 足够,而且 Python 标准库里的http.clientrequests就能搞定,不需要引入额外的网络框架。只有在需要视频流实时传输的场景(比如远程门禁监控),才需要考虑 RTSP 或自定义 TCP 流协议。

2.3 服务端核心数据表:人员表、签到表、设备表的设计

服务端的第一件事是把数据结构定下来。用 SQLite 还是 MySQL?这取决于规模。如果只是毕设或小团队内部使用,SQLite 足够,部署简单;如果考勤人数超过 500 人,建议直接上 MySQL,避免后续迁移。

我建议至少建三张表:

人员表(person):主键 ID、工号、姓名、部门、人脸特征(base64 编码的 feature 向量)、状态(在职/离职)、创建时间。

签到表(attendance):主键 ID、人员 ID、签到时间、签到类型(上班/下班)、现场照片路径、设备编号、是否异常(迟到/早退/正常)。

设备表(device):主键 ID、设备编号、设备名称、最后在线时间、所在位置。

签到表必须包含“设备编号”字段,用来区分不同客户端的签到来源。如果将来有多个门禁点,可以通过这个字段判断员工在哪个位置打卡。

CREATE TABLE person ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, department VARCHAR(50), face_feature TEXT, -- base64 编码的特征向量或模型路径 status TINYINT DEFAULT 1, -- 1 在职,0 离职 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL, check_time DATETIME DEFAULT CURRENT_TIMESTAMP, check_type TINYINT DEFAULT 1, -- 1 上班,2 下班 photo_path VARCHAR(255), device_no VARCHAR(50), status TINYINT DEFAULT 1, -- 1 正常,2 迟到,3 早退 FOREIGN KEY (person_id) REFERENCES person(id) ); CREATE TABLE device ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_no VARCHAR(50) UNIQUE NOT NULL, device_name VARCHAR(100), last_seen DATETIME, location VARCHAR(100) );

这里有个关键点:person.face_feature存的是特征向量而不是图片路径。LBPH 人脸识别器训练后生成的是模型文件,而 OpenCV 的face.recognize()返回的是置信度。更多时候我们会用face_recognition库提取 128 维特征向量,这个向量可以直接 base64 存入数据库,客户端拉取后在内存里还原成 numpy 数组做比对。

2.4 客户端启动流程:拉取模型、打开摄像头、进入识别循环

客户端初始化顺序如果不对,会出现摄像头打不开、模型加载失败等灵异问题。我一般按这个顺序走:

import cv2 import numpy as np import requests import json import base64 # 1. 从服务端拉取人员特征库 def fetch_face_db(server_url): resp = requests.get(f"{server_url}/api/face_db", timeout=10) data = resp.json() face_encodings = [] person_ids = [] for item in data["data"]: enc = np.frombuffer(base64.b64decode(item["feature"]), dtype=np.float64) face_encodings.append(enc) person_ids.append(item["id"]) return face_encodings, person_ids # 2. 初始化摄像头 def init_camera(camera_id=0, width=640, height=480): cap = cv2.VideoCapture(camera_id) cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) # 宽度设小一点,提升帧率 cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) if not cap.isOpened(): raise RuntimeError(f"Camera {camera_id} open failed") return cap # 3. 主识别循环(脱敏:实际使用时请接入真实模型) def recognition_loop(cap, face_encodings, person_ids): while True: ret, frame = cap.read() if not ret: continue # 这里调用人脸检测 + 特征提取 + 比对 # 比对命中后,将 person_id 写入签到队列 pass

fetch_face_db用的是requests.get,超时设置为 10 秒。如果服务端不可用,客户端不能崩溃,应该捕获异常并提示“服务端连接失败,请检查网络”。

摄像头的分辨率不一定要用默认的 1280x720。帧率优先的场景,640x480 通常是识别精度和性能的平衡点。分辨率调低后,LBPH 或face_recognition的检测速度会有明显提升,而签到场景下人脸通常离摄像头很近,清晰度损失可以接受。

主循环里有一个容易被忽视的点:不要把比对结果直接写在循环里。识别命中后要进入一个“冷却期”,比如 2 秒内不重复签到,否则摄像头连续抽帧,每次命中都会上报一条记录,签到表会被塞满。这个防重逻辑可以放在客户端,也可以放在服务端,后面在第 4 章展开讲。

3. 人脸注册与签到识别:OpenCV 识别的完整实现

3.1 人脸注册的两种做法:单张图片提特征 vs. 多帧采样训练模型

人脸注册是整个系统的起点。员工第一次使用系统时,需要在客户端录入人脸。常见的做法有两种:

做法一:单张图片提取特征(基于 face_recognition)

def register_face(image_path, person_id): import face_recognition image = face_recognition.load_image_file(image_path) encodings = face_recognition.face_encodings(image) if not encodings: raise ValueError("No face detected in image") feature = encodings[0] # 128 维向量 feature_b64 = base64.b64encode(feature.tobytes()).decode("ascii") # 上传到服务端,关联 person_id

这种做法的优点是快,一张照片即可完成注册,而且face_recognition库底层用的是 dlib 的人脸关键点检测,对姿态变化有一定容忍度。缺点是单张照片的鲁棒性差——如果这张照片恰好模糊、侧脸、光线奇怪,后续签到时误识率会偏高。

做法二:多帧采样 + LBPH 训练(基于 OpenCV 自带的识别器)

第二种方法是用 OpenCV 的LBPHFaceRecognizer,拍摄 10-20 帧人脸图片,每帧裁剪出人脸区域,标注 ID,然后训练生成一个模型文件。这种方法的优势在于,LBPH 模型天然把“同一个人的不同姿态”做进了模型里,识别时对光线、角度变化的容忍度更高。

# 人脸对齐和裁剪(简化版) def detect_and_crop_face(frame): face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80) ) if len(faces) == 0: return None x, y, w, h = faces[0] return gray[y:y+h, x:x+w] # 返回灰度图,LBPH 输入要求灰度

在实际项目中,“多帧采样 + 模型训练”更贴近商用方案。但这里有个效率问题:如果公司有几百号人,每人都几十帧图片,训练时间会随模型尺寸线性增长。面对这种规模,更推荐的做法是把每个人当成一个独立的特征向量存入数据库——这就是第 2 章里face_feature字段的由来。

3.2 识别循环里的三个参数:detectMultiScale 的 scaleFactor、minNeighbors、minSize

detectMultiScale是人脸检测的入口,它的三个参数直接决定了检测的召回率和误检率。这个函数用滑动窗口 + 级联分类器的方式扫描整张图像,参数含义如下:

scaleFactor:控制每次缩放图像的尺度,默认值是 1.1,表示每轮扫描图像缩小 10%。这个值越小,扫描次数越多,检测越慢,召回率越高;值越大,速度快但容易漏检。

minNeighbors:每个候选矩形需要保留的邻近矩形数。值越大,误检越少,但可能漏掉真实人脸。当签到摄像头距离人脸较近、背景较干净时,可以调到 5-6;如果场景复杂(比如背光、多人走动),可以降到 3,换取更高的召回率。

minSize:检测到的人脸最小尺寸,单位是像素。minSize=(80, 80)意味着小于 80x80 的人脸直接忽略。这个参数对性能影响很大,因为窗口扫描时最小的搜索窗口就是 minSize,尺寸设得越小,需要扫描的位置越多,帧率下降越明显。

这里有几个经验值:

场景scaleFactorminNeighborsminSize备注
门禁考勤(人距离摄像头 0.5-1.5 米)1.15(80, 80)默认配置,均衡型
闸机(人快速通过,画面中有移动)1.23(100, 100)提速度,降误检
弱光环境(有轻微噪声)1.056(80, 80)慢但稳定,避免漏检

人脸检测完成后,送进特征提取和比对环节的应当是裁剪后的灰度人脸图。如果直接送原始彩色图像,第一是特征提取计算量大,第二是部分识别器(如 LBPH)本身只接受灰度输入。注意灰度图预处理时最好做一次直方图均衡化(cv2.equalizeHist),用来减轻光线不均匀的影响。

3.3 特征比对与置信度阈值:欧氏距离阈值怎么定

face_recognition库中,两个人脸的相似度通过 128 维特征向量的欧氏距离来衡量。距离越小,越有可能是同一个人。

import numpy as np def match_face(unknown_encoding, known_encodings, threshold=0.45): distances = np.linalg.norm(known_encodings - unknown_encoding, axis=1) min_idx = int(np.argmin(distances)) if distances[min_idx] < threshold: return min_idx, distances[min_idx] return None, distances[min_idx]

threshold 的默认值在face_recognition库中通常取 0.45-0.5,但实际项目中不能照搬。这个值取决于注册照片的拍摄条件:

  • 同一摄像头、同一光线、同一角度下注册,欧氏距离通常在 0.25-0.35,阈值可以设得严格一些,比如 0.4,降低误识率。
  • 注册照片和签到场景差异大(比如注册用的是证件照,签到用摄像头实拍),阈值要放宽到 0.5-0.55,否则会造成大量拒识。

一个稳妥的做法是上线前拿 20 个人做交叉验证:每个人签到 3 次,统计“异类距离”和“同类距离”,取两者的中间值作为阈值。不要用官方默认值,人脸识别系统的定制化空间很大程度就落在这个参数上。

3.4 现场照片与签到记录如何关联

签到不仅需要“谁在几点打了卡”,还需要留下证据,防止后续纠纷。打开摄像头识别成功的那一刻,客户端截取当前帧,保存为 JPG 图片,然后把图片路径或图片本身随签到请求一起上报。图片的命名规则建议使用“员工工号 + 时间戳”,避免重名覆盖。

def save_snapshot(frame, emp_no): timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{emp_no}_{timestamp}.jpg" # 覆盖目录,注意只保留最近 N 天,防止磁盘被占满 path = os.path.join("snapshots", filename) cv2.imwrite(path, frame) return path

照片文件放到独立的snapshots目录,路径写入数据库的photo_path字段。如果将来需要做 Web 端查看,直接把这个目录用 Nginx 或 FastAPI 映射成静态资源即可。注意保留图片的年份/月份子目录,否则一年下来几万张照片放在同一目录,文件系统会变得极慢。

4. 签到防重与状态判断:服务端如何保证记录可靠

4.1 服务端防重的三层逻辑:时间窗口、人员状态、设备冷却

前面提到客户端要做好“识别冷却”,但服务端也必须做防重,因为客户端很可能临时被绕过,或者多个客户端同时对同一员工进行识别。服务端防重通常分三层逻辑:

第一层:时间窗口防重。同一个员工在 60 秒内只能签到一次。在签到表中查最近一条记录,如果时间差小于 60 秒,直接拒绝重复上报。

第二层:状态防重。员工已经是“在职”状态且今日已完成上班签到,再来的签到请求可能属于下班签到。这里不要用主键约束去硬防,而是用业务逻辑去判断。

第三层:设备冷却。同一台设备在 3 秒内的所有上报都只接受第一条,防止摄像头识别人群时连续多次触发。设备编号 + 时间窗口做成一个唯一键,可以用 Redis 的SETNX实现。

# 服务端签到接口示例(FastAPI 风格) import redis r = redis.Redis(host="localhost", port=6379, db=0) def checkin(person_id, device_no): now = int(time.time()) # 设备冷却:同一设备 3 秒只接受一次请求 lock_key = f"device_lock:{device_no}" if not r.set(lock_key, now, nx=True, ex=3): return {"code": 4001, "msg": "device is busy"} # 人员防重:同一人 60 秒只记录一次 person_key = f"person_lock:{person_id}" if not r.set(person_key, now, nx=True, ex=60): return {"code": 4002, "msg": "duplicate checkin"} # 业务规则:按时间判断上班 / 下班 # 写入数据库 attendance 表 return {"code": 0, "msg": "checkin success"}

防重后的响应会进入一个待同步队列。如果服务端暂时不可用,客户端可以把签到记录缓存到本地 SQLite,等网络恢复后再批量上传。这比简单地报错“网络异常”体验好得多,而且数据不丢失。

4.2 迟到、早退、缺卡的判断规则与 SQL 实现

签到记录入库之后,考勤状态需要在查询时实时计算,而不是在写入时用 Python 硬编码——因为上班时间是可能调整的,写死之后改起来很痛苦。

典型的规则是:默认上班时间为 09:00,迟到判断按分钟粒度实现。一条用于统计每日考勤的查询,按人员分组,取当日最早的签到记录作为上班时间,最晚的作为下班时间:

SELECT person_id, MIN(check_time) AS first_in, MAX(check_time) AS last_out, CASE WHEN TIME(MIN(check_time)) > '09:00:00' THEN 'late' WHEN MAX(check_time) IS NULL THEN 'missing' ELSE 'normal' END AS day_status FROM attendance WHERE DATE(check_time) = '2024-11-20' GROUP BY person_id;

实际项目中凌晨打卡、跨日出勤等情况会更复杂,但这里展示了两个关键点:分组聚合条件分支。具体来说,考勤系统按日期取当天最早/最晚记录,用 SQL 的MIN/MAX配合GROUP BY完成,可以避免把整表数据拉回 Python 里手算。

4.3 客户端断网续传:本地 SQLite 缓存表的设计

客户端识别成功后,如果网络请求失败,不能直接把结果丢掉。本地 SQLite 缓存签到记录是通用做法,设计上要区分“未同步”和“已同步”两种状态。

import sqlite3 def init_local_db(): conn = sqlite3.connect("local_cache.db") conn.execute(""" CREATE TABLE IF NOT EXISTS pending_checkin ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL, check_time DATETIME NOT NULL, photo_path TEXT, device_no TEXT, sync_status INTEGER DEFAULT 0 -- 0 未同步,1 已同步 ) """) return conn def cache_checkin(conn, person_id, check_time, photo_path, device_no): conn.execute( "INSERT INTO pending_checkin (person_id, check_time, photo_path, device_no)" "VALUES (?, ?, ?, ?)", (person_id, check_time, photo_path, device_no), ) conn.commit()

后台线程每隔 10 秒尝试同步一次:

def sync_pending(server_url, conn): rows = conn.execute( "SELECT * FROM pending_checkin WHERE sync_status = 0" ).fetchall() for row in rows: try: resp = requests.post(f"{server_url}/api/checkin", json=row, timeout=5) if resp.status_code == 200: conn.execute( "UPDATE pending_checkin SET sync_status = 1 WHERE id = ?", (row[0],), ) conn.commit() except requests.exceptions.RequestException: break # 网络不通,保留下次继续

sync_status字段是断网续传的核心,它把“本地已记录”和“服务端已确认”两个状态区分开。需要注意,一旦记录同步到服务端但客户端崩溃,理论上存在重复上报的可能——这就是为什么第 4.1 节的时间窗口防重非常重要,它天然地兜住了这一层重复。

5. 识别性能与参数调优:让 OpenCV 在人脸签到场景跑得更稳

5.1 帧率与 CPU 占用的取舍:缩小检测区域始终是第一优化手段

很多人一上来就改detectMultiScale的参数,其实最大的性能瓶颈根本不在这里,而是全图扫描。摄像头采集的画面里,人脸通常只占画面中央的一小块区域。把检测区域裁剪到中央 60% 区域,帧率可以提升近一倍,CPU 占用直线下降。

def center_crop_for_detection(frame, crop_ratio=0.6): h, w = frame.shape[:2] x = int(w * (1 - crop_ratio) / 2) y = int(h * (1 - crop_ratio) / 2) return frame[y:y + int(h * crop_ratio), x:x + int(w * crop_ratio)]

识别时先裁剪中央区域,检测到人脸并完成比对后,再回到完整帧截取现场照片。这样既保证签到照片画面完整,又不让全图扫描拖慢识别速度。

5.2 LBPH 与 face_recognition 的选型对比:什么时候用哪个

两种识别方案的性能差异明显:

维度LBPH(OpenCV 自带)face_recognition(dlib 封装)
模型体积几百 KB约 100+ MB(含模型文件)
单人识别耗时约 5-15ms约 50-150ms(CPU)
训练复杂度需要每人数张图单张图即可提取特征
精度光线变化敏感更鲁棒,支持姿态变化
人工标注需求每次新增人员需重新训练直接入库,无需整体训练

结论是:如果识别人数少于 100 人、摄像头位置固定、光线稳定,LBPH 是性价比最高的方案;如果团队规模大、或者公司有移动考勤需求(手机端拍照签到),face_recognition 更合适。在独立客户端嵌入式设备上,LBPH 的模型体积优势会直接体现在部署时间上。

5.3 识别日志与现场照片保留策略:磁盘空间怎么控制

签到系统运行一段时间后,最大的存储消耗不是数据库,而是现场照片。一张 JPG 约 50-200KB,100 人每人每天 2 次签到,一天就是 20-40MB,一年就是 7-15GB。长期运行,磁盘迟早被照片塞满。

常见策略是:照片按月份归档,超过 6 个月的自动压缩为低分辨率版本或直接清理。清理用服务端的定时任务来完成:

# 定时任务,每天凌晨执行 import os, datetime, shutil archive_dir = "snapshots/archive" today = datetime.date.today() for d in os.listdir("snapshots/2024"): month_dir = os.path.join("snapshots/2024", d) if datetime.date(2024, int(d), 1) < today - datetime.timedelta(days=180): shutil.move(month_dir, archive_dir)

数据库里只保留photo_path字符串路径,清理照片不影响数据结构。也可以更进一步,在上传照片到服务端的时候就把原图降采样到 640x480,因为签到记录并不需要保留原始分辨率照片。

5.4 一个容易被忽略的坑:OpenCV 窗口不能正常关闭,摄像头被占用

开发调试阶段,OpenCV 的imshow窗口经常出现“点关闭没反应”的情况,关掉终端后发现摄像头无法再次打开,提示设备被占用。这是因为cap.release()没有在窗口关闭回调中执行。正确处理方式,是在主循环检测到用户按 ESC 或 Q 键时,先释放窗口和摄像头,再退出进程:

while True: ret, frame = cap.read() # ... 识别逻辑 ... if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

如果你是在嵌入式设备上跑,还有一个隐藏问题:VideoCapture(0)打开失败时,OpenCV 不会抛异常,而是返回一个未就绪的捕获对象,此时继续调用read()会返回(False, None)。初始化时必须检查isOpened(),否则会出现“摄像头打不开但程序不报错”的诡异现象。

6. 日结与异常补登:把签到数据变成工作日终的可用报表

6.1 日结统计的落库:在服务端用一条 SQL 生成考勤汇总

人脸识别签到的最终产出,不是“识别成功”那一刻的弹出框,而是下班后管理员能看到一份“今天谁正常、谁迟到、谁没来”的表格。我通常的做法是:每天 23:50 跑一个定时任务,把当天各人员的考勤状态写入一张daily_summary表,这样早晨查看报表时不需要实时扫描大表,速度会快很多。

INSERT INTO daily_summary (person_id, work_date, first_in, last_out, status) SELECT p.id, CURDATE(), a.first_in, a.last_out, CASE WHEN a.last_out IS NULL THEN 'missing' WHEN TIME(a.first_in) > '09:30:00' THEN 'late' ELSE 'normal' END FROM ( SELECT person_id, MIN(check_time) AS first_in, MAX(check_time) AS last_out FROM attendance WHERE DATE(check_time) = CURDATE() GROUP BY person_id ) a RIGHT JOIN person p ON p.id = a.person_id;

这里用RIGHT JOIN把人脸库里所有在职员工都列出来,包括没有签到记录的人,标记为missing。这一步很关键——只有签到记录的人做汇总,等于把“忘打卡”的人静默忽略掉了,这在考勤制度上是站不住脚的。

6.2 异常补登接口:管理员手工修正迟到记录

人脸识别的误拒(比如员工今天化妆或有遮挡)无法完全避免,因此日报表里会预留“异常补登”的能力。管理员在 Web 端选择人员和日期,设置正确的签到时间,系统将补登记录写入一张独立的attendance_override表,并将该员工的日状态更新为已修正。

# FastAPI 补登接口示例 @app.post("/api/attendance_override") def override_attendance(person_id: int, work_date: str, check_time: str): # 插入补登表 # 同时更新 daily_summary 中该员工当天的状态为 'override' pass

补登记录与原始识别记录区分存放,而不是直接修改attendance表,好处是保留了审计线索——你可以知道哪些记录是机器识别出来的、哪些是人工修正过的。这在答辩演示和实际人事管理中都是加分项。

6.3 用一个 Shell 命令做每日数据备份

签到数据是企业人事数据,备份不能省。最简单可靠的方案,是每天凌晨用 cron 执行一次 SQLite 或 MySQL 的导出,保留最近 30 天备份文件:

#!/bin/bash BACKUP_DIR="/data/backup/attendance" DATE=$(date +%Y%m%d) mysqldump -u root -p*** attendance > "${BACKUP_DIR}/attendance_${DATE}.sql" find "${BACKUP_DIR}" -name "attendance_*.sql" -mtime +30 -exec rm {} \;

不管底层用的是 SQLite 还是 MySQL,备份一定要保留多天的版本,不要只留一份“昨日备份”然后用新数据覆盖它。员工数据的价值远超代码本身,磁盘富余的多留几天没有坏处。

6.4 报表输出的最后一步:从数据库到 Excel

日终统计完成后,管理人员需要一份能直接打开的报表。用 Python 的openpyxl库,把daily_summary表的内容导出成带格式的 Excel 文件:

from openpyxl import Workbook wb = Workbook() ws = wb.active ws.append(["工号", "姓名", "日期", "上班时间", "下班时间", "状态"]) rows = cursor.execute("SELECT ... FROM daily_summary WHERE work_date = ?", (today,)).fetchall() for row in rows: ws.append(row) wb.save(f"reports/attendance_{today}.xlsx")

Sheet 名默认叫 “Sheet”,可以直接改成日期,多份日报放在同一个工作簿里分 Sheet 归档,比每天生成一个独立文件更整洁。输出报表放在 cron 任务里自动执行,每天早晨管理员打开邮箱或共享目录就能拿到前一天的考勤结果。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:49:39

VTK医学影像三维重建实战:从DICOM到STL临床级流程

简介&#xff1a;本资源是一个基于VTK的医学影像三维重建完整实践项目&#xff0c;面向医学图像处理初学者、计算机视觉开发者及生物医学工程相关专业学生&#xff0c;解决从DICOM数据读取、预处理、分割到三维可视化的一整套技术落地问题。压缩包共318个文件&#xff0c;含10个…

作者头像 李华
网站建设 2026/9/14 2:49:35

C#人脸识别考勤系统开发实战:从选型到语音播报

简介&#xff1a;C#人脸识别考勤系统完整源码&#xff0c;内置语音播报&#xff0c;面向C#开发者、计算机专业学生及需要快速落地考勤系统的技术团队。项目将人脸识别、USB摄像头采集、考勤时段控制与TTS语音反馈整合于一体&#xff0c;并提供用户界面交互&#xff0c;能有效提…

作者头像 李华
网站建设 2026/9/14 2:49:16

SpringBoot点餐推荐系统实战:Slope One与协同过滤算法融合

简介&#xff1a;一款基于Spring Boot的智能推荐点餐系统设计与实现完整项目&#xff0c;适合正在学习Spring Boot整合开发、推荐算法落地及餐饮系统设计的开发者。项目采用前后端分离架构&#xff0c;业务逻辑涵盖登录、点餐、支付等核心流程&#xff0c;并利用协同过滤或基于…

作者头像 李华
网站建设 2026/9/14 2:47:42

STM32驱动DS1302实时时钟:GPIO模拟时序从零实现

1. 项目背景与整体设计思路 做嵌入式开发的同学&#xff0c;几乎都会遇到需要给设备加一个“时间戳”的场景。不管是做数据采集器、智能家居网关&#xff0c;还是毕业设计里的电子时钟&#xff0c;都绕不开实时时钟&#xff08;RTC&#xff09;这颗小芯片。市面上常见的RTC方案…

作者头像 李华
网站建设 2026/9/14 2:47:05

SpringBoot+Vue全栈在线教育系统开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华