☰
mpesa 库鸿蒙化适配:Flutter 接 M-Pesa 支付全流程拆解
2026/10/15 4:26:15 网站建设 项目流程

做移动支付对接的朋友,对 mpesa 这个 Flutter 三方库应该不陌生。它是社区里比较成熟的 M-Pesa 支付接入方案,封装了 Daraja API 的鉴权、C2B 收款、B2C 打款以及交易查询等核心能力。但问题来了:当项目要跑在鸿蒙设备上时,这套三方库的 Android/iOS 原生插件代码直接失效,Dart 层的协议逻辑也依赖原生的网络通道,不改造基本没法用。这篇文章我想把 mpesa 库鸿蒙化适配的完整思路记录下来,重点围绕 Daraja API 的接入、C2B/B2C 支付流的打通,以及实时交易查询在鸿蒙端的落地,给正在做同类适配的 Flutter 开发者一些可直接落地的参考。

整个适配过程涉及的环节不少:既有对 Daraja 协议本身的拆解,也有 Flutter 插件机制在鸿蒙上的特殊写法,还有 ArkTS 原生层的网络请求封装。我会尽量把每一步的“为什么这么做”也讲清楚,而不是只给结论。无论你是第一次接触 M-Pesa 支付,还是已经在 Android/iOS 上集成过 mpesa 库、现在需要迁到鸿蒙,这篇内容都应该能帮你省下不少踩坑的时间。

1. 为什么 mpesa 这个三方库需要单独做鸿蒙化适配

1.1 项目背景:支付能力在鸿蒙 App 上的断点

先看一个很典型的场景:团队用 Flutter 开发了一款面向海外市场的电商 App,M-Pesa 是当地用户最常用的移动支付方式,所以集成了 mpesa 这个三方库。Android 和 iOS 版本跑得挺好,C2B 收款、B2C 退款都正常。但鸿蒙版本一打包,打开支付页面直接白屏,日志里全是 platform channel 的异常。原因很简单:鸿蒙的 Flutter 运行环境不认 Android 的 MethodChannel 实现,mpesa 库内部通过原生插件发起的 HTTP 请求、证书加密等操作,在鸿蒙上没有任何原生端去承接。

这里要明确一个概念:Flutter 本身是跨平台的,Dart 代码理论上可以运行在鸿蒙的 Flutter 引擎上,但 mpesa 这类三方库一旦依赖了原生能力(网络、存储、加解密、系统上下文等),就必须有对应鸿蒙平台的原生实现。mpesa 库虽然对外是统一的 Dart API,但它底层用了 platform channel 去调 Android/iOS 的原生网络库。鸿蒙没有 Android 的 okhttp,也没有 iOS 的 NSURLSession,如果不对原生层做替换,整个库就是一堆无法执行的空壳。

这个断点不是修一两个文件就能解决的。它涉及三个层面的问题:其一,Dart 层能否独立完成 Daraja API 的协议逻辑;其二,平台通道在鸿蒙 Flutter 引擎上的注册方式是否兼容;其三,原生能力(网络请求、数据存储)在 ArkTS 里有没有对应替代方案。这三个问题不拆清楚,适配就是盲人摸象。

1.2 适配层面拆解:Dart、原生通道、业务协议

我在做适配前,先把 mpesa 库的代码结构翻了一遍,整理出三个需要分别处理的层面。

第一层是 Dart 层的协议逻辑。包括 Daraja API 的 OAuth token 获取、STK Push 的密码拼接、交易查询的请求体构造、响应解析等等。这一层其实是纯 Dart 实现的,理论上可以直接复用。但要注意一点:mpesa 库内部获取 token、发请求时,可能依赖了一些插件包的底层能力,比如 dio 对自定义 HttpClient 的依赖。在鸿蒙的 Flutter 引擎上,如果这些网络库内部走了 native socket 或者证书校验的 native 接口,就可能出问题。解决办法是把网络请求抽象出来,优先用纯 Dart 的 HTTP 库,避免走原生通道。

