import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../data/models/user_Info.dart'; import '../../../data/services/pay_service.dart'; import '../../../data/services/version_update_service.dart'; 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'; import 'dart:async'; import 'package:app_links/app_links.dart'; /// goods 模块基础控制器,封装通用的商品加载与支付流程 abstract class GoodsBaseController extends GetxController { final RxList items = [].obs; final RxBool loading = false.obs; final RxString error = ''.obs; final RxBool isPaying = false.obs; Timer? _payingTimeoutTimer; late final PayService _payService = Get.find(); StreamSubscription? _deeplinkSub; late final AppLinks _appLinks = AppLinks(); @override void onInit() { super.onInit(); _init(); // 处理冷启动场景:如果 App 通过深链直接启动 _handleInitialDeepLink(); } @override void onClose() { _deeplinkSub?.cancel(); _deeplinkSub = null; _payingTimeoutTimer?.cancel(); _payingTimeoutTimer = null; super.onClose(); } Future _init() async { loading.value = true; error.value = ''; try { final list = await fetchProducts(); items.assignAll(list); // iOS 环境下,在页面加载后批量查询当前商品的 IAP 详情,便于排查 StoreKit 可用商品 await _debugQueryIapProductsIfIos(); } catch (e) { error.value = e.toString(); } finally { loading.value = false; } } /// 子类实现:返回商品列表(统一使用 DBGoods) Future> fetchProducts(); /// iOS 批量查询当前页面商品的 IAP 详情(仅真机 iOS 生效,Web/Android 跳过) Future _debugQueryIapProductsIfIos() async { try { final isIOS = GetPlatform.isIOS && !kIsWeb; if (!isIOS) return; // 收集页面商品ID;若为空,使用预设的基础套餐ID进行测试 final ids = items .map((e) => (e.id ?? '').trim()) .where((id) => id.isNotEmpty) .toSet(); if (ids.isEmpty) { ids.addAll({ 'ai_001', }); } print('[IAP-ios] 批量查询页面商品IDs: ${ids.join(', ')}'); await _payService.debugQueryProducts(ids); } catch (e) { print('[IAP-ios] 批量查询异常: $e'); } } /// 统一入口:根据后端配置决定是否弹出支付方式选择面板 Future purchase(DBGoods item) async { // 读取渠道配置 final enabled = _getEnabledPayMethods(); // 无配置或多个渠道:弹出选择面板 _PayMethod? method; if (enabled.length != 1) { method = await _showPayMethodSheet(enabled); if (method == null) return; } else { // 仅一个渠道:直接走对应支付 method = enabled.first; } bool ok = false; String failMsg = '支付失败,请重试'; isPaying.value = true; _startPayingTimeout(); if (method == _PayMethod.wechat) { ok = await payWechat(item, onResult: (success, msg) { if (!success) failMsg = msg ?? failMsg; }); } else if (method == _PayMethod.alipay) { ok = await payAlipay(item, onResult: (success, msg) { if (!success) failMsg = msg ?? failMsg; }); } else if (method == _PayMethod.paypal) { 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) { Get.snackbar('提示', failMsg, snackPosition: SnackPosition.BOTTOM, margin: const EdgeInsets.all(12)); } } /// 解析后端 paychannels 并结合平台过滤可用支付方式 List<_PayMethod> _getEnabledPayMethods() { try { final paychannels = VersionUpdateService.to.cachedAppInfo?.paychannels ?? ''; final raw = paychannels .split(',') .map((e) => e.trim().toLowerCase()) .where((e) => e.isNotEmpty) .toList(); final bool isIOS = GetPlatform.isIOS && !kIsWeb; final bool isAndroid = GetPlatform.isAndroid && !kIsWeb; final List<_PayMethod> methods = []; for (final ch in raw) { switch (ch) { case 'wxpay': case 'wechat': methods.add(_PayMethod.wechat); break; case 'alipay': methods.add(_PayMethod.alipay); break; case 'paypal': methods.add(_PayMethod.paypal); break; case 'apple': if (isIOS) methods.add(_PayMethod.apple); break; case 'google': if (isAndroid) methods.add(_PayMethod.google); break; default: break; } } // 若后端未下发或全部被平台过滤,则回退为展示所有平台支持的方式 if (methods.isEmpty) { final fallback = <_PayMethod>[ _PayMethod.wechat, _PayMethod.alipay, _PayMethod.paypal ]; if (isIOS) fallback.add(_PayMethod.apple); if (isAndroid) fallback.add(_PayMethod.google); return fallback; } return methods; } catch (_) { // 任意异常则回退为旧逻辑:展示所有 final bool isIOS = GetPlatform.isIOS && !kIsWeb; final bool isAndroid = GetPlatform.isAndroid && !kIsWeb; final fallback = <_PayMethod>[ _PayMethod.wechat, _PayMethod.alipay, _PayMethod.paypal ]; if (isIOS) fallback.add(_PayMethod.apple); if (isAndroid) fallback.add(_PayMethod.google); return fallback; } } /// 底部弹窗:选择支付方式(仅展示后端允许的渠道) Future<_PayMethod?> _showPayMethodSheet(List<_PayMethod> allowed) async { 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( decoration: BoxDecoration( color: isDark ? Colors.grey[900] : Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Container( width: 40, height: 4, decoration: BoxDecoration( color: isDark ? Colors.white24 : Colors.black12, borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( '选择支付方式', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (allowed.contains(_PayMethod.wechat)) GestureDetector( onTap: () => Get.back(result: _PayMethod.wechat), 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.wechat, color: Colors.green[600]), ), const SizedBox(height: 8), Text( '微信支付', style: TextStyle( fontSize: 13, color: isDark ? Colors.white : Colors.black, ), ), ], ), ), if (allowed.contains(_PayMethod.alipay)) GestureDetector( onTap: () => Get.back(result: _PayMethod.alipay), 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.payment, color: Colors.blue[600]), ), const SizedBox(height: 8), Text( '支付宝', style: TextStyle( fontSize: 13, color: isDark ? Colors.white : Colors.black, ), ), ], ), ), if (allowed.contains(_PayMethod.paypal)) GestureDetector( onTap: () => Get.back(result: _PayMethod.paypal), 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.account_balance_wallet, color: Colors.indigo[600], ), ), const SizedBox(height: 8), Text( 'PayPal', style: TextStyle( fontSize: 13, color: isDark ? Colors.white : Colors.black, ), ), ], ), ), if (isIOS && allowed.contains(_PayMethod.apple)) 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 && allowed.contains(_PayMethod.google)) 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), ], ), ), ], ), ), ), isScrollControlled: false, ignoreSafeArea: false, backgroundColor: Colors.transparent, ); } /// 指定微信支付(直接下单并拉起微信) Future payWechat(DBGoods item, {PaymentResultCallback? onResult}) async { final cents = item.price; // DBGoods.price 单位:分 final serverUrl = DioManager().baseUrl; final paymentData = await _payService.createWechatPayOrder( productId: item.id, productName: item.name, amount: cents, notifyUrl: '$serverUrl/api/home/pay_wechatnotify', ); if (paymentData == null) { onResult?.call(false, '创建微信订单失败'); isPaying.value = false; _cancelPayingTimeout(); return false; } final orderId = paymentData['orderId'] as String? ?? _payService.lastOrderId; return _payService.wechatPay( paymentData: paymentData, onResult: (success, msg) async { // 先透传给外部回调 onResult?.call(success, msg); if (success) { isPaying.value = false; _cancelPayingTimeout(); await _confirmOrderAndShow(orderId, channel: '微信', initialMessage: msg); } else { isPaying.value = false; _cancelPayingTimeout(); _showPayResultDialog(false, msg ?? '支付失败'); } }, ); } /// 指定支付宝支付(直接下单并拉起支付宝) Future payAlipay(DBGoods item, {PaymentResultCallback? onResult}) async { final cents = item.price; // DBGoods.price 单位:分 final orderInfo = await _payService.createAlipayOrder( productId: item.id, productName: item.name, amount: cents, notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify', ); if (orderInfo == null) { onResult?.call(false, '创建支付宝订单失败'); isPaying.value = false; _cancelPayingTimeout(); return false; } final orderId = _payService.lastOrderId; // 创建订单时已缓存 return _payService.alipay( orderInfo: orderInfo, onResult: (success, msg) async { onResult?.call(success, msg); if (success) { isPaying.value = false; _cancelPayingTimeout(); await _confirmOrderAndShow(orderId, channel: '支付宝', initialMessage: msg); } else { isPaying.value = false; _cancelPayingTimeout(); _showPayResultDialog(false, msg ?? '支付失败'); } }, ); } /// 指定 Apple 内购(App Store IAP) Future payAppleIAP(DBGoods item, {PaymentResultCallback? onResult}) async { try { // 先创建服务端订单,记录 order_id 以便后续回查 final orderId = await _payService.createAppleIapOrder( productId: item.id, productName: item.name, amount: item.price, ); if (orderId == null || orderId.isEmpty) { onResult?.call(false, '创建 Apple 订单失败'); isPaying.value = false; _cancelPayingTimeout(); return false; } // 使用商品id作为内购的 productId(与后台保持一致) final productId = item.id; // 解析为 App Store 的真实商品ID(支持前缀/映射) final iosProductId = _payService.resolveAppleProductId(productId); // 调试:在发起购买前先查询并打印该商品的内购详情/错误,便于定位问题 await _payService.debugQueryProducts({iosProductId}); return _payService.processApplePayment( productId: productId, onResult: (success, msg) { // 透传并根据阶段展示 onResult?.call(success, msg); if (!success) { _showPayResultDialog(false, msg); isPaying.value = false; _cancelPayingTimeout(); } else { // 支付成功或额度待刷新时弹窗,其余阶段用轻提示 final showDialog = msg.contains('支付成功'); if (showDialog) { _showPayResultDialog(true, msg); // 仅在支付成功阶段,携带校验参数主动查询订单 _askOrderWithIapVerification(); isPaying.value = false; _cancelPayingTimeout(); } else { Get.snackbar('提示', msg, snackPosition: SnackPosition.BOTTOM, margin: const EdgeInsets.all(12)); } } }, ); } catch (e) { onResult?.call(false, 'Apple 内购异常: $e'); isPaying.value = false; _cancelPayingTimeout(); return false; } } /// 指定 Google Play 内购(Play Billing IAP) Future payGoogleIAP(DBGoods item, {PaymentResultCallback? onResult}) async { try { // 先创建服务端订单,记录 order_id 以便后续回查 final orderId = await _payService.createGoogleIapOrder( productId: item.id, productName: item.name, amount: item.price, ); if (orderId == null || orderId.isEmpty) { onResult?.call(false, '创建 Google 订单失败'); isPaying.value = false; _cancelPayingTimeout(); return false; } isPaying.value = true; _startPayingTimeout(); // 使用商品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); isPaying.value = false; _cancelPayingTimeout(); } else { final showDialog = msg.contains('支付成功'); if (showDialog) { _showPayResultDialog(true, msg); // 仅在支付成功阶段,携带校验参数主动查询订单 _askOrderWithIapVerification(); isPaying.value = false; _cancelPayingTimeout(); } else { Get.snackbar('提示', msg, snackPosition: SnackPosition.BOTTOM, margin: const EdgeInsets.all(12)); } } }, ); } catch (e) { onResult?.call(false, 'Google 内购异常: $e'); isPaying.value = false; _cancelPayingTimeout(); return false; } } /// 调试:打印当前页面所有商品的 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, int retries = 5, int intervalMs = 1500, }) async { // 若无法获取订单号,直接弹成功提示但提醒稍后刷新 if (orderId == null || orderId.isEmpty) { _showPayResultDialog(true, '${channel}支付成功,订单处理中,请稍后查看。'); return; } String message = initialMessage ?? '${channel}支付成功'; bool success = false; try { // 简单轮询回查,避免回调延迟 for (int i = 0; i < retries; i++) { final resp = await Api.payverifyorder({'order_id': orderId}); print("查询订单 resp: $resp"); final order = resp?['order'] as Map?; final user = resp?['user'] as Map?; if (order != null && user != null) { final orderStatus = order["status"]; if (orderStatus == 1) { User.instance.updateUserInfo(user); _payService.setVipExptime(User.instance.vipexptime); success = true; break; } } await Future.delayed(Duration(milliseconds: intervalMs)); } // 若标记成功但未拿到用户信息,使用token登录接口回拉用户资料,确保额度刷新 if (success) { try { final response = await Api.tokenlogin(); if (response is Map && response['user'] is Map) { User.instance .updateUserInfo(response['user'] as Map); } } catch (_) {} } } catch (e) { message = '查询支付结果异常: $e'; } _showPayResultDialog(success, message); } /// Apple/Google 内购:无法监听平台回调时,主动查询订单并携带验证数据 Future _askOrderWithIapVerification() async { try { final params = await _payService.buildIapAskParams(); final resp = await Api.payverifyorder(params); print('[IAP] 主动查询订单 resp: $resp'); final order = resp?['order'] as Map?; final user = resp?['user'] as Map?; if (order != null) { final orderStatus = order['status']; if (orderStatus == 1 && user != null) { User.instance.updateUserInfo(user); _payService.setVipExptime(User.instance.vipexptime); Get.snackbar('提示', '订单已确认,额度已刷新', snackPosition: SnackPosition.BOTTOM, margin: const EdgeInsets.all(12)); return; } } // 若未确认成功,尝试回拉用户信息确保额度同步 try { final response = await Api.tokenlogin(); if (response is Map && response['user'] is Map) { User.instance .updateUserInfo(response['user'] as Map); } } catch (_) {} } catch (e) { print('[IAP] 主动查询订单异常: $e'); } } void _showPayResultDialog(bool success, String message) { final context = Get.context; final isDark = context != null && Theme.of(context).brightness == Brightness.dark; Get.dialog( Dialog( insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), backgroundColor: isDark ? Colors.grey[900] : Colors.white, child: Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: success ? (isDark ? Colors.green[800] : Colors.green[100]) : (isDark ? Colors.red[800] : Colors.red[100]), borderRadius: BorderRadius.circular(12), ), child: Icon( success ? Icons.check_circle : Icons.error, color: success ? Colors.green[600] : Colors.redAccent[200], size: 28, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( success ? '支付成功' : '支付失败', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: isDark ? Colors.white : Colors.black, ), ), const SizedBox(height: 6), Text( message, style: TextStyle( fontSize: 13, color: isDark ? Colors.white70 : Colors.grey[700], ), ), ], ), ), ], ), const SizedBox(height: 16), Divider(color: isDark ? Colors.white24 : Colors.black12), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Get.back(), child: const Text('知道了'), ), ], ), ], ), ), ), barrierDismissible: true, ); } void _startPayingTimeout({int seconds = 30}) { _payingTimeoutTimer?.cancel(); _payingTimeoutTimer = Timer(Duration(seconds: seconds), () { isPaying.value = false; _payingTimeoutTimer = null; }); } void _cancelPayingTimeout() { _payingTimeoutTimer?.cancel(); _payingTimeoutTimer = null; } /// 指定 PayPal 支付(创建订单并打开授权链接) Future payPaypal(DBGoods item, {PaymentResultCallback? onResult}) async { final cents = item.price; // DBGoods.price 单位:分 final approvalUrl = await _payService.createPaypalOrder( productId: item.id, productName: item.name, amount: cents, notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify', ); if (approvalUrl == null) { onResult?.call(false, '创建 PayPal 订单失败'); isPaying.value = false; _cancelPayingTimeout(); return false; } final orderId = _payService.lastOrderId; // 创建订单时已缓存 print('[PayPal] 创建完成,orderId: $orderId, approvalUrl: $approvalUrl'); // 启动深链监听:当浏览器重定向到 deepvoiceclient://paypal/return 或 /cancel 时处理 _deeplinkSub?.cancel(); _deeplinkSub = _appLinks.uriLinkStream.listen((uri) async { if (uri == null) return; print('[PayPal] 深链回跳: ${uri.toString()}'); final token = uri.queryParameters['token'] ?? ''; if (token.isNotEmpty) { print('[PayPal] 回跳 token(订单ID): $token'); } final scheme = uri.scheme; final host = uri.host; final path = uri.path; if (scheme == 'deepvoiceclient' && host == 'paypal') { isPaying.value = false; if (path == '/return') { isPaying.value = false; _cancelPayingTimeout(); print('[PayPal] 检测到 /return,使用 orderId 回查: $orderId'); await _confirmOrderAndShow( orderId, channel: 'PayPal', initialMessage: '支付完成,正在确认', retries: 1, intervalMs: 0, ); } else if (path == '/cancel') { isPaying.value = false; _cancelPayingTimeout(); _showPayResultDialog(false, '已取消支付'); } // 一次性监听,处理后即取消 _deeplinkSub?.cancel(); _deeplinkSub = null; } }, onError: (e) { isPaying.value = false; _cancelPayingTimeout(); // 忽略监听错误,保持现有轮询逻辑 }); return _payService.paypalPay( approvalUrl: approvalUrl, onResult: (success, msg) async { onResult?.call(success, msg); if (success) { // 仅提示已打开授权页面,不在此处回查与弹窗,等待深链回跳后再处理 print('[PayPal] 已打开授权页面,等待回跳后再确认。orderId: $orderId'); Get.snackbar('提示', msg ?? '已打开 PayPal,请在浏览器完成支付', snackPosition: SnackPosition.BOTTOM, margin: const EdgeInsets.all(12)); } else { // 无法打开授权页才提示失败 _showPayResultDialog(false, msg ?? '无法打开 PayPal 支付页面'); } }, ); } Future _handleInitialDeepLink() async { try { final uri = await _appLinks.getInitialLink(); if (uri == null) return; print('[PayPal] 冷启动深链: ${uri.toString()}'); final token = uri.queryParameters['token'] ?? ''; if (token.isNotEmpty) { print('[PayPal] 冷启动 token(订单ID): $token'); } if (uri.scheme == 'deepvoiceclient' && uri.host == 'paypal') { if (uri.path == '/return') { final orderId = _payService.lastOrderId; if (orderId != null) { print('[PayPal] 冷启动 /return,使用 orderId 回查: $orderId'); await _confirmOrderAndShow( orderId, channel: 'PayPal', initialMessage: '支付完成,正在确认', retries: 1, intervalMs: 0, ); } } else if (uri.path == '/cancel') { _showPayResultDialog(false, '已取消支付'); } } } catch (_) { // 忽略异常,维持原有逻辑 } } } enum _PayMethod { wechat, alipay, paypal, apple, google }