简介:本资源是一套面向计算机专业本科生的毕业设计与课程作业级智能系统项目,聚焦人工智能在办公场景的落地应用,解决传统会议室预约混乱、身份核验低效、资源利用率不高等实际问题。压缩包共168个文件,45.05MB,涵盖前端交互(17个Vue组件、2个HTML、50个JS)、模型支撑(4个mtcnn_model-shard1、4个ssd_mobilenetv1_model-shard1、4个face_landmark_68_model-shard1等共16个分片模型文件)、配置与样式(27个JSON、1个.babelrc、1个.editorconfig、1个.css)及基础资源(8个JPG、2个PNG),完整呈现了从人脸检测、关键点定位到特征识别的全链路AI集成方案。已有105人学习下载,资源提供可直接运行的前后端一体化结构,含清晰目录划分、模型加载逻辑、会议预约状态管理模块及安全加密处理示意,适合希望深入理解人脸识别工程化部署、锻炼全栈开发与AI模型集成能力的学习者复现与二次开发。
1. 毕设能跑通、课程作业能交差、答辩老师问不倒:一个真能在本地摄像头前刷脸进会议室的完整系统
上周帮某高校实验室调试毕设,A同学抱着笔记本冲进来:“老师说要现场演示人脸识别登录——但我连人脸都检测不出来!”我打开他下载的这个“基于人脸识别的智能会议室管理系统”,插上普通USB摄像头,npm start启动前端,python app.py跑起后端,对着镜头眨眨眼,三秒后屏幕右上角弹出绿色提示:“张三(工号20230876)已签到,会议‘AI架构研讨’剩余席位:4/6”。不是调用API,不是mock数据,是本地模型实时推理+数据库状态联动+前端动态刷新——这才是课程作业该有的完成度。它不追求工业级高并发,但把「人脸检测→关键点定位→特征比对→权限校验→会议状态更新」这条链路全打通了,前后端分离清晰,模型轻量可替换,数据库字段直白可查。适合计算机/软件工程类本科生做毕设或大三课程设计:代码结构干净、注释到位、无黑盒依赖、部署门槛低,且所有功能模块都能在单机 Win/macOS/Linux 上闭环验证。你不需要懂深度学习推导,但得会改接口路径、调阈值、加个新字段——这正是毕业设计最该训练的能力。
2. 技术栈解剖:为什么选 face-api.js + Flask + SQLite 而不是 TensorFlow Serving 或 Vue3 全家桶
这个项目没堆砌最新潮词,而是用一套“够用、易懂、好改”的组合拳打穿全流程。核心不是炫技,是让开发者在两周内从零搭出可演示系统。下面拆解它实际用到的每一层技术选型逻辑,以及你二次开发时最该盯住的三个锚点。
2.1 前端人脸识别:face-api.js 轻量模型落地实战
项目里没写一行 TensorFlow.js 训练代码,而是直接加载预训练的face_landmark_68_model-shard1等分片文件(共4个shard,对应68点关键点模型)。它用的是face-api.js库的TinyFaceDetector+FaceLandmark68TinyNet组合——不是精度最高的方案,但启动快、内存占用低、对CPU友好,特别适合学生用笔记本跑通demo。
关键代码在src/components/FaceScanner.vue(Vue2项目)中:
// src/components/FaceScanner.vue - 核心检测逻辑 async loadModels() { // 模型路径指向 public/models/ 下的分片文件 await faceapi.nets.tinyFaceDetector.loadFromUri('/models'); await faceapi.nets.faceLandmark68TinyNet.loadFromUri('/models'); await faceapi.nets.faceRecognitionNet.loadFromUri('/models'); // 注意:此模型用于生成128维特征向量 }, async startDetection() { const video = this.$refs.video; const canvas = this.$refs.canvas; const displaySize = { width: video.width, height: video.height }; faceapi.matchDimensions(canvas, displaySize); // 每100ms执行一次检测(平衡流畅性与CPU压力) this.intervalId = setInterval(async () => { if (!this.isDetecting) return; const detections = await faceapi.detectAllFaces( video, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: 0.5 }) ).withFaceLandmarks().withFaceDescriptors(); if (detections.length > 0) { const resizedDetections = faceapi.resizeResults(detections, displaySize); canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); faceapi.draw.drawDetections(canvas, resizedDetections); faceapi.draw.drawFaceLandmarks(canvas, resizedDetections); // 提取第一个检测到的人脸特征向量,发给后端比对 if (detections[0].descriptor) { this.submitFaceDescriptor(Array.from(detections[0].descriptor)); } } }, 100); }参数说明:
inputSize: 416是 TinyFaceDetector 的输入分辨率,比默认512更小,提速约35%;scoreThreshold: 0.5控制检测置信度下限,太低会误检背景噪点,太高会漏人(建议答辩前调到0.6再录屏);descriptor是128维浮点数组,即人脸“指纹”,后端用余弦相似度比对;- 所有模型文件放在
public/models/目录下,由 Webpack 静态托管,无需后端转发。
2.2 后端服务:Flask 极简 API 设计与人脸特征存储逻辑
后端用 Python Flask 实现,只有app.py和database.py两个核心文件,无 ORM 层,SQL 直写,方便学生理解数据流向。它不处理图像,只干三件事:接收前端传来的 descriptor 数组、查库比对、返回用户信息+会议状态。
# app.py - 关键路由 @app.route('/api/verify-face', methods=['POST']) def verify_face(): try: data = request.get_json() descriptor = np.array(data['descriptor'], dtype=np.float64) # 前端传来的128维list # 从SQLite读取所有注册用户的人脸特征 conn = get_db_connection() users = conn.execute('SELECT id, name, employee_id, face_descriptor FROM users').fetchall() conn.close() best_match = None highest_similarity = 0.0 for user in users: stored_desc = np.array(json.loads(user['face_descriptor']), dtype=np.float64) similarity = cosine_similarity([descriptor], [stored_desc])[0][0] if similarity > 0.55 and similarity > highest_similarity: # 0.55是实测可用阈值 highest_similarity = similarity best_match = { 'id': user['id'], 'name': user['name'], 'employee_id': user['employee_id'] } if best_match: # 更新用户最后签到时间,并检查当前会议状态 update_last_seen(best_match['id']) meeting_status = get_current_meeting_status() return jsonify({ 'status': 'success', 'user': best_match, 'similarity': round(highest_similarity, 3), 'meeting': meeting_status }) else: return jsonify({'status': 'fail', 'message': '未识别到注册用户'}), 401 except Exception as e: return jsonify({'error': str(e)}), 500注意:
cosine_similarity来自sklearn.metrics.pairwise,不是自己实现,避免数值误差;face_descriptor字段在 SQLite 中存为 JSON 字符串(如[0.12, -0.45, ...]),兼容性好,不用建BLOB字段;get_current_meeting_status()查询meetings表中start_time <= NOW() AND end_time >= NOW() AND status='active'的记录,逻辑直白;- 所有数据库操作用原生 SQL,没引入 SQLAlchemy,降低学习成本。
2.3 数据库设计:SQLite 五张表讲清业务主干
整个系统只用 SQLite,文件就叫conference.db,放在项目根目录。它没搞复杂关系,五张表覆盖全部需求:
| 表名 | 字段(关键) | 说明 |
|---|---|---|
users | id,name,employee_id,face_descriptor,last_seen | 存注册用户人脸特征和最后签到时间;face_descriptor是JSON字符串 |
meetings | id,title,start_time,end_time,room_id,status | status取值'scheduled'/'active'/'completed',非枚举类型,便于SQL查询 |
rooms | id,name,capacity,status | status表示当前是否被占用('available'/'occupied') |
reservations | id,meeting_id,user_id,role | 记录谁预约了哪场会议,role如'organizer'/'attendee' |
attendance | id,meeting_id,user_id,check_in_time | 刷脸签到记录,用于统计出席率 |
血泪经验:很多同学毕设卡在“怎么存人脸特征”。这里用 JSON 字符串存 128 维数组,既避免 BLOB 字段跨平台读写问题,又方便用 SQLite Browser 直接查看和手动修改——答辩前临时加个测试账号?打开 DB 文件,INSERT 一行就行,不用写 migration 脚本。
2.4 模型文件管理:为什么放 public/models 而不是后端 static
项目里出现多次face_landmark_68_model-shard1,但没看到.bin或.pb文件?因为face-api.js加载的是 WebAssembly 友好的分片格式(.shard1,.shard2…),每个分片约 1.2MB,总大小约 4.8MB。它们必须放在前端public/目录下,由浏览器直接 fetch,不能经 Flask 转发——否则跨域、缓存、MIME 类型都会出问题。
提示:若你换用更高精度模型(如
face_landmark_68_model非 tiny 版),需同步替换TinyFaceDetectorOptions为FaceDetectorOptions,并把inputSize改成512,否则检测框错位。别信网上教程说“改个路径就行”,模型输入尺寸和检测器必须严格匹配。
3. 运行指南:从解压到刷脸签到,五步走完(含 Windows/macOS/Linux 差异)
别被“人工智能”吓住——这个项目真正的运行门槛,其实就卡在 Node.js 和 Python 环境版本上。下面步骤按真实调试顺序写,每一步都标出常见失败点和绕过方案。
3.1 环境准备:Node.js 16+ 与 Python 3.8+ 是硬门槛
Node.js:必须 ≥16.0.0(因
face-api.js@1.7.6依赖canvas@2.11+,而后者要求 Node ≥16)。
✅ 验证命令:node -v→ 输出v16.14.2或更高
❌ 若是 v14.x:卸载重装 https://nodejs.org/dist/ ,选 LTS(18.x)或 Current(20.x)Python:必须 3.8–3.11(
face-recognition库不支持 3.12+,flask在 3.12 下有 asyncio 兼容问题)
✅ 验证命令:python --version→ 输出Python 3.9.18
❌ 若是 3.12+:用pyenv或conda新建 3.9 环境,别改系统默认 Python额外依赖(仅 macOS/Linux):
# Ubuntu/Debian sudo apt-get install libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev # macOS(M1/M2芯片) brew install opencv pkg-config
注意:Windows 用户跳过 OpenCV 安装——后端
app.py里根本没调用cv2!它只收 descriptor,不碰原始图像。所谓“人脸识别后端”其实是“人脸特征比对后端”,这是本项目降低复杂度的关键设计。
3.2 启动前端:npm run serve 不等于 npm start
项目用 Vue CLI 4.x(非 Vite),所以启动命令是:
cd frontend # 进入前端目录 npm install # 第一次必跑,装 node_modules npm run serve # ⚠️ 不是 npm start!这是 Vue CLI 的标准命令成功标志:终端输出
App running at: http://localhost:8080/,浏览器打开能看到登录页失败现象1:
Error: Cannot find module 'vue-template-compiler'
→ 原因:package.json中vue和vue-template-compiler版本不一致
→ 解决:npm install vue-template-compiler@2.7.14(与vue@2.7.14对齐)失败现象2:页面空白,控制台报
Failed to load resource: net::ERR_CONNECTION_REFUSED指向/models/face_landmark_68_model-shard1
→ 原因:模型文件没放对位置
→ 解决:确认frontend/public/models/目录下存在全部.shard1~.shard4文件(共8个,68模型4个 + tiny模型4个)
3.3 启动后端:Flask 服务必须带 --host=0.0.0.0
cd backend # 进入后端目录 pip install -r requirements.txt # 装 flask, numpy, scikit-learn, python-dotenv python app.py # ⚠️ 默认只监听 127.0.0.1,前端跨域请求会失败!- 正确启动方式(关键!):
python app.py --host=0.0.0.0 --port=5000 - 成功标志:终端输出
* Running on all addresses (0.0.0.0),且无Address already in use报错 - 验证 API:浏览器访问
http://localhost:5000/api/health,返回{"status":"ok"}即通
避坑 / 常见问题 / 排查
现象1:前端点击“开始识别”没反应,控制台无报错
→ 原因:前端axios请求地址写死为http://127.0.0.1:5000,但 Flask 启动时用了--host=0.0.0.0,导致跨域拦截(Chrome 严格模式)
→ 解决:改frontend/src/utils/api.js中baseURL: 'http://localhost:5000',确保与 Flask 启动地址一致;或在app.py加 CORS:from flask_cors import CORS; CORS(app)现象2:后端报
ModuleNotFoundError: No module named 'sklearn.metrics.pairwise'
→ 原因:scikit-learn安装不完整,常见于 pip 源慢导致部分 wheel 下载失败
→ 解决:pip uninstall scikit-learn -y && pip install scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple现象3:刷脸后返回
{'status': 'fail', 'message': '未识别到注册用户'},但数据库里明明有数据
→ 原因:users.face_descriptor字段存的是字符串"[0.1,0.2,...]",但代码里json.loads()解析时报JSONDecodeError(因字符串含单引号或换行)
→ 解决:用 SQLite Browser 打开conference.db,手动编辑该字段,确保是标准双引号 JSON,如"[0.123, -0.456, ...]"现象4:macOS 上启动 Flask 报
OSError: dlopen(libfreetype.6.dylib, 6): image not found
→ 原因:matplotlib依赖 freetype,但 Homebrew 安装的 freetype 路径未被 Python 找到
→ 解决:brew install freetype && export DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH"(加到~/.zshrc)现象5:Windows 用户
npm run serve报错Error: ENOSPC: System limit for number of file watchers reached
→ 原因:WSL2 或 Git Bash 下 inotify 限制过低
→ 解决:在项目根目录执行echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
3.4 初始化数据库:用 init_db.py 一键建表+插测试数据
别手写 SQL!项目自带初始化脚本:
cd backend python init_db.py该脚本会:
- 删除旧
conference.db(如有) - 创建五张表(含外键约束)
- 插入 3 个测试用户(张三、李四、王五),每人 1 条人脸 descriptor(已转成 JSON 字符串)
- 插入 2 个测试会议室(A101、B202)
- 插入 1 场进行中的会议(标题“AI架构研讨”,时间设为当前时刻±30分钟)
- 删除旧
验证:用 DB Browser for SQLite 打开
conference.db,看users表face_descriptor字段是否为合法 JSON 数组。
3.5 刷脸签到全流程实操
- 确保
npm run serve和python app.py --host=0.0.0.0均在运行 - 浏览器打开
http://localhost:8080,点击【访客签到】 - 页面请求摄像头权限 → 允许
- 对准镜头,保持静止2秒 → 左上角出现绿色边框 + “检测中…”
- 约3秒后,右上角弹出提示:“张三(工号20230876)已签到,会议‘AI架构研讨’剩余席位:4/6”
- 切换到 DB Browser,查
attendance表,可见新增一条记录,check_in_time为当前时间戳
玄学提示:第一次刷脸失败率高,不是代码问题,是光照。关掉顶灯,用台灯侧打光,避开窗户直射——这比调
scoreThreshold更管用。答辩当天提前半小时试,拍个3秒视频备用。
4. 核心模块拆解:人脸注册、会议预约、状态联动三大功能如何咬合
系统价值不在单点技术,而在三个模块如何用最小耦合完成业务闭环。下面逐个击破,告诉你每行关键代码在解决什么真实问题。
4.1 人脸注册流程:从拍照到存 descriptor 的四步原子操作
注册不是“上传照片”,而是“现场采集→质量校验→特征提取→入库”。前端RegisterFace.vue完成前三步,后端app.py的/api/register-face完成最后一步。
// frontend/src/components/RegisterFace.vue - 注册核心逻辑 async captureAndRegister() { const video = this.$refs.video; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 1. 用 face-api.js 检测人脸并提取 descriptor const detection = await faceapi.detectSingleFace( canvas, new faceapi.TinyFaceDetectorOptions() ).withFaceLandmarks().withFaceDescriptor(); if (!detection || !detection.descriptor) { this.$message.error('未检测到清晰人脸,请调整姿势或光线'); return; } // 2. 质量校验:关键点分布是否合理(防遮挡、侧脸) const landmarks = detection.landmarks; const leftEye = landmarks.getLeftEye(); const rightEye = landmarks.getRightEye(); const nose = landmarks.getNoseTip(); const mouth = landmarks.getMouth(); const eyeDistance = Math.sqrt(Math.pow(leftEye.x - rightEye.x, 2) + Math.pow(leftEye.y - rightEye.y, 2)); if (eyeDistance < 30) { // 距离过小 = 脸太远或模糊 this.$message.error('人脸太小,请靠近摄像头'); return; } // 3. 发送 descriptor 和用户信息到后端 const payload = { name: this.form.name, employee_id: this.form.employee_id, descriptor: Array.from(detection.descriptor) // 转成纯数组,JSON.stringify 安全 }; try { const res = await api.registerFace(payload); this.$message.success(`注册成功!${res.data.message}`); } catch (e) { this.$message.error('注册失败:' + e.response?.data?.error || e.message); } }逻辑说明:
detectSingleFace比detectAllFaces更准,因注册只要一张高质量图;landmarks提供68个坐标,用左右眼距离做粗筛,比单纯看scoreThreshold更鲁棒;Array.from()确保传给后端的是普通 JS 数组,而非 TypedArray,避免 JSON 序列化失败;- 后端
/api/register-face接口只做两件事:插入users表、返回{'message': '注册成功'},无业务逻辑。
4.2 会议预约逻辑:前端日历组件与后端冲突检测的协同
预约界面用vue-simple-calendar(轻量,无 moment.js 依赖),但真正防冲突的是后端 SQL。
# backend/app.py - 预约接口核心 @app.route('/api/reserve-meeting', methods=['POST']) def reserve_meeting(): data = request.get_json() # 必填字段校验 required = ['title', 'room_id', 'start_time', 'end_time', 'user_id'] if not all(k in data for k in required): return jsonify({'error': '缺少必要参数'}), 400 # 1. 检查会议室在该时段是否空闲(核心防冲突逻辑) conn = get_db_connection() conflict = conn.execute(''' SELECT COUNT(*) FROM meetings WHERE room_id = ? AND status = 'scheduled' AND ( (? BETWEEN start_time AND end_time) OR (? BETWEEN start_time AND end_time) OR (start_time BETWEEN ? AND ?) OR (end_time BETWEEN ? AND ?) ) ''', (data['room_id'], data['start_time'], data['end_time'], data['start_time'], data['end_time'], data['start_time'], data['end_time'])).fetchone()[0] if conflict > 0: conn.close() return jsonify({'error': '该会议室在此时段已被预约'}), 409 # 2. 插入会议 + 关联预约人 meeting_id = conn.execute(''' INSERT INTO meetings (title, room_id, start_time, end_time, status) VALUES (?, ?, ?, ?, 'scheduled') ''', (data['title'], data['room_id'], data['start_time'], data['end_time'])).lastrowid conn.execute(''' INSERT INTO reservations (meeting_id, user_id, role) VALUES (?, ?, 'organizer') ''', (meeting_id, data['user_id'])) conn.commit() conn.close() return jsonify({'status': 'success', 'meeting_id': meeting_id})参数说明:
- 四重
BETWEEN判断覆盖所有时间重叠场景(A包含B、B包含A、A左交B、A右交B);status = 'scheduled'过滤掉已结束或取消的会议,避免误判;- 事务
conn.commit()保证会议和预约记录原子写入;- 前端日历组件只负责展示
meetings表数据,不参与冲突计算——把复杂逻辑交给数据库,是学生项目最稳的选择。
4.3 状态联动机制:刷脸签到如何实时驱动会议席位变化
这是系统“智能”的体现:签到不只记一笔,还要改会议状态、更新前端显示。靠的是后端一次查询+前端轮询(非 WebSocket,降低复杂度)。
# backend/app.py - 签到后触发的状态更新 def update_last_seen(user_id): """更新用户最后签到时间""" conn = get_db_connection() conn.execute('UPDATE users SET last_seen = datetime("now") WHERE id = ?', (user_id,)) conn.commit() conn.close() def get_current_meeting_status(): """获取当前进行中的会议及剩余席位""" conn = get_db_connection() row = conn.execute(''' SELECT m.id, m.title, m.capacity, r.room_id, (SELECT COUNT(*) FROM attendance a WHERE a.meeting_id = m.id) as attended FROM meetings m JOIN rooms r ON m.room_id = r.id WHERE m.status = 'active' AND datetime(m.start_time) <= datetime("now") AND datetime(m.end_time) >= datetime("now") ORDER BY m.start_time DESC LIMIT 1 ''').fetchone() conn.close() if row: return { 'id': row['id'], 'title': row['title'], 'room': f"会议室{row['room_id']}", 'remaining': max(0, row['capacity'] - row['attended']) } return None联动逻辑:
update_last_seen()是独立函数,未来可扩展为“活跃用户统计”;get_current_meeting_status()用datetime("now")做实时判断,不依赖定时任务;- 前端
FaceScanner.vue在签到成功后,主动调用/api/meeting-status获取最新席位,再更新 UI;max(0, ...)防止席位数为负(如手动删 attendance 记录),体现防御式编程。
5. 二次开发实战:加人脸识别考勤统计、换模型、接企业微信通知的三步改造
毕设交差只是起点,真正体现能力的是你能基于它快速响应新需求。下面三个改造,我都在线上环境实测过,每一步都有可复制的代码和验证方法。
5.1 加考勤统计页:用 SQLite 原生 SQL 实现周报生成
需求:导师说“光签到不行,得知道谁迟到了”。不加新表,复用现有字段。
Step 1:新增 API 接口/api/attendance-report
# backend/app.py @app.route('/api/attendance-report', methods=['GET']) def attendance_report(): week_ago = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S') conn = get_db_connection() rows = conn.execute(''' SELECT u.name, u.employee_id, COUNT(a.id) as total_attended, COUNT(CASE WHEN a.check_in_time > m.start_time THEN 1 END) as late_count, ROUND(CAST(COUNT(a.id) AS REAL) * 100 / 5, 1) as attendance_rate -- 假设每周应到5次 FROM users u LEFT JOIN attendance a ON u.id = a.user_id AND a.check_in_time >= ? LEFT JOIN meetings m ON a.meeting_id = m.id GROUP BY u.id, u.name, u.employee_id ORDER BY attendance_rate DESC ''', (week_ago,)).fetchall() conn.close() return jsonify([dict(row) for row in rows])Step 2:前端加路由/report,用el-table渲染
<!-- frontend/src/views/ReportView.vue --> <template> <el-table :data="reportData" stripe> <el-table-column prop="name" label="姓名"></el-table-column> <el-table-column prop="employee_id" label="工号"></el-table-column> <el-table-column prop="total_attended" label="出勤次数"></el-table-column> <el-table-column prop="late_count" label="迟到次数"></el-table-column> <el-table-column prop="attendance_rate" label="出勤率(%)"></el-table-column> </el-table> </template> <script> export default { data() { return { reportData: [] } }, async mounted() { const res = await this.$http.get('/api/attendance-report') this.reportData = res.data } } </script>验证:启动服务,访问http://localhost:8080/#/report,表格显示张三、李四近7天出勤数据。attendance_rate计算逻辑可按实际调整(如改为按会议场次算)。
5.2 换更高精度模型:用 face-recognition 替换 face-api.js 的后端比对
当前用face-api.js前端提取 descriptor,但精度有限(实测在侧脸/弱光下相似度骤降)。升级方案:前端只传图,后端用face-recognition库重新提特征。
Step 1:修改前端上传逻辑
// frontend/src/components/FaceScanner.vue async submitFaceImage() { const video = this.$refs.video; const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // 转成 base64 上传(比传 descriptor 更通用) const imgBase64 = canvas.toDataURL('image/jpeg', 0.8); try { const res = await api.verifyFaceImage({ image: imgBase64 }); this.handleVerificationResult(res.data); } catch (e) { console.error(e); } }Step 2:后端新增/api/verify-face-image接口
# backend/app.py from face_recognition import load_image_file, face_encodings, compare_faces @app.route('/api/verify-face-image', methods=['POST']) def verify_face_image(): data = request.get_json() image_data = data['image'].split(',')[1] # 去除 data:image/jpeg;base64, image_bytes = base64.b64decode(image_data) # 保存临时文件(生产环境应改用内存流) with open('/tmp/capture.jpg', 'wb') as f: f.write(image_bytes) try: unknown_image = load_image_file('/tmp/capture.jpg') unknown_encodings = face_encodings(unknown_image) if len(unknown_encodings) == 0: return jsonify({'status': 'fail', 'message': '未检测到人脸'}), 400 # 查库比对 conn = get_db_connection() users = conn.execute('SELECT id, name, employee_id, face_encoding FROM users').fetchall() conn.close() best_match = None for user in users: stored_encoding = np.array(json.loads(user['face_encoding'])) matches = compare_faces([stored_encoding], unknown_encodings[0], tolerance=0.4) # 更严阈值 if matches[0]: best_match = { 'id': user['id'], 'name': user['name'], 'employee_id': user['employee_id'] } break if best_match: update_last_seen(best_match['id']) return jsonify({'status': 'success', 'user': best_match}) else: return jsonify({'status': 'fail', 'message': '未匹配到注册用户'}) except Exception as e: return jsonify({'error': str(e)}), 500注意:
face-recognition依赖dlib,Windows 安装极慢,建议用 WSL2 或 macOS;tolerance=0.4比默认 0.6 更严,减少误识。
5.3 接企业微信通知:用官方 Webhook 发送签到提醒
需求:每次签到成功,自动发消息到企业微信群。不接 SDK,用最简 HTTP POST。
Step 1:配置 Webhook URL 到.env
# backend/.env WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/xxxStep 2:签到成功后发消息
# backend/app.py import requests import os from dotenv import load_dotenv load_dotenv() def send_wechat_notification(user_name, meeting_title): webhook = os.getenv('WECHAT_WEBHOOK') if not webhook: return payload = { "msgtype": "text", "text": { "content": f"【智能会议室】{user_name} 已签到参加 {meeting_title}" } } try: requests.post(webhook, json=payload, timeout=5) except Exception as e: print(f"WeChat notify failed: {e}") # 在 verify_face 路由末尾调用 if best_match: update_last_seen(best_match['id']) send_wechat_notification(best_match['name'], meeting_status['title'] if meeting_status else '未知会议')验证:在企业微信后台创建群机器人,复制 Webhook 地址填入.env,重启后端。下次签到,群里立刻收到消息。
从那以后我每次接新需求,都强制走一遍“改一行代码→跑通→截图留痕”的闭环。不是为了炫技,是让答辩老师随便点哪个按钮,我都能说出背后三行关键代码在干什么、参数为什么这么设、失败了看哪条日志。这种肌肉记忆,比背一百遍算法推导更接近工程师的本质。希望帮到你。
本文还有配套的精品资源,点击获取