干过门禁对接的人都清楚,真正耗时间的从来不是设备能不能跑通,而是业务系统和设备之间怎么“对话”。鸿蒙人脸识别门禁机看着是个嵌入式硬件,但背后牵涉的协议设计、数据模型、消息可靠性和运维可观测性,一点不比开发一个企业级后台简单。这篇文章想聊聊我实际落地鸿蒙人脸识别门禁对接业务系统时,沉淀下来的一套 API + MQTT 双层通道方案和工程规范。它能解决什么问题?简单说就是:让人脸识别设备的事件上报、远程开门、人员底库下发、设备状态监控这些事情,有一套清晰、稳定、可扩展的对接方式,而不是今天补一个接口明天加一个回调。无论你是做智慧园区、企业考勤、访客系统,还是校园安防,只要手头有鸿蒙底座的智能门禁终端,这篇文章提供的接口定义、Topic 规划、QoS 选型和避坑经验,都可以直接拿去做参考基线。
1. 对接之前,先把鸿蒙门禁当作“三类能力”来看
1.1 大多数团队卡在哪里
很多团队拿到人脸识别门禁机的第一反应是看 SDK 文档,然后照着文档把“远程开门”“抓拍上报”几个函数跑通,就算完事。结果项目一扩大就露馅:设备从十几台变成上百台,业务系统从一套变成考勤、访客、报警三套,每个平台都来对接,代码改得乱七八糟,消息时不时丢,设备离线也没人知道。
这里的问题不是某个接口写得不好,而是从一开始就把设备看成了“一堆 API 的集合”,没有从能力维度去拆解。门禁终端在业务系统眼里,不是一串接口,而是三件事:开门与布防控制、人员底库管理、识别事件上报。这三件事的实时性要求、数据量级、失败处理方式完全不同,自然不能共用一套对接通道。
1.2 三类能力对通道的要求完全不同
我把三类能力在正式对接前先列成一张表,团队按这张表去约定通道,后面基本不会打架。
| 能力分类 | 典型操作 | 实时性要求 | 推荐通道 | 原因 |
|---|---|---|---|---|
| 门禁控制 | 远程开门、常开/常闭、布防撤防 | 极高,秒级响应 | MQTT 指令下行 | 命令简单、频次低,需要快速到达并确认 |
| 人员底库管理 | 新增/修改/删除人员、下发人脸底库 | 低,秒级到分钟级均可 | HTTP API | 数据量大、需要同步知道处理结果、需要做全量/增量同步 |
| 识别事件上报 | 识别成功、识别失败、陌生人、防拆报警 | 高,大部分需要实时推送 | MQTT 事件上行 | 设备主动产生、突发性强、平台需要被动接收 |
门禁控制走 MQTT,是因为它的核心诉求是“尽快把指令送到设备”,而且设备执行完还能异步回执。人员底库走 API,则是因为一个几千人的园区,一次性下发人脸照片可能有几十兆数据,用 MQTT 去推既不合适也不可控,HTTP 的分页、断点续传、超时重试机制更成熟。识别事件走 MQTT,是因为这类消息是设备在某个随机时刻主动冒出来的,不可能让业务系统拿着 HTTP 接口不停轮询,那样既浪费资源又做不到实时。
1.3 鸿蒙端为什么适合做这件事
很多朋友对“鸿蒙人脸识别门禁”有误解,以为它只是把 Android 换了个壳。实际上现在的鸿蒙底座门禁设备,已经是一台可以裁剪、可以离线的边缘计算终端。人脸比对直接在端侧 SDK 里完成,原始人脸图和特征值不需要上云,业务系统拿到的只是“比对结果 + 人员ID + 抓拍图片URL”。这个特性对工程对接影响很大:即使网络抖动、平台宕机,门禁机依然能本地核验并放行,事件先缓存,等网络恢复再补传。
所以对接方案要从第一天就支持“弱网可生存”,不能假设设备永远在线。这也是为什么我坚持 API 和 MQTT 双通道并行,而不是把鸡蛋都放在一个篮子里。
2. API 通道怎么设计:接口清单、鉴权、幂等与数据格式
2.1 先定接口范围,别把 API 当垃圾桶
API 通道承载的是“需要知道结果”的操作。接口数量不宜贪多,我一般控制在一个设备接入需要的最小集合:
- 设备注册与鉴权:
POST /api/v1/devices/register,设备首次上线换取 accessToken。 - 人员底库管理:
POST/PUT/DELETE /api/v1/devices/{deviceId}/persons,支持单个操作和批量操作。 - 底库查询:
GET /api/v1/devices/{deviceId}/persons,分页返回,用于全量对账。 - 设备动作:
POST /api/v1/devices/{deviceId}/actions,例如远程开门、重启、校时。 - 设备状态同步:
GET /api/v1/devices/{deviceId}/status,供平台主动探测。 - 抓拍图与事件补传:
POST /api/v1/devices/{deviceId}/events/batch,离线缓存的事件通过这个口补上来。
这些接口的共同特征是“请求-响应”模式:平台发一个请求,设备必须明确告诉平台成没成。比如远程开门用 API 去调也不是不行,但 HTTP 的超时、重试、连接池占用,都会让“开门”这种需要极低延迟的操作变得不可控,所以它更适合走 MQTT。而人员下发不同,平台需要确认设备底库到底更新到哪一条,漏了还要能对账,这种场景用 API 恰好合适。
2.2 鉴权与安全:Token 轮换、设备级密钥、HTTPS
设备不是只活在内网,很多项目里设备在园区各个角落,通过 4G 或 Wi-Fi 接入平台,所以鉴权不能省。我常用的方案是动态 Token 加设备级密钥:
设备第一次注册时携带设备序列号和预置密钥,平台校验通过后返回accessToken和refreshToken。accessToken有效期两个小时,设备每次调用 API 时放在Authorization: Bearer <token>里;过期后用refreshToken换取新的。这样做的好处是,即使某个 Token 泄露,攻击者拿到的最多是一张两小时有效的临时凭证。
MQTT 这边我不用 Token,而是给每台设备下发独立的 MQTT 用户名和密码,用户名直接用 deviceId,密码由平台根据设备密钥动态签名生成,可以定期轮换。这样一条连接泄露不会拖累全网设备。传输层全部要求 HTTPS 和 MQTTS(TLS 加密),不要图省事用明文,否则人脸ID、设备ID在网络里裸奔,安全隐患太大。
2.3 数据格式与错误码:统一就是生产力
接口联调最烦的就是各写各的返回结构。我的约定是所有 API 统一返回:
{ "code": 0, "message": "success", "requestId": "6f6f9f2a-9e5a-4cbb-9cba-4f1ca4f9e6c1", "data": {} }code用整数,0 表示成功,非 0 表示失败。错误码要有分域设计,不能一锅粥:
- 1xxx:网关与鉴权错误,如 1001 Token 过期、1002 设备未注册。
- 2xxx:设备侧错误,如 2001 设备离线、2002 人脸底库已满。
- 3xxx:业务侧错误,如 3001 人员不存在、3002 照片格式不支持。
每个请求还必须带幂等键。为什么?因为 HTTP 在弱网下超时是家常便饭,平台重试一次,如果设备那边已经执行成功,就会重复开门或重复插入人员。我在请求头里约定Idempotency-Key,取值统一为“设备ID + 操作类型 + 本地时间戳”的 MD5。设备收到带同一幂等键的请求,直接返回上一次的结果,而不是重复执行。
3. MQTT 通道怎么设计:事件上行/指令下行、Topic 规划与 QoS 选型
3.1 Topic 树设计是第一步
MQTT 用得好不好,一半看 Topic 设计。Topic 本质上是一棵树,设计得好,订阅关系、权限控制、后续扩展都会很省心。我用的方案是三层结构:
- 事件上行:
dg/edge/{deviceId}/event/{eventType} - 指令下行:
dg/plat/{deviceId}/cmd/{cmdType} - 指令回执:
dg/edge/{deviceId}/cmd/reply - 设备状态:
dg/edge/{deviceId}/status/online、dg/edge/{deviceId}/status/offline
edge和plat分开,是为了在服务端做 ACL 权限控制时,能明确区分哪些客户端允许发布、哪些允许订阅。平台端只订阅dg/edge/#,设备端只订阅dg/plat/{deviceId}/#,双向隔离,不会串线。
event/{eventType}里 eventType 具体展开就是face_success、face_fail、stranger、alarm这类。每个主题承载一类事件,好处是业务系统可以只订阅自己关心的类型,比如访客系统只关注face_success,报警系统只关注alarm,互不干扰。
3.2 QoS 选型:不同消息不同可靠性
MQTT 的 QoS 是三档,很多第一次用的人直接全选 QoS 0,省事是省事,丢消息也丢得悄无声息。我按消息类型做了分级:
| 消息类型 | 建议 QoS | 理由 |
|---|---|---|
| 远程开门指令 | QoS 1 | 至少送达一次,不能丢,丢一次就是安全事故 |
| 识别成功/失败事件 | QoS 1 | 作为考勤记录不能丢,允许重复后靠幂等去重 |
| 陌生人或报警事件 | QoS 1 | 涉及安防,必须可靠 |
| 设备心跳 | QoS 0 | 丢了下次心跳还能继续判断,没必要占用可靠性开销 |
| 日志类消息 | QoS 0 | 可丢,不影响核心业务 |
QoS 2 我基本不用。它虽然能保证“恰好一次”,但性能消耗大,而且不少开源 broker 对 QoS 2 的支持并不完善。工程上用 QoS 1 + 业务幂等,是性价比最高的组合。
远程开门指令需要特别小心。它不仅要 QoS 1,还要带cmdId和超时时间。设备收到指令后,无论开门成功还是失败,都要往dg/edge/{deviceId}/cmd/reply发一条回执。平台如果在一定时间内没收到回执,就自动进入告警状态,而不是傻等。
3.3 消息体与指令确认机制
事件消息体我统一用 JSON,所有事件都有一个公共头:
{ "eventId": "a3f2c0e1-7b4a-4c6d-9e1f-2d8b6a0c4e5d", "deviceId": "DG-HM-001", "eventType": "face_success", "timestamp": 1735689600123, "data": { "personId": "P10086", "name": "张三", "score": 0.9723, "doorId": "DOOR-A-01", "captureUrl": "https://oss.example.com/capture/xxx.jpg?expire=180&sign=abc", "temperature": 36.4 } }eventId是全局唯一的,业务系统拿到它必须做去重。timestamp用毫秒级 UTC,不能用设备本地时间格式化字符串,否则不同设备时区一乱,整个事件排序就废了。captureUrl是带签名的临时地址,平台需要下载抓拍图时直接用,过期自动失效,比在 MQTT 里传图片二进制优雅得多。
指令下行的消息体里,必须包含cmdId,设备回执时原样带回,这样平台才能把回执和指令一一对应。
{ "cmdId": "cmd-20250101-001", "cmdType": "remote_unlock", "deviceId": "DG-HM-001", "timestamp": 1735689600000, "params": { "doorId": "DOOR-A-01", "reason": "访客接待" } }收到这份指令后,设备执行开门动作,再往回执主题发一条:
{ "cmdId": "cmd-20250101-001", "code": 0, "message": "ok", "timestamp": 1735689601500 }平台根据cmdId找到对应的指令上下文,标记为“已执行”。这套机制看着简单,但能解决至少一半“指令到底执行没有”的扯皮问题。
4. API 与 MQTT 怎么分工:什么时候走同步请求,什么时候走异步消息
4.1 三条判断标准:实时性、数据量、是否要求结果回到发起方
刚开始做这类对接的团队经常问:到底哪些走 API,哪些走 MQTT?我归纳出三条判断标准,照着套就行。
第一,发起方是谁。如果是平台主动操作且需要立刻知道结果,走 API;如果是设备端在某一个外部事件触发下主动产生数据,走 MQTT。
第二,数据量多大。人员底库全量同步、抓拍图批量补传,动辄几兆甚至几十兆,走 MQTT 会把消息通道堵死,走 API 更稳;而一条“识别成功”事件只有几百字节,走 MQTT 毫无压力。
第三,能不能接受延迟。远程开门、布防撤防这种操作,用户按了手机按钮,门必须在一两秒内打开,MQTT 的长连接天然保证低延迟;而新增一个员工、修改一张人脸照片,晚几秒生效完全没人在意,API 就够了。
4.2 典型流程拆解
以访客系统为例,把流程完整串一遍,就能理解双通道怎么配合。
访客在公众号预约,业务系统生成访客记录,然后调用 API 下发访客的姓名和人脸照片到指定的门禁设备,接口返回“下发成功”。这一步是低频、需要确认的操作,必须用 API。到了访客到访时间,访客在门禁机前刷脸,设备端本地比对通过,立刻通过 MQTT 往平台推送一条face_success事件,事件里带着 personId、抓拍图 URL 。平台收到事件后,更新访客的到访状态,通知接待人。这个过程平台是被动接收,必须用 MQTT。
远程开门更典型。保安在监控中心看到陌生人在门口,点了“远程开门”,平台把开门指令塞进 MQTT,设备秒级收到并执行,再回执给平台。整个过程如果用 HTTP,最怕的就是设备正好断网重连、请求超时,保安连点五次,门开了五回,这就是事故。
4.3 混合场景怎么处理
有些场景天然是混合的,比如抓拍图。我在前面的事件体里用了captureUrl,意思是图片上传走 HTTP,事件通知走 MQTT。设备先调用 API 上传图片,拿到 URL 后把这个 URL 塞进 MQTT 事件里,业务系统不必接收二进制大文件。这样既绕开了 MQTT 对大消息不友好的问题,又保证了事件的实时性。
离线补传也是混合场景。设备断网期间,人脸识别照常工作,事件先缓存在本地。网络恢复后,设备不是一股脑用 MQTT 重推,而是先调用 API 查询平台最后收到的事件ID,再通过POST /api/v1/devices/{deviceId}/events/batch把缺失的事件批量补传。这样平台侧做增量补齐,而不是重复处理一大堆已经收到过的消息。
5. 工程规范落地:字段字典、错误码、时间同步与安全加固
5.1 文档先行:先定协议后写代码
我在项目启动时一定会逼团队先写三张表,不写清楚不许动代码。这三张表是接口定义表、事件定义表、字段字典。
- 接口定义表:每个 API 的路径、方法、请求参数、返回结构、错误码、调用频率限制。
- 事件定义表:每个 MQTT 主题、事件类型、触发时机、事件体各字段含义。
- 字段字典:所有字段的统一命名、类型、是否必填、取值范围、示例。
字段字典尤其重要。比如“人员ID”这个字段,如果设备厂商 SDK 里叫userId,业务系统里叫staffNo,平台数据库里叫personId,对接起来就是一场灾难。我在文档里强制统一为personId,数据类型字符串,正则限制为字母数字下划线,最大长度 32。
5.2 时间同步:一切问题之母
门禁事件能不能准确排序、考勤记录准不准,全看时间对不对。设备端的本地 RTC 经常因为断电、扣电池跑偏,所以必须做两件事:
一是所有接口和消息里的时间戳统一用毫秒级 UTC,禁止用"2025-01-01 12:00:00"这种带时区的格式。业务展示层要显示本地时间,由业务系统自己做转换,设备不负责。
二是设备每天必须调用一次校时 API,或者通过 MQTT 消息里的平台时间来回校。平台侧在收到事件时,也要记录 broker 的到达时间,防止设备时间错误导致整个事件流排序混乱。这样,即使设备时间错得离谱,平台仍然知道消息实际是什么时候到的。
5.3 安全加固:设备不是只跑在内网
很多门禁项目因为设备在办公园区内网,就放松了安全要求,这是大忌。设备分布在各个物理位置,物理安全、网络安全都不完全可控,该做的加固一样不能少。
人脸特征数据不能明文出现在消息体里。事件消息里的 personId 可以认为是相对不敏感的标识,但如果业务系统需要同步人脸特征(不是抓拍图,而是用于比对的特征向量),必须用不可逆的加密摘要处理,并且通过带鉴权的 API 下发,不能落到 MQTT 里。原始人脸图片一律传对象存储,用短期签名 URL 访问,不能暴露一个永久的图片地址。
API 网关要做限流和设备白名单。单设备每分钟调用次数超过阈值直接拒绝,未注册的 deviceId 直接丢掉。MQTT Broker 的 ACL 规则也要配好,设备只能发布自己的事件主题,不能往别的设备主题里发消息。
5.4 可观测性:设备日志与消息追踪
联调阶段最头疼的就是一条消息丢了,不知道丢在设备端、网络还是平台端。我的解决办法是全面铺开链路追踪:API 请求头里带requestId,MQTT 消息体里带eventId或cmdId,设备日志、平台日志都带着这些 ID 落盘。一旦出问题,拿 eventId 一查,就知道设备有没有发、网络有没有转、平台有没有消费。
设备在线率是另一项硬指标。平台必须有一个监控任务,如果设备超过 N 分钟没有任何消息上报,就判定离线,推告警给运维。这个能力靠 MQTT 的遗嘱消息实现:设备异常断网时,Broker 会自动往dg/edge/{deviceId}/status/offline主题发一条遗嘱消息,平台只要订阅了这个主题,就能秒级感知离线。
6. 我在实际对接中踩过的坑:断线重连、重复事件、离线缓存与联调顺序
6.1 MQTT 断线重连:指数退避 + 随机抖动
设备在弱网环境下断线重连是常态,但最容易出的问题还不是“连不上”,而是“重连风暴”。几百台设备断网后同时恢复,一起向 Broker 发起连接,Broker 直接拒绝服务。我的做法是设备端重连时用指数退避加随机抖动,第一次重连等 1 秒,第二次等 2 秒,第三次 4 秒,上限 30 秒,每次随机加 0 到 1 秒的抖动。这样设备都卡在不同的时间点重连,不会扎堆。
重连成功后不是直接完事,设备要立刻调用一次 API 刷新接入凭据,因为断网期间 Token 可能已经过期。用过期 Token 去连 MQTTS,连接必被拒,很多设备就卡在“一直重连一直失败”的循环里。
6.2 重复事件:QoS 1 的“陷阱”
QoS 1 是“至少一次”,也就是说同一事件在网络抖动时很可能被投递两遍。头一回上线的时候,平台收到一条重复的识别成功事件,结果给访客重复放行、重复生成考勤记录、还给访客的接待人发了两遍通知,现场很尴尬。
解决办法是消费端做幂等。我用的是 Redis 的 SETNX,拿事件的eventId当 key,如果 key 不存在说明是第一次收到,正常处理;如果 key 已存在说明是重复事件,直接丢弃。数据库一层也要给eventId建唯一索引,双保险。这个坑基本每个新项目都会踩一次,提前在规范里写死“所有事件消费必须幂等”,能省大量屁滚尿流的 bug 修复时间。
6.3 离线缓存:先存本地再等 ACK
设备断网的时候如果还在持续产生识别事件,而这些事件只是试着往 MQTT 里发、发不出去就丢,那断网这一段时间的通行记录就全没了。考勤、安防场景这是不可接受的。
所以我要求设备端必须有一个“待确认事件队列”,事件先写进本地掉电安全存储,成功发到平台并收到 ACK 之后才删除。网络恢复后,先走 API 批量补传。这个队列要注意磁盘磨损,不能频繁写,所以队列长度要有限制,满了之后按策略丢弃最旧的日志类事件,但识别成功、报警这类核心事件不允许丢,只能阻塞。
6.4 联调顺序:先 API 后 MQTT 再混合
最后说说联调顺序,这是我自己趟出来的经验。千万不要一上来就把所有功能一把梭,否则出了 bug 根本定位不到是哪一层的问题。
我建议分三步走。第一步,先调 API:设备注册、Token 换取、人员下发、状态查询全部跑通,确保设备具备基本的“被管理”能力。第二步,再调 MQTT:设备建连、心跳、事件上报、指令下行与回执,确保长连接链路稳定。第三步,才做混合场景:离线补传、图片上传 + 事件通知、Token 轮换后自动重连。
每一步都要有模拟器配合验证。比如让设备模拟断网 5 分钟再恢复,看看离线事件补传是不是完整、顺序是不是正确。这种测试做扎实了,上线后才不会天天被现场运维电话叫醒。
结合我自己这几年的经验,这套 API + MQTT 的工程规范算不上什么高深发明,但它确实把“设备和系统对话”这件事从“能用”推进到了“靠谱”。如果你也在做鸿蒙人脸识别门禁对接,我建议先从 Topic 规划和消息幂等这两处下手,这两块设计扎实了,后面的开发和运维会顺得非常明显。