多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

鸿蒙人脸识别门禁对接业务系统:API与MQTT工程规范实战

鸿蒙人脸识别门禁对接业务系统:API与MQTT工程规范实战 鸿蒙人脸识别门禁这东西单机跑起来不难真正让人头疼的是怎么跟业务系统打通。前阵子我正好在做一个园区项目设备端基于鸿蒙系统做人脸识别门禁后端要对接一套现成的综合管理平台。刚开始我天真地以为不就是调几个接口嘛结果真正进到联调阶段人员名单同步超时、开门事件推送丢失、接口鉴权被网关拦截各种问题轮着来。这篇文章我把整个对接过程中沉淀下来的API与MQTT工程规范整理出来从方案选型到接口设计再到鸿蒙端落地给正在做或者准备做同类项目的朋友一个可以参考的底稿。先说清楚这篇文章适合谁看如果你正在做鸿蒙设备端开发需要把门禁机、闸机、考勤机这类硬件接到自研或第三方的业务管理系统或者你是后端开发要设计一套供设备终端调用的开放接口那这篇文章能帮你省去不少试错成本。全文不涉及具体某家厂商的私有协议讲的是通用的对接方法和工程规范。1. 先搞清楚门禁机为什么要对接业务系统1.1 从单机可用到联网可管的跨越很多人第一次接触人脸识别门禁觉得设备本身就能完成人脸采集、特征提取、比对、开门那不就完事了吗确实单机模式下设备本地维护一个名单库录入人员直接在设备上操作识别通过就开锁这个流程完全跑得通。但问题在于一旦设备数量上来比如一个园区有几十台门禁、几千号人员单机模式就彻底失控了。人员入职、离职、临时访客授权、跨区域调岗这些变动如果都靠人工跑到每台设备上去操作效率极低不说还容易漏更新。更关键的是业务系统里需要记录每一次的通行记录、生成考勤报表、做访客追溯这些数据单机设备根本同步不上来。所以门禁设备必须对接业务系统本质上是把设备纳入到业务闭环里。从工程角度看这个对接涉及两条主要数据流一是人员基础数据和权限名单从业务系统下发到设备端这是下行链路二是识别记录、设备状态、异常告警从设备端上报到业务系统这是上行链路。两条链路都要稳定可靠缺一不可。1.2 对接方案选型的两个核心判断在定技术方案之前我通常先问自己两个问题这个对接场景是请求-响应为主还是事件推送为主实时性要求到底有多高门禁场景有个特点它的核心动作是开门而开门这个动作必须毫秒级完成不能等业务系统回复。所以真正的开门判断一定在设备端本地完成人脸特征值要提前同步到设备上。那设备端和业务系统之间的通信更多是定时同步和事件异步上报。这种情况下如果全用HTTP API每次设备都要主动去拉取或推送实时性会受限而且设备多了以后轮询压力很大。如果全用MQTT但MQTT更适合小消息、高频率的异步事件处理大批量名单同步这种重操作又不太合适。所以我最后的方案是双通道并存业务数据同步走HTTP API比如人员名单全量/增量下发、设备注册、配置拉取实时事件和设备控制走MQTT比如通行记录上报、远程开门指令、设备状态变更。这基本上也是目前市面上主流门禁平台的标准做法。后面的内容我会分别拆开讲。2. 接口设计规范API对接怎么定才不会返工2.1 接口整体规划与版本管理API对接最容易犯的错就是没有规划想到一个接口写一个最后前端要适配、设备端要写死、后端要兼容处处是坑。我的经验是先梳理清楚业务边界再统一设计。门禁业务系统对接通常绕不开以下几类接口设备注册与鉴权类设备首次上线注册、获取AccessToken、心跳保活。人员名单管理类人员新增、修改、删除、批量查询、增量拉取。设备配置类设备参数下发、时间校正、白名单策略、识别阈值配置。记录查询类通行记录查询、图片抓拍回传、异常事件补拉。每一类接口都要放在同一套路径和版本规则下管理。我习惯用/api/v1/{resource}/{action}的格式例如/api/v1/person/sync。版本号必须从v1开始后续如果有破坏性变更直接升v2老版本留一段时间过渡避免设备升级不同步导致全部挂掉。接口命名上动词尽量统一。查询用query/pull新增用add/create修改用update删除用delete/remove同步用sync。不要一会儿用getPersonList一会儿用queryUsers设备端和后端各写各的最后联调的时候光对字段就要对半天。响应格式也要全局统一。我使用的是下面这个结构{ code: 0, message: success, data: {}, requestId: a3f2c1d4-9e8f-4b7a-8c2d-1f0e9a8b7c6d }code为0表示成功非0表示失败。requestId是每次请求的唯一标识生产排查问题的时候太重要了后端日志和设备日志通过requestId关联能快速定位是哪一次请求出的问题。错误码不要自己随手定义出一份错误码表比如1001参数错误、1002鉴权失败、1003资源不存在、1004系统繁忙设备端根据错误码做对应的容错处理。2.2 人员数据同步接口的字段设计人员名称字段。下面是我在项目中使用的同步接口结构{ personId: P20240516001, name: 张三, department: 技术部, faceImage: /9j/4AAQSkZJRgABAQAAAQ..., faceFeature: 0.123,0.456,0.789,..., validTime: { start: 2024-05-16 00:00:00, end: 2025-05-16 23:59:59 }, doorPermissions: [D001, D002], status: 1, updateTime: 2024-05-16 10:30:00 }facpImage和faceFeature二选一还是都传取决于设备端人脸识别算法的部署方式。如果设备端自带人脸库而且用的算法是端侧推理那最好是下发faceFeature特征值设备直接比对速度快、依赖少如果设备端没有本地特征库需要把图片传给云端识别那下发faceImage就够了。两种方式我都实际测过端侧特征比对识别耗时基本可以做到300毫秒以内而云端识别算上网络传输至少600毫秒起步还要考虑弱网环境。所以只要设备端硬件算力够尽量走端侧方案。2.3 鉴权与安全机制怎么落地设备接口不能裸奔这一点不用多强调。门禁设备处于物理环境中存在被伪造或破解的可能所以所有API请求都必须鉴权。我用的是AppKey HMAC签名的方式不依赖传统的用户名密码比较适合设备端这种场景。签名的规则也不复杂简单描述一下设备端持有AppKey和AppSecret每次请求Header里带上AppKey、Timestamp、Nonce然后把请求方法、请求路径、请求体内容加上这三个参数拼成字符串用HMAC-SHA256算法加上AppSecret做签名最后把签名放在Header的X-Sign字段里。服务端收到请求后用自己的AppSecret重新计算签名比对一致才放行。这个方案的好处是AppSecret永远不会出现在网络传输中抓包也拿不到真实密钥。要特别注意一点Nonce必须保证一次请求一个值服务端要做去重防止重放攻击。时间戳的校验窗口我建议设置成5分钟太严了设备时钟偏差会导致大量鉴权失败太松了又给重放攻击留下空间。还有一个容易被忽略的点照片和人脸特征值属于敏感个人信息。传输链路必须全链路HTTPS加密MQTT通道也要启用TLS。本地存储的时候人脸特征值建议加密后落盘不能明文存到数据库里这一点在做等保测评或者客户验收的时候经常会被问到。3. MQTT通道实时事件推送的正确姿势3.1 MQTT在门禁场景里的定位MQTT是轻量级的发布/订阅消息协议设计目标就是用在网络不稳定、设备资源受限的物联网环境里跟门禁设备的使用场景天然匹配。在门禁对接中MQTT主要承担两类消息一是设备端上报事件比如某某人在某台门禁上识别通过二是平台端下发指令比如远程开门重启设备同步配置。有人可能会问这些操作用HTTP API不也能做吗确实能但两者有个本质区别。HTTP是请求-响应模型设备端是被动的只能主动去请求才能拿到数据MQTT是长连接一旦建立服务端可以随时主动推消息给设备。远程开门这种场景如果走HTTP设备得一直轮询有没有新指令效率低、实时性差走MQTT平台一条消息推过去设备端毫秒级就能收到并执行。所以实时指令类场景MQTT是无法替代的。设备状态感知也是MQTT的强项。设备意外断电、网络断开MQTT的遗嘱消息和心跳机制能够自动感知设备离线状态并在业务系统里实时更新设备状态。HTTP轮询做这个最快也要一个轮询周期才能发现而且会平白增加服务器压力。3.2 主题设计与QoS选择MQTT的主题设计直接影响后续的扩展性和权限控制。我推荐按设备维度设计主题格式如下iot/{productKey}/{deviceId}/event/post iot/{productKey}/{deviceId}/command/set iot/{productKey}/{deviceId}/command/replyproductKey是产品级标识同一型号门禁共用一个productKeydeviceId是设备唯一标识每台设备一个。这样设计的好处是后端服务可以用通配符订阅整个产品线的消息比如iot/{productKey}//event/post而给特定设备下发指令时又能精确路由到iot/{productKey}/{deviceId}/command/set。关于报文格式我建议统一定义一个消息壳避免每个功能点各写各的{ msgId: uuid, timestamp: 1715846400000, deviceId: DEV001, type: event, eventType: door.open, data: { personId: P20240516001, authResult: 1, doorId: D001, openMethod: face, captureUrl: http://... } }msgId是消息的唯一标识上报事件的去重就靠它。我遇到过设备端在网络波动时重复上报同一条开门记录业务系统如果没有按msgId去重考勤数据就会算重。所以不管客户端还是服务端都必须对同一个msgId的消息做幂等处理。QoS级别这块我踩过坑简单说一下我的配置设备端上行事件用QoS1保证至少到达一次配合msgId去重平台端下行指令也用QoS1同样配合消息确认机制。QoS0虽然性能最好但会丢消息门禁事件丢了就补不回来不推荐用。QoS2虽然有且仅有一次但握手开销大在低带宽场景下会明显增加时延对于门禁这种场景意义不大。3.3 心跳、遗嘱与断线重连MQTT长连接要说最关键的工程细节就是心跳和遗嘱。心跳用于维持连接和探测网络。设备端设置KeepAlive的时间我一般设成60秒也就是设备每60秒发一次PINGREQ超过两个周期没有收到服务端响应就判定连接断开。心跳太短会增加网络流量和服务器压力太长则离线感知不及时。60秒是我在多个项目里测下来比较平衡的值。遗嘱消息Last Will是一个容易被忽略但极其重要的功能。设备连接MQTT时可以同时指定一条遗嘱消息正常情况下这条消息不会发出去但设备异常断开时Broker会代替设备发布这条遗嘱消息。我是这样利用它的设备上线时在遗嘱里设置{deviceId:DEV001,status:offline,timestamp:...}发布到iot/{productKey}/{deviceId}/status主题。设备正常退出时主动发送一条onlinefalse的消息异常掉线时Broker自动发布遗嘱消息。业务系统订阅这个主题就能实时感知设备离线状态并在大屏上显示设备离线告警。但遗嘱只能告诉你设备掉线了不能告诉你掉线原因。这就需要在设备端做断线重连逻辑。我实现的策略是指数退避重连初始间隔3秒每次翻倍最大到60秒连续重连成功后重置间隔。重连成功后设备要主动补发上线通知并且把离线期间的未上报事件从本地缓存队列逐个补发。4. 鸿蒙端对接实战从API到MQTT的落地实现4.1 鸿蒙端HTTP调用业务系统鸿蒙系统上做HTTP请求可以直接用系统自带的kit.NetworkKit里的http模块。下面是一个简单的POST请求封装我在项目里直接用的这套代码import { http } from kit.NetworkKit; export class ApiClient { private static readonly BASE_URL https://api.domain.com/api/v1; static async post(path: string, body: object): Promiseany { const httpRequest http.createHttp(); try { const timestamp Date.now().toString(); const nonce this.generateNonce(16); const content JSON.stringify(body); const sign this.sign(path, content, timestamp, nonce); const response await httpRequest.request( this.BASE_URL path, { method: http.RequestMethod.POST, header: { Content-Type: application/json, X-AppKey: your-app-key, X-Timestamp: timestamp, X-Nonce: nonce, X-Sign: sign }, extraData: content, connectTimeout: 10000, readTimeout: 15000 } ); return JSON.parse(response.result as string); } finally { httpRequest.destroy(); } } }这里有几个工程细节必须提醒。连接超时和读取超时一定要显式设置鸿蒙端默认的超时时间偏长一旦业务系统接口异常设备端线程会长时间被占用影响其他任务。我生产环境用的连接超时10秒、读取超时15秒具体按实际网络环境调整。如果请求的数据是超大JSON字符串比如批量人员同步压缩后传输能显著提升效率。鸿蒙端可以用zlib做gzip压缩服务端在收到请求后先解压再处理。实测一份30MB的人员名单压缩到3MB左右传输时间能缩短将近80%效果非常明显。4.2 鸿蒙端MQTT客户端接入鸿蒙端接入MQTT有一些第三方库可以用比如paho.mqtt也可以通过封装NAPI将C语言的MQTT库桥接到鸿蒙应用层。我这里只讲关键实现逻辑不绑定具体的库。连接和事件处理的模板大致如下import { mqtt } from ohos/mqtt; export class MqttClient { private client: mqtt.MqttClient | null null; connect() { const options: mqtt.MqttConnectOptions { clientId: DEVICE_001, username: deviceToken, password: tokenValue, cleanSession: false, keepAliveInterval: 60, connectionTimeout: 30, will: { topic: iot/yourProductKey/DEVICE_001/status, payload: JSON.stringify({ status: offline }), qos: 1, retain: true } }; this.client mqtt.createMqttClient(ssl://mqtt.domain.com:8883, options); this.client.on(connect, () { this.subscribeCommand(); this.publishStatus(online); }); this.client.on(message, (topic, payload) { this.handleMessage(topic, payload); }); this.client.on(close, () { this.reconnect(); }); } }clientId要保证全局唯一我习惯直接用deviceId方便在Broker后台定位设备。cleanSession设置成false很重要这样设备离线期间Broker会为它保留QoS1以上的离线消息重连后自动补发。但这个设置也有代价如果设备长期离线Broker堆积的消息会越来越多所以要合理设置消息过期时间我一般设成24小时过期消息直接丢弃。TLS加密这里多说一句。设备端连接MQTT Broker时建议用ssl://前缀走TLS端口一般用8883。如果只是一个POC或者内网环境可以用明文但生产环境一律TLS否则人脸特征值和通行记录在网络里裸奔出了问题谁都担不起这个责任。4.3 本地缓存与数据一致性处理设备端不可能保证永远在线所以本地缓存是必修课。我是在设备本地建了一个SQLite数据库专门存待上报事件。识别出一次通行记录先写SQLite再发MQTT收到服务端的ACK后才标记这条记录为已上报。下次开机或者网络恢复的时候扫描数据库里所有未上报的记录逐个补发。这种做法要处理一个重复上报的问题。设备把数据发出去了但ACK没收到数据库里这条记录还是未上报状态设备重连后就会再次上报。所以服务端必须按照msgId去重否则同一条通行记录会被记两次。我在设计msgId时直接用设备ID时间戳序号生成保证全局唯一。名单同步也有类似的一致性要求。业务系统把人员数据下发到设备设备和业务系统之间要保持状态一致。我是这么做的设备端维护一个syncVersion字段每次业务系统推送增量数据时带上当前版本号设备应用完成后同步更新本地的syncVersion。设备如果发现长时间没有收到增量推送可以主动发起一次全量同步请求用本地的syncVersion和服务端比对服务端返回有新版本才拉全量数据避免无脑全量拉取造成网络拥堵。5. 联调阶段的常见问题与排查实录5.1 人员名单同步不上这个是我被问得最多的问题。现象是接口调用返回成功但设备上就是找不到那个人的名单。先看接口的返回数据和设备端日志通常会找到以下几种原因。第一种是图片格式问题人脸图片的Base64编码里有data:image/jpeg;base64,前缀设备端解析的时候没去掉就直接解码导致图片数据损坏。第二种是字段类型不匹配比如服务端返回的validTime是字符串设备端代码里用整数去解析类型转换直接抛异常。第三种是设备存储容量满了人脸库写不进去接口却返回成功因为服务端只管自己下发成功不管设备端是否入库成功。排查这类问题我建议在设备端日志里把每次同步的关键节点都打出来接口返回码、人员ID、人脸特征值长度、入库结果。日志要按时间戳和人员ID建立索引不然排一次问题能看哭。另外增量同步和全量同步要设计合理的触发时机。全量同步放在设备空闲时段比如凌晨两点避免白天高峰期占用带宽。增量同步通过MQTT推送通知触发设备收到通知后主动调用HTTP接口拉取增量数据。这样既保证实时性又不让HTTP请求铺满整个白天。5.2 开门事件丢消息MQTT的QoS1理论上不会丢消息但实际联调中还是会遇到服务端收到的事件少了的情况。我碰到过的一个典型案例是设备上报事件走QoS1服务端消费正常但数据库里总是少几条记录。最后排查下来发现MQTT Broker的retain消息或者服务端消费线程池满了消息被静默丢弃。这类问题要从多个环节卡缺一不可。排查顺序上是这样的先在Broker侧看订阅关系是不是正常设备发的消息有没有进到Broker再在服务端消费逻辑里把每条消息的msgId打到日志看消费端是不是有丢失最后看数据库写入的并发量和连接池配置。我最后的结论是服务端消费线程池核心线程数太小高峰期消息堆积后超时被丢弃。把线程池调大并且给消费逻辑加上重试机制问题就解决了。还有一点要注意设备端发送MQTT消息时不能阻塞业务线程。人脸识别开门是毫秒级操作如果每次开门事件都同步等待MQTT发送成功再执行下一步一旦网络有抖动用户就会感到开锁延迟明显。正确做法是识别成功立即开锁同时把事件丢到异步队列里后台线程池负责发布到MQTT。5.3 时间同步与签名失败API签名验证失败是最容易排查也最容易被忽略的一类问题。我遇到过一种情况设备刚部署完调接口一直报鉴权失败后台日志一看设备端的时间比服务器时间快了整整7分钟而我的时间窗口设置是5分钟直接拒了。门禁设备在户外或者弱网环境下没有NTP校准的话时间会慢慢漂移。所以设备上电后第一件事就是通过NTP或业务系统的时间校正接口同步时间而且每天至少同步一次。设备端不能完全相信自己本地的时钟尤其是做签名校验、有效期判断这些跟时间强相关的逻辑时。另外一个容易被忽略的场景是设备跨时区。如果业务系统部署在云端设备分布在不同区域建议所有时间参数统一用UTC或带时区偏移的格式展示层再做本地化转换。否则服务端和客户端用各自本地时间签名比对结果天然不一致联调起来非常痛苦。5.4 常见问题速查表问题现象可能原因排查手段与解决建议人员同步接口超时图片未压缩、一次性同步数量过大图片压缩到640x640以内全量同步拆分为批量任务每批100条图片能上传但无法识别Base64带前缀、图片格式不符设备端先去掉header前缀再解码服务端统一为JPEG格式下发设备在线但MQTT收不到指令订阅主题错误、QoS不匹配检查设备订阅的主题通配符确认服务端发布主题与订阅主题完全一致事件上报重复设备重连后重发、ACK应答丢失服务端按msgId做幂等去重设备缓存确认机制严格按ACK删除记录设备频繁掉线心跳设置过长、网络信号弱缩短心跳到30~60秒启用遗嘱消息实现指数退避重连远程开门偶发失败指令消息在Broker过期、设备离线期间指令堆积设置合理的消息过期时间或改为设备上线后主动拉取待执行指令鉴权大量失败设备时钟漂移、Nonce重复每天NTP校时Nonce生成采用UUID或雪花ID服务端做防重放校验设备本地名单被清空全量同步误触发、工厂复位设置设备端数据保护开关全量同步前二次确认复位后重新拉取6. 上线前一定要做的检查项项目验收之前我习惯拿着下面这个清单一项项打勾每一项都是实际踩坑踩出来的。第一断网测试不能省。把设备网线直接拔掉观察设备是否正常开门本地识别必须不受影响是否会产生N条缓存事件恢复网络后缓存是否能自动补发、补发是否重复。这一连串动作能暴露缓存设计、重连机制、幂等设计的所有问题。第二高峰期压力测试。早上8点到9点是考勤打卡高峰期几十台设备同时上报通行事件业务系统能不能扛住我建议用脚本模拟高并发消息推送观察服务端消费延迟和数据库写入耗时。如果消费延迟超过5秒就要考虑批量入库、加队列、分库分表这些手段了。第三TLS证书有效期要纳入监控。设备端内置的CA证书或者服务端证书过期会导致所有设备突然断连且无法重连这种故障是最要命的。上线前把证书到期时间加入运维监控提前一个月告警。第四设备固件升级通道要预留。门禁设备分布在各个点位不可能靠人跑到现场一台台升级。设计对接方案时就要考虑OTA远程升级的能力升级通道最好独立于业务通道避免升级失败影响正常开门。第五给设备端留一个本地调试入口。生产环境的设备端如果出了问题只能通过远程日志查看效率极低。我现在的做法是设备端保留一个调试模式开启后可以在本地查看运行日志、测试API连通性、查看MQTT连接状态这个入口生产环境要用强密码保护并默认关闭。7. 一些关于后续扩展的思考对接做完后我其实还在想一个事目前这套方案虽然能跑通开门、考勤、远程控制这些常规功能但很多客户现在都会问能不能跟视频监控联动能不能做个访客预约的APP之类的需求。这些本质上都不是门禁机本身的事而是业务系统的能力边界扩展。如果业务系统想做得更灵活建议在API设计上留好扩展位。比如人员同步接口的data字段做成open对象不同业务方可以传不同的扩展字段设备端只保留核心字段扩展字段透传或忽略都不影响主流程。MQTT的消息壳也是一样eventType和data字段的扩展不影响现有版本新增事件类型时老设备不会因为无法识别而报错。设备端和数据平台之间的数据模型如果从一开始就设计得松耦合后面接访客系统、消防联动、梯控系统就只是在业务系统侧增加适配器的事设备端不用频繁升级固件。这套思路在当前阶段跑下来我觉得是整个项目里最值得的一笔前期投入。
返回列表