我先把话说在前面:这类"鸿蒙化适配",难点往往不在Dart代码,而在"你以为它是纯Dart写的,其实它悄悄依赖了一大堆平台能力"。如果你手上正拿着一个基于Daraja API的Flutter支付库,要往鸿蒙上迁移,这篇内容应该能帮你少走不少弯路。我会从Daraja API的协议细节讲起,把C2B、B2C和实时交易查询这几种流在鸿蒙侧落地时需要改什么、为什么这么改,以及我在沙箱联调阶段踩过的坑,一次说清楚。
1. 先别急着改代码:鸿蒙化之前要搞清楚的三个问题
1.1 这个Flutter库到底依赖了多少"原生侧能力"
很多人在鸿蒙化一个Flutter三方库的时候,第一反应是"把源码拉下来,跑一次鸿蒙的flutter build看看报什么错"。这个思路没错,但在动手之前,我建议你先冷静下来,把仓库里的pubspec.yaml和lib/目录结构认真过一遍,回答一个核心问题:这个库除了Dart层的逻辑,有没有通过MethodChannel、EventChannel去碰原生能力?
以mpesa这类Daraja API封装库为例,绝大部分业务逻辑其实发生在Dart层:构造请求参数、拼接Base64、发起HTTP请求、解析JSON响应。理论上一套Dart代码在所有支持Flutter的平台上都能跑。但实际项目中,你经常会看到库作者为了方便,把以下几类操作放到了原生侧:获取设备信息、读取安全存储里的密钥、调用平台级加密能力、甚至是直接发通知。这些小操作在Android和iOS上被封装得非常简单,鸿蒙化时如果不提前盘点,后期就会频繁踩"这个Channel在鸿蒙上没人响应"的坑。
我在实际操作里会先做一次"平台通道依赖审计":把仓库里所有MethodChannel("xxx")、EventChannel("xxx")、Platform.isAndroid之类的关键字搜索一遍,列出一张表,标明每个通道在现有Android实现中承担了什么职责。这一步能直接决定你后续选择哪条改造路线。
1.2 鸿蒙能跑Flutter,但跑的是哪套运行时
这里要说清楚一个容易混淆的点。鸿蒙系统现在跑Flutter应用,不是把Android APK塞进去跑兼容层,而是通过鸿蒙方维护的Flutter适配SDK,把Dart代码编译成鸿蒙应用包(HAP)的一部分。对于应用开发者来说,Dart层的代码和Flutter框架层的API基本是通用的,但底层引擎、插件注册机制、平台通道的通信链路,跟Android是完全不同的两套实现。
这意味着什么呢?意味着你在Android上用的那些"插件自动注册+反射调用"的机制,在鸿蒙上不一定照搬。鸿蒙侧做Flutter插件,要遵循鸿蒙自己的插件注册接口,把MethodChannel的handler注册到引擎上去。Dart侧发起的invokeMethod,最终会落到鸿蒙侧你用ArkTS/TypeScript实现的对应方法上。
另外要注意,鸿蒙对HAP的权限管理方式和Android差别不小。最常见的坑是网络权限:Android里有uses-permission android.permission.INTERNET,鸿蒙里也有对应的权限声明,但很多人只把权限写在AndroidManifest里,忘了在鸿蒙工程里声明,结果就是真机上一跑网络请求就静默失败,日志里报的错还不够直白。
1.3 Daraja API与mpesa库的对照关系
在真正动手改库之前,还有一个认知层面的功课要做:搞清楚"这个库"和"这个API"之间到底是什么关系。mpesa库本质上只是Daraja API的一个客户端封装,它帮你做了请求构造、响应解析、数据模型转换。Daraja API本身是Safaricom开放出来的HTTP接口,部署在远端服务器上,跟鸿蒙系统不存在任何绑定关系。
所以从架构上看,鸿蒙化一个mpesa库,其实要解决的核心问题是三类:
- 网络通不通:鸿蒙应用能否发出带正确Header的HTTPS请求,能否正确处理TLS证书。
- 加密和签名能不能用:Daraja API要求用SecurityCredential(密码的Base64密文)、OAuth Token、回调验签等一系列安全操作,Dart侧加解密库在鸿蒙上是否兼容。
- 回调能不能接住:C2B/B2C流程中Safaricom服务器会主动回调你的服务器,这就涉及到鸿蒙应用是否实现在线回调接收能力,或者你是否有配套的服务端模块。
把这几个问题想清楚,你就明白鸿蒙化适配不是说"把所有平台通道都平移一遍"就完事,而是要做一次"能力对齐":找出Dart层假设的底层能力,在鸿蒙上一一补齐。接下来我先把Daraja API本身的协议细节讲透,因为很多适配问题其实是"你根本没理解API的调用方式,以为代码写错了,其实是协议用错了"。
2. Daraja API三兄弟:C2B、B2C与实时查询的协议细节
2.1 C2B支付流:客户发起、回调通知
C2B是最常见的"用户付钱给你"的支付流。以Daraja API的C2B接入为例,整体流程可以理解为三步:客户通过手机端或支付码渠道发起支付,Safaricom受理之后把支付结果POST到你预留的回调URL,你的服务端拿到回调并完成订单状态更新。
在库的封装层,你需要关注的请求参数主要包括:
ShortCode:你的商户短码,通常6位数字。CommandID:C2B场景常用CustomerPayBillOnline(用户主动扫支付码)或CustomerBuyGoodsOnline(购买商品支付)。Amount/ Msisdn / BillRefNumber:金额、客户手机号、账单号。
我见过不少人在C2B上栽跟头,原因不是参数传错,而是误以为C2B的响应就是支付成功的结果。实际上,C2B接口的同步响应里只有一个OriginatorConversationID,告诉你"Safaricom已受理该请求",真正的支付成功结果,是异步回调里带过来的。
这就给鸿蒙化提出了一个非常现实的要求:客户端不能以接口同步返回作为支付成功标志,必须依赖服务端回调来确认终态。如果你把库里的simulateC2B方法拿来做演示,你只能在沙箱环境里模拟,生产环境的真实支付,还是要走回调闭环。
2.2 B2C支付流:企业发起、主动出金
B2C则好理解得多,是"你主动打钱给用户",典型场景是奖励发放、退款、工资代发。它的调用方式和C2B有一个显著不同:B2C是纯异步的,连"受理凭证"和最终结果都拆成了两个不同的回调URL。
B2C请求相关的核心参数有:
CommandID:BusinessPayment(企业付款)、SalaryPayment(工资)、PromotionPayment(营销奖励)。Initiator与SecurityCredential:发起人用户名和加密后的密码凭证。PartyA / PartyB / Amount:付款方短码、收款方手机号、金额。ResultURL和QueueTimeOutURL:结果回调和超时回调,必须提交两个独立地址。
这里有个特别容易出问题的设计点:SecurityCredential不是明文密码,而是通过Safaricom提供的公钥证书对密码做RSA加密后得到的Base64字符串。如果这个mpesa库的Dart侧实现是在本地动态生成密钥对,那鸿蒙化的时候只需要保证加解密库能跑;如果它依赖Android原生KeyStore去保存密钥,那鸿蒙侧就要重新想方案。
我在适配B2C的时候,最深的体会是:B2C的参数校验比C2B严格得多,沙箱环境里账号未激活、CommandID用错、Party A/B格式不对,都会导致回调迟迟不来,而你在同步响应里看到的却是一串"看起来成功了"的信息。所以适配B2C,日志和回调链路追踪是必不可少的。
2.3 QueryTransactionStatus:避免对账靠猜
实时交易查询(Transaction Status Query)接口是为了解决一个问题:当回调丢失、网络超时、订单状态不确定时,主动去问Safaricom"这笔交易到底成没成"。
这个接口的调用方式也是POST,关键参数包括:
Initiator和SecurityCredential:同B2C,都需要发起人身份和加密密码。TransactionID:你想查询的那笔交易的编号。CommandID:通常为TransactionStatusQuery。PartyA和IdentifierType:发起方短码和标识类型。ResultURL和QueueTimeOutURL:查询结果的异步回调地址。
注意,这个查询接口在设计上依然不是同步返回最终状态的。响应里给的是"查询请求已受理",最终结果还是要等异步回调。也就是说,支付状态机里的"终态"几乎全靠回调驱动,客户端只负责发起。
所以,当你在做鸿蒙化适配时,你可以把"实时交易查询"理解成"多了一类请求构造和响应解析的Dart逻辑"。它没有特殊的平台依赖,重点在于库的内部是否支持传入不同的ResultURL,以及你拿到回调后能否正确更新状态。我建议在适配阶段就先把"查询状态"的接口写到沙箱测试用例里,验证从收到TransactionID到收到查询回调节点的整条链路。
下面用一个对比表格,把三种支付流的关键差异列清楚,方便你对照自己的适配计划:
| 维度 | C2B | B2C | TransactionStatusQuery |
|---|---|---|---|
| 业务方向 | 客户付给商户 | 商户付给客户 | 商户主动查询 |
| 同步响应含义 | 仅表示受理 | 仅表示受理 | 仅表示受理 |
| 终态来源 | ResultURL回调 | ResultURL回调 | ResultURL回调 |
| 核心身份参数 | ShortCode | Initiator + SecurityCredential | Initiator + SecurityCredential |
| 常见CommandID | CustomerPayBillOnline | BusinessPayment / SalaryPayment | TransactionStatusQuery |
| 鸿蒙化关注点 | 回调接收 | 加密凭证与回调链路 | 查询构造与轮询策略 |
3. 鸿蒙化适配的主战场:Dart层网络与平台通道的取舍
3.1 一次性搞懂MethodChannel在鸿蒙侧的落点
在鸿蒙系统上,Flutter应用里的Dart代码要和原生能力通信,一样是通过MethodChannel,但落点不同。Android里实现一个方法通道,是把端点写在MainActivity或者插件类里;鸿蒙上则是在HAP工程的模块中,通过鸿蒙提供的Flutter Plugin API去注册同一个channel name的handler。
这里有一个非常关键的认知转变:Dart侧MethodChannel("com.xxx.mpesa/security")的通道名,在鸿蒙侧必须对应注册同名通道,否则Dart调用会一直返回MissingPluginException。排查卡顿的时候,很多人会盯着Dart代码看半天,其实问题就是鸿蒙侧根本没有注册handler。
我在适配的时候习惯先在鸿蒙侧写一个最小化的通道验证:先注册一个MethodChannel("flutter_mpesa/health"),Dart侧调一下,确认能返回字符串,再逐步把真实业务方法填进去。这样可以把"通道连通性"和"业务逻辑正确性"分开验证,排查问题时省很多时间。
3.2 改造mpesa库的三种路线
具体到mpesa这个库,鸿蒙化改造路线我通常分成三种,取决于它的内部实现到底碰了多少原生函数。
路线A:纯Dart网络请求,只做桥接和配置适配
这是最理想的情况。如果仓库里所有网络请求是用dio或http发的,没有用任何平台通道,那么你在Dart层几乎不用大改。你需要做的只是把依赖替换或升级到兼容鸿蒙的版本,然后在鸿蒙工程里配好网络权限、证书环境,就能直接跑通。遇到这种库,建议不要为了"适配"而强行抽MethodChannel,那是给自己加戏。
路线B:部分平台能力被Channel封装,需要鸿蒙侧补齐
这种情况最常见。比如库用MethodChannel去读取设备型号、生成安全随机数、访问KeyChain。路线B的做法是:保持Dart侧通道名和协议不变,在鸿蒙侧实现一版同样的handler。说白了就是给每个通道写一个ArkTS的实现,把Android/iOS原生翻译一次。注意翻译的时候不要把业务逻辑放进去,原生侧越薄越好。
路线C:库本身依赖了不支持鸿蒙的三方插件
如果库里嵌套依赖了某个已经废弃或者对鸿蒙支持不好的插件,你可能就需要做一个"替换适配层":把依赖从该插件切换到另一个API兼容的库,或者直接fork仓库把那一节按标准方式重写。路线C改动量最大,一般只推荐在B走不通的时候做。
还有一点要提醒你:不要为了追求"完全本地化"而把所有HTTP请求都挪到原生侧去发。Daraja API的请求本身就是标准HTTPS,Dart的http库完全能胜任。你把网络层挪到鸿蒙原生的唯一后果,是让Dart侧无法做单元测试、也没有统一日志。
3.3 加密与证书:TLS双向认证在鸿蒙侧的落地
Daraja API对安全的要求比较高。常规接入时,至少会涉及三个安全件:
- OAuth Token:先调用鉴权接口拿到
access_token,再在业务请求Header里带Authorization: Bearer <token>。 - SecurityCredential:B2C和交易查询中,需要用Safaricom公钥对初始密码做RSA加密,得到Base64密文。
- HTTPS证书校验:生产环境必须走可信的TLS链路。
鸿蒙化时,前两项基本是Dart侧的crypto加解密库的工作。只要这个Dart库在鸿蒙上能正常编译,通常问题不大。真正容易出问题的是第三项:如果在测试阶段使用了自签证书或者代理工具,鸿蒙会对用户证书和系统证书有严格的区分,这会导致你在Dart侧临时信任某个证书的方式失效。
我的建议是:测试环境也尽量走合法证书,不要为了省事关闭TLS校验。支付类应用一旦在证书校验上开洞,上线后被抓包、篡改、重放的风险会指数级上升。鸿蒙开发调试时,如果需要抓包,优先选择在电脑端配置系统信任的抓包证书,而不是在客户端代码里跳过校验。
4. 实操:从Fork仓库到打出第一个HAP包
4.1 环境准备与工程改造
现在开始讲实操。假设你已经有一个鸿蒙工程,并且能正常跑起来一个空的Flutter页面。我一般会这样做:
- 把mpesa库的仓库fork一份到自己的代码库中。
- 用适配好的分支替换原依赖,在项目的
pubspec.yaml里通过path引用,方便随时改源码:dependencies: flutter_mpesa: path: ./third_party/flutter_mpesa - 在替换后,先跑一次
flutter pub get和flutter analyze,观察有没有跨平台的不兼容引用。
有人说为什么不直接升级到支持鸿蒙的官方版本?现实是,很多金融类三方库的鸿蒙支持进度不一,你手上这份代码大概率还是老的Android/iOS实现。fork一份自己维护,是性价比最高的做法。后续官方一旦有了官方鸿蒙路线,你再切回去也不迟。
4.2 权限与网络安全配置:INTERNET和用户证书
接下来把工程级配置处理好。这部分太容易被忽略,我踩过不止一次。
在鸿蒙工程的模块配置文件(module.json5)里,需要显式声明网络相关权限。通常至少要有ohos.permission.INTERNET。如果应用要用到网络状态判断,可能还需要ohos.permission.GET_NETWORK_INFO。你可以在配置目录下直接编辑,也可以用IDE的权限工具界面勾选。
同时,如果你发现库在HTTPS请求过程中需要信任某种特殊的证书链(比如内网沙箱网关的自签证书),不要直接改全局配置关闭校验。正确做法是只在开发阶段使用调试配置,并且把"生产环境强制系统证书校验"作为一条验收标准写在项目文档里。
4.3 实现鸿蒙侧C2B/B2C的通道方法
如果库内部确实走了MethodChannel,你需要找到Dart侧的通道调用协议,然后在鸿蒙侧实现对应的handler。下面这个代码是一个简化示意,重点展示文件组织方式和注册逻辑:
// 示意代码:鸿蒙侧注册MethodChannel handler import { MethodChannel } from '@flutter/plugin'; export class FlutterMpesaPlugin { constructor(engine) { const channel = new MethodChannel(engine, 'flutter_mpesa/security'); channel.setMethodCallHandler((call) => { if (call.method === 'encryptCredential') { // 调用鸿蒙加密框架实现RSA加密,返回Base64 const result = securityCredential.encrypt(call.arguments.password); return Promise.resolve(result); } if (call.method === 'readDeviceInfo') { return Promise.resolve(deviceInfoModel.get()); } return Promise.reject('Method not implemented'); }); } }注意,不同版本的鸿蒙Flutter SDK对插件注册API的命名有差异,实际以你接入的SDK文档为准。我这份代码想强调的不是具体函数名,而是通道名必须一致、方法名必须一致、返回类型必须能转成Dart能理解的JSON结构。
C2B/B2C的实际请求流程,如果你决定放在Dart层统一处理,那鸿蒙侧其实不需要写任何业务代码,只需要保证网络和加密能力可用即可。这也是我在多数项目里推荐的方式:原生侧能力最小化,业务逻辑留在Dart层。
4.4 沙箱联调:先用模拟器验证支付回执
有了HAP包之后,不要急着上真机跑完整支付,先在沙箱环境做一次"最小闭环"验证。我会按这个顺序来:
- 先调OAuth Token接口,确认能拿到
access_token。这一步失败,后面全免谈。 - 用沙箱的商户短码发起一笔最小金额的C2B模拟支付,确认同步响应里有
OriginatorConversationID。 - 配置一个能收到回调的测试服务端,把回调内容打印出来,确认回调能携带交易状态。
- 再走B2C流程,重点检查
SecurityCredential是否能被沙箱端解密校验通过。
沙箱环境的一大问题,是模拟器的时间和证书有时和真实网络环境不一致。你可能会遇到"在模拟器上Token生命周期很短"或者"回调延迟非常大"的情况。这些不是代码问题,不要因为沙箱抖动就把适配方案推翻重来。
另外,鸿蒙模拟器对网络代理的支持和Android模拟器有区别。如果你需要通过抓包工具观察请求细节,尽量让模拟器走局域网代理,并保证代理工具支持HTTPS解密。否则你会花很多时间在"验证请求确实发出去了"这件事上。
5. 上线前必须踩平的坑:沙箱参数、回调验签与超时兜底
5.1 沙箱与生产的Base URL切换为何容易翻车
Daraja API的沙箱和生产环境使用不同基地址和证书,这是所有接支付的人都知道的。但正因为大家都知道,适配时反而容易犯一个低级错误:把沙箱的商户短码、沙箱的Consumer Key直接留到生产环境。
我在鸿蒙化时踩过一次:沙箱跑得好好的,切到生产环境后Token一直401,排查了很久才想起来,生产环境的Consumer Key和Secret根本还没配置到应用里。为了避免这种问题,我后来会在代码里加一个显眼的运行环境标识,比如在启动日志里打印当前环境是SANDBOX还是PROD,测试阶段看到环境名才能安心。
另外建议把Base URL、Consumer Key、商户短码这类配置集中在一个配置类里,不要散落在各个方法中。鸿蒙工程里可以直接用环境配置管理机制,但代码里至少要做好常量集中管理。
5.2 回调验签:防重放与防伪造的实操
支付回调是所有支付流程的信任锚点。如果你的服务端只按照"回调URL里出现一个交易ID,就把订单标记为已支付",那攻击者完全可以直接伪造同一个回调请求,把订单状态改掉。
在Daraja API的接入中,安全做法是:
- 回调请求到达后,先校验请求来源IP或签名头是否符合Safaricom官方文档约定。
- 再校验业务字段的签名是否匹配。
- 最后做到幂等处理:同一个
TransactionID的处理结果只允许生效一次,重复的回调请求不能改变订单状态。
在鸿蒙应用环境里,如果回调是打到你的服务器,那验签逻辑应该在服务端完成;如果某些场景(比如混合应用内回调)需要客户端参与验签,那么密钥绝对不能硬编码在Dart代码里,必须放到鸿蒙的安全存储中,通过MethodChannel让Dart侧取用。
我在项目中还会加一层"最小延迟重放"的校验:记录每笔ConversationID的处理时间戳,对于30秒内重复到达的同ID回调,直接按重复请求丢弃。这是一个成本低但有效的手段。
5.3 超时、重试与幂等:支付场景的救命稻草
B2C和查询接口都是异步回调的模式,这意味着你在发起请求后,可能很长一段时间内看不到结果。糟糕的是,网络层不管成功失败,都会给你一个"响应"。所以适配时必须考虑三类异常:
- 请求超时:同步响应迟迟不来,可能是Safaricom侧拥堵,也可能是网络被拦截。建议设置合理的连接超时和读超时时间,比如15秒左右。
- 回调超时:请求已受理但回调迟迟不来,就需要主动调用TransactionStatusQuery去查证。你可以设计一个定时任务或者客户端手动查询入口。
- 重试污染:如果客户端在收到"受理失败"后马上重试,可能会重复扣款。稳妥做法是重试前先确认上一笔请求是否真的失败,而不是无脑重放。
我在设计Dart层封装时,会额外暴露三个工具方法,极大提升排障效率:
// 统一记录请求轨迹 Future<MpesaResponse> sendWithTrace(Future<MpesaResponse> Function() requestAction) async { final start = DateTime.now(); try { final response = await requestAction(); _logger.info('request completed in ${DateTime.now().difference(start).inMilliseconds}ms'); return response; } catch (e) { _logger.error('request failed: $e'); rethrow; } } // 查询兜底 Future<TransactionStatusResult> queryAsFallback(String transactionId) { ... } // 幂等校验 bool isDuplicateCallback(String transactionId, double amount) { ... }这些方法和平台无关,鸿蒙化时不需要额外适配,只要Dart层逻辑完整,就可以直接复用。有没有意识写这些工具方法,决定了你的支付功能在线上出问题时,是不知所措还是有据可查。
6. 我做完这次适配的几点心得
最后聊几句实际的感受。说实话,把mpesa这种支付库迁移到鸿蒙,最大的阻力通常不是技术,而是"对平台的敬畏心不够"。你可能觉得Dart代码都差不多,实际上鸿蒙的插件注册机制、权限模型、安全存储接口都和Android/iOS有差异,任何一层没对好,线上出现的就是"偶发回调丢失""Token老是失效"这类诡异问题。
我个人操作中的几个习惯,供你参考:
第一,永远先保证"最小闭环"能通。先把OAuth Token跑通,再跑一个C2B沙箱请求,再跑一次真实回调,不要一上来就完整适配所有API。支付链路不像普通接口,状态流转慢,问题定位难,小步走才能快速发现问题。
第二,日志要当成一等公民。我在适配阶段会在Dart层给每个关键节点都加上结构化日志:请求URL、Header、Body摘要、同步响应、异步回调内容。这些日志在鸿蒙真机上也能通过日志工具拉出来看。没有日志,遇到"沙箱回调没来"这种问题,你只能干等。
第三,把沙箱当成生产来要求。沙箱不是"随便测测"的地方,要对参数校验、超时处理、幂等逻辑做和正式环境一样的严谨验证。否则等上线后再发现这些问题,每一笔异常交易都是在真金白银地交学费。
这次适配做完之后,我把仓库里的平台通道梳理了一遍,打了三块文档:通道清单、鸿蒙侧能力映射表、沙箱联调手册。后面团队其他人再接同类支付库的鸿蒙化,基本可以照着文档往下走。如果你手头的库也涉及Daraja API的C2B/B2C和交易查询,希望这篇总结能帮你把改造范围想清楚,少踩一些我已经替你踩过的坑。