简介:基于Python+Django深度学习的身份证识别考勤系统设计与实现,是一份面向计算机相关专业毕业设计、课程项目开发的完整方案文档,重点解决传统线下签到信息不全、考勤效率低等问题。文档围绕深度学习身份证识别与考勤管理展开,融合CNN模型、Django框架、MySQL数据库与B/S架构,并兼顾前端交互、数据存储及安全验证等设计要点。压缩包内为1个docx文档,大小981KB,结构包含中英文摘要、目录、系统设计、实现与关键技术说明,便于参考整体框架与核心思路。已有156人学习,适合正在筹备毕业设计或希望掌握Python深度学习应用开发的读者,可作为开题、架构设计及论文撰写的直接参考资料。
1. 基于Python+Django深度学习的身份证识别考勤系统:一份毕业设计项目包的落地拆解
先说结论:这份项目包不是一套纯展示的Demo,而是一条完整的毕业设计技术路线——从Django的B/S架构、MySQL三张核心表,到用深度学习CNN模型做身份证信息识别,再落到打卡、考勤统计、用户管理这些真实业务模块。传统考勤靠手写签到或工牌刷卡,信息不全、补卡扯皮是常态;开发者真正要解决的是“如何把身份证照片变成一条可靠的考勤记录”。如果你是准备做Python方向毕业设计的学生,或者想在公司内部搭一套低成本、免硬件的考勤系统的工程师,这份资源值得照着拆一遍。项目源代码、论文文档、数据库脚本都能拿到,我下面说的每个模块都能在代码里找到对应实现。
2. 先理清架构与数据:B/S、Django、MySQL 三张表怎么搭
考勤系统这类项目,最大的特点就是“业务逻辑不复杂,但数据关系绕不开”。一个员工有身份证号、联系方式、部门信息,一天可能有多条打卡记录,管理员要能查、能改、能删。所以动手写代码之前,先把架构选型和数据模型定死,后面基本就是往模板里填逻辑。
2.1 选型理由:为什么是B/S、Django、MySQL的组合
项目正文里写得很直白,这套系统采用B/S访问结构。B/S和C/S最大的区别在于:C/S要装客户端,B/S只要有浏览器就能访问。对一个需要人事部门、考勤管理员、普通员工多角色使用的系统来说,B/S意味着不需要给任何一台电脑装软件,更新逻辑也只发生在服务器端。这就是项目里说的“不用安装任何东西”。这个优势在后期维护时特别明显——你改了后端代码,刷新浏览器就是新版本,不存在客户端老旧版本不兼容的问题。
后端框架选Django而不是Flask,我猜作者考虑的是两点。第一,Django自带Admin后台、ORM、Session会话管理和模板引擎,考勤系统这种CRUD密集型的业务,用Django能省掉一大半重复代码。第二,Django的ORM对MySQL的支持很成熟,迁移命令一套就自动建表,不用手写一堆JDBC或者pymysql的样板代码。项目正文里反复提到“日后的升级和需求可以通过多种途径来解决,毕竟还是开源的体系”,Django社区活跃、文档全、出问题一搜就有答案,对毕业设计这种时间紧的开发场景非常友好。
数据库选MySQL而不是Oracle或者SQL Server,核心原因是成本和体量。项目正文里算过一笔账:如果系统要覆盖几十万员工的量级,Oracle的授权费用根本不是一个毕业设计能承受的。MySQL免费、开源、并发能力在线,配合InnoDB引擎做行级锁和事务,考勤记录这种高频写入场景完全扛得住。另外本项目没有特别复杂的事务嵌套,MySQL默认的隔离级别足够用了。
这部分我见过太多人翻车:上来不梳理数据关系,直接写前端页面,结果做到一半发现用户表和考勤表对不上,又回头改数据库。正确顺序永远是先定义数据模型,再写业务逻辑,最后才碰页面。
2.2 数据库设计:从E-R图到三张核心表的建表SQL
项目正文的第四章给了E-R图的实体描述:管理员信息有用户名、密码、编号;用户信息有编号、姓名、性别、年龄、电话、邮箱、地址、身份证号;打卡信息对应考勤记录。按我的习惯,会把它拆成三张表:管理员表、员工表(也就是被考勤的人)、考勤记录表。
建表时要注意几个细节。身份证号字段必须加唯一索引,否则同一个员工会被重复建档,后面打卡匹配会出大问题。密码字段用VARCHAR(64),因为MD5加密后的十六进制字符串正好是32位,留64位是给将来换更强算法做余量。考勤记录表要用“员工ID+日期”做联合唯一键,这样一个人一天只能有一条核心记录,重复拍照打卡会被判定为更新下班时间而不是插入新记录。
-- 管理员表 CREATE TABLE admin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录用户名', password VARCHAR(64) NOT NULL COMMENT 'MD5加密后的密码值', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员账号表'; -- 员工信息表 CREATE TABLE employee ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT NOT NULL DEFAULT 0 COMMENT '0未知 1男 2女', age INT UNSIGNED DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, address VARCHAR(255) DEFAULT NULL, id_card_no CHAR(18) NOT NULL UNIQUE COMMENT '身份证号,唯一索引防止重复建档', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表'; -- 考勤记录表 CREATE TABLE attendance ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, employee_id INT UNSIGNED NOT NULL, work_date DATE NOT NULL COMMENT '考勤日期', check_in_time DATETIME DEFAULT NULL COMMENT '上班打卡时间', check_out_time DATETIME DEFAULT NULL COMMENT '下班打卡时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1迟到 2早退 3缺卡', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_emp_date (employee_id, work_date), FOREIGN KEY (employee_id) REFERENCES employee(id), CONSTRAINT chk_status CHECK (status IN (0, 1, 2, 3)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤记录表';上面这段SQL的逻辑重点有三个。第一,utf8mb4字符集必须写清楚,否则姓名里的生僻字、少数民族名字里的特殊符号存进去就变问号,这是身份证识别考勤系统里最容易踩的隐性坑。第二,attendance表的联合唯一键uk_emp_date很重要,它把“一个人一天只能有一条考勤记录”这个业务规则直接压到数据库层面,比在代码里写if exists再判断要可靠得多。第三,外键约束在毕业设计里可以加,但生产环境我一般会去掉外键,只保留普通索引,因为外键在高并发写入时会影响性能,这是后话。
状态字段status我用TINYINT而不是VARCHAR,是为了让迟到、早退、缺卡这些判断逻辑放在后端代码里算好再落库,而不是存一堆中文文本。文本字段做统计的时候GROUP BY很难看,数字枚举才是正道。项目正文里提到的“修改密码”“个人信息”模块,本质都是对admin表和employee表的增删改查,不用额外建表。
2.3 系统模块划分和一个打卡请求的完整数据流
项目正文的功能需求分析里列了这些模块:首页、后台打卡、考勤管理、修改密码、个人信息、用户管理。画成模块图就是两层:一层是面向普通员工的打卡和查看个人信息,另一层是面向管理员的用户管理和考勤管理。很多人做这类系统会纠结要不要做角色区分,我的建议是“员工和管理员共用一张登录表,用字段区分角色”,比建两张登录表省事,也符合这个项目的体量。
一个打卡请求在Django里的完整数据流是这样的:浏览器上传带身份证照片的POST请求,Django的URL路由根据/api/checkin/把请求分发到视图函数,视图函数把图片交给身份证识别模块,识别出身份证号后去employee表做精确匹配,匹配成功就往attendance表插入或更新记录,最后返回一个JSON结果。整个过程不涉及页面跳转,所以前端只需要一个上传控件和一个结果提示框,这比传统表单提交要清爽得多。
模块划分这里要特别提醒一点:不要把身份证识别逻辑直接写在视图函数里。正确做法是单独建一个recognition.py模块,视图函数只负责“拿图片、调接口、存结果”。这既方便以后换更好的识别模型,也方便单元测试——你不可能每次测试都翻出身份证重新拍照。项目源码里如果能看到views.py和recognition.py分开,那这个项目的结构就是合格的。
数据安全方面,项目正文提到密码用MD5加密。这里我要多说一句:MD5在今天只能算是“基础保护”,不是“安全方案”。毕业设计用MD5没问题,论文里也好解释,但真要部署到公司内网,我建议至少换成Django自带的pbkdf2_sha256算法,或者直接用bcrypt。好消息是,由于我们建表时密码字段留了64位,后面升级算法不需要改表结构,只需要在verify函数里加一个版本标识。
3. 身份证识别怎么落地:CNN 训练骨架、预处理与推理对接
项目标题里最扎眼的词是“深度学习”,但很多人打开代码后发现找不到训练过程,只有一堆权重文件和调用代码。这很正常——身份证识别这种场景,训练和推理是两套完全不同的工程。模型训练需要GPU、需要标注数据、需要几天甚至几周的时间,而考勤系统里跑推理只需要CPU和几十毫秒。所以我们要做的是两件事:搞清楚模型是怎么训练的,以及部署时怎么把模型接进Django。
3.1 身份证识别的本质:目标检测 + 文字识别两步走
身份证照片识别不是“一张图扔进CNN就出结果”这么简单。身份证上有姓名、性别、民族、出生日期、住址、身份证号、头像照片,我们需要的是身份证号这一串18位数字和姓名,其余都是干扰信息。所以常见方案是拆成两个阶段:第一阶段用目标检测定位身份证区域,甚至更细一点,定位到身份证号码那一行的位置;第二阶段对定位出来的区域做文字识别。
第一个阶段其实可以手工完成。考勤场景下摄像头拍摄的身份证位置虽然有一定随机性,但角度不会太离谱,用OpenCV的轮廓检测加透视变换就能把身份证区域矫正成一张正面的矩形图。第二个阶段才轮到CNN上场:把号码区域按字符切分成18个小图,每个小图过一个数字分类CNN,最后拼出完整号码。
为什么用CNN而不用传统OCR?文本识别用Tesseract也能跑,但身份证号码是印刷体数字,背景有长城图案、有防伪纹理、还有反光干扰,传统OCR的模板匹配在这种低对比度场景下会频繁翻车。CNN的卷积核能自动学习“数字轮廓”和“背景纹理”的差异,对光照变化、倾斜、模糊的鲁棒性比手工特征强很多。项目摘要里提到的“利用深度学习模型,如卷积神经网络(CNN),对身份证上的文字和图像信息进行分析和识别”,指的就是这个阶段。
3.2 预处理流程:灰度化、透视矫正、字符切分
不管用什么模型,预处理决定了识别的上限。我见过太多人把原始照片直接喂给CNN,结果识别率只有60%,然后就抱怨模型不行。实际上问题几乎都出在预处理上。
第一步是透视矫正。身份证是矩形,拍照时镜头有角度,照片里的身份证就变成了梯形。用OpenCV的findContours找到证件四个角点,再用getPerspectiveTransform做四点变换,把梯形拉回矩形。这一步不做,后面的字符切分全是歪的,CNN再强也白搭。
第二步是灰度化和二值化。身份证背景有底纹,直接切分会把底纹误判成字符。一般用大津法(Otsu)自动算阈值做二值化,把前景字符和背景分离。注意光照不均时,全局阈值效果会变差,这时候可以先用adaptiveThreshold做局部自适应二值化,效果比全局阈值好一个档次。
第三步是按行投影切分。二值化之后,对图像做水平投影(统计每一行有多少个黑色像素点),字符行会有明显的峰值,空白行值接近零,用这个峰谷位置就能把号码行切出来。同理再做垂直投影,就能把18位数字逐个切分。每个数字再归一化到统一尺寸,比如32×128像素,喂给CNN。
import cv2 import numpy as np def preprocess_for_recognition(image_path): img = cv2.imread(image_path) # 灰度化:去掉颜色干扰,只保留亮度信息 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊:抑制传感器噪点,避免二值化后出现杂点 gray = cv2.GaussianBlur(gray, (3, 3), 0) # 透视矫正:检测矩形轮廓并拉正 edges = cv2.Canny(gray, 50, 150) contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 按轮廓面积排序,身份证区域通常是画面中最大的四边形 contours = sorted(contours, key=cv2.contourArea, reverse=True) for cnt in contours[:5]: approx = cv2.approxPolyDP(cnt, 0.02 * cv2.arcLength(cnt, True), True) if len(approx) == 4: # 四点变换,把透视角度下的四边形矫正为矩形 pts = approx.reshape(4, 2).astype(np.float32) width, height = 300, 190 dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype=np.float32) matrix = cv2.getPerspectiveTransform(pts, dst) warped = cv2.warpPerspective(gray, matrix, (width, height)) break else: raise ValueError("未检测到身份证区域,可能是照片背景过于复杂") # 大津法二值化 _, binary = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return binary这段预处理代码的逻辑核心有两个。第一,approxPolyDP的第二个参数是“最大距离误差”,取轮廓周长的2%是一个经验值,太大容易把四边形拟合成三角形,太小会保留锯齿状细节。第二,warpPerspective的目标尺寸我固定为300×190像素,这是身份证的2:1比例,统一尺寸之后字符切分的坐标才可预测。THRESH_OTSU会自动计算最佳阈值,比写死THRESH_BINARY的127要稳,因为它会根据每个样本的灰度分布动态调整。
预处理做完,下一步是字符切分。这一步没有特别漂亮的算法,就是基于投影的峰谷分析。切分失败通常发生在身份证号码区域和姓名区域混在一起时,所以二值化之前先锁定号码区域的位置很重要——常见做法是用一个固定比例框,身份证号码一般位于证件底部1/3区域。
3.3 CNN训练骨架:一个可以直接改参数跑起来的模型
真正要自己从头训练一个身份证识别模型,数据量小很容易过拟合。网上能找到的开源身份证OCR数据集不多,常见做法是自拍几百张身份证照片,然后用数据增强把样本量扩到几千张。如果只做毕业设计,也可以在开源OCR模型基础上做迁移学习——冻结前面的卷积层,只训练最后几层全连接层,这样几百张样本就够用了。
下面给一个标准的CNN训练骨架,基于TensorFlow/Keras。注意这里训练的是“单字符分类器”,也就是只辨别0到9十个数字,不直接端到端识别整串身份证号。这样做的好处是,输出类别少、收敛快、每个字符独立预测,只要切分正确,准确率很容易做到99%以上。
from tensorflow.keras import layers, models from tensorflow.keras.preprocessing.image import ImageDataGenerator def build_char_cnn(input_shape=(32, 128, 1), num_classes=10): # 输入是单通道灰度图,32x128 是统一后的字符尺寸 model = models.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation='relu'), layers.Flatten(), layers.Dropout(0.5), # 防止小数据集过拟合 layers.Dense(256, activation='relu'), layers.Dense(num_classes, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) return model # 数据增强:随机平移、旋转、缩放,提升模型对拍摄角度变化的鲁棒性 datagen = ImageDataGenerator( rotation_range=5, width_shift_range=0.1, height_shift_range=0.1, zoom_range=0.1, validation_split=0.2 ) model = build_char_cnn() train_flow = datagen.flow_from_directory( 'data/chars_train', target_size=(32, 128), color_mode='grayscale', class_mode='categorical', subset='training', batch_size=32 ) val_flow = datagen.flow_from_directory( 'data/chars_train', target_size=(32, 128), color_mode='grayscale', class_mode='categorical', subset='validation', batch_size=32 ) model.fit(train_flow, validation_data=val_flow, epochs=30) model.save('char_cnn.h5')这段代码我做了两个关键取舍。第一,输入尺寸用32×128而不是常规的224×224,因为单个数字字符本身信息量很少,不需要大尺寸,小尺寸反而能加快推理速度、减少显存占用。第二,每层卷积核数量从32、64到128逐层翻倍,网络结构足够简单,但参数容量对10分类任务完全够用,不用堆到ResNet那个级别。
rotation_range=5只做小幅旋转,因为身份证号码本来就是印刷体,旋转超过10度切分阶段就已经失败了,数据增强不应该去弥补预处理该解决的问题。训练30个epoch在小数据集上够用了,如果验证准确率长时间不涨,优先检查切分后的字符图像是否干净,而不是盲目增加epoch。
3.4 推理对接:把识别结果变成一条考勤记录
模型训练好之后,部署到Django里有一个最常见的坑:模型文件是HDF5格式,加载时需要TensorFlow环境。如果Django跑在纯CPU的服务器上,加载模型第一次会特别慢,可能要两三秒。解决办法是在视图模块的顶层加载一次模型,让模型常驻内存,而不是每次请求都重新加载。
推理代码的流程是:接收上传图片,调用预处理函数拿到二值化字符图,按水平垂直投影切出每个数字的小图,每个小图经过model.predict得到分类结果,最后把所有数字拼接成18位身份证号。注意predict接收的输入张量形状是(batch, 32, 128, 1),所以每个字符都要走一次np.expand_dims加维度。
识别得到身份证号之后,就是纯粹的数据库操作了。去employee表按id_card_no精确匹配,查不到就返回“该身份证未登记”,查到就写入attendance表。这里有个容易忽略的小坑:识别结果偶尔会把8和6混淆,或者把1和7看错。解办法是在匹配之前对识别结果做一次数字合法性校验,身份证号前6位是地区码,第7到14位是出生日期,日期部分可以做基本格式检查,明显不合法的直接判定为识别失败,让员工重新拍一张,比录入一条脏数据再人工修复省心得多。
4. 考勤核心模块实现:登录、打卡、考勤统计的 Django 代码
数据库和识别模块准备好之后,就到了把业务逻辑串起来的阶段。这一章只讲三个最核心的模块:登录、打卡、考勤统计。用户管理、修改密码、个人信息这些本质上是同一个套路,会了这三个,剩下的就是复制粘贴。
4.1 登录与密码安全:MD5 加密、Session 会话
登录模块的设计很简单,就是用Session保存登录状态,每次请求时检查Session里有没有管理员或员工ID。项目正文里说密码加密用MD5,这里我按项目的原始设计实现,但代码里留出了升级空间。
import hashlib from django.shortcuts import render, redirect from .models import Admin def md5_encode(raw): # 这里可以加盐:md5_encode(raw + app_settings.SALT) return hashlib.md5(raw.encode('utf-8')).hexdigest() def login_view(request): if request.method == 'POST': username = request.POST.get('username', '').strip() password = request.POST.get('password', '') admin = Admin.objects.filter( username=username, password=md5_encode(password) ).first() if admin: # 登录成功,把用户ID写进session request.session['admin_id'] = admin.id request.session['role'] = 'admin' return redirect('/dashboard/') error = '用户名或密码错误' return render(request, 'login.html', {'error': error}) return render(request, 'login.html') def logout_view(request): # 清除session中的登录标志 request.session.flush() return redirect('/login/')MD5加密的32位十六进制输出决定了数据库字段长度,这也是我在第二章建表时把password字段设计成VARCHAR(64)的原因。hashlib.md5的输入必须是字节串,所以要先做encode('utf-8')再加密。.strip()处理用户名首尾空格,这是一个很小的细节,但能避免“用户复制粘贴多了个空格导致登录失败”的售后问题。
Session方面,Django默认会把Session数据存到数据库的django_session表里,不需要额外配置。但要注意两点:第一,SESSION_COOKIE_AGE默认是两周,如果觉得太短可以调长;第二,登录接口一定要用POST请求,而且模板里要加{% csrf_token %},否则Django的CSRF中间件会直接拒绝请求,页面会报403。
4.2 身份证识别打卡:上传照片到写入考勤记录的完整视图
打卡是身份证考勤系统的核心动作。前端是一个文件上传控件,用户拍照上传;后端接收图片、调用识别模块、匹配员工、写入考勤表。这里要处理两个边界情况:识别失败怎么办,重复打卡怎么办。
import os from datetime import datetime, date from django.http import JsonResponse from django.views.decorators.http import require_POST from .recognition import recognize_id_card from .models import Employee, Attendance @require_POST def checkin_view(request): photo = request.FILES.get('photo') if not photo: return JsonResponse({'code': 400, 'msg': '未收到身份证照片'}) # 把上传的图片存到临时目录,识别完成后删除 temp_path = os.path.join('/tmp', photo.name) with open(temp_path, 'wb') as f: for chunk in photo.chunks(): f.write(chunk) try: id_card_no, name = recognize_id_card(temp_path) except ValueError as e: return JsonResponse({'code': 422, 'msg': f'识别失败:{e}'}) finally: os.remove(temp_path) emp = Employee.objects.filter(id_card_no=id_card_no).first() if not emp: return JsonResponse({'code': 404, 'msg': '该身份证未在系统中登记'}) today = date.today() now = datetime.now() # get_or_create:同一员工同一天只保留一条考勤记录 att, created = Attendance.objects.get_or_create( employee=emp, work_date=today, defaults={'check_in_time': now, 'status': 0} ) if not created: # 第二次打卡视为下班,更新check_out_time att.check_out_time = now att.status = 2 if (now - att.check_in_time).total_seconds() < 4 * 3600 else 0 att.save() return JsonResponse({'code': 0, 'msg': f'{emp.name} 打卡成功'})这段代码的逻辑分四步。第一,request.FILES.get('photo')拿上传文件,用photo.chunks()分块写盘,避免大图片一次性读入内存把服务搞崩。第二,recognize_id_card返回身份证号和姓名,如果内部OpenCV没找到身份证区域会抛ValueError,这里用try/except接住并返回422错误。第三,get_or_create是Django ORM里非常实用的一个方法,它会先按employee + work_date查记录,查不到就插入一条,查到就返回现有记录和created=False,正好对应第一次打卡和第二次打卡两种场景。第四,考勤状态的判定被我写在了更新分支里:如果两次打卡间隔小于4小时,判定为早退,状态置为2;否则置为正常。
这里有一个设计上的取舍想说明一下。为什么不在上午9点整自动判断迟到,而是在后端写死规则?因为考勤系统的迟到判定标准因企业而异,有的公司弹性工作制,有的按排班表,把这些都写死在代码里反而不好维护。项目里把status字段暴露出来,管理员可以在后台手动调整,这比自动判定来得更灵活。
4.3 考勤统计:按日期查询、按员工聚合
考勤统计的逻辑是考勤管理页面的主体:给定一个日期,展示所有员工的上下班打卡时间和状态;给定一个员工,展示他一个月内的考勤汇总。用Django ORM做这块非常顺手,但要注意查询效率问题,特别是员工数量上来之后。
from django.db.models import Count from django.http import JsonResponse from .models import Attendance, Employee def attendance_by_date(request): date_str = request.GET.get('date', date.today().isoformat()) rows = Attendance.objects.filter(work_date=date_str).select_related('employee') data = [] for r in rows: data.append({ 'name': r.employee.name, 'id_card_no': r.employee.id_card_no, 'check_in': r.check_in_time.strftime('%H:%M:%S') if r.check_in_time else '-', 'check_out': r.check_out_time.strftime('%H:%M:%S') if r.check_out_time else '-', 'status': r.get_status_display(), }) return JsonResponse({'code': 0, 'data': data}) def attendance_summary(request, emp_id): month = request.GET.get('month', datetime.now().strftime('%Y-%m')) rows = Attendance.objects.filter( employee_id=emp_id, work_date__startswith=month ) summary = rows.values('status').annotate(total=Count('id')) return JsonResponse({'code': 0, 'summary': list(summary)})select_related是用来解决外键查询的N+1问题的。如果不加,ORM在循环里访问r.employee.name时,每一条记录都会再发一次SQL查询,100个人就是101次查询;加了这个方法,Django会用一张JOIN把员工表的数据一次性带出来。这是Django开发里几乎必考的性能优化点,面试官问到这块能答上来会很加分。
values('status').annotate(total=Count('id'))会按状态分组统计数量,返回一个类似[{'status': 0, 'total': 22}, {'status': 1, 'total': 1}]的结构,前端拿到这个数据画饼图还是做表格都很方便。startswith对日期字段做前缀匹配,传2024-06就能把整个六月的记录捞出来。
5. 避坑与排查:身份证考勤系统最常见的五个坑
做这种“深度学习+Web系统”结合的项目,代码本身的难度往往不是最大的,真正的坑集中在图像预处理、数据库字符集、Session配置和模型部署这些边缘环节。下面这五条是我认为最容易翻车的地方,每一条都按“现象→原因→解决”写清楚。
5.1 身份证照片歪斜导致识别结果全错
现象:员工正常拍照打卡,但系统要么识别失败,要么把身份证号里的数字看错好几个,比如把8识别成6,把1识别成7。
原因:摄像头拍出来的身份证不可能是严格正面的,多少都会有一定透视角度。如果跳过透视矫正直接把照片喂给字符切分,切出来的字符就是歪的,CNN再强也认不出来。很多人的代码里只做了灰度化和二值化,恰恰漏了最关键的getPerspectiveTransform。
解决:在识别流程里强制加入透视矫正步骤。OpenCV里先findContours找到最大四边形的四个角点,再用warpPerspective拉正。如果确认代码里已经做了矫正但还是识别错误,优先检查角点检测是否把外面的背景框当成了证件边界,可以用approxPolyDP的精度参数和轮廓面积过滤来排除干扰。
5.2 姓名生僻字在页面和数据库里变成问号
现象:员工姓名里带有“頔、鑫、曦”等生僻字,录入系统时显示正常,但刷新页面后变成“?”或者乱码。
原因:MySQL数据库和表的默认字符集是latin1或utf8,而utf8在MySQL里最多只能存3字节的字符,很多生僻字需要4字节,也就是utf8mb4才能存下。项目里如果建库时没指定字符集,连接也没指定编码,写进去的中文就会在存储层被截断。
解决:建库时指定DEFAULT CHARACTER SET utf8mb4,建表时同步指定,同时检查MySQL连接串里有没有加charset='utf8mb4'。如果数据库已经建好了,可以用ALTER DATABASE和ALTER TABLE CONVERT TO CHARACTER SET utf8mb4补救,但最干净的方案是在项目初始化脚本里就统一字符集。Django的settings里数据库配置加OPTIONS: {'charset': 'utf8mb4'},这一步能省很多事。
5.3 登录后跳转页面又回到登录页
现象:登录成功后进系统首页没问题,但再点一个菜单跳到其他页面,就又被踢回登录页。
原因:Django的Session会话状态没有正确保持。最常见的情况是Session的Cookie过期时间太短,或者SESSION_SAVE_EVERY_REQUEST没有开启,导致长时间停留在某个页面后Session过期。另一种可能是浏览器设置了阻止第三方Cookie,但本地开发时很少碰到。
解决:在settings.py里把SESSION_COOKIE_AGE设为适当值,比如60 * 60 * 8(8小时),同时加SESSION_SAVE_EVERY_REQUEST = True,让用户每次刷新页面都会刷新Session过期时间。验证办法很直接:登录后打开开发者工具的Application面板,看Cookie里有没有sessionid,再看请求头有没有把这个Cookie发回服务器,两步就能定位问题出在生成还是传递。
5.4 前端页面静态文件和上传图片访问404
现象:本地调试一切正常,但把项目部署到服务器或者开启DEBUG=False之后,CSS、JS、上传的员工头像全部打不开,控制台全是404。
原因:Django默认的静态文件服务和上传文件服务只在开发模式下生效。DEBUG=False时,Django不会自动处理STATIC_URL和MEDIA_URL,这是新手最容易踩的部署坑。项目里上传的身份证照片、员工头像走的是MEDIA_ROOT,如果不配这个目录和URL映射,上传成功也看不到图片。
解决:开发阶段可以保持DEBUG=True,生产部署时用whitenoise挂静态文件,或者用Nginx直接把/static/和/media/指到对应目录。如果只是应急跑通,也可以在urls.py里加一段static()函数做临时映射,但这段代码在生产环境坚决不能留。
5.5 CPU推理速度慢,员工排队打卡卡成PPT
现象:摄像头拍照后点提交,要转两到三秒才能出结果,高峰期排队的人一多,体验非常差。
原因:深度学习模型推理是计算密集操作,尤其第一次请求要加载HDF5模型文件,CPU上加载就要一两秒。如果每次请求都重新加载模型,或者模型没有做量化,速度就很难上去。
解决:第一个技巧是把模型加载放到模块顶层,让模型常驻内存,不要在视图函数里加载。第二个技巧是模型优化,把训练好的Keras模型转成ONNX格式,用onnxruntime跑CPU推理,速度能提升一倍以上;如果追求极致可以把权重从32位浮点降到16位甚至8位整数。第三个技巧是改造成异步任务:照片上传后立刻返回“处理中”,识别完再通知前端出结果,排队问题就变成了吞吐量问题。这个方案在项目源码里可能没有,但做生产升级时是必选项。
6. 上生产前的验证与加固:自测脚本、异步推理与 HTTPS
项目跑通只是第一步,离真正能用的考勤系统还差两道关:识别准确率够不够、数据链路安不安全。这一章我给三个马上能用的验证和加固手段。
先做一个模型自测脚本。不要凭感觉说“识别挺准”,要用真实样片跑出数字。找5到10张不同光线、不同角度下拍摄的身份证照片,标注好真实身份证号,批量调用识别接口,统计号码级准确率和字符级准确率。号码级准确率低于90%的时候,不要急着调模型,先看失败的照片是不是都栽在预处理上。
# evaluate.py 识别准确率自测脚本 from recognition import recognize_id_card cases = [ ('samples/01.jpg', '110101199001011234'), ('samples/02.jpg', '110105199202022345'), ('samples/03.jpg', '310101198803033456'), ] total = 0 exact_hit = 0 char_total = 0 char_hit = 0 for img_path, true_no in cases: pred_no, _ = recognize_id_card(img_path) total += 1 if pred_no == true_no: exact_hit += 1 for a, b in zip(pred_no, true_no): char_total += 1 if a == b: char_hit += 1 print(f'{img_path}: 预测 {pred_no} 真实 {true_no}') print(f'号码级准确率: {exact_hit / total:.2%}') print(f'字符级准确率: {char_hit / char_total:.2%}')这个脚本的价值在做回归测试。改一次预处理参数、换一个模型,跑一遍脚本看数字变化,比肉眼观察客观得多。我一般会把准确率低于95%的样本单独存到一个文件夹,逐个看是切分歪了还是分类错了,这样能快速定位问题出在哪个环节。自测脚本里zip截断的问题要注意:如果预测号码长度不是18位,zip只会比较较短的序列,所以只要发现len(pred_no) != 18,直接记为零分。
再做异步推理改造。打卡体验卡顿的根源是同步推理占满了请求线程。快速方案是做一个全局线程池,识别任务提交到线程池,前端轮询结果接口。这个方案不需要引入Celery和Redis,改动量最小——在Django项目里建一个tasks.py,用concurrent.futures.ThreadPoolExecutor(max_workers=4)包住识别函数,视图函数提交任务后立即返回任务ID,另写一个查询接口对应任务状态。线程池方案虽然不支持分布式,但单机撑一个几百人的工厂足够。
最后是传输安全加固。项目正文里花了很大篇幅讲数据安全和网络传输安全,实际落地时两件事最重要。第一,把密码算法从MD5换成Django内置的pbkdf2_sha256,只需要自定义一个make_password函数,兼容方案是在数据库的password字段里加前缀标识,比如pbkdf2_sha256$开头,旧数据用MD5校验,新数据用新算法,两套逻辑并行不相互影响。第二,部署时配上HTTPS证书,Nginx配置里加SSL,把HTTP请求301跳转到HTTPS。证书用免费的Let's Encrypt就行,配置好之后浏览器地址栏出现锁标识,考勤数据在网络传输过程中就不会被人裸抓包了。
做这类项目一次之后,我的习惯就固定成了:先定数据模型,再写识别模块,最后接视图;上线前强制跑一遍回归测试脚本,号码级准确率不过90%不上线。从那以后我每次接这种“深度学习落地”项目,都会先花半天把输入样本的质检规则定死,再碰模型——照片不合格宁可提示重拍,也不放行进入流程,这比事后补数据省心得多。希望帮到你。
本文还有配套的精品资源,点击获取