第二层是 platform channel 的桥接层。mpesa 库为了让 Flutter 层能调用 Android/iOS 的原生能力,会注册一个 method channel。鸿蒙的 Flutter 适配框架提供了对应的插件接口,需要在 ohos 原生目录下创建同名的 channel,并用 ArkTS 实现 setMethodCallHandler。这层是鸿蒙化适配的核心工作区,也是最容易踩坑的地方,后面第 3 章我会详细展开。

第三层是业务协议层。这一层跟 Flutter 无关,是 M-Pesa 官方 Daraja API 的规则。比如 C2B 支付时 Password 的生成算法、B2C 支付时 SecurityCredential 的证书加密格式、回调通知的字段解析等等。这一层不解决平台差异,但解决“为什么支付失败”的问题。很多适配做完后调不通,不是鸿蒙代码写错了,而是 Daraja 协议本身的细节理解有偏差。

1.3 总体方案选型:薄原生层 + 厚 Dart 层

综合三层拆解,我在方案选型上做了一个比较关键的取舍:原生层尽量做薄,只是一个通用的 HTTP 代理;Dart 层尽量做厚,所有 Daraja 业务的协议逻辑都留在 Dart 里。

选择“薄原生层 + 厚 Dart 层”的思路,主要是从两个角度考虑的。第一是调试体验,Dart 层的代码可以通过 Flutter 热重载快速迭代,而 ArkTS 原生代码改一次就要重新编译安装,周期很慢。把协议逻辑尽量放在 Dart,意味着绝大多数问题都能在 Flutter 侧快速定位。第二是可复用性,Dart 层不依赖鸿蒙专有能力,未来如果还要适配其他平台,只要换一个原生薄代理就能跑起来。

当然,这不是唯一的选择。有些团队为了稳妥,会把网络请求全部下沉到原生,用 ArkTS 的 HTTP 客户端处理一切,Dart 只负责传参和展示结果。这种方案的优势是更贴近原生,但坏处是代码量大、调试成本高,而且平台通道传大 JSON 时存在序列化开销。我最终采用的是折中方式:token 缓存、时间戳生成、密码拼接、证书加密这些协议逻辑放 Dart;ArkTS 只提供一个 post 方法,接收 url、headers、body,把结果返回给 Dart。实践证明这个方案在鸿蒙上跑得很顺畅。

2. Daraja API 对接前的协议基础:鉴权、C2B、B2C与交易查询

2.1 鉴权流程与 Token 生命周期管理

不管调 Daraja API 的哪个接口,第一步都是先拿 access token。Daraja 用的是 OAuth 2.0 的 client_credentials 流程:把 Consumer Key 和 Consumer Secret 拼成key:secret,做一次 Base64 编码,然后作为 Authorization 头去请求 token 接口。

拿到 token 后有个细节必须注意:token 的有效期大约是 3599 秒,也就是快一小时。很多开发者在适配时犯的第一个错误就是每次都重新请求 token,这样做不仅慢,而且容易被 Daraja 网关限流;另一个错误是没有做过期判断,token 失效后还在继续用,结果支付请求返回 401。我建议在 Dart 层维护一个 TokenCache,记录 access_token 和 expiresAt,在有效期内直接复用,提前 300 秒就当过期处理,给它一个刷新缓冲。

这个逻辑在鸿蒙适配时还有一个额外考量:token 缓存放在哪里。如果放在 Flutter 侧的内存里,App 重启就没了,每次启动都要重新请求一次 token,其实也可以接受。如果希望跨启动保持,可以考虑用 ArkTS 的轻量级偏好存储,但通常没有必要,因为 token 有效期不长,重新获取一次的成本很低,没必要引入额外的存储依赖。

2.2 C2B 支付流:STK Push 的参数与密码算法

C2B 是“客户到商家”的支付场景,比如用户在 App 里下单后选择 M-Pesa 付款。Daraja API 对 C2B 的标准实现是 STK Push:App 先调用支付接口,M-Pesa 会给用户手机推送一个支付确认弹窗,用户输入 PIN 码确认,钱才真正从用户账户划到商家账户。

