void dispose() { WidgetsBinding.instance.removeObserver(this); //销毁观察者 super.dispose(); }

/// 应用状态监听 @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: { if (Platform.isAndroid && _isPaying) { _isPaying = false; // 监听到时安卓设备并且支付还在进行中,程序员要根据业务做一下处理 break; } default: break; } super.didChangeAppLifecycleState(state); } }

到此,微信支付很愉快的解决了,以上代码是抽象出来的工具类,可以直接使用;但是不涉及任何业务流程的开发,这个需要使用者自己去补充。 综上,微信支付流程主线可简单粗暴总结为:服务端生成订单 → 客户端调起支付 → 客户端通知服务端核验订单 → 客户端拿到最终结果 → 客户端final支付。 整个过程形成闭环,有理有据,数据都由后端去操作安全合理。(最重点是前端工作量简直不要太少)。

可是,iOS就不一样了,简直不要太恶心!

iOS IAP应用内支付

IAP,即in-app Purchase,苹果推出的App内购买虚拟商品的方式,基于AppStore账户的支付方式。由于iOS整个体系都是基于自己的一套系统的(不像上面的微信支付,是第三方支付平台),因此在开发之前,我们需要到Apple开发者中心完成以下步骤:

1. 签署协议和银行业务 2. 在后台创建App内购买项目,这里所有的价格都是Apple规定好的,我们只有选择的资格,没办法自定价格。创建完成后,每个项目会有sku和productId 3. 添加沙盒测试员Apple 以上步骤参考内容引自站内大神:Geniune

支付流程:应用通过sku向服务端获取商品列表 → 列表中取出对应产品请求支付 → 进入appStore支付 → 页面监听支付回调拿到验证票据 → 业务后台拿到应用接收到的票据后去Apple官网进行校验即可。

流程很简单,简单到几乎不用跟业务后台打交代,但是坑却随之而来:

① 支付数据完全依赖前端应用,很难跟业务后台的订单系统一一对应; ② 针对①的问题,IAP支付支持传递skPayment对象,里面的applicationUsername经常用来保存系统的OrderId; 但是应用支付成功后收到的回调中,applicationUsername却偶尔会出现为null的情况,没有了对应关系,就没办法核销业务系统中的订单从而为用户充值; ③ iOS支付回调非常不稳定,有时延迟严重;且没有任何注定查询的方法; ④ iOS应用内支付有很多异常情况要处理,最常见的就是没有登录、没有同意最新的iOS支付协议等,都会发送给app支付失败的回调; 但是当用户登录或是同意后,iOS系统又会触发新的支付,导致旧的附带业务订单号的支付无效,莫名又多出一个没有订单号的新支付; ⑤ 国内网上资料极度缺乏,基本都是19年以前的,Flutter的文章更是少的可怜,可参考性不强。 ⑥ 测试文档对于中断购买的测试流程有巨坑,后面菜单一定不要错过~

通过查看文档和不断调试,我们发现:

① 支付错误的回调,基本能马上收到; ② 上面流程说到IAP支付需要手动结束支付流程。同时iOS规定不能对同一个skuId重复发起多次支付的,只要当前skuId有没有final的支付,再次发起都会失败; ② 无论支付成功或失败,只要app没有主动对当前支付进行final,每次启动app后,app都会收到这个支付信息的通知; ③ 关于applicationUsername,只有在支付完成马上收到回调的情况下,回调信息才会有这个信息;到②中的情况,肯定不会返回applicationUsername; ④ 没有applicationUsername就意味着订单对不上,因此我们需要进行凑单机制。

综上,我们对异常处理有了确定方案:

① app发起支付后,需要将业务OrderId和skuId进行持久化存储(即卸载应用都不会删除的数据); ②只要持久化存储不为空,启动app就需要马上启动监听,以接收iOS系统的订单推送; ③ 支付出错可以final当前支付,但是支付成功必须明确接收到iOS推送并且后台核验成功后,才能final,并删除持久化存储。

最终,结合到业务系统和特殊情况的处理后,支付流程应该如下:

业务后台返回商品列表时,需要附加返回对应的skuIdapp通过skuId请appStore请求商品信息app对商品发起支付,并将业务订单号存储在applicationUsername中,发起成功写入持久化存储,状态为pending接收iOS系统回调,失败马上final支付,更改对应持久化存储状态为cancle;成功拿到票据和业务OrderId发送给后台后台调取Apple服务端接口,传入票据(票据其实储存着最新的时间,appStore用户信息等)后台获取到Apple返回的当前appStore用户所有支付的前100条记录,拿到productId到数据库有中匹配该用户是否有未核销的订单,并对应修改业务订单状态app确认核销成功,final支付,并且删除持久化存储

同时还需要做一些特殊处理:

app刚启动时,若是持久化存储不为空,需要马上启动iOS支付订阅监听,以接收iOS对未完成订单的推送;由于iOS限制了同一个skuId不能重复发起支付,因此持久化存储中,一个skuId永远只会有一条记录。因此当app接收到的支付推送applicationUsername为null,采取凑单机制,原则是:通过skuId找到存储记录,拿到其对应的OrderId,发给后台核验。

接下来进入开发,Futter采用的是in_app_purchase插件,官方提供的,支持google和IAP支付;而持久化存储用的是flutter_secure_storage插件。

依据上面的流程,我同样封装了工具类。而且由于可能会在多个地方调用起监听,所有必须是单例模式,代码如下:

import ‘dart:async’;

import ‘package:flutter_secure_storage/flutter_secure_storage.dart’; import ‘package:in_app_purchase/in_app_purchase.dart’;

// iOS支付单一实例 final iOSPayment = IOSPayment();

class IOSPayment { /// 单例模式 static final IOSPayment _iosPayment = IOSPayment.init();

factory IOSPayment() { return _iosPayment; }

IOSPayment.init();

// 应用内支付实例 InAppPurchaseConnection purchaseConnection = InAppPurchaseConnection.instance; FlutterSecureStorage storage = new FlutterSecureStorage();

// iOS订阅监听 StreamSubscription subscription;

/// 判断是否可以使用支付 Future isAvailable() async => await purchaseConnection.isAvailable();

// 开始订阅 void startSubscription() async { if (subscription != null) return; print(‘>>> start subscription’); // 支付消息订阅 Stream purchaseUpdates = purchaseConnection.purchaseUpdatedStream; subscription = purchaseUpdates.listen( (purchaseDetailsList) { purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { if (purchaseDetails.status == PurchaseStatus.pending) { print(‘>>> pending’); // 业务代码略:有订单开始支付,向外部发出通知,并记录到缓存中; } else { if (purchaseDetails.status == PurchaseStatus.error) { print(‘>>> error’); // 业务代码略:有订单支付错误,向外部发出通知 // 下面是删除 String value = await storage.read(key: purchaseDetails.productID); String orderId = value.split(‘¥’)[0]; writeStorage(purchaseDetails.productID, orderId, ‘cancel’); finalTransaction(purchaseDetails); } else if (purchaseDetails.status == PurchaseStatus.purchased) { print(‘>>> purchased’); String orderId = purchaseDetails.skPaymentTransaction.payment.applicationUsername; if (orderId == null || orderId.isEmpty) { // 如果applicationUsername为空,执行凑单 orderId = await foundRecentOrder(purchaseDetails.productID); } if (orderId.isEmpty) { // 凑单失败,找不到业务单号,结束 finalTransaction(purchaseDetails); BlocProvider.of(Application.navigatorState.currentContext).add(IosPayFailureEvent(errorMessage: ‘支付出错啦,请稍后再试~’)); return; } // 业务代码略:支付成功,向外部发出通知 // 业务代码略:开始核验订单,核验结果由外部监听 ); } } }); }, onDone: () { stopListen(); }, onError: (error) { stopListen(); }, ); }

/// 检查sku是否有对应商品 Future checkProductBySku(String sku, {Function(String err) onError}) async { if (!await isAvailable()) { onError?.call(‘无法连接AppStore,请稍后再试’); return false; } ProductDetailsResponse appStoreProducts = await purchaseConnection.queryProductDetails([sku].toSet()); if (appStoreProducts.productDetails.length == 0) { onError?.call(‘没有找到相关产品,请联系管理员’); return false; } return true; }

/// 启动支付 void iosPay(String sku, String orderId, {Function(String err) onError}) async { // 获取商品列表 ProductDetailsResponse appStoreProducts = await purchaseConnection.queryProductDetails([sku].toSet()); // 发起支付 purchaseConnection .buyNonConsumable( purchaseParam: PurchaseParam( productDetails: appStoreProducts.productDetails.first, applicationUserName: orderId, ), ) .then((value) { if (value) { // 只要能发起,就写入 writeStorage(sku, orderId, ‘pending’); } }).catchError((err) { onError?.call(‘当前商品您有未完成的交易,请等待iOS系统核验后再次发起购买。’); print(err); }); }

writeStorage(String key, String value, String status) { storage.write(key: key, value: ‘

v

a

l

u

e

value¥

value¥status’); }

// 关闭交易 void finalTransaction(PurchaseDetails purchaseDetails) async { await purchaseConnection.completePurchase(purchaseDetails); // 每完成一张订单进行缓存的清除 if (!await checkStorage()) { stopListen(); } }

// 凑单机制 Future foundRecentOrder(String sku) async { String orderId = ‘’; String values = await storage.read(key: sku);

if (values != null) { orderId = values.split(‘¥’)[0]; } return orderId; }

// 校验是否还有缓存 Future checkStorage() async { Map remainingValues = await storage.readAll(); return remainingValues.isNotEmpty; }

// 关闭监听 stopListen() async { subscription?.cancel(); subscription = null; } }

页面调用时,建议启用定时器,因为iOS回调不稳定,所以监听到应用回到前台时开始30秒计时;30秒内没有收到支付回调,需要做对应提示,这一块也是存业务流程,我这里不做代码展示。 下面代码是如何调用上面工具类的:

iOSPayment.startSubscription(); iOSPayment.iosPay( state.skuId, state.model.orderId, onError: (String err) { if (!mounted) return; // 支付遇到错误,马上停止定时器,并且关掉弹框 }, );

// 应用启动时 if (Platform.isIOS && await iOSPayment.checkStorage()) { // 启动订阅:支付缓存未清除完毕、机型可使用应用内支付 iOSPayment.startSubscription(needDelayed: true); }

测试IAP中断购买的测试

这个测试是模拟用户点击购买协议的操作,当弹出系统协议弹框时,iOS会发出一个支付错误的消息;这个时候我们的代码会final这个支付,并且将持久化中对应skuId的信息状态改为cancel;然后用户同意后,iOS会再发起一个同样的不带OdrerId(是的,被弄丢了。。。。)的订单,用户支付成功后,我们的代码就会收到支付成功的没有OdrerId的推送,在持久化存储中执行凑单机制后,再发给后台核销。

如何模拟这个流程呢?看看官方文档描述,下面是译文:

设置测试

通过登录App Store Connect启用对Sandbox Apple ID的中断购买,然后:

在“用户和访问”中,单击边栏中沙箱下的“测试器”。在右侧,您可以查看您的Sandbox Apple ID。 选择您要为其启用中断购买的Sandbox Apple ID。如果已启用,则会在“中断购买”列下看到一个复选标记。 在出现的对话框中,选择“此测试仪的中断购买”。

开始测试

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

最后对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

img-DmmAa6DY-1712621340240)]

[外链图片转存中…(img-qsafLJJF-1712621340240)]

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

精彩链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: