☰
Flutter for OpenHarmony下钱包功能实现:剧本杀组队场景实战解析
2026/10/6 3:46:00 网站建设 项目流程

做剧本杀组队App最容易被低估的一个模块,就是钱包。表面上它就是个余额展示加转账流程,但真把需求收上来一看:组队要交报名费、开赛要冻结爽约金、结束要AA分摊、退款要按规则算比例,还全部得跟组队状态联动。我这次接到的任务是给一套存量Flutter代码跑上OpenHarmony平台,钱包模块顺势成为第一个完整落地的核心功能。这篇文章就围绕“Flutter for OpenHarmony环境下钱包功能怎么具体实现”展开,把架构拆分、数据模型、状态通信、平台差异和实际踩过的坑一并记录下来。

先说说项目背景,剧本杀组队App的典型用户路径是这样的:用户刷到有人发起的剧本杀车队,想加入,第一步就是支付报名费占坑;到约定时间后,全队到店开始游戏,这中间可能涉及临时改时间、有人放鸽子、有人中途跳车;游戏结束再由房主发起结算,按人头把主持人费用和场地费A掉。这一条链路走下来,钱包的职责早就超出了“充钱、花钱”的范畴,它实际上成了整个组队业务的状态机。

技术选型几乎没有悬念。原始代码就是Flutter写的,需要适配OpenHarmony手机、平板和带屏设备,求稳求快就只能走Flutter for OpenHarmony这条路。说句实话,Flutter在OpenHarmony上的适配已经比早两年靠谱太多了,但依然有不少和Android/iOS不一样的细节。尤其是一些报错、渲染引擎、平台通道问题,不实际跑一遍真不知道。这一篇我把钱包功能的完整实现思路写清楚,也会顺带把组件通信和OpenHarmony平台差异的部分讲透。


1. 项目背景与钱包需求拆解

1.1 剧本杀组队场景下的支付刚需

剧本杀的“组队”和普通拼车不一样,它的付费规则是分阶段的。我梳理出来的核心流程有四段:

  • 参团付费:用户点击“确认加入车队”,系统生成一笔报名费订单,钱从钱包余额里冻结,而不是直接扣走。
  • 爽约金冻结:从“确认参团”到“游戏开始前24小时”之间,如果用户主动退团,报名费全额退回;但如果在24小时内退团,要扣掉20%作为爽约金,剩下的退回余额。
  • 自动结算:游戏结束后,房主点击“结束并结算”,系统按人数把全场总费用AA分摊到每个人头上,再结合每人之前是否交过报名费,算出每个人实际应付的补差金额。
  • 信用联动:累计爽约次数会影响用户组队的信用等级,信用等级又会影响可组队时长和最大可报名场次。钱包里的流水记录里需要带上“爽约”这种类型,方便后续信用系统取数。

这些需求刚拿到手的时候看着不复杂,真正拆开才发现,钱包不是简单的加减法,它得和业务状态机深度耦合。冻结、退款、分摊、补差,每一步都要有流水,都要有状态变化记录。

1.2 Flutter for OpenHarmony的选型理由

为啥不直接用ArkUI原生开发?存量代码是一个原因,团队技术栈是另一个原因。3万多行Flutter代码,涉及组队、房间、聊天、钱包四个大模块,全部用ArkUI重写太伤了。Flutter for OpenHarmony目前的思路是复用Flutter引擎能力,把Dart代码跑在OpenHarmony的OS上,通过平台通道调用底层系统能力,这正好符合“一套代码多端跑”的诉求。

实际开发中我也确实尝到了甜头:钱包页、组队页、结算页的UI和业务逻辑,在Android和OpenHarmony之间基本零改动。需要改的只是支付SDK接入、系统参数读写这类边界部分。团队里新来的同学,只要会Flutter,不用额外学ArkTS,也能快速上手改需求。

1.3 需求优先级与开发节奏

钱包这种模块,涉及钱,优先级最高的是“不能错账”,其次是“不能超卖”。我排的开发节奏是这样的:

  1. 先做数据层:数据库表结构、金额处理工具、流水记录逻辑,保证账目模型稳固。
  2. 再做核心链路:充值入账、参团冻结、结算分摊、退团退款,把主流程跑通。
  3. 最后做外围:钱包首页卡片、流水筛选、优惠券展示、信用分提示,这些偏展示形态,可以后置。