STK Push 接口的路径是/mpesa/stkpush/v1/processrequest,请求参数里最关键的有这几个:BusinessShortCode(商家短码)、Password、Timestamp、TransactionType(CustomerPayBillOnline)、Amount、PartyA(用户手机号)、PartyB(商家短码)、PhoneNumber、CallBackURL、AccountReference 和 TransactionDesc。

Password 的生成算法是新人最容易卡住的点。它不是简单的静态字符串,而是BusinessShortCode + Passkey + Timestamp拼接后的 Base64 编码。Passkey 是开通 Daraja 应用时拿到的密钥,Timestamp 必须是yyyyMMddHHmmss格式的东八区无关的当前时间。我见过很多适配失败的案例,最后定位到的问题就是时间字符串格式不对,比如用了带横线的yyyy-MM-dd,或者小时部分用了 12 小时制。支付和交易类接口对时间的敏感度极高,这个细节一定要验到位。

调 STK Push 接口会同步返回一个 ResponseCode 和 CheckoutRequestID。注意,同步返回不等于支付成功,只代表 Daraja 受理了请求。真正的支付结果是通过 CallBackURL 异步回调回来的,回调数据里包含 ResultCode,0 才代表成功。这个异步机制决定了你必须在服务器上部署回调接收地址,并且要处理好幂等,因为 Daraja 可能重复回调。

2.3 B2C 支付流:证书加密与异步回调

B2C 是“商家到客户”的支付场景,常用于佣金发放、退款、营销奖励。它跟 C2B 的区别在于,发起方是商家,资金从商家账户转到用户手机账户。接口路径是/mpesa/b2c/v1/paymentrequest,请求参数包括:InitiatorName、SecurityCredential、CommandID、Amount、PartyA(商家短码)、PartyB(用户手机号)、Remarks、Occasion、ResultURL 和 QueueTimeOutURL。

B2C 里有一个和 C2B 完全不同的安全机制:Initiator 的密码不能明文传输,必须用 Daraja 提供的证书公钥做一次 RSA 加密,然后 Base64 编码成 SecurityCredential。这意味着适配工作里需要增加一个证书加密环节。在 Dart 层实现时,要引入 RSA 加密相关的库,把证书文件以字符串形式内置,读取公钥后加密密码。

CommandID 有三种取值:SalaryPayment(工资)、BusinessPayment(业务付款)、PromotionPayment(促销奖励)。这个参数直接决定了交易的资金属性,选错会影响账务分类,甚至可能导致支付失败。另外,B2C 要求 InitiatorName 是一个有操作权限的独立账号,而不是商家短码本身,这一点很多初次对接的人会搞混。

和 C2B 一样,B2C 的结果也不是同步返回的。Daraja 会把支付结果 POST 到你的 ResultURL,同时如果请求超时,会回调 QueueTimeOutURL。做鸿蒙化适配时,B2C 的 App 端只需要关心请求是否受理成功,真正的打款结果靠服务器回调后推送。

2.4 实时交易查询:TransactionStatus 的调用与对账

交易查询是支付系统里很常用的能力,尤其是在对账、客服查单、边缘超时场景下,需要主动向 M-Pesa 发起查询。Daraja 提供的接口是/mpesa/transactionstatus/v1/query,请求参数里要有 Initiator、SecurityCredential(同样由账号密码加密生成)、CommandID(固定为 TransactionStatusQuery)、TransactionID、PartyA(商家短码)、IdentifierType(4 表示短码)、ResultURL、QueueTimeOutURL。

有一个很多人容易误解的地方:TransactionStatusQuery 接口返回的同样不是一个最终查询结果,而是一个受理确认,真正的交易状态会通过 ResultURL 异步推送回来。换句话说,这本质上是一个“异步查询任务”:你发起查询,Daraja 去查,查到以后回调给你。所以在设计 App 端的“实时查询”功能时,不能简单地等一个 HTTP 响应就刷新状态,而是要配合服务器端的回调接收,再通过推送或轮询把结果下发到 Flutter 客户端。

