前阵子我接手了一个有点特殊的门禁管理 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 项目形态和角色拆解整个系统拆成四个角色会清晰很多住户手机 AppFlutter 编写负责远程开门、蓝牙近场开门、通行记录、家庭成员管理。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 秒用户很容易怀疑没点成功然后重复提交。我用了一个发送中状态按钮转圈并把文案改成“正在发送邀请”同时请求层做了幂等键重复点击也不会创建多条邀请记录。Futurevoid 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 项目建议先把业务实体画清楚再去碰蓝牙和平台通道能少走很多弯路。