实战下来这个节奏是对的。很多团队一上来就把页面做得花里胡哨,结果底层数据模型是错的,返工成本巨大。钱包功能,底层账目模型永远是地基。


2. 钱包模块架构设计与数据模型

2.1 模块化设计:钱包服务与页面解耦

钱包不能只是一个页面,它应该是独立的一层服务。我的做法是把钱包拆成两层:

  • 钱包服务层(WalletService):负责充值、支付、退款、查询流水、余额变更,只暴露Dart层方法,不依赖任何UI组件。
  • 钱包展示层(WalletPage / WalletCard / BalanceView):负责余额展示、充值档位选择、流水列表渲染,通过状态管理连接服务层。

这样做的好处是,组队模块调用钱包能力时,直接依赖WalletService就好,不用去关心按钮和弹窗。比如用户点击“参团”后,组队模块调用WalletService.freezePayment(),钱包模块内部把事情做完,通过通知机制告诉组队模块“冻结成功”或“余额不足”。模块之间的边界就非常干净。

2.2 数据库选型与表结构设计

本地数据存储我用的是sqflite_common_ffi,这个库在OpenHarmony上能正常跑SQLite,兼容性实测稳定。为啥要本地放一份数据?因为钱包余额、流水和组队状态强关联,如果每次都走网络接口,断网场景下用户连自己的余额都看不到,体验太差。我的方案是本地缓存为主、服务端对账为辅。

核心设计了三张表:

wallet_account 账户表

字段类型说明
user_idTEXT用户ID,主键
balanceINTEGER可用余额,单位分
frozen_balanceINTEGER冻结余额,单位分
total_rechargeINTEGER累计充值额
total_spentINTEGER累计消费额
updated_atINTEGER更新时间戳

wallet_transaction 流水表

字段类型说明
tx_idTEXT流水号,全局唯一
user_idTEXT用户ID
amountINTEGER金额,正数入账,负数出账
balance_afterINTEGER交易后可用余额
typeINTEGER1充值 2参团 3结算 4退款 5爽约扣款
biz_idTEXT关联业务ID,比如组队ID
statusINTEGER1成功 2处理中 3失败
create_timeINTEGER创建时间戳

group_payment 组队支付订单表

字段类型说明
group_idTEXT组队ID
user_idTEXT用户ID
freeze_amountINTEGER冻结金额
final_amountINTEGER结算后最终扣款
refund_amountINTEGER退款金额
statusINTEGER1已冻结 2已结算 3已退款 4已扣爽约金

把这三张表放在一个数据库文件里,用数据库外键关系做关联查询。实际开发中,流水记录的balance_after字段帮了大忙,任何一笔错账都能通过流水回溯到当时的状态,排查问题非常快。

2.3 金额精度:永远用“分”做整数运算

第一个必须说的经验:Flutter里钱包金额绝对不能用double。0.1 + 0.2在浮点数体系里不等于0.3,这是固有的二进制精度问题,支付场景一旦出现分毫误差就是事故。我的做法是所有金额字段统一用int类型,以“分”为最小单位存储。

页面展示的时候再转成“元”,并且做格式化处理。我自己封装了一个MoneyUtil:

class MoneyUtil { /// 分转元,保留两位小数 static String fenToYuan(int fen) { if (fen == 0) return "0.00"; final int yuan = fen ~/ 100; final int jiao = (fen % 100) ~/ 10; final int fenPart = fen % 10; return "$yuan.$jiao$fenPart"; } /// 元字符串转分,输入校验过滤非法字符 static int yuanToFen(String yuanText) { final cleaned = yuanText.trim(); final parts = cleaned.split("."); final int yuanPart = int.tryParse(parts[0]) ?? 0; if (parts.length == 1) return yuanPart * 100; final decimalPart = parts[1].padRight(2, '0').substring(0, 2); final int fenPart = int.tryParse(decimalPart) ?? 0; return yuanPart * 100 + fenPart; } }

这段代码里最容易被忽视的是~/整除运算,Dart里不能用/,否则得到的是浮点数。MoneyUtil在项目里被复用了几百次,像余额展示、充值输入、结算分摊都直接调用,从源头杜绝了精度问题。


3. 钱包核心功能实现与源码拆解

3.1 余额展示:首页钱包卡片与自动刷新

余额展示看似简单,难点在“及时刷新”。我的首页钱包卡片通过Consumer<WalletModel>监听余额变化,任何一笔交易成功后,WalletModel内部更新余额并调用notifyListeners(),整个UI树中依赖余额的地方自动刷新。

钱包卡片的核心代码如下:

class WalletCard extends StatelessWidget { const WalletCard({super.key}); @override Widget build(BuildContext context) { return Consumer<WalletModel>( builder: (context, walletModel, _) { final balanceFen = walletModel.balance; return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("我的余额"), Text( MoneyUtil.fenToYuan(balanceFen), style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, ), ), Row( children: [ TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (_) => const RechargePage(), ), ), child: const Text("充值"), ), TextButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (_) => const TransactionPage(), ), ), child: const Text("流水"), ), ], ), ], ), ); }, ); } }