实时查询在适配中还会遇到一个实际问题:对账一致性。回调通知可能丢失、重复、延迟,所以不能只依赖回调来更新订单状态。我做的方案是:所有 M-Pesa 相关交易都维护一份本地流水表,回调来了更新流水,同时定期用 TransactionStatusQuery 做主动对账,两边一致才将订单置为终态。鸿蒙端只负责发起查询和接收展示,真正的状态机建议放在自己的服务器上。

3. 鸿蒙端实操:MethodChannel 桥接与 ArkTS 原生实现

3.1 Flutter 插件壳与通道设计

鸿蒙化适配的代码工程,第一步是确认 Flutter 插件在鸿蒙侧的插件壳。以当前主流的鸿蒙 Flutter 适配框架为例,插件目录下要有一个ohos/目录,里面放 ArkTS 源码和 module 配置。插件壳的主类需要继承框架提供的 FlutterPlugin 接口,在 onAttach 生命周期里完成 MethodChannel 的注册。

我在实际工程里把通道名设计成mpesa_harmony/http,避免跟 mpesa 库原本的通道重名,也方便后续排查。通道的 method 只有两个:post和get,没有把 Daraja 的每个接口都拆成一个原生方法。这样做的原因是原生层只需要做一件事——把 Flutter 发来的 HTTP 请求转发出去,再把响应返回给 Flutter。业务逻辑全部留在 Dart 层,原生方法数量少、接口稳定,测试面也小。

通道通信的数据格式我统一用 JSON 字符串。Dart 侧把 url、headers、body 序列化成 Map,通过 invokeMethod 传给原生;原生解析参数后发起请求,把响应体原样返回给 Dart。这个设计有一个好处:任何 Daraja 接口的调整都只影响 Dart 层代码,原生层完全不用动。

Dart 侧通道调用的代码大概是这样的:

class MpesaHarmonyHttp { static const _channel = MethodChannel('mpesa_harmony/http'); static Future<Map<String, dynamic>> post( String url, Map<String, dynamic> body, { Map<String, String>? headers, }) async { final jsonStr = await _channel.invokeMethod<String>('post', { 'url': url, 'body': jsonEncode(body), 'headers': headers ?? {}, }); return jsonDecode(jsonStr ?? '{}') as Map<String, dynamic>; } }

使用这套壳之后,mpesa 库内部的网络调用全部可以替换成 MpesaHarmonyHttp,对外接口保持不变。

3.2 ArkTS 侧通用 HTTP 代理实现

ArkTS 侧要做的事情很纯粹:解析 Flutter 传来的参数,调用鸿蒙的 HTTP 客户端发请求,把结果返回。鸿蒙提供的网络库是@kit.NetworkKit里的http模块,用法跟其他平台的 HTTP 库类似。

在实现时我会注意几个细节。第一个是超时设置,Daraja 的接口响应有时会因为网关处理变慢而延迟,默认超时时间建议设得比普通请求长一些,比如 30 秒,避免 Flutter 侧已经超时取消了、原生请求还在跑。第二个是响应处理,必须区分 HTTP 层错误和业务层错误:HTTP 200 不代表支付成功,业务 code 才是关键;如果 HTTP 返回 4xx/5xx,要把状态码透传给 Dart,方便排查。第三个是线程切换,鸿蒙 http 模块的请求是异步的,回调时要确保回到主线程再调用 MethodChannel 的 result,否则 Flutter 侧可能收不到回调。

ArkTS 侧的核心实现大致如下:

import { http } from '@kit.NetworkKit'; import { BusinessError } from '@kit.BasicServicesKit'; // 在 onAttach 中注册 channel 后 channel.setMethodCallHandler((call, result) => { if (call.method === 'post') { const args = call.arguments as Record<string, Object>; const url = args['url'] as string; const body = args['body'] as string; const headers = args['headers'] as Record<string, string>; this.doPost(url, body, headers).then((resp) => { result.success(resp); }).catch((err: BusinessError) => { result.error('HTTP_ERROR', err.message, err.code); }); } }); private async doPost(url: string, body: string, headers: Record<string, string>): Promise<string> { const httpRequest = http.createHttp(); const res = await httpRequest.request(url, { method: http.RequestMethod.POST, header: headers, extraData: body, connectTimeout: 30000, readTimeout: 30000, }); return JSON.stringify(res.result); }

