最近在开发一个基于 uni-app 的微信小程序,核心功能是接入百度云的人脸录入和人脸识别。项目里既要支持用户自助录入人脸,又要在门禁/签到场景下完成 1:N 识别。这一套做完之后,身边好几个朋友都在问人脸能力在小程序里到底怎么接、要注意哪些坑。这篇文章我把整套实现思路从头撸一遍,包含百度云平台准备、人脸录入和人脸识别的前后端核心代码、微信小程序环境下的关键配置,以及我实测下来遇到的高频问题和处理方式。无论你是正在做考勤门禁、会员识别,还是想给自己的小程序加一个人脸登录,这篇文章都值得先收藏再慢慢看。
1. 方案选型与整体架构思考
1.1 为什么用 uni-app + 百度云这套组合
先说选型。小程序原生开发其实也能做人脸识别,但你一旦有 App 端、H5 端的潜在需求,原生开发的维护成本就上来了。uni-app 的优势在于一套代码编译到微信小程序、App、H5 等多个平台,我这次的项目就是先出微信小程序,后续很可能要同步出 App 版本,所以直接选了 uni-app。
人脸识别服务这块,百度云(现在叫百度智能云)是我的首选。理由很简单:开通简单、有免费额度、接口文档比较规范,而且人脸检测、人脸注册、人脸搜索这些能力都是标准 RESTful 接口,uni-app 里用uni.request就能对接,不需要额外引入重量级 SDK。对比了一些离线识别方案,离线 SDK 在微信小程序里基本跑不动,云识别反而是最务实的路线。
顺便提醒一下,这里说的百度云不是存资源的百度网盘,是百度智能云的 AI 开放平台,别搞混了。人脸识别相关能力都在“人脸识别”产品下面,控制台搜索就能找到。
1.2 整体流程与技术链路
先把整体链路理清楚,后续写代码才不会乱。
人脸录入流程:小程序端打开摄像头拍照 -> 图片压缩转 base64 -> 先调百度人脸检测接口做质量校验 -> 校验通过后调人脸注册接口 -> 百度把人脸特征存入人脸库,返回注册结果。
人脸识别流程:小程序端拍照 -> 同样先压缩转 base64 -> 调百度人脸搜索接口 -> 后端拿到返回的 user_id 和匹配分数 -> 根据业务规则放行或拒绝。
注意我的流程里多了一步“人脸检测前置”。百度的人脸注册接口本身就带quality_control参数,为什么还要多调一次?核心原因是体验。检测接口可以提前拿到模糊度、光照、人脸角度这些信息,我可以在前端给出“请正对屏幕”“光线太暗”“请摘掉遮挡物”这类明确提示,而不是等注册接口返回一个冷冰冰的错误码。用户感知完全不一样,录入成功率也更高。
1.3 关键决策:密钥必须放后端
这是我个人最想强调的一点。百度的 API Key、Secret Key 绝对不能放在小程序前端代码里。小程序包本质上就是一份可以被反编译的静态资源,密钥一旦泄露,别人就能拿你的账号调接口,轻则产生费用,重则被人恶意写入大量人脸数据。
正确做法是后端封装一层转发接口:前端把图片 base64 和用户标识传给后端,后端拿着密钥去调百度接口,再把结果返回前端。如果你没有独立服务器,可以考虑 uniCloud 云函数,用云函数转发请求,省去服务器运维成本。我这次项目后端是用的 Node.js,文章后面给的示例代码也是 Node 风格。
2. 百度云接入准备:开通服务与凭证管理
2.1 创建应用与人脸库规划
首先在百度智能云控制台完成账号登录,搜索“人脸识别”,开通服务。然后在控制台左侧找到“应用列表”,创建一个新应用,创建完成后能看到应用对应的 API Key 和 Secret Key,这两个值就是后面调接口的凭证。
接着要规划人脸库的结构。百度人脸识别里的核心概念是group_id(用户组)和user_id(用户 ID)。一个组下面可以有多个用户,一个用户下面可以有多张人脸。门禁场景通常按部门或场所建组,比如building_a_group;我用的是staff_group。这个组 ID 后面注册和搜索都要用,一定要约定好,不要随手写几个字符串就丢一边。
免费额度这块,百度会给你一定的免费调用量,个人开发测试完全够用。但如果识别频率很高,提前在控制台看一下计费说明,避免产生意外费用。
2.2 access_token 获取与缓存技巧
调用百度人脸识别 API 时,绝大多数接口都要带access_token。它是用 API Key 和 Secret Key 换来的临时凭证,有效期大约 30 天。
这里有个非常容易踩的坑:不能每次请求都去换 token。百度对获取 token 的接口有频率限制,频繁调用会报错,而且每次都多一次网络请求,白白浪费时间。正确做法是后端获取一次,然后把 token 缓存起来,只在快要过期时重新获取。
下面是我项目里用的 Node 端获取和缓存 token 的示例,配合一个简单的 JSON 文件做缓存。
const axios = require('axios'); const fs = require('fs'); const path = require('path'); const API_KEY = '你的API_KEY'; const SECRET_KEY = '你的SECRET_KEY'; const TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token'; const CACHE_FILE = path.join(__dirname, 'token_cache.json'); async function getAccessToken() { // 读缓存,如果没过期直接用 if (fs.existsSync(CACHE_FILE)) { const cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); if (cache.access_token && cache.expire_time > Date.now()) { return cache.access_token; } } const res = await axios.get(TOKEN_URL, { params: { grant_type: 'client_credentials', client_id: API_KEY, client_secret: SECRET_KEY } }); const { access_token, expires_in } = res.data; // 提前 5 分钟过期,避免边缘时间被拒 const expire_time = Date.now() + (expires_in - 300) * 1000; fs.writeFileSync(CACHE_FILE, JSON.stringify({ access_token, expire_time })); return access_token; }这个缓存逻辑很简单:启动后第一次请求真实获取 token,后续直接读缓存;缓存文件里记录的过期时间到了,下次自动换新的。实测下来非常稳定,几乎不会碰到 429 限流。
2.3 核心接口版本与请求地址
百度人脸识别接口我目前用的是 v3 版本,请求域名是aip.baidubce.com。三个核心接口的 path 如下:
- 人脸检测:
/rest/2.0/face/v3/detect - 人脸注册:
/rest/2.0/face/v3/faceset/user/add - 人脸搜索:
/rest/2.0/face/v3/search
如果后续平台接口版本升级,用法可能略有变化,建议以官方文档“人脸识别 API”为准。我下面的示例基于 v3 接口,思路依然适用于新版。
3. 人脸录入功能的完整实现
3.1 小程序端拍照与图片压缩处理
录入人脸第一步是拿到清晰的正脸照片。微信小程序里调起相机拍照,我推荐用uni.chooseMedia,它比老接口uni.chooseImage功能更全,支持直接指定sourceType: ['camera']。
拿到临时文件路径后,不要急着转 base64 上传。相机拍出来的原图通常有几 MB,直接转 base64 会让请求体变得很大,不仅上传慢,还可能触发百度接口的大小限制。所以要先压缩。uni-app 提供了uni.compressImage接口,可以按质量压缩,也可以指定压缩后的宽度。
压缩参数我实测下来的经验是:目标宽度设置 500px 左右,质量压到 80,既能保证人脸特征清晰,又能把 base64 控制在几百 KB 以内。这个体积对网络传输和百度接口都比较友好。
压缩完成后,把图片转成 base64。微信小程序端可以通过FileSystemManager读取本地文件并指定编码为 base64:
function fileToBase64(filePath) { return new Promise((resolve, reject) => { const fs = uni.getFileSystemManager(); fs.readFile({ filePath: filePath, encoding: 'base64', success: (res) => resolve(res.data), fail: (err) => reject(err) }); }); }拿到 base64 字符串后,接下来就可以带着它去请求后端或直接调检测接口了。
3.2 人脸质量前置检测,把废图挡在门外
这一步是我强烈建议加的环节。调用百度人脸检测接口,能拿到照片里人脸的概率、角度、模糊度、光照、完整度等质量信息。把这些信息拿来做前置判断,注册成功率会有非常明显的提升。
检测接口的核心参数:
image:base64 图片字符串image_type:固定BASE64face_field:指定要返回的质量字段,至少要包含quality,angle,face_probability
后端转发逻辑写在 Node 里,可以用 axios 把请求打到百度。下面是检测接口的封装示例:
const axios = require('axios'); async function faceDetect(base64Image, accessToken) { const url = `https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=${accessToken}`; const res = await axios.post(url, { image: base64Image, image_type: 'BASE64', face_field: 'face_probability,angle,quality' }); return res.data; }拿到返回结果之后,重点看这几个字段:
face_probability:是人脸的概率,建议大于 0.8angle_list:人脸左右偏转、俯仰、平面旋转角,建议角度控制在 30 度以内quality.blur:模糊程度,越小越清晰quality.illumination:光照强度,不能太暗也不能太亮,百度有个合理范围quality.completeness:完整度,最好接近 1,表示五官没有遮挡
我项目里的判断逻辑是:概率太低提示“请正对屏幕”,角度太大提示“请保持正脸”,模糊度偏高提示“请保持稳定不要晃动”,光照不合适提示“请到光线均匀的地方”。每个阈值具体是多少,建议你拿自己手机在真实环境下多测几轮再定,因为百度的质量分和实际感受有时候会有偏差,自己定出来的阈值最贴合产品体验。
3.3 人脸注册接口调用与用户 ID 映射
质量检测通过之后,就该调注册接口了。人脸注册接口的路径是/faceset/user/add,参数比较多,逐个说:
image:base64 图片image_type:BASE64group_id:人脸库组 IDuser_id:用户自定义 IDuser_info:用户备注信息,可以传姓名或手机号quality_control:质量控制,建议NORMALliveness_control:活体控制,建议NORMAL
user_id这里有个关键点:百度对user_id的格式有要求,只能数字、字母、下划线,而且长度有限制。微信小程序的 openid 虽然大多是字母数字下划线组合,但为了安全和长度考虑,不建议直接用 openid 当user_id。我后端的做法是:根据 openid 生成一个自增 ID,或者用 md5 对 openid 做哈希再截断,然后把 openid 和user_id的映射关系存到数据库里。
注册接口的 Node 转发代码:
const axios = require('axios'); async function faceRegister(base64Image, accessToken, groupId, userId, userInfo) { const url = `https://aip.baidubce.com/rest/2.0/face/v3/faceset/user/add?access_token=${accessToken}`; const res = await axios.post(url, { image: base64Image, image_type: 'BASE64', group_id: groupId, user_id: userId, user_info: userInfo || '', quality_control: 'NORMAL', liveness_control: 'NORMAL' }); return res.data; }返回结果里,如果error_code为 0,说明注册成功。如果返回user_id已存在,就需要你根据业务决定是提示用户“已录入”,还是调用更新接口faceset/user/update覆盖旧人脸。我建议在注册前先查一下用户是否已录入,已录入的走更新流程,这样重复录入不会报错,用户体验更流畅。
4. 人脸识别功能的完整实现
4.1 1:N 人脸搜索逻辑
识别场景最常用的是 1:N 搜索,也就是拿一张人脸去库里找他是谁。搜索接口路径是/rest/2.0/face/v3/search。
请求参数:
image:待识别的 base64 图片image_type:BASE64group_id_list:要搜索的组,多个组用逗号分隔,比如staff_groupquality_control:NORMALliveness_control:NORMAL
搜索接口的返回结果里有一个score,代表匹配度。这个值非常重要,一定要在后端设定过滤阈值来决定是否放行。比如门禁场景,低于 80 分一律拒绝,因为低分代表可能不是同一个人,甚至可能是陌生人。
为什么单独强调这个阈值?因为百度搜索接口不管是不是库里的人,都会给你返回一个最接近的结果。如果你直接拿 top1 当作识别结果,那陌生人也会被当成某个员工放进去,门禁形同虚设。阈值定多少看你的场景:安全要求高的可以定 85 甚至 90,要求宽松、重点是防重复签到的可以定 75 左右。我建议上线前拿真实用户照片做一轮测试,画一个分数分布图再定。
后端搜索封装:
const axios = require('axios'); async function faceSearch(base64Image, accessToken, groupIdList) { const url = `https://aip.baidubce.com/rest/2.0/face/v3/search?access_token=${accessToken}`; const res = await axios.post(url, { image: base64Image, image_type: 'BASE64', group_id_list: groupIdList, quality_control: 'NORMAL', liveness_control: 'NORMAL' }); return res.data; }4.2 与微信登录体系打通:code 换 token 换取 openid
人脸识别返回的是user_id,但业务里往往还要知道这个人的身份,比如姓名、工号。所以录入人脸前,一定要把微信的 openid 和后端的user_id绑定起来。
小程序的登录流程是这样的:前端uni.login拿到code,把 code 传给后端,后端拿 code 去微信接口jscode2session换 openid 和 session_key。这个流程就是大家常说的 code 换 token。
const axios = require('axios'); async function code2Session(code) { const appid = '你的小程序APPID'; const secret = '你的小程序SECRET'; const url = 'https://api.weixin.qq.com/sns/jscode2session'; const res = await axios.get(url, { params: { appid, secret, js_code: code, grant_type: 'authorization_code' } }); return res.data; // { openid, session_key, ... } }拿到 openid 后,后端先查库里有没有这个 openid,没有就创建一条用户记录,生成新的user_id;已经有就直接返回绑定的user_id。这样人脸搜索命中后,后端就能用user_id反查到用户信息,返回姓名、部门、工号之类的前端展示数据。
这块设计好的好处是:以后用户换手机、换微信环境,人脸数据都不会丢,因为人脸是和user_id绑定的,和微信登录态解耦。
4.3 识别结果的业务落地
识别接口通了之后,真正要做的其实是业务层。门禁和考勤是典型场景,识别成功后需要记一条通行记录或打卡记录。记录里至少要包含时间、地点、设备/小程序标识、识别分数。识别失败也要记录,方便事后排查。
我当时还额外处理了“重复打卡”问题:限制同一用户 5 分钟内只能有一次有效打卡记录,防止用户频繁刷脸刷出多条数据。这个逻辑很简单,就是在写入打卡记录前查一下最近一条记录的时间。
如果你的业务需要更严格的身份核验,比如“确保操作者是账号本人”,可以考虑百度的人脸比对接口/rest/2.0/face/v3/match,让用户登录后直接拍一张照片和库里的人脸做 1:1 比对。这个能力适合支付、隐私信息查看等高安全场景,接入方式也类似,只是把 search 换成 match。
5. 微信小程序环境下的关键细节
5.1 域名白名单与 network unavailable 报错
微信小程序有个让很多人头疼的规矩:wx.request只能请求指定域名下的接口。上线环境下,所有请求域名都必须在微信公众平台后台“开发管理 - 开发设置 - 服务器域名”里配置,而且必须是 HTTPS。
开发调试阶段,你可以在微信开发者工具右上角“详情 - 本地设置”勾选“不校验合法域名”,这样本地接口能正常调通。但一旦要用真机预览,或者上传体验版,这个开关就不生效了。很多人在真机上遇到request:fail或network unavailable,第一反应是网络问题,其实大概率是域名校验没过,或者证书有问题。
我实测的一个排查顺序:第一,看接口地址是不是 HTTPS,域名有没有备案;第二,看微信公众平台后台的 request 合法域名有没有填对;第三,看证书链是否完整,很多免费证书在手机端会提示证书无效,回到开发者工具里反而正常;第四,看代码里有没有拼错域名或端口。大多数网络层报错都是这四个原因。
5.2 HBuilderX 发行微信小程序的几个要点
uni-app 项目在 HBuilderX 里打包发布到微信小程序,流程其实很固定,但新手容易卡在几个地方。
先在manifest.json的“微信小程序配置”里填上自己的 AppID,注意不是测试号就是正式 AppID。然后开发调试时,点击 HBuilderX 菜单栏“运行 - 运行到小程序模拟器 - 微信开发者工具”。如果没反应,很大概率是微信开发者工具没开启服务端口。你需要打开微信开发者工具的“设置 - 安全设置”,开启“服务端口”选项。
正式发布就在 HBuilderX 菜单栏点“发行 - 小程序-微信”,构建后会在项目dist/build/mp-weixin目录下生成微信小程序代码,然后用微信开发者工具导入这个目录,点击“上传”上传版本,再去微信公众平台提交审核。
这里额外提醒一句:如果你的小程序要用摄像头并涉及人脸信息,提审时需要在“用户隐私保护指引”里明确声明摄像头、相册等隐私接口的使用目的。不声明的话,审核阶段很可能会被打回。
5.3 相机授权与兼容性优化
相机权限是使用人脸录入前绕不开的一步。uni-app 里可以用uni.authorize来申请权限,如果用户拒绝过,再次调用授权会直接失败,这时要引导用户去设置页手动打开。
当前端拿到图片 base64 后,建议做一个统一的请求封装,设置合理的超时时间。人脸检测、注册、搜索接口因为涉及图片上传和云端计算,通常比普通接口慢,超时时间我一般设置 10 秒以上,避免在网速差的场景下被前端提前判定失败。
还有一点要留意:部分 Android 机型拍出来的照片自带旋转信息,可能在转 base64 后出现人脸角度不对的问题。我在项目中用uni.compressImage压缩后基本能消除大部分旋转问题,但如果你的用户群体里有大量老旧安卓机,建议在真机上多测几个品牌。iPhone 这边的兼容性整体会好一些。
6. 常见问题与排查技巧实录
6.1 高频报错与解决方案速查表
| 报错现象 | 出现阶段 | 原因分析 | 解决办法 |
|---|---|---|---|
| 百度返回“人脸未找到” | 注册/识别 | 拍照时正脸不完整、遮挡较多 | 前置检测接口先判断,优化拍摄引导文案 |
| 百度返回“图像质量差” | 注册 | 光线过暗或过曝、图片分辨率过低 | 压缩保留人脸区域,提示用户到光线均匀处拍摄 |
| 百度返回“用户已存在” | 注册 | 同一个 user_id 重复注册 | 注册前查询,已存在则调用 update 接口覆盖 |
| 小程序请求报 url not in domain list | 所有接口 | 域名白名单没配或开发者工具未关校验 | 后台配置合法域名,或开发时勾选不校验 |
| 真机请求报 network unavailable | 所有接口 | 证书不合法、域名未备案、本机网络异常 | 按 5.1 节的排查顺序逐项确认 |
| 搜索接口返回 score 过低 | 识别 | 人脸被遮挡、光线变化大、录入照片太旧 | 调整阈值,引导用户补录多张人脸 |
这块内容建议直接复制到你的项目文档里,后续同事接手少走很多弯路。遇到百度返回的error_code时,一定不要把错误码硬编码在判断逻辑里,先对照官方文档确认这个错误码在当前版本下是否仍存在,因为云厂商的文档更新频率不低。
6.2 避坑清单
- AK/SK 和 access_token 永远不要出现在前端代码里,前端只传图片和业务需要的标识。
- access_token 一定要做缓存,每次请求都换 token 不仅慢,还会触发限流。
- 注册前一定要做人脸质量检测,宁可多一次请求,也别让用户录一张废图进人脸库。
- 搜索接口必须设置 score 阈值,否则陌生人会被当作库里的人返回。
- 图片 base64 体积控制在 1M 以下,压缩目标 500px 宽度、80 质量是比较稳的组合。
- 活体控制参数记得打开,
liveness_control设为NORMAL或HIGH,能挡掉一部分照片和视频攻击。注意它不是绝对安全,安全要求极高的场景需要额外做二次核验。 - 不要在微信开发者工具里把模拟器当成最终测试环境,模拟器对摄像头的模拟能力和真机差异很大,人脸这种强依赖硬件的功能,必须真机测试。
- 隐私合规别偷懒,小程序涉及人脸数据,用户协议和隐私指引要写清楚用途,否则审核会卡。
另外再分享一个我在实际项目中的体会:人脸能力接入本身不难,真正花时间的是调优“通过率”和“误识别率”的平衡。比如同一间办公室,上午逆光、下午顺光,拍出来的照片质量完全不同,阈值定死了一个固定值,实际使用中总有人怎么刷都不过。后来我在后台上线了一个动态阈值配置接口,先按 80 分默认值跑,观察一周真实识别分数的分布,再根据场景微调。这种灰度调优的思路,比一上来就把阈值定死要稳妥得多。
整套功能上线到现在已经稳定跑了几个月,录入的人脸数据也在持续增加。后续我打算把识别记录和告警通知串起来,比如识别到陌生人时给管理员推一条消息。这次先写到这里,希望这套实现思路能帮你少踩几个坑。