前阵子我接手了一个有点特殊的门禁管理 App:手机端用 Flutter 写,小区门口机和前台管理终端跑 OpenHarmony,整个系统还要支持“添加家庭成员”这种每天都会用到的操作。这个组合乍一听有点怪——Flutter 是跨平台 UI 框架,OpenHarmony 是偏设备端的开源系统,怎么就凑到一起了?做完了我才发现,这个组合在小区门禁、智能家居这类场景里其实挺能打。
这篇文章我打算把完整的落地过程捋一遍,从环境准备、工程结构,到门禁业务怎么拆、添加家庭成员怎么做,再到真机联调时踩过的坑,都整理出来。如果你也在做 OpenHarmony 上的跨端应用,或者正在给门禁、安防、智能硬件配套做用户端 App,这篇应该能帮你省不少时间。
1. 项目从哪来:为什么偏偏是 Flutter 加 OpenHarmony
先说说项目背景。小区门禁不是什么新东西,但很多存量小区换设备的成本很高,不可能把门口机全部换掉。这次的需求是:门口机是 OpenHarmony 系统的触控一体机,住户手机端需要一个能远程开门、查看通行记录、管理家庭成员的 App,物业那边还要有个简单的管理后台。手机端不可能只为这个小区单独做一个原生应用,团队里 Flutter 的技术栈是现成的,所以很自然地选择用 Flutter 写跨端手机 App,再通过协议跟 OpenHarmony 设备对接。
1.1 跨端复用的收益在哪里
选择 Flutter 不只是因为团队会,而是因为它能一次性覆盖 Android 和 iOS。门禁类 App 的使用者就是小区住户,什么手机都有,你不可能规定大家必须用某个系统。Flutter 的 UI 自绘特性,也方便做门禁这种偏“工具型”的界面——我们家门口机的开屏页、拨号盘、蓝牙配对页都是同一套组件逻辑,省了两套设计和两套开发。
更重要的是,Flutter 在 OpenHarmony 上并不是不能跑。这几年 OpenHarmony 社区已经有不少 Flutter 适配的工作,可以通过官方适配分支或第三方引擎跑起 Flutter UI,再通过统一的平台通道去调 OpenHarmony 的原生服务。这样一来,手机端和门口机的界面层可以复用很大一部分代码,只有真正跟硬件打交道的地方才需要针对 OpenHarmony 单独写。
1.2 项目形态和角色拆解
整个系统拆成四个角色会清晰很多:
- 住户手机 App:Flutter 编写,负责远程开门、蓝牙近场开门、通行记录、家庭成员管理。
- OpenHarmony 门口机:负责本地呼叫、密码键盘、蓝牙广播、对接后端门禁服务。
- 门禁服务端:一份独立的服务,负责权限校验、下发开门指令、记录通行日志。
- 物业/管理员端:一个轻量管理界面,管理小区单元、门禁点和住户关系。
这四个角色之间,手机 App 和门口机的直接通信只发生在近场蓝牙场景。其余操作都走服务端,比如远程开门、添加家庭成员,都是服务端先做权限校验,再决定要不要放行。
这种拆法的好处是:权限判断不依赖端上,端上只负责展示和发起请求。就算手机被 root、门口机被刷机,伪造请求也拿不到开门权限,因为真正关键的服务端不会信任客户端传过来的任何角色标记。
2. 环境搭建、工程骨架与桥接层设计
真正动手之前,先把环境说清楚。OpenHarmony 的应用开发一般用 DevEco Studio,而 Flutter 侧则需要一个支持 OpenHarmony 的引擎适配版本。装好之后,你会在工程里同时看到 Flutter 的 dart 目录和 OpenHarmony 的 ets 工程目录,两边是协作关系,不是替代关系。
2.1 OpenHarmony 侧的环境准备
我的建议是先装好 DevEco Studio 和对应的 OpenHarmony SDK,然后用官方或社区维护的 Flutter 适配分支创建一个 Flutter 工程。这个工程里会多出一块类似ohos的宿主目录,里面放着 OpenHarmony 的入口和桥接代码。
创建完工程,第一件事不是写业务,而是确认三样东西:
- DevEco Studio 的签名配置:真机调试必须配置签名,否则安装不上。
- Flutter 适配分支的版本:尽量跟你的 OpenHarmony SDK 版本匹配,版本错位会出现一些很诡异的渲染问题。
- ohpm 依赖源:OpenHarmony 侧用到的原生依赖,需要通过 ohpm 拉取,确认网络和仓库地址没问题。
注意:Flutter 不是 OpenHarmony 的一等公民,所以你在 pubspec 里的插件不一定都能直接编译。碰到这种情况,要么找替代插件,要么自己写平台通道把能力桥接过去。
2.2 Flutter 工程骨架和目录划分
项目目录我习惯这样分:
lib/ main.dart # 入口 core/ # 网络、本地存储、常量、工具 data/ # 数据模型、数据源、仓库 features/ home/ # 首页 door/ # 门禁控制 member/ # 家庭成员管理 records/ # 通行记录 shared/ # 通用组件features按业务域划分,而不是按页面划分。门禁和家庭成员是两个独立的域,但它们会共享一套数据仓库,因为“添加家庭成员”这个操作最终会影响到“门禁授权”。
2.3 原生硬件能力桥接:MethodChannel 和 EventChannel
门禁 App 离不开原生能力:蓝牙扫描、Wi-Fi 状态、本地推送。Flutter 侧通过MethodChannel发起调用,OpenHarmony 侧去执行具体操作;反过来,门口机的状态变化(比如有人按门铃)通过EventChannel主动推给 Flutter UI。
这个设计看起来简单,但有三个细节一定要处理:
- 通道名要统一管理:我把所有通道名放在一个常量类里,避免大小写和拼写错误。真出问题时最烦的就是两边名字对不上。
- 调用要超时:原生侧如果卡住,MethodChannel 调用会一直挂起。我给每个调用都包了一层超时,5 秒没返回就提示用户稍后重试。
- 事件要带状态枚举:EventChannel 推过来的不能是裸字符串,至少是一个带 code 的对象,方便区分“蓝牙已连接”“收到呼叫”“设备离线”。
3. 门禁核心业务落地:远端开门、蓝牙近场、通行记录
门禁这种业务,核心其实不是 UI,而是权限模型和通信链路。UI 做得再花哨,权限判断错了就是安全事故。所以我在设计业务时,把大部分时间花在了数据模型和状态流转上。
3.1 数据模型和协议设计
我们先定义几个核心实体:小区、单元、门禁点、住户、家庭成员、通行记录、授权记录。
一个住户可以属于多个小区、多个单元;一个家庭可以有多个成员,每个成员继承家庭的授权范围,但可以单独设置生效时间和失效时间。这些关系不搞清,后面“添加家庭成员”就是纯添乱。
数据库表设计我简化成这样的关系:
community:小区基础信息。unit:单元楼信息,归属于小区。door:具体门禁点,归属于单元,包含设备编号和设备类型。family:家庭组,归属于小区。family_member:家庭成员,归属于家庭,包含姓名、手机号、角色、授权开始/结束时间。access_log:通行记录,包含开门人、门禁点、时间、开门方式、结果。
3.2 远端开门链路
远端开门是用户最常用的功能。你人在快递柜旁边,家里人告诉你门禁开了,你拿起手机点一下“开门”,门口机就响了。这条链路的时序是这样:
- 手机 App 拿到用户身份凭证,向服务端发起开门请求。
- 服务端校验用户是否在该门禁点的授权范围内,校验时间是否在有效期内。
- 服务端向目标门口机下发开门指令。
- 门口机执行继电器动作,回传结果。
- 服务端记录通行日志,并推送结果给手机 App。
这里最关键的是校验逻辑不能放在手机上。哪怕手机端显示“你有权限”,也必须让服务端再查一次。因为你无法保证手机没有被动过手脚,也无法保证本地缓存的授权没有被篡改。
超时也要重点设计。门禁场景下,网络抖动是很正常的,门口机是嵌入式设备,网络稳定性不如手机。我给开门请求设置了 8 秒超时,同时做了幂等处理:同一用户、同一门禁点、5 秒内的重复请求直接合并,避免用户在弱网环境下狂点导致服务端重复开门。
3.3 蓝牙近场开门与离线补交
近场开门是另一个高频场景:人走到单元门口,手机自动识别门口机蓝牙广播,然后发起加密的开门请求。这比扫码快,也比远程开门稳,因为不依赖移动网络。
蓝牙广播的格式要提前约定好。门口机广播自己的设备编号,手机端接受到后,向服务端请求一个短期有效的开门令牌,然后用这个令牌跟门口机做本地握手。这个设计解决了两个问题:
- 手机没有网络时,也能用离线令牌开门(前提是提前拿到了有效期内的令牌)。
- 门口机不用每次都回连服务端,降低了对嵌入式设备网络的要求。
离线令牌的有效期我设的是 10 分钟,超过就作废。开门成功后,手机端会缓存一条待上传的通行记录,等网络恢复后补交到服务端。服务端收到重复记录时通过幂等键去重。
4. 添加家庭成员完整实现:从邀请到授权
“添加家庭成员”看起来只是个表单页,真正做起来才发现它牵扯到权限模型、邀请流程、通知触达、多设备同步。这一块是整个 App 里最容易出 bug 的地方,因为我一开始只把它当成了“新增一条记录”。
4.1 需求拆解:一个“家庭成员”到底包含什么
家庭成员不是一个简单的联系人,它至少包含:
- 姓名和手机号(用于展示和联系)。
- 角色(家主人/成员/访客,不同角色权限不同)。
- 授权范围(哪些门禁点可以用)。
- 授权时间(永久、限时、单次)。
- 关联的身份证或手机设备标识(用于蓝牙鉴权)。
我们做的是“户主邀请成员”的模式,而不是“用户主动申请加入”。住户成为家庭主人后,可以邀请自己的家人,被邀请人收到通知后完成绑定。
这个模式的好处是权限可控,物业不用介入每家每户的家务事,家庭主人自己管理成员就行。缺点是如果户主不用 App,整个家庭都没法添加成员,所以我还加了一个“物业代邀请”的通道,物业后台可以替住户添加成员。
4.2 邀请-绑定流程的前后端配合
完整的添加流程我拆成四步:发起邀请、接收邀请、确认绑定、权限下发。
发起邀请:家庭主人在 App 里输入成员的手机号,服务端生成一条邀请记录,状态为“待接受”,并向被邀请手机号发送短信通知。
接收邀请:被邀请人打开 App,如果还没注册,要先进去注册并绑定手机号;如果已经是用户,则直接进入邀请确认页。
确认绑定:被邀请人确认加入家庭后,服务端把邀请状态改成“已接受”,同时创建家庭成员记录。
权限下发:服务端根据家庭成员角色的默认权限模板,自动生成门禁授权记录。如果是限时访客,还会生成一个到期时间。
这里有个很容易忽略的问题:邀请码有效期。如果不过期,用户收到一年前的短信还能加入,这明显不合理。我设的是 24 小时有效,超时需要户主重新发起。
数据模型大致长这样:
class Invitation { final String id; final String familyId; final String inviterId; final String inviteePhone; final InvitationStatus status; // pending, accepted, rejected, expired final DateTime expireAt; } class FamilyMember { final String id; final String familyId; final String userId; final String name; final String phone; final MemberRole role; // owner, member, guest final DateTime? effectiveStart; final DateTime? effectiveEnd; }4.3 状态管理和 UI 落地
Flutter 侧我用了 Riverpod 管理这个流程的状态。页面拆了三个:家庭列表页、添加成员页、成员详情页。
家庭列表页展示当前家庭已添加的成员,按角色分组。添加成员页是表单页,输入手机号、选角色、设授权时间。成员详情页展示单个成员的信息,支持修改授权时间、禁用成员、移除成员。
实际操作中,我发现最需要关注的是按钮loading态。添加成员的接口如果要做校验、发短信、建授权记录,耗时往往超过 2 秒,用户很容易怀疑没点成功然后重复提交。我用了一个发送中状态,按钮转圈并把文案改成“正在发送邀请”,同时请求层做了幂等键,重复点击也不会创建多条邀请记录。
Future<void> submitInvitation() async { setState(() => _submitting = true); try { await _invitationRepository.sendInvitation( phone: _phoneController.text, role: _selectedRole, ); // 跳转成功页 } on ApiException catch (e) { // 显示错误提示,保留表单内容 } finally { setState(() => _submitting = false); } }给成员的授权时间我默认给“永久”,但限时访客场景给的是具体时间段。这个设计要在表单里做联动:选择“访客”角色时,必须设置生效起止时间,否则提交被拦截。
5. 真机联调与常见坑:我从这个项目里学到的
这个项目遇到的坑不少,有些是 Flutter 通用的,有些是 OpenHarmony 特有的,我挑几个最有代表性的写出来。
5.1 蓝牙通道在后台不可用
真机测试时发现一个现象:手机锁屏后,蓝牙近场开门偶尔不灵敏。排查后发现,Flutter 的 EventChannel 在 App 退到后台后,事件接收会被挂起,导致门前广播接收不及时。
后续解法是:把蓝牙广播接收放在 OpenHarmony 原生侧,发现目标设备后,通过本地通知提醒用户,同时唤起 App 到前台。Flutter 侧的 EventChannel 只是辅助,不承担“唤醒”职责。
5.2 OpenHarmony 上 Flutter 渲染的诡异问题
有一阵子,页面切换时会闪白屏,后来定位到是 Flutter 适配层在 OpenHarmony 上对 GPU 上下文的切换支持不完善。解决办法不算优雅:做了渲染降级,切换页面时强制等待一个新的帧回调再导航,虽然有一点点延迟,但闪白屏消失了。
这个问题在不同适配版本上表现不一样,所以如果你的团队也在做 OpenHarmony 的 Flutter 项目,建议第一时间写一个页面切换的冒烟用例,每次换引擎版本都跑一遍。
5.3 添加家庭成员后的多设备同步
家庭成员添加成功后,户主手机、成员本人手机、门口机需要都能立刻看到变化。最开始我只在服务端更新数据,手机端靠下拉刷新,结果就是户主添加了成员,成员自己的 App 上半天看不到授权记录。
后来在服务端加了变更推送:家庭成员、门禁授权数据发生变更时,向相关用户的 App 推送一个“数据已更新”的消息,客户端收到消息后自动拉取最新数据。这个方案比定时轮询省流量,也比纯靠手动刷新体验好。
5.4 排查工具清单
联调阶段我常用的工具总结如下:
| 问题类型 | 工具/方法 | 说明 |
|---|---|---|
| Flutter 侧异常 | DevTools 的日志和网络面板 | 看请求耗时和报错堆栈 |
| OpenHarmony 侧能力 | DevEco Studio 自带日志 | 看原生插件有没有挂 |
| 门禁指令不通 | 服务端日志 | 看指令有没有下发到门口机 |
| 蓝牙广播异常 | 手机蓝牙抓包工具或者门口机侧日志 | 确认广播和握手时序 |
日志里我强制要求打全链路追踪 ID,请求从 App 发出到服务端处理再到门口机执行,同一个 ID 贯穿始终。这样排查问题时,不用互相问“你看到的那条记录是哪次请求”,直接按 ID 过滤就能定位。
6. 发布前不能省略的适配与合规动作
这个项目从开发到可以上线,中间还有不少收尾工作。
6.1 权限声明要收敛
门禁 App 需要蓝牙、位置、相机、通知等权限,但不需要的权限坚决不要申请。我这里就犯过错:一开始把定位权限声明成了“精确定位”,应用市场上架时被要求说明用途。后来改成“仅在使用时访问大致位置”才通过,因为蓝牙扫描只需要定位权限来做安全过滤,并不需要精确到几米。
6.2 签名和渠道分离
OpenHarmony 侧的签名和 Flutter 侧的构建签名是分开管理的。我建议用 CI 脚本把两边构建串起来,避免人工点构建漏了某一步。签名文件不要提交进 Git,放密钥管理服务里,构建时拉取。
6.3 数据安全相关
门禁数据涉及住户的行踪和家庭关系,服务端接口要加访问控制,App 本地存储的通行记录要加密。我们用了设备级加密存储,密钥存在系统安全区,防止被直接拷贝数据库文件。
我个人在实际操作中的体会是,这类项目最大的难点不在写 UI 代码,而在于理清权限边界和数据一致性。只要家庭成员、门禁授权、通行记录这三类数据的关系没搞错,后面怎么加功能都不会乱。如果你正在做类似的 OpenHarmony 加 Flutter 项目,建议先把业务实体画清楚,再去碰蓝牙和平台通道,能少走很多弯路。