ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Flask与微信小程序的人脸识别考勤系统:从设计到部署全解析

2026/9/28 23:54:18 拓冰建站 浏览量
基于Flask与微信小程序的人脸识别考勤系统:从设计到部署全解析 从大四上学期开始我就在琢磨怎么把自己课上那个替人答到的顽疾给治了。学校里有些课靠签到表传阅有些课靠老师点名结果就是前排同学一人签一排后排睡觉的同学稳如泰山。后来我干脆把这个想法做成了一整套系统Python后端用Flask搭建人脸识别做身份校验微信小程序做学生端入口再加上GPS定位来做签到范围限制顺便把选课功能也叠了进去。这篇文章就把我整个开发过程、架构设计、踩过的坑一次讲清楚希望能给准备做类似项目尤其是毕业设计方向是Python、Flask、微信小程序这几样组合的同学一个完整的参考。先说清楚这套系统大概是什么样学生在微信小程序里登录选课到上课时间进入签到页小程序端调用摄像头拍一张人脸照片同时获取当前的GPS定位坐标一起提交到Flask后端。后端先校验坐标是否在老师设定的签到范围比如教室半径100米内再调用人脸识别模块比对证件照库里的照片识别通过后才把签到记录写入数据库。老师端同样是微信小程序或者后台网页可以查看出勤统计、导出考勤报表、管理课程和学生名单。1. 从一个课堂痛点开始这套考勤系统到底值不值得做1.1 传统课堂考勤的三个老大难问题做这个项目之前我其实先翻了学校里的考勤需求。传统考勤方式的痛点非常明显每一个都值得被技术解决。第一是代签问题。纸质签到表一人写一排的玩法几乎没法防就算老师当面点名后排同学帮忙应一声也没辙。第二是考勤数据统计麻烦。纸质数据要助教手动录入Excel出错率高期末算平时分的时候翻车是常有的事。第三是考勤结果无法跟教学互动结合。老师只知道谁来了谁没来却不知道学生是否真的在教室范围内比如有些学生卡点到、签完就走实质上课堂参与度仍然为零。人脸识别和定位这两个能力刚好能解决前三者而选课系统则给考勤数据加了一层课程维度的组织关系。你想想如果一个学生选了三门课不同课程在不同教室那签到记录就必须绑定到具体课程和具体节次上不然出勤数据就是乱账。所以选课、签到、定位、人脸识别这四块功能是咬合在一起的整体拆开任何一个都会让系统不完整。1.2 技术选型为什么是Flask、微信小程序和开放的人脸识别库这块是当初纠结最久的部分。先给结论对课程设计、毕业设计级别的项目来说这套组合是目前综合成本和效果最平衡的方案。Flask相比Django更轻路由简单透明非常适合一个人快速搭建API服务。你不需要理解Django那种大而全的Admin后台、ORM迁移体系Flask从app Flask(name)到跑起来只需要几分钟。而考勤系统本质上就是一组REST接口加一个数据库没有特别复杂的业务状态机用Flask刚好合适。微信小程序的选型理由更直接学生不需要安装额外App扫码或搜索即可进入而且微信提供了稳定的定位接口wx.getLocation和相机调用能力wx.chooseMedia。比起还得上架应用商店、处理签名证书的原生App小程序的迭代和分发成本要低一个量级。如果你以后想扩展到其他学校把小程序代码上传审核就行不需要每个用户都经历安装过程。人脸识别这里我没有选择从头训练深度学习模型而是用了face_recognition这个Python库它封装的dlib模型在中小规模人脸库场景下识别精度足够而且API极其简单。当然它有两个坑后面我会专门讲一个是对生产环境部署不太友好模型文件大、依赖重另一个是在ARM架构或国内云服务器上有时候编译装不上。但考虑到这个项目是课堂考勤场景一个班几十人一张照片几秒内能出结果完全够用。1.3 系统的核心流程一次完整签到的链路我用大白话把这套系统的完整流程串一遍。学生在小程序里先登录微信授权手机号或者用户名密码都行登录后进入选课页面勾选本学期课程。到上课时间段系统在小程序首页推送签到入口学生点击后先授权定位小程序获取当前GPS坐标随后弹起拍照界面学生对着镜头拍一张正脸照片小程序把坐标照片课程ID学生ID打包POST到Flask后端的签到接口。后端接口收到请求后干四件事第一解析出学生ID和课程ID检查该学生是否选了这门课第二校验坐标是否在签到范围这里用的是GPS经纬度与课程预设中心点的球面距离计算不严谨但够用第三调用人脸识别服务把学生上传的照片与数据库预存的证件照人脸特征做比对第四全部通过后写入出勤记录响应里返回签到成功。整个过程从拍照到拿到结果实测在班级人数50人以内时基本能控制在3到5秒主要耗时在照片上传和CPU上的人脸特征提取。2. 后端Flask接口设计从数据库表结构到人脸校验接口的实现2.1 数据库表结构设计五张表撑起整个考勤业务做后端第一件事不是写接口而是把表结构想清楚。我用的SQLite零配置、单文件、足够撑起几百个用户的量级。如果你嫌SQLite不专业换MySQL或者PostgreSQL也容易ORM层用SQLAlchemy的话切换成本很低。我建了五张表用户表users、学生选课表enrollments、课程表courses、签到记录表attendance_records和人脸特征表face_features。用户表存学生和老师的基础信息字段大致是id、username、password_hash我用的werkzeug自带的generate_password_hash、role区分student/teacher、real_name、student_id。这里特别说明密码一定不要明文存哪怕只是课设项目被人扒出来明文密码挂在GitHub上都是社死现场。课程表存course_id、course_name、teacher_id、latitude、longitude、radius签到半径、start_time、end_time、weekday。经纬度和半径就是定位签到的核心配置老师在创建课程的时候直接把教室的GPS坐标填进去半径默认100米。选课表主要是把学生和课程做多对多关联字段是id、student_id、course_id。签到记录表是整个系统最核心的表id、student_id、course_id、timestamp、latitude、longitude、photo_path、recognize_score、status1为成功0为失败2为定位不在范围内。人脸特征表我用来缓存每个人的人脸特征向量字段是user_id、feature_blob。为什么要单独放一张表而不是存在用户表里因为特征向量是1024维的float数组序列化后体积不小和其他频繁读取的用户信息混在一起会影响查询性能。2.2 签到接口的关键代码定位校验和人脸比对的完整逻辑签到接口是整个项目的重头。我用Flask蓝图来组织路由核心签到逻辑写在一个attendance_bp里。先看定位校验这段这里用的是两个经纬度点间的Haversine公式计算球面距离对比课程设置的半径来判断是否允许签到。from math import radians, sin, cos, sqrt, asin def calculate_distance(lat1, lng1, lat2, lng2): # Haversine公式计算两个GPS坐标点的球面距离单位米 R 6371000 phi1, phi2 radians(lat1), radians(lat2) d_phi radians(lat2 - lat1) d_lambda radians(lng2 - lng1) a sin(d_phi / 2) ** 2 cos(phi1) * cos(phi2) * sin(d_lambda / 2) ** 2 return 2 * R * asin(sqrt(a)) def check_location(lat, lng, course): distance calculate_distance(lat, lng, course.latitude, course.longitude) if distance course.radius: return False, distance return True, distance这里注意一个问题GPS坐标在室内的漂移往往比室外大一栋教学楼内一般会偏移十几米甚至几十米所以签到半径不要卡得太死。我实测过设置100米半径基本能覆盖教室楼内各个位置但如果你设置50米有些学生站在窗边、走廊就会出现误判。这个参数在课程表里是可调的建议默认100到150米之间。人脸比对这段代码更重要。学生上传的照片经过图像处理后在后端与预存特征做欧氏距离计算低于阈值才判定为同一人。这里我用face_recognition这个库整体逻辑做了简化但流程是完整的import face_recognition import numpy as np def verify_face(upload_photo_path, stored_feature_blob): # 从上传照片中提取人脸特征 uploaded_image face_recognition.load_image_file(upload_photo_path) uploaded_encodings face_recognition.face_encodings(uploaded_image) if len(uploaded_encodings) 0: return False, 未检测到人脸 if len(uploaded_encodings) 1: return False, 检测到多张人脸请确保只有自己入镜 uploaded_feature uploaded_encodings[0] stored_feature np.frombuffer(stored_feature_blob, dtypenp.float64) distance np.linalg.norm(uploaded_feature - stored_feature) # 欧氏距离小于0.6判定为同一人这是我调过的合理阈值 if distance 0.6: return True, round(distance, 4) else: return False, round(distance, 4)阈值0.6是dlib模型的一个经验值你如果觉得识别过松或者过严可以根据实际测试调整。阈值越低越严格建议在0.5到0.6之间做AB测试。这里我还要提醒一个细节如果学生照片里检测不到人脸或者检测到多张人脸接口一定要返回明确的错误提示写清楚是哪一种原因不然前端会一直懵着不知道到底哪里出问题。2.3 人脸数据入库证件照上传与特征预处理人脸比对的输入是特征向量不是照片本身。所以我需要提前把每个人的证件照做一次特征提取存成二进制BLOB。老师端有一个人脸库管理页面上传学生照片后后端调face_recognition提取特征然后把特征序列化存入人脸特征表。from flask import request, jsonify app.route(/api/upload_face, methods[POST]) def upload_face(): file request.files[photo] user_id request.form.get(user_id) temp_path temp_face.jpg file.save(temp_path) image face_recognition.load_image_file(temp_path) encodings face_recognition.face_encodings(image) if len(encodings) ! 1: return jsonify({code: 1, msg: 请上传只有一张正脸的高清照片}) feature_bytes encodings[0].tobytes() # upsert到face_features表 ...我碰到过的坑是有人上传生活照侧脸太厉害导致特征提取失败或者提取出的特征与正脸照片比对距离过大。所以上传页面上要加一个请上传正面免冠照保证光线充足的提示后台也可以在库里跑一遍同一人的两张照片做自检提前把不合格的拦下来。2.4 API路由设计一览不只是签到接口除了签到接口后端还需要一组支撑业务的路由。我按蓝图划分了四组auth_bp登录POST /api/login、注册POST /api/register、获取当前用户信息GET /api/mecourse_bp创建课程POST /api/courses、查询课程列表GET /api/courses、选课POST /api/enroll、退选DELETE /api/enroll/course_idattendance_bp签到POST /api/attendance、查看我的考勤记录GET /api/attendance/my、老师查看某门课考勤统计GET /api/attendance/course/course_idface_bp上传人脸照片POST /api/upload_face、更新人脸照片PUT /api/face接口返回格式我统一用JSON固定带code0为成功非0为各类错误码、msg和data三个字段。这样小程序端解析的时候就特别省事不用每个接口写一堆出错判断分支。错误码要提前约定清楚比如1001代表未选课不能签到1002代表定位不在范围内1003代表人脸比对失败前端拿到这些码可以精确提示用户问题在哪。3. 微信小程序端开发从登录到拍照签到的完整链路3.1 小程序页面结构设计与页面跳转逻辑小程序端的页面我控制在五个以内避免让用户在小程序里迷路。首页是课程列表页展示当前用户已选的课程点击进入课程详情课程详情页有签到按钮、考勤记录和选课入口签到页是最核心的页面负责定位、拍照和提交个人中心页展示用户信息和人脸照片状态。页面跳转关系不复杂登录后默认进首页首页两个tab我的课和个人中心。选课入口我放在首页顶部点进去是可选课程列表选了以后回到首页刷新课程列表。课程详情页点签到就跳转签到页签到成功后返回详情页同时刷新下方的考勤记录。3.2 登录授权的两种实现方式微信小程序的登录跟普通Web登录有区别。我实现了两种方案一种是纯账号密码登录适合演示环境不依赖后端AppID配置另一种是微信授权登录适合正式部署到微信平台。账号密码登录就是在小程序里放一个输入框收集用户名密码后调后端登录接口。JWT用的是PyJWT生成token后存到小程序全局变量和storage里每次请求带上Authorization头。微信授权登录则是先调wx.login拿code再把code传给后端后端用code去微信的接口换openid用openid作为唯一标识在库里找用户找不到就自动创建。这个方案更符合微信生态但前提是你得有一个小程序AppID并且配置好合法域名。3.3 定位授权wx.getLocation的使用与常见坑小程序的定位签名是wx.getLocation它默认返回type为wgs84的坐标这里有个大坑国内地图普遍用火星坐标系GCJ-02而你如果用纯wgs84坐标和教室设的坐标去算距离会有一个几百米的系统性偏移。这个偏移在大学城那种开阔区域不算大但在城市楼群里可能导致定位签到的范围偏出去一栋楼。所以我建议统一用type:gcj02后端课程创建的时候老师定位也用相同坐标系。如果你实在要先拿wgs84那就后端做一次坐标转换把wgs84转gcj02再算距离。我的做法是后端保存和计算全部用gcj02并在接口里加注释提醒自己别混坐标系。调wx.getLocation的时候要在app.json里声明requiredPrivateInfos字段同时在页面里做好授权失败的处理如果用户拒绝了定位授权小程序端不要直接放弃而是弹窗提示需要定位才能签到并引导去设置页重新授权。// 小程序端定位签到代码片段 wx.getLocation({ type: gcj02, isHighAccuracy: true, success(res) { const latitude res.latitude const longitude res.longitude // 这里继续拍照流程把坐标暂存到data里 that.setData({ latitude, longitude }) }, fail(err) { wx.showModal({ title: 提示, content: 需要获取位置信息才能完成签到请前往设置开启定位权限, confirmText: 去设置, success(res) { if (res.confirm) { wx.openSetting() } } }) } })3.4 拍照与人脸照片处理拍照功能小程序里最合适的组件是wx.chooseMedia它能调起摄像头也可以从相册选择。注意签到场景一定要控制只能实时拍照不能从相册选否则学生完全可以上传一张预先准备好的别人照片。canvas拍照流我没用因为兼容性和处理成本都更高。拍照拿到图片后可以先看看大小微信返回的图片经常有1到3MB直接传后端会让接口变慢。我建议前端用canvas把图片压缩到640x480左右再上传质量设为0.8这样体积能压到一两百KB后端识别速度会快很多。wx.chooseMedia({ count: 1, mediaType: [image], sourceType: [camera], sizeType: [compressed], camera: front, success(res) { const tempFilePath res.tempFiles[0].tempFilePath that.setData({ photoPath: tempFilePath }) // 展示预览让用户确认是本人正脸 } })拍照这个环节有两个细节让我吃过亏。一是必须强制前置摄像头如果用户在拍照界面切换成后置摄像头很容易拍到别人或者场景我检查过souceType和camera的参数但个别安卓机对camera参数的响应不完整所以前端最好在拿到照片后做人脸检测如果检测不到人脸或者检测到多人脸直接提示重新拍。二是有学生反应拍出来的人脸是暗的教室光线不好我后来在页面上加了一个请在光线充足处拍摄的提示并做了拍照后的清晰度检查。4. 定位考勤的防作弊设计和实测数据4.1 为什么定位人脸识别比单纯一种方案可靠得多一个常见的误区是只要有人脸识别就不会代签。实际不是这样人脸识别只能证明来的人是本人但证明不了本人在教室里。所以定位信息不是可选功能而是必要维度。反过来定位能证明设备在教室里却永远证明不了设备主人就是拿着手机的人。所以只有人脸定位同时过这套考勤结果才能同时回答是不是本人和人是否在教室这两个问题。这也是我在设计时坚持两个校验都要做的原因任何一项做为可选项都会留下作弊漏洞。4.2 细节上的防作弊策略不只是阈值那么简单我针对常见的作弊方法做了几层防御这部分我在答辩时被问到过提前讲一下思路。第一种是照片翻拍拿证件照或者别人照片去拍照识别。我的应对策略是人脸活体检测的初级版前端强制实时拍照并定时调用内置的人脸关键点检测如果检测不到关键点或者关键点数异常就终止流程更彻底的做法是接入第三方活体检测API但课设阶段成本太高初级版足够了。第二种是改定位数据用模拟定位软件把打卡坐标改到教室。这个层面防不胜防因为小程序端和后端都很难区分真实GPS和模拟GPS。我的应对思路是记录设备指纹比如提取手机型号和系统版本如果一台设备多次以不同模拟位置签到并且位置跳跃异常后台会提示异常考勤让老师人工核验。这个方案不能完全杜绝作弊但能降低作弊的隐蔽度。第三种是照片中检测到的多张人脸比如一个学生拿了三部手机帮三个人签到。我的接口里限定了uploaded_encodings的数量必须恰好检测到一张人脸就算照片里同时出现多张脸也只有正中间那一张才会被取出来比对其余人会被拦截。实际测试中这个限制效果不错。4.3 定位精度实测记录不同楼宇环境下的表现这部分数据我花了两个星期在不同教学楼里测出来的。在开阔的操场边GPS精度在5到10米普通教学楼里走廊和教室靠窗的位置定位精度约15到30米老旧教学楼深处、没有靠窗的内侧教室GPS漂移最厉害可以达到50米以上甚至有一次测出过110米的偏移。所以签到半径的设置根本不能一刀切。我给老师端的建议是创建课程的时候先实地打开地图在教学楼中心取一个点根据教学楼面积和教室分布把半径设置在100到200米之间。如果想更精准还可以让学生提前在教室里打几个参考点后端自动算出教室中心减少老师手动定位的工作量。5. 部署上线中的踩坑记录依赖、模型和前后端联调5.1 face_recognition安装的灾难现场这个库的安装是我在整个项目里踩过最深的一个坑。在macOS上装它相对轻松但在Windows上如果你没装好Visual C Build Tools和dlib的编译环境pip install face_recognition能给你报出一堆红字错误。dlib需要编译Boost和CMake很多人在这步就直接放弃了。后来我找到的最省事方案是用Anaconda装dlibconda install -c conda-forge dlib会好很多。然后是face_recognition的模型文件它默认从网上下载dlib的预训练模型大约60到100MB国内网络有时候下不动我建议先把模型文件手动下载好放到项目目录并设置环境变量指向它。不然上线那天现场下载几节课都等不起。5.2 前后端联调的典型错误坐标丢失和文件上传超时我在联调阶段遇到了两个非常典型的错误都是那种单个模块正常、拼起来就废的问题。第一个是定位坐标丢失。有段时间签到接口时报缺少latitude参数的频率很高排查发现小程序端用户点击签到太快定位还没返回就跳到拍照页等照片拍完要提交时经度纬度为0。我最后的解决办法是用户在签到页先点准备签到按钮小程序先请求定位定位成功并显示定位已锁定距离教室XX米后再启用拍照按钮。这样从流程上杜绝了坐标丢失。第二个是照片上传超时。班级人数多一些之后同一时间十几个学生签到Flask默认的开发服务器Werkzeug是单线程的文件上传请求和Face Recognition推理会互相卡住一个照片推理耗时两三秒十几个请求排队就是半分钟起步。我临时改用gunicorn启动4个worker排队问题基本消失。另外前端压缩图片也很关键如果一张原图3MB直接上传接口响应就要6到8秒小程序的超时配置还不一定扛得住。5.3 Flask生产运行的补充不要用调试模式对外服务调试模式app.run(debugTrue)下Flask会带一个交互式调试器这在本地很舒服但一旦对外开放简直就是风险敞口。我上线时用的是gunicorn跑Flask命令大概是gunicorn -w 4 -b 0.0.0.0:5000 app:app如果服务器本身配置不高worker数不要开太多因为每个worker都会加载一次face_recognition库和模型内存占用很大。我在2核4G的云服务器上开4个worker内存直接涨到3.2G后来改回2个worker才稳定。5.4 小程序审核相关的心得如果你的小程序要正式发布需要过微信的审核。审核团队会重点检查人脸识别相关功能的隐私合规要求你说明收集人脸照片的用途、存储方式、是否加密、是否有删除机制。我在后台加了一个用户可申请删除人脸数据的功能申诉材料里把这个截图放进去审核通过率会高不少。这里涉及的内容较多如果只是本地演示用可以先忽略。6. 这套系统的后续进阶方向项目做完一个完整版本之后我反而觉得它更像是一个考勤系统骨架后续扩展空间非常大。比如考勤数据可以接入已有的教务系统做最终成绩联动人脸识别可以从静态照片升级成摄像头实时流识别配合活体检测识别安全性会提升一个档次定位可以用WiFi指纹或者蓝牙信标iBeacon方案替代GPS解决教学楼深处GPS漂移的问题。还有一个很实际的方向把签到页做成随堂测验模式老师签到的时候出一道选择题学生做完题签到就算完成这样不光防代签还能把考勤环节变成教学互动的一部分这可能才是老师真正想要的。从最初想治一治纸质签到表的小念头到这个系统完整跑通中间虽然踩了不少坑但回过头来看整个架构的实用价值很高。如果你也在做类似的选课签到考勤项目我的建议是先把人脸定位双校验这条主链路跑通再做外围功能不要一上来就想着功能大而全否则排错和打磨的时间会成倍增加。