这里有个小细节:Consumer的builder方法里,walletModel发生变化时只重建当前组件,不重建整个页面,性能上是OK的。如果首页有多个地方需要监听钱包状态,就各自包Consumer,避免组件树顶层重建。

3.2 充值流程:订单生成与支付渠道对接

充值流程我设计成四步:生成订单 -> 拉起支付 -> 确认结果 -> 刷新余额。

class RechargeService { Future<RechargeResult> recharge({ required int amountInFen, required PaymentChannel channel, }) async { // 1. 通过服务端生成充值订单 final orderId = await OrderApi.createRechargeOrder(amountInFen); // 2. 调用平台支付渠道 final launched = await channel.launchPayment(orderId: orderId, amountInFen: amountInFen); if (!launched) { return RechargeResult.fail("支付渠道拉起失败"); } // 3. 轮询订单状态,等待支付成功 final pollResult = await _pollOrderStatus(orderId, timeoutSeconds: 60); // 4. 刷新本地余额 if (pollResult) { await WalletDb.refreshAccount(); } return pollResult ? RechargeResult.success() : RechargeResult.fail("支付超时"); } }

轮询这步有个容易犯错的地方。千万不能用while死循环加Future.delayed的方式去阻塞逻辑,那样会非常丑陋。我用的方式是递归调用自身,避免阻塞事件循环:

Future<bool> _pollOrderStatus(String orderId, {required int timeoutSeconds}) async { final sw = Stopwatch()..start(); Future<bool> poll() async { final status = await OrderApi.queryOrderStatus(orderId); if (status == OrderStatus.paid) return true; if (sw.elapsedSeconds >= timeoutSeconds) return false; await Future.delayed(const Duration(seconds: 2)); return poll(); } return poll(); }

说到Future.delayed,这里顺带提一个Dart异步的细节:then回调默认是放在微任务队列里的,而Future.delayed的回调是放到事件队列里的。这意味着你在then里写的逻辑,会比事件队列里的任务更早执行。刚接触Flutter的同事有时候会在轮询场景里对执行顺序感到困惑,搞明白微任务和事件队列的区别,就很容易理解了。

OpenHarmony上的支付渠道,和Android略有差别。Android上直接接微信/支付宝SDK就完事,OpenHarmony上部分设备需要走系统统一的支付服务,三方SDK的支持情况也参差不齐。我的做法是封装了一个抽象的PaymentChannel接口,针对不同平台实现不同的子类,上层业务完全感知不到差异。

3.3 参团支付的“冻结”与“确认”事务

组队模块调用钱包服务时,最核心的是“冻结”和“确认”两个动作。用户点击“确认参团”,系统先把报名费从可用余额转入冻结余额,这笔钱没有被花掉,但也不能被其他消费使用。游戏结束后结算时,再把冻结余额转成实际扣款。

冻结操作的数据库事务如下:

Future<bool> freezePayment({ required String groupId, required String userId, required int amountFen, }) async { final db = await WalletDatabase.instance.database; try { return await db.transaction((txn) async { // 1. 检查余额是否充足,这里加了一个行锁保护 final account = await txn.query( 'wallet_account', where: 'user_id = ?', whereArgs: [userId], ); if (account.isEmpty) return false; final balance = account.first['balance'] as int; if (balance < amountFen) { throw WalletException('余额不足'); } // 2. 扣可用余额,加冻结余额 await txn.update( 'wallet_account', { 'balance': balance - amountFen, 'frozen_balance': (account.first['frozen_balance'] as int) + amountFen, 'updated_at': DateTime.now().millisecondsSinceEpoch, }, where: 'user_id = ?', whereArgs: [userId], ); // 3. 写流水 await txn.insert('wallet_transaction', { 'tx_id': generateTxId('FRE'), 'user_id': userId, 'amount': -amountFen, 'balance_after': balance - amountFen, 'type': 2, 'biz_id': groupId, 'status': 1, 'create_time': DateTime.now().millisecondsSinceEpoch, }); // 4. 写组队支付订单表 await txn.insert('group_payment', { 'group_id': groupId, 'user_id': userId, 'freeze_amount': amountFen, 'status': 1, }); return true; }); } on WalletException { return false; } }

这段代码有几个重点:

第一,db.transaction保证原子性。比如扣可用余额成功、但写流水失败,整个操作回滚,账户不会出现“钱扣了但没记录”的情况。

第二,查询和更新在同一个事务里,SQLite的写事务默认加锁,并发情况下不会出现两个请求同时读到同一个旧余额的问题。

第三,业务上这里用的是“冻结”而不是直接扣款,意味着游戏没结束时,这笔钱还是用户的,查询余额时前端展示的是balance而不是balance + frozen_balance。UI层要有提示,把冻结金额展示出来,避免用户疑惑“我明明有50块,怎么不能支付40块的订单”。

3.4 结算分摊:多人AA的金额计算逻辑

游戏结束后房主发起结算,系统把全场总费用按人数分摊。分摊金额要处理“除不尽”的情况,不能简单四舍五入到分,否则总账会对不上。

我的做法是“先除后补差”:

List<int> splitAmount(int totalFen, int count) { if (count <= 0) return []; final base = totalFen ~/ count; final remainder = totalFen % count; // 前remainder个人多出1分钱,保证总和精确等于totalFen return List.generate(count, (index) { return index < remainder ? base + 1 : base; }); }

比如总费用100.01元(10001分),4人分摊,base = 2500(25元),remainder = 1,结果是第一个人付25.01元,其余三人各付25.00元,总额正好等于100.01元。这个方案比单纯四舍五入靠谱得多。

分摊完还要结合每个人之前的冻结金额算“补差”。假如某人的报名费冻结了20元,人均分摊是25元,他就需要再补5元;如果分摊是18元,多冻结的2元就退回余额。整个计算过程我在服务端做,本地只做展示,关键账目以服务端为准。

3.5 爽约金和退款逻辑

退款在业务上要区分场景:

  • 开赛前24小时之外退团:冻结金额全额退回。
  • 开赛前24小时之内退团:扣掉20%爽约金,剩余80%退回。
  • 游戏结束后:结算产生的退款走补差逻辑。

退款操作本质上是冻结余额的逆操作,同样走事务。里面有一个小坑,就是爽约金扣掉的部分也要写流水,类型标记为5,方便信用系统读取。我之前遇到过只退钱没记流水,结果信用系统的爽约次数统计不出来,排查了半天才发现是流水类型漏了。


4. 组件通信与钱包状态联动

4.1 组件通信的几种方式怎么选

Flutter社区关于组件通信的方案讨论一直很多,热搜上也经常看到“flutter组件通信”这个关键词。做钱包和组队联动时,我根据场景不同用了三种方式:

  • Provider + ChangeNotifier:适合跨页面的状态共享,钱包余额、交易列表这种全局状态。
  • 回调函数:适合父子组件之间的单向通信,比如充值页把“充值成功”的事件回调给钱包页。
  • 事件总线(EventBus):适合完全解耦的模块通信,比如组队模块发起结算后,钱包模块需要感知并刷新,但两者没有直接的父子关系。

在OpenHarmony上,Provider是纯Dart实现,没有任何Native依赖,兼容性最稳定,所以做主状态管理没有问题。

4.2 组队详情页如何调用钱包冻结能力

组队详情页点击“参团”后,流程是这样的:

class GroupDetailModel extends ChangeNotifier { final WalletService _walletService; Future<bool> joinGroup(String groupId) async { final feeFen = await GroupApi.getJoinFee(groupId); final canPay = await _walletService.hasEnoughBalance(feeFen); if (!canPay) { _joinStatus = JoinStatus.insufficientBalance; notifyListeners(); return false; } final frozen = await _walletService.freezePayment( groupId: groupId, userId: currentUser.id, amountFen: feeFen, ); if (frozen) { _joinStatus = JoinStatus.joined; notifyListeners(); return true; } return false; } }

这里组队模块完全不关心钱包内部怎么实现,只知道“冻结成功”还是“余额不足”。钱的事归钱包管,队的事归组队管,边界清晰。后续如果要把冻结改成花呗扣款,或者改成外部支付,组队模块一行代码都不需要改。

4.3 “参团成功”之后如何刷新钱包卡片

一个非常典型的联动场景:用户在组队详情页参团成功后,回到首页,钱包卡片上的余额要立刻变化。这里的实现我做了两步:

第一步,WalletModel在冻结成功后更新内存中的余额字段。

class WalletModel extends ChangeNotifier { int _balance = 0; int get balance => _balance; void updateBalance(int newBalance) { _balance = newBalance; notifyListeners(); } }

第二步,首页钱包卡片和组队页共用同一个WalletModel实例(通过上层Provider注入),组队页调用freezePayment成功后,再手动调用walletModel.updateBalance(newBalance),首页的Consumer立刻感知到变化,UI自动刷新。

有同学问为什么不用EventBus,让钱包自己广播“余额变了”。实测下来,在这个场景里EventBus反而不够直观,因为余额更新的时机是明确由当前操作触发的,直接调方法更可控。EventBus适合的是那种“不知道谁会引起变化”的场景,比如信用分变动同时影响多个页面。

4.4 异步链路的状态管理

钱包和组队通信的异步链路比单页面复杂得多。用户点击“参团”之后,要经历本地冻结、服务端确认、本地余额更新三个阶段,任何一步失败都要回滚。为了不让用户等得焦虑,我在UI层维护了三种状态:待提交、提交中、提交完成。提交中的按钮置灰并显示加载动画,防止重复点击。

状态管理方案对钱包这种强交互强状态模块来说,最核心的要求是“状态可预测”。Provider的ChangeNotifier模型刚好满足这一点,每次状态变化都走notifyListeners,逻辑完全是线性的。我个人不建议在这种强业务模块里引入太重的事件流方案,调试成本会高很多。


5. Flutter for OpenHarmony踩坑实录

5.1 e/flutter错误:Dart VM初始化失败

搜热词的时候看到很多人遇到过e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] unhandled...这类报错,我在OpenHarmony设备上也踩过。这个错误通常是Flutter引擎在Native层初始化失败,或者Dart代码里有全局非空的变量没有被初始化,导致VM启动直接崩掉。

排查思路一般是:

  1. 确认.so库是否打入包内,尤其是libflutter.so,检查构建后的APK/HAP里是否存在。
  2. 确认abiFilters是否和目标设备的CPU架构匹配。常见的是设备是arm64-v8a,但包只打了armeabi-v7a。
  3. 在main()入口处加全局异常捕获,先定位是不是Dart层代码问题。
void main() { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError = (details) { // 把错误上报到自己的日志系统 Logger.report(details.exceptionAsString()); }; runApp(const MyApp()); }

这个习惯我从第一版就开始保持了,后面排查问题省了太多时间。

5.2 Impeller渲染引擎要不要开

Flutter 3.x默认启用Impeller渲染引擎,它是在Skia基础上的优化,设计目标是解决Skia的GPU线程卡顿问题。但我在OpenHarmony设备上实测,部分国产GPU对Impeller的支持并不好,表现是页面偶发花屏、启动黑屏。

遇到这种情况,可以在构建或运行时关闭Impeller改用Skia兜底:

flutter run --no-enable-impeller

如果你已经打包成HAP,可以在项目的AndroidManifest.xml或者对应的配置里加上io.flutter.embedding.android.EnableImpeller=false。但注意,OpenHarmony上具体配置的位置可能不同,建议在Flutter引擎初始化参数里处理,兼容性更好。

我的建议是:如果你的App对渲染性能要求不极端,先在OpenHarmony目标设备上关掉Impeller跑一阵观察稳定性。钱包页面有大量文本、列表、卡片这类普通UI,Skia渲染完全够用。

5.3 减少PlatformView的使用

OpenHarmony的PlatformView适配,比Android要费劲一些。传统WebView内嵌H5页面的方案,在OpenHarmony上遇到输入框弹不出键盘、滚动卡顿的概率很高。我用钱包功能的“用户协议”页面测试过,WebView方案在部分设备上体验很差。

最终钱包模块里只有支付结果页用了PlatformView,其余协议、公告、帮助中心全部用Flutter原生组件重写了。这个决策大大减少了后续的兼容工作。如果你必须用PlatformView,建议把WebView的初始化放晚一点,避免页面启动时主线程卡死。

5.4 OpenHarmony XTS认证对钱包应用的影响

OpenHarmony上架应用前要做XTS兼容性认证,其中和钱包功能最相关的是权限声明和应用隐私政策。钱包涉及用户余额、交易记录、支付敏感操作,对权限的要求比普通App严格。

我踩过的坑是:初版隐私弹窗只给了“同意/不同意”,没有列出具体采集的数据项,XTS认证直接不过。后来把隐私协议按规范拆成数据收集列表,明确写明“余额信息、交易记录用于展示账户状态,支付信息仅用于完成交易”,才顺利过审。提醒所有做OpenHarmony钱包类应用的同学,这块一定要前置到开发阶段,别等要上架了才临时改。


6. 常见问题速查表与避坑技巧

问题现象可能原因排查方向
支付成功后余额没刷新轮询超时或回调顺序错乱检查支付回调、轮询状态,确认余额刷新是否被并发覆盖
并发点两次“参团”没有做事务和状态标记检查数据库事务、加行锁、UI层防重复提交
金额显示精度错乱用了double存储金额统一改int分存储,用MoneyUtil转换
组队页冻结成功但流水缺失事务中途失败检查事务是否回滚,流水写入是否和余额更新同一事务
首页钱包卡片不刷新Provider实例不唯一检查是否创建了多个WalletModel实例
OpenHarmony设备花屏Impeller兼容问题关闭Impeller,回退Skia渲染

6.1 并发点击“参团”导致余额被扣穿

这个是我实战中遇到的最严重问题之一。用户在弱网环境下连续点了两次参团按钮,两个请求先后发出去,如果没有事务保护,就会发生“余额被扣穿”的情况:

第一次请求读到余额50元,冻结20元,剩30元; 第二次请求也读到余额50元(第一次还没提交事务),再次冻结20元,结果余额变30元,但实际应该只剩10元。虽然第二次冻结会因为“余额只有30”而失败,但极端情况下,两个事务交错执行,就可能出现脏读。

解决方案是三层防护:

  1. UI层:提交中按钮置灰,防止连点。
  2. 服务端:对同一用户的未完成订单做幂等校验,已存在待支付的参团订单就不让重复下单。
  3. 数据库层:db.transaction事务内做查询和更新,SQLite的写锁会把这个场景卡住。

6.2 微任务和事件队列执行顺序干扰页面刷新

这是热搜词“flutter future的then回调 是放入微任务队列吗”相关的题。Dart中,Future的then回调默认放进微任务队列,而Future.delayed的回调放进事件队列。微任务会优先于事件队列执行。

在钱包支付轮询场景里,我曾经写过一个bug:

await Future.delayed(const Duration(seconds: 2)); setState(() { ... });

看起来是2秒后刷新,但因为setState在then里,实际刷新时机比预期早很多。原因是外层代码被微任务调度抢先了。正确写法是把刷新逻辑放到明确的异步上下文里,或者使用Future.microtask保证边界清晰。

6.3 组队和钱包模块的联调自检清单

钱包功能开发完,我总结了一份联调自检清单,分享给大家:

  • 新用户首次进入,钱包余额显示0元,不闪白屏。
  • 余额不足时点击参团,页面提示“余额不足请充值”,并且不可继续操作。
  • 充值流程中杀进程,重新打开App后,订单状态通过服务端恢复正常。
  • 参团成功后,首页钱包卡片、组队详情页、流水页三处余额一致。
  • 结算分摊后,所有玩家的流水金额总和等于业务总流水,分毫不差。
  • 退款后,冻结余额清零,可用余额增加,流水记录完整。
  • 弱网环境下断网,本地余额仍能展示,网络恢复后自动同步服务端对账结果。

这张清单我们每次发版前都会过一遍,虽然不复杂,但能防止大量低级回归。


我个人在这一轮Flutter for OpenHarmony实战里最大的体会是:钱包功能真正的难点从来不在Flutter本身,而在数据一致性和跨模块通信的设计上。金额精度、事务边界、状态刷新、并发控制,这些基本功扎实了,无论后面接什么样的支付渠道、跑什么平台,都能稳得住。OpenHarmony适配带来的额外工作量,主要集中在渲染引擎和平台通道,这些属于一次性成本,沉淀成排查手册后,后续项目基本可以直接抄作业。

最后再补充一个小技巧:钱包相关的State和Model,我习惯在main.dart里用MultiProvider统一注入,而不是在每个页面单独创建。这样可以保证全局只有一份钱包状态,任何页面刷新都是同一份数据,调试的时候不用来回找实例在哪里,省了很多不必要的麻烦。

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

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

立即咨询