主题
客户端小程序 — 人脸录入与重录
上级文档:客户端小程序
概述
对应主文档 §8.4、§9.4(合规)、§13.1
人脸录入是用户刷脸入场的前置条件。小程序负责采集照片并上传至云端,不在端上做识别。录入前需展示隐私说明。
防薅羊毛设计:人脸数据与用户账号强绑定,重录行为受到多层安全约束,防止用户通过更换人脸将会员权益转借他人。
页面路由
| 路由 | 页面 | 说明 |
|---|---|---|
/pages/face/guide | 人脸录入引导 | 首次使用说明,可跳过 |
/pages/face/enroll | 人脸采集 | 调起摄像头采集照片 |
/pages/face/liveness | 活体检测(可选) | 前置活体检测增强安全性 |
/pages/profile/face-manage | 人脸管理 | 个人中心 → 重录入口(已入场成功后隐藏) |
显示与隐藏规则
核心设计:用户不可查看自己已录入的人脸照片(避免隐私不安感),只展示录入状态和重录入口。
| 用户状态 | 人脸管理入口 | 重录按钮 | 说明 |
|---|---|---|---|
| 未录脸 | 显示「去录入」 | — | 引导完成首次录入 |
| 已录脸、从未入场成功 | 显示 | 可点击(受安全约束) | 允许重录 |
| 已录脸、曾入场成功 | 隐藏 | — | 入口从个人中心菜单消失 |
入场成功判断:用户至少有一次刷脸进门记录(
faceLog存在成功的 enter 事件),即判定为「曾入场成功」。前端通过GET /api/v1/user/face/status接口的hasSuccessfulEntry字段判断。
录脸触发场景
| 场景 | 行为 | 强度 |
|---|---|---|
| 新用户首次登录 | 引导页,可跳过 | 弱引导 |
| 支付成功且未录脸 | 支付结果页显示「完成人脸录入」按钮 | 强引导 |
| 会员页查看且未录脸 | 黄色提醒条「未录入人脸无法入场」 | 中引导 |
| 个人中心 → 人脸管理 | 始终显示人脸状态 | 常驻入口 |
首次录入
引导页要素
- 隐私说明文字(合规要求)
- 拍摄要求提示(正对摄像头、确保光线充足、避免佩戴帽子墨镜)
- 「开始录入」主操作按钮
- 「暂时跳过」次操作
采集页要素
- 摄像头实时预览(使用
camera组件) - 人脸框辅助对齐
- 底部提示文案(随质量检测结果动态变化)
- 「拍照」按钮(通过前端质量检测后可点击)
前端质量检测
| 检测项 | 条件 | 不通过提示 |
|---|---|---|
| 正脸 | 面部角度偏转 < 15° | 「请正对摄像头」 |
| 光线 | 画面亮度在合理范围 | 「光线太暗/太亮,请调整」 |
| 清晰度 | 图像模糊度 < 阈值 | 「请保持不动,画面模糊」 |
| 遮挡 | 眼/鼻/嘴无遮挡 | 「请摘下口罩/墨镜/帽子」 |
| 多人 | 画面仅 1 张人脸 | 「请确保只有您一人在画面中」 |
前端仅做基础质量检测,最终判断由云端 API 完成。前端通过后自动拍照上传。
录入结果
成功:提示「人脸录入成功」+ 「返回首页」按钮
失败:展示失败原因(如「未检测到人脸」)+ 「重新拍摄」按钮
重录人脸 — 安全机制
核心原则:用户可以合理地重录自己的脸(如剃须、发型变化等),但不能借此将会员权益转借给他人。以下多层机制协同防薅。
重录触发路径
- 个人中心 → 人脸管理 → 「重新录入」(仅在从未入场成功时可见)
人脸数据存储策略
服务端永久保存用户的原始人脸和最后一次录入的人脸,用于安全比对。
| 数据 | 说明 |
|---|---|
originalFace | 用户首次录入时的人脸特征向量,永不覆盖 |
latestFace | 用户最近一次重录的人脸特征向量,每次重录覆盖更新 |
比对逻辑:
重录时,云端同时比对新上传的人脸与 originalFace 和 latestFace:
| 比对对象 | 规则 | 说明 |
|---|---|---|
新脸 vs originalFace | 相似度 ≥ 0.6 | 确认仍是本人(防止用户 A 首次录入 → 重录换人 B) |
新脸 vs latestFace | 相似度 ≥ 0.6 | 确认与上次录入变化不大(如不满足,需走人工审核) |
两项比对均通过才允许重录成功。任一不通过 → 拒绝并引导联系客服人工审核。
设计意图:原始人脸是最可信的基准(首次录入时攻击者尚未获利),最后一次人脸是最近的状态。双基准比对可同时防止「首次录入后换人」和「反复渐进换脸」。
重录安全约束(分层设计)
第 1 层:时间冷却
| 规则 | 说明 |
|---|---|
| 冷却期 | 两次重录之间至少间隔 24 小时 |
| 触发点 | 上一次重录成功的时间戳 |
| 前端展示 | 冷却期内显示「XX 小时后可重新录入」,按钮置灰 |
| 后端兜底 | 即使前端绕过,后端同样校验并拒绝 |
目的:防止用户高频重录,限制薅羊毛的操作窗口。
第 2 层:有效期/体验卡冻结期
| 规则 | 说明 |
|---|---|
| 有效会员期间 | 如用户有未到期的会员权益,不允许重录人脸 |
| 体验卡已使用 | 体验卡已进入健身房(次数已耗尽)后,不允许重录人脸 |
| 无权益 / 已过期 | 允许重录 |
目的:最核心的防薅规则。有有效权益时冻结重录,从源头杜绝「录脸 → 换脸 → 他人进入」的漏洞。
前端展示逻辑:
| 用户状态 | 人脸管理入口 | 重录按钮 | 提示文案 |
|---|---|---|---|
| 无权益、无录脸 | 显示「去录入」 | — | — |
| 无权益、已录脸、从未入场 | 显示 | 正常可点击 | 「重新录入」 |
| 无权益、已录脸、曾入场成功 | 隐藏 | — | — |
| 有有效月卡/季卡/年卡 | 显示 | 置灰 + 不可点击 | 「会员有效期内不支持更换人脸」 |
| 有有效次卡(剩余次数 > 0) | 显示 | 置灰 + 不可点击 | 「次卡有效期内不支持更换人脸」 |
| 有效次卡但次数为 0 | 显示 | 正常可点击 | 「重新录入」 |
| 体验卡已进入(次数耗尽) | 显示 | 置灰 + 不可点击 | 「体验卡已使用,不支持更换人脸」 |
| 体验卡已退款 | 显示 | 正常可点击 | 「重新录入」 |
| 体验卡未使用(待生效) | 显示 | 置灰 + 不可点击 | 「体验卡有效期内不支持更换人脸」 |
有有效权益时用户尚未入场成功属于异常状态(刚购买还没来),此时保留入口但冻结重录。
第 3 层:人脸相似度比对(双基准)
| 规则 | 说明 |
|---|---|
| 比对方式 | 云端 API 同时对比新上传的人脸与原始人脸(originalFace)和最后录入人脸(latestFace) |
| 阈值 | 两项比对均需 ≥ 0.6(行业通用阈值,可配置) |
| 通过 | 两项均 ≥ 0.6,允许重录 |
| 部分通过 | 新脸 vs 原始 ≥ 0.6 但 vs 最后一次 < 0.6 → 提示「人脸变化较大,如需更换请联系客服人工审核」 |
| 不通过 | 新脸 vs 原始 < 0.6 → 直接拒绝,「人脸验证失败,请联系客服人工审核」 |
目的:双基准比对。原始人脸是最可信的基准(首次录入时尚未产生权益),最后一次人脸反映近期状态。同时通过两项比对可防止「首次录本人 → 多次重录逐步换成他人」的渐进式攻击。
第 4 层:云端黑名单比对
| 规则 | 说明 |
|---|---|
| 比对方式 | 云端比对新上传的人脸特征与全局黑名单库 |
| 触发条件 | 每次录入/重录时 |
| 命中处理 | 拒绝录入,提示「人脸录入失败,请联系客服」 |
| 黑名单来源 | 管理后台手动添加(因薅羊毛、违规等被封禁的用户) |
第 5 层:工控机特征同步延迟
| 规则 | 说明 |
|---|---|
| 同步机制 | 重录成功后,最新特征(latestFace)通过 MQTT 推送至全部门店工控机 |
| 延迟容忍 | 推送消息设计为替换型(非追加),确保旧特征被覆盖 |
| 离线门店 | 工控机下次上线时全量同步,期间使用旧特征 |
| 并发保护 | 同一用户同时只能有一个有效特征,新特征覆盖旧特征 |
重录完整流程
个人中心 → 人脸管理 → 点击「重新录入」(仅从未入场成功时可见)
│
├─ 前端检查冷却期(24h)
│ └─ 未冷却 → 置灰按钮 + 倒计时提示
│
├─ 前端请求用户状态接口,检查权益冻结期
│ └─ 有有效权益 → 置灰按钮 + 冻结提示
│
├─ 弹出确认:「重新录入将覆盖您当前的人脸数据,确认?」
│ └─ 确认 → 进入采集页
│
├─ 拍照 + 前端质量检测
│ └─ 通过 → 上传至云端
│
├─ 云端处理(按顺序执行)
│ ├─ ❶ 冷却期校验(兜底)
│ ├─ ❷ 权益冻结期校验(兜底)
│ ├─ ❸ 人脸相似度比对(双基准:originalFace + latestFace,均 ≥ 0.6 通过)
│ ├─ ❹ 全局黑名单比对
│ ├─ ❺ 更新 latestFace,保留 originalFace 不变
│ └─ ❻ MQTT 推送所有门店工控机同步最新特征
│
├─ 重录成功 → 提示「人脸更新成功」
│
└─ 重录失败 → 提示原因 + 引导联系客服(如适用)人脸冻结期的运营放行
管理后台提供人工审核重录功能,用于特殊场景(如用户确实面貌变化很大、整容等)。
管理后台 — 人脸重录审核
| 功能 | 说明 |
|---|---|
| 审核入口 | 会员管理 → 用户详情 → 人脸管理 → 「审核重录申请」 |
| 用户申请路径 | 用户在小程序点击「更换人脸被拒绝」→ 联系客服 → 客服在管理后台提交审核 |
| 审核流程 | 管理员上传用户本人持身份证照片(或人工核验)→ 确认后解锁重录权限 |
| 审核通过 | 用户 24 小时内可重录一次,过期需重新申请 |
| 审核拒绝 | 告知用户原因 |
特殊场景处理
| 场景 | 处理方式 |
|---|---|
| 用户面貌自然变化(剃须、发型) | 正常重录,相似度比对通常可通过 |
| 用户整容 | 需联系客服,管理后台人工审核解锁 |
| 用户手机被盗 + 账号泄露 | 用户联系客服冻结账号 → 客服清除人脸数据 → 用户重新登录录入 |
| 非本人操作(账号借给他人) | 相似度比对不通过,拒绝重录 |
合规要求
- 录入前必须展示隐私说明,明确告知人脸数据用途
- 用户可跳过首次录入,不影响其他功能使用
- 重录受安全约束(冷却期 + 冻结期 + 相似度比对 + 黑名单),但无权益时用户仍可自由操作
- 生物特征数据的删除路径需在帮助或客服流程中可被告知
- 用户协议和隐私政策需可随时查看
- 人脸黑名单机制需在隐私政策中披露
异常处理
| 异常 | 处理 |
|---|---|
| 相机权限被拒 | 提示开启方式(设置 → 隐私 → 相机) |
| 质量不达标 | 提示具体原因,引导调整后重试 |
| 上传失败/超时 | 支持重试,设置 3 次上限 |
| 录入失败(服务端) | 展示具体错误原因 |
| 冷却期内重录 | 前端置灰 + 后端拒绝,显示剩余时间 |
| 有效权益期重录 | 前端置灰 + 后端拒绝,提示冻结原因 |
| 人脸相似度不通过 | 拒绝重录 + 引导联系客服人工审核 |
| 命中黑名单 | 拒绝录入 + 「请联系客服」 |
| 工控机同步失败 | MQTT 重试 + 工控机下次上线全量同步 |
接口
text
# 首次录入
POST /api/v1/user/face/enroll
Auth: JWT
Body: { image: string } // Base64
Response: { success: boolean, message: string }
# 检查是否允许重录(前端在进入重录前调用)
GET /api/v1/user/face/re-enroll/check
Auth: JWT
Response: {
allowed: boolean,
reason: string | null, // 不允许时的原因
cooldownExpiresAt: string | null, // 冷却期到期时间(ISO 8601)
freezeReason: string | null // 冻结原因:active_membership / used_trial
}
# 重录人脸
POST /api/v1/user/face/re-enroll
Auth: JWT
Body: { image: string } // Base64
Response: {
success: boolean,
message: string,
similarityScores: { // 双基准相似度分数
vsOriginal: number | null, // 与原始人脸的相似度
vsLatest: number | null // 与最后一次录入人脸的相似度
}
}
# 人脸管理信息
GET /api/v1/user/face/status
Auth: JWT
Response: {
enrolled: boolean,
enrolledAt: string | null,
lastReEnrollAt: string | null,
canReEnroll: boolean,
reEnrollReason: string | null,
hasSuccessfulEntry: boolean // 是否曾刷脸成功入场(控制人脸管理入口显隐)
}管理后台接口(新增)
text
# 提交人脸重录审核申请
POST /api/v1/admin/users/:userId/face/re-enroll-approve
Auth: JWT (管理员)
Body: { reason: string, identityProofUrl?: string }
Response: {
success: boolean,
unlockExpiresAt: string // 解锁截止时间(24h 后过期)
}
# 添加/移除人脸黑名单
POST /api/v1/admin/face/blacklist
DELETE /api/v1/admin/face/blacklist/:id
Auth: JWT (管理员)