From 7e6f9060137f34b94ea34ac4a809bd2287def77d Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 11 Nov 2025 17:42:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0google=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/network/api.dart | 9 + lib/data/services/pay_service.dart | 250 ++++++++++++++++++ .../controllers/goods_base_controller.dart | 154 ++++++++++- pubspec.yaml | 1 + 4 files changed, 413 insertions(+), 1 deletion(-) diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 4f8b9a258..3c7ab245a 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -190,6 +190,15 @@ class Api { ); } + //内购收据验证 + static payiapverify([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/pay_iapverify', + params: params, + ); + } + //获取模版数据 static getEchomeetTemplates([params]) { return DioManager().request( diff --git a/lib/data/services/pay_service.dart b/lib/data/services/pay_service.dart index c58275c25..9a5c66419 100644 --- a/lib/data/services/pay_service.dart +++ b/lib/data/services/pay_service.dart @@ -1,8 +1,12 @@ import 'package:get/get.dart'; import 'dart:convert'; +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:voitrans/data/services/network/api.dart'; import 'package:tobias/tobias.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:voitrans/data/models/user_Info.dart'; import '../../modules/login/controllers/weixinLogin.dart'; @@ -13,6 +17,11 @@ class PayService extends GetxService { late FlWX _flwx; late Tobias _tobias; // 添加 Tobias 实例 String? _lastOrderId; // 最近一次创建的订单号 + // 内购 + final InAppPurchase _inAppPurchase = InAppPurchase.instance; + StreamSubscription>? _iapSubscription; + final RxBool iapAvailable = false.obs; + PaymentResultCallback? _iapOnResult; String? get lastOrderId => _lastOrderId; @@ -33,6 +42,7 @@ class PayService extends GetxService { // 初始化支付宝SDK _tobias = Tobias(); // 初始化 Tobias 实例 _initAlipay(); + _initIAP(); } /// 初始化支付宝SDK @@ -46,6 +56,246 @@ class PayService extends GetxService { } } + /// 初始化内购(Apple/Google) + Future _initIAP() async { + try { + // Web 环境不支持内购 + if (kIsWeb) { + iapAvailable.value = false; + return; + } + final available = await _inAppPurchase.isAvailable(); + iapAvailable.value = available; + if (!available) return; + + // 监听购买结果流 + _iapSubscription?.cancel(); + _iapSubscription = _inAppPurchase.purchaseStream.listen( + _handlePurchaseUpdates, + onDone: () => _iapSubscription?.cancel(), + onError: (e) { + _iapOnResult?.call(false, '内购异常: $e'); + }, + ); + } catch (e) { + print('初始化内购失败: $e'); + } + } + + /// 调试:批量查询并打印内购商品详情(用于确认商品ID是否存在) + Future debugQueryProducts(Set productIds) async { + try { + // 先原样打印传入的 productIds,便于核对入参 + print('[IAP] debugQueryProducts 输入 IDs(Set): $productIds'); + final idsList = productIds.toList(); + print('[IAP] debugQueryProducts 输入 IDs(List): ${idsList.join(', ')}'); + for (final id in productIds) { + print('[IAP] 待查询商品ID: $id'); + } + // 仅在非 Web 平台执行;平台信息用于日志 + await _initIAP(); + final platform = _platformString(); + if (!iapAvailable.value) { + print('[IAP-$platform] 当前平台内购不可用,无法查询商品。'); + return; + } + print('[IAP-$platform] 开始查询商品,IDs=${productIds.join(', ')}'); + final resp = await _inAppPurchase.queryProductDetails(productIds); + if (resp.error != null) { + print('[IAP-$platform] 查询错误: ${resp.error}'); + } + if (resp.productDetails.isEmpty) { + print('[IAP-$platform] 未返回任何商品。notFound=${resp.notFoundIDs}'); + } else { + for (final p in resp.productDetails) { + print( + '[IAP-$platform] 商品: id=${p.id}, title=${p.title}, price=${p.price}, currency=${p.currencyCode}'); + } + if (resp.notFoundIDs.isNotEmpty) { + print('[IAP-$platform] 未找到的IDs: ${resp.notFoundIDs}'); + } + } + } catch (e) { + print('[IAP] 调试查询异常: $e'); + } + } + + Future _queryIapProduct(String productId) async { + try { + final pid = productId.trim(); + print('[IAP] 开始查询单个商品: id="$pid"'); + final resp = await _inAppPurchase.queryProductDetails({pid}); + if (resp.error != null) { + print('[IAP] 查询商品失败: ${resp.error}'); + } + if (resp.productDetails.isEmpty) { + print('[IAP] 查询结果为空。notFound=${resp.notFoundIDs}'); + return null; + } + for (final p in resp.productDetails) { + print( + '[IAP] 命中商品: id=${p.id}, title=${p.title}, price=${p.price}, currency=${p.currencyCode}'); + } + // 避免 Dart 泛型推断在 Android 平台出现 orElse 类型不匹配(ProductDetails vs GooglePlayProductDetails) + ProductDetails? matched; + for (final p in resp.productDetails) { + if (p.id == pid) { + matched = p; + break; + } + } + return matched ?? resp.productDetails.first; + } catch (e) { + print('[IAP] 查询内购商品异常: $e'); + return null; + } + } + + /// Apple/Google 内购统一流程(消耗型) + Future _processIapPayment({ + required String productId, + PaymentResultCallback? onResult, + }) async { + _iapOnResult = onResult; + print( + '[IAP] 准备发起购买: id="${productId.trim()}" iapAvailable=${iapAvailable.value}'); + if (!iapAvailable.value) { + onResult?.call(false, '内购不可用'); + return false; + } + final product = await _queryIapProduct(productId); + if (product == null) { + onResult?.call(false, '未找到内购商品: $productId'); + return false; + } + try { + final purchaseParam = PurchaseParam(productDetails: product); + final ok = await _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: true, + ); + if (!ok) { + onResult?.call(false, '无法发起内购'); + } else { + onResult?.call(true, '已发起内购,请完成支付'); + } + return ok; + } catch (e) { + onResult?.call(false, '发起内购失败: $e'); + return false; + } + } + + Future _handlePurchaseUpdates(List detailsList) async { + for (final details in detailsList) { + switch (details.status) { + case PurchaseStatus.pending: + _iapOnResult?.call(true, '支付处理中…'); + break; + case PurchaseStatus.error: + _iapOnResult?.call(false, details.error?.message ?? '支付错误'); + break; + case PurchaseStatus.purchased: + case PurchaseStatus.restored: + // 完成购买并进行服务端收据验证 + try { + await _verifyIapWithServer(details); + } catch (e) { + _iapOnResult?.call(false, '收据验证失败: $e'); + } + break; + case PurchaseStatus.canceled: + _iapOnResult?.call(false, '已取消支付'); + break; + } + if (details.pendingCompletePurchase) { + try { + await _inAppPurchase.completePurchase(details); + } catch (_) {} + } + } + } + + Future _verifyIapWithServer(PurchaseDetails details) async { + final platform = _platformString(); + final verificationData = + details.verificationData.serverVerificationData ?? ''; + final transactionId = details.purchaseID ?? ''; + final params = { + 'platform': platform, + 'product_id': details.productID, + 'transaction_id': transactionId, + 'server_verification_data': verificationData, + }; + try { + final resp = await Api.payiapverify(params); + // 兼容多种返回结构,优先拿到用户信息 + Map? user; + if (resp is Map) { + if (resp['user'] is Map) { + user = resp['user'] as Map; + } else if (resp['data'] is Map && + (resp['data']['user'] is Map)) { + user = resp['data']['user'] as Map; + } + } + if (user != null) { + User.instance.updateUserInfo(user); + _iapOnResult?.call(true, '支付成功'); + } else { + _iapOnResult?.call(true, '支付成功,额度将稍后刷新'); + } + } catch (e) { + _iapOnResult?.call(false, '服务器验证异常: $e'); + } + } + + String _platformString() { + if (kIsWeb) return 'web'; + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 'ios'; + case TargetPlatform.android: + return 'android'; + case TargetPlatform.macOS: + return 'macos'; + case TargetPlatform.windows: + return 'windows'; + case TargetPlatform.linux: + return 'linux'; + case TargetPlatform.fuchsia: + return 'fuchsia'; + } + } + + /// 发起 Apple 内购 + Future processApplePayment({ + required String productId, + PaymentResultCallback? onResult, + }) async { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) { + onResult?.call(false, '当前平台不支持 Apple 内购'); + return false; + } + // 重新初始化内购,避免早期可用性判断为 false + await _initIAP(); + return _processIapPayment(productId: productId, onResult: onResult); + } + + /// 发起 Google Play 内购 + Future processGooglePayment({ + required String productId, + PaymentResultCallback? onResult, + }) async { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + onResult?.call(false, '当前平台不支持 Google 内购'); + return false; + } + // 重新初始化内购,避免早期可用性判断为 false + await _initIAP(); + return _processIapPayment(productId: productId, onResult: onResult); + } + /// 查询支付结果 /// /// [orderId] 订单ID diff --git a/lib/modules/goods/controllers/goods_base_controller.dart b/lib/modules/goods/controllers/goods_base_controller.dart index 133b09ffd..79370527a 100644 --- a/lib/modules/goods/controllers/goods_base_controller.dart +++ b/lib/modules/goods/controllers/goods_base_controller.dart @@ -7,6 +7,7 @@ import '../../../data/models/goods.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../../../data/services/network/dio_manager.dart'; import '../../../data/services/network/api.dart'; +import 'package:flutter/foundation.dart'; /// goods 模块基础控制器,封装通用的商品加载与支付流程 abstract class GoodsBaseController extends GetxController { @@ -57,6 +58,14 @@ abstract class GoodsBaseController extends GetxController { ok = await payPaypal(item, onResult: (success, msg) { if (!success) failMsg = msg ?? failMsg; }); + } else if (method == _PayMethod.apple) { + ok = await payAppleIAP(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); + } else if (method == _PayMethod.google) { + ok = await payGoogleIAP(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); } if (!ok) { @@ -71,6 +80,8 @@ abstract class GoodsBaseController extends GetxController { final context = Get.context; final isDark = context != null && Theme.of(context).brightness == Brightness.dark; + final isIOS = GetPlatform.isIOS && !kIsWeb; + final isAndroid = GetPlatform.isAndroid && !kIsWeb; return await Get.bottomSheet<_PayMethod>( SafeArea( child: Container( @@ -208,6 +219,78 @@ abstract class GoodsBaseController extends GetxController { ], ), ), + if (isIOS) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.apple), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon( + Icons.apple, + color: isDark ? Colors.white : Colors.black, + ), + ), + const SizedBox(height: 8), + Text( + 'Apple 内购', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + if (isAndroid) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.google), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon( + Icons.shop, + color: Colors.green[700], + ), + ), + const SizedBox(height: 8), + Text( + 'Google 内购', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), ], ), const SizedBox(height: 12), @@ -287,6 +370,75 @@ abstract class GoodsBaseController extends GetxController { ); } + /// 指定 Apple 内购(App Store IAP) + Future payAppleIAP(DBGoods item, + {PaymentResultCallback? onResult}) async { + // 使用商品id作为内购的 productId(与后台保持一致) + final productId = item.id; + return _payService.processApplePayment( + productId: productId, + onResult: (success, msg) { + // 透传并根据阶段展示 + onResult?.call(success, msg); + if (!success) { + _showPayResultDialog(false, msg); + } else { + // 支付成功或额度待刷新时弹窗,其余阶段用轻提示 + final showDialog = msg.contains('支付成功'); + if (showDialog) { + _showPayResultDialog(true, msg); + } else { + Get.snackbar('提示', msg, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + } + } + }, + ); + } + + /// 指定 Google Play 内购(Play Billing IAP) + Future payGoogleIAP(DBGoods item, + {PaymentResultCallback? onResult}) async { + // 使用商品id作为内购的 productId(与后台保持一致) + final productId = item.id; + // 调试:查询并打印当前商品的内购详情,便于确认 ID + await _payService.debugQueryProducts({productId}); + return _payService.processGooglePayment( + productId: productId, + onResult: (success, msg) { + onResult?.call(success, msg); + if (!success) { + _showPayResultDialog(false, msg); + } else { + final showDialog = msg.contains('支付成功'); + if (showDialog) { + _showPayResultDialog(true, msg); + } else { + Get.snackbar('提示', msg, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + } + } + }, + ); + } + + /// 调试:打印当前页面所有商品的 Google IAP 查询结果 + Future debugPrintGoogleIapProducts() async { + if (!GetPlatform.isAndroid || kIsWeb) { + print('[IAP] 当前平台非 Android 或为 Web,跳过商品查询调试。'); + return; + } + try { + final products = await fetchProducts(); + final ids = products.map((e) => e.id).toSet(); + await _payService.debugQueryProducts(ids); + } catch (e) { + print('[IAP] 调试查询商品异常: $e'); + } + } + Future _confirmOrderAndShow(String? orderId, {required String channel, String? initialMessage}) async { // 若无法获取订单号,直接弹成功提示但提醒稍后刷新 @@ -429,4 +581,4 @@ abstract class GoodsBaseController extends GetxController { } } -enum _PayMethod { wechat, alipay, paypal } +enum _PayMethod { wechat, alipay, paypal, apple, google } diff --git a/pubspec.yaml b/pubspec.yaml index d569d1b46..7f00789b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: fluwx: ^5.7.1 tobias: ^5.2.0 # 支付宝SDK url_launcher: ^6.3.1 + in_app_purchase: ^3.1.11 firebase_core: ^4.0.0 firebase_auth: ^6.0.1 google_sign_in: ^6.2.2