这样原生层就做成了一个不感知业务规则的通用请求代理,dart 侧业务逻辑可以自由迭代。

3.3 回调通知链路:从 Daraja 到 App 的实时展示

支付回调是鸿蒙适配里最容易被忽略的一环。C2B 的 CallBackURL、B2C 的 ResultURL,指向的都是你自己的服务器,而不是 App 本身。这意味着 Daraja 并不知道你的 App 正在运行,它只认公网可达的 HTTPS 地址。所以在鸿蒙 App 里要做到“用户付完款,页面实时变状态”,必须自己搭一条通知链路。

我的做法是这样的:服务器端部署一个回调接收接口,收到 Daraja 的支付结果后,根据订单号找到对应连接,通过 WebSocket 推送给鸿蒙客户端;Flutter 端收到推送后刷新支付状态。如果暂时不想上 WebSocket,也可以做成客户端轮询自己服务器的订单查询接口,每两三秒查一次,虽然实时性差一点,但实现成本低很多,适配初期可以先跑通这个方案。

在适配阶段,回调地址的配置还有一个常见坑:本地开发时回调地址需要公网可达,你不可能把 Daraja 的回调指向localhost。一般的做法是用公网可达的测试服务器,或者内网穿透工具把本机端口映射成一个公网地址。安全上要注意,Daraja 要求生产环境回调必须是 HTTPS,沙盒环境对 HTTPS 证书的要求相对宽松,但正式上线前一定要把证书链配全,否则回调会因证书校验失败而中断。

3.4 构建配置、权限声明与依赖收编

最后是工程配置层面。鸿蒙 Flutter 插件的 module 配置文件是module.json5,必须显式声明网络权限,否则所有 HTTP 请求都会被系统拦截。这个权限声明很容易漏,因为 Flutter 工程本身可能不需要这个权限,一旦切到鸿蒙原生侧发起请求,没有权限就会抛出网络异常。

权限声明的大致结构如下:

{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }

构建流程上,鸿蒙插件不是通过 pub 直接拿到的现成产物,需要先用 DevEco Studio 打开插件的ohos/目录,编译成 hap 或者 framework 之后,再配合 Flutter 工程整体打包。这一步很容易跟普通的 Flutter 插件混淆,我第一遍适配时在 DevEco Studio 里折腾了很久才搞明白构建顺序。

依赖收编方面,建议把 mpesa 库升级到版本尽量新的稳定版,并固定 lockfile,避免因传递依赖导致鸿蒙侧编译不通过。另外,如果前后用了 dio 或者 http 这类网络库,要确认它们在鸿蒙 Flutter 引擎上确实可用。我在实践中有过 dio 在鸿蒙上因连接器配置问题导致请求失败的案例,后来干脆把 dio 换成了标准 http 包,问题就消失了。这类问题通常不是 Dart 层逻辑错,而是底层网络栈对特殊套接字配置不兼容,遇到时先怀疑依赖,再怀疑代码。

4. 常见问题排查与上线前检查清单

4.1 鉴权与 Token 相关报错

鸿蒙化适配推进过程中,我遇到的第一类高频问题集中在 token 获取阶段。报错信息五花八门,常见的是 HTTP 401 或者返回invalid_grant。排查看三点:Consumer Key 和 Consumer Secret 是否抄错、Basic 认证头的 Base64 编码是否符合key:secret格式、系统时间是否准确。Daraja 的 token 接口对时间偏差比较敏感,如果测试设备没有开启自动同步,时间差个几分钟就会导致鉴权失败。

另一个容易犯的错是 token 缓存没有处理好。某些实现里,每次请求都会重新生成 token,导致短时间大量请求触发限流;还有实现是 token 永久缓存,过期后不刷新。我建议按前面说的方案,在 Dart 层做有效期倒计时,提前 5 分钟刷新,并且把 token 获取逻辑加上互斥锁,避免并发请求同时刷新 token。

4.2 支付参数与回调问题

C2B 支付失败还有一个很经典的参数坑:手机号格式。Daraja API 要求手机号以国家码开头,比如2547XXXXXXXX,但用户输入通常是07XXXXXXXX。如果直接传用户输入的手机号,接口会返回错误。适配时统一在 Dart 层做格式化,去掉开头的0,补上254,再传给接口。这个逻辑放在 Dart 层还有个好处,就是可以写单元测试覆盖各种输入格式。

回调不触发的排查思路一般是这样的:先确认回调地址在公网真的能访问,可以用浏览器或者命令行工具直接访问回调地址验证连通性;再查看服务器日志,看 Daraja 是否发起了请求、请求体是否完整;最后检查回调响应格式,Daraja 回调要求接收方返回特定格式的响应,如果响应不被识别,它可能会重试或放弃。

4.3 交易查询与对账不一致处理

使用 TransactionStatusQuery 之后还会遇到一个新问题:查询结果和回调结果不一致。比如回调显示支付成功,但查询接口返回的状态还是 pending,或者反过来。这通常是因为 Daraja 的查询接口存在一定延迟,支付刚完成时立即查询,状态还没更新。处理方式是做“延迟对账”,不要在下单当天立刻把所有订单都查一遍,而是设置一个合理的延迟窗口,比如 T+1 对账,或者对可疑订单间隔一段时间重试查询。

另一个对账不一致的原因是本地订单号和 M-Pesa 的 TransactionID 没有正确关联。C2B 下单时拿到的 CheckoutRequestID,和回调结果里的 TransactionID、MpesaReceiptNumber,是不同粒度的标识,需要建立一张映射表,否则查询接口就是用错了交易标识,自然查不到结果。

4.4 上线前检查清单与灰度建议

适配最后一步是检查清单。我整理了一份自用的固化清单,每次上线都按这个走一遍:

检查项说明
环境地址切换沙盒 baseUrl 切到生产 baseUrl,同时确认回调 URL 同步切换
凭据替换Consumer Key/Secret、短码、Passkey、Initiator 账号密码全部检查一遍
证书安全SecurityCredential 的生成证书要用生产证书重新生成,不能用沙盒证书
金额单位确认金额按最小货币单位整数传输,避免精度丢失
线上灰度先小额真实支付测试,覆盖 C2B 收款、B2C 打款、交易查询三条链路
日志监控上线后保留至少一周的 Daraja 回调原始日志,方便回溯问题

灰度时我习惯先放 1% 的流量,重点观察 C2B 支付成功的回调延迟、B2C 打款的失败率,以及交易查询接口的命中率。等这些指标稳定两天,再逐步放开到全量。

关于密钥管理,我还有一条额外提醒:如果 App 端直接内置 Consumer Key 和 Passkey,任何拿到安装包的人都能把这些凭据逆向出来,虽然短码和账号权限会有一定约束,但风险依然很大。更稳妥的做法是把 Daraja API 的请求收敛到自己的服务器,App 只请求你自己服务器的接口,由服务器去跟 Daraja 网关交互。如果业务阶段不允许立刻改造,也至少要把密钥做成动态下发,不要硬编码在客户端代码里。

这套鸿蒙化适配做下来,我最大的体会是:平台迁移的本质不是把代码翻译一遍,而是把职责边界重新画清楚。Dart 能做的逻辑全留在 Dart,原生只做 Dart 做不了的事,这条原则让 mpesa 的鸿蒙化过程少走了很多弯路。后面如果还要适配类似的支付三方库,整个流程基本可以照着这套思维模型走一遍:拆协议、薄原生、厚 Dart、埋点验证。先把通道打通,再把业务细节慢慢磨平,支付流程在鸿蒙上跑通就是水到渠成的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询