|
|
@ -54,7 +54,7 @@ class PayService extends GetxService { |
|
|
/// 初始化支付宝SDK |
|
|
/// 初始化支付宝SDK |
|
|
Future<void> _initAlipay() async { |
|
|
Future<void> _initAlipay() async { |
|
|
try { |
|
|
try { |
|
|
// 检查支付宝是否安装 - 修复用法 |
|
|
// 检查支付宝是否安x装 - 修复用法 |
|
|
final isInstalled = await _tobias.isAliPayInstalled; |
|
|
final isInstalled = await _tobias.isAliPayInstalled; |
|
|
print('支付宝是否安装: $isInstalled'); |
|
|
print('支付宝是否安装: $isInstalled'); |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
@ -99,8 +99,11 @@ class PayService extends GetxService { |
|
|
print('[IAP] 待查询商品ID: $id'); |
|
|
print('[IAP] 待查询商品ID: $id'); |
|
|
} |
|
|
} |
|
|
// 仅在非 Web 平台执行;平台信息用于日志 |
|
|
// 仅在非 Web 平台执行;平台信息用于日志 |
|
|
await _initIAP(); |
|
|
|
|
|
final platform = _platformString(); |
|
|
final platform = _platformString(); |
|
|
|
|
|
if (!iapAvailable.value) { |
|
|
|
|
|
final available = await _inAppPurchase.isAvailable(); |
|
|
|
|
|
iapAvailable.value = available; |
|
|
|
|
|
} |
|
|
if (!iapAvailable.value) { |
|
|
if (!iapAvailable.value) { |
|
|
print('[IAP-$platform] 当前平台内购不可用,无法查询商品。'); |
|
|
print('[IAP-$platform] 当前平台内购不可用,无法查询商品。'); |
|
|
return; |
|
|
return; |
|
|
@ -108,7 +111,9 @@ class PayService extends GetxService { |
|
|
print('[IAP-$platform] 开始查询商品,IDs=${productIds.join(', ')}'); |
|
|
print('[IAP-$platform] 开始查询商品,IDs=${productIds.join(', ')}'); |
|
|
final resp = await _inAppPurchase.queryProductDetails(productIds); |
|
|
final resp = await _inAppPurchase.queryProductDetails(productIds); |
|
|
if (resp.error != null) { |
|
|
if (resp.error != null) { |
|
|
print('[IAP-$platform] 查询错误: ${resp.error}'); |
|
|
final err = resp.error!; |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP-$platform] 查询错误: code=${err.code}, source=${err.source}, message=${err.message}, details=${err.details}'); |
|
|
} |
|
|
} |
|
|
if (resp.productDetails.isEmpty) { |
|
|
if (resp.productDetails.isEmpty) { |
|
|
print('[IAP-$platform] 未返回任何商品。notFound=${resp.notFoundIDs}'); |
|
|
print('[IAP-$platform] 未返回任何商品。notFound=${resp.notFoundIDs}'); |
|
|
@ -132,7 +137,9 @@ class PayService extends GetxService { |
|
|
print('[IAP] 开始查询单个商品: id="$pid"'); |
|
|
print('[IAP] 开始查询单个商品: id="$pid"'); |
|
|
final resp = await _inAppPurchase.queryProductDetails({pid}); |
|
|
final resp = await _inAppPurchase.queryProductDetails({pid}); |
|
|
if (resp.error != null) { |
|
|
if (resp.error != null) { |
|
|
print('[IAP] 查询商品失败: ${resp.error}'); |
|
|
final err = resp.error!; |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP] 查询商品失败: code=${err.code}, source=${err.source}, message=${err.message}, details=${err.details}'); |
|
|
} |
|
|
} |
|
|
if (resp.productDetails.isEmpty) { |
|
|
if (resp.productDetails.isEmpty) { |
|
|
print('[IAP] 查询结果为空。notFound=${resp.notFoundIDs}'); |
|
|
print('[IAP] 查询结果为空。notFound=${resp.notFoundIDs}'); |
|
|
@ -187,31 +194,43 @@ class PayService extends GetxService { |
|
|
if (isSubscription) { |
|
|
if (isSubscription) { |
|
|
print('[IAP-android] 订阅商品(ENV),使用 buyNonConsumable'); |
|
|
print('[IAP-android] 订阅商品(ENV),使用 buyNonConsumable'); |
|
|
final gpParam = GooglePlayPurchaseParam(productDetails: product); |
|
|
final gpParam = GooglePlayPurchaseParam(productDetails: product); |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP-android] 调用 buyNonConsumable 参数: productId=${product.id}'); |
|
|
ok = await _inAppPurchase.buyNonConsumable(purchaseParam: gpParam); |
|
|
ok = await _inAppPurchase.buyNonConsumable(purchaseParam: gpParam); |
|
|
|
|
|
print('[IAP-android] buyNonConsumable 返回: $ok'); |
|
|
} else { |
|
|
} else { |
|
|
// 普通一次性/消耗型商品,使用 buyConsumable |
|
|
// 普通一次性/消耗型商品,使用 buyConsumable |
|
|
final purchaseParam = PurchaseParam(productDetails: product); |
|
|
final purchaseParam = PurchaseParam(productDetails: product); |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP-android] 调用 buyConsumable 参数: productId=${product.id} autoConsume=true'); |
|
|
ok = await _inAppPurchase.buyConsumable( |
|
|
ok = await _inAppPurchase.buyConsumable( |
|
|
purchaseParam: purchaseParam, |
|
|
purchaseParam: purchaseParam, |
|
|
autoConsume: true, |
|
|
autoConsume: true, |
|
|
); |
|
|
); |
|
|
|
|
|
print('[IAP-android] buyConsumable 返回: $ok'); |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
// iOS 或非订阅商品,按消耗型处理(如需支持 iOS 订阅,可通过 ENV 切换调用 buyNonConsumable) |
|
|
// iOS 或非订阅商品,按消耗型处理(如需支持 iOS 订阅,可通过 ENV 切换调用 buyNonConsumable) |
|
|
final purchaseParam = PurchaseParam(productDetails: product); |
|
|
final purchaseParam = PurchaseParam(productDetails: product); |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP-ios] 调用 buyConsumable 参数: productId=${product.id} autoConsume=true'); |
|
|
ok = await _inAppPurchase.buyConsumable( |
|
|
ok = await _inAppPurchase.buyConsumable( |
|
|
purchaseParam: purchaseParam, |
|
|
purchaseParam: purchaseParam, |
|
|
autoConsume: true, |
|
|
autoConsume: true, |
|
|
); |
|
|
); |
|
|
|
|
|
print('[IAP-ios] buyConsumable 返回: $ok'); |
|
|
} |
|
|
} |
|
|
if (!ok) { |
|
|
if (!ok) { |
|
|
onResult?.call(false, '无法发起内购'); |
|
|
onResult?.call(false, '无法发起内购'); |
|
|
|
|
|
print('[IAP] 发起内购失败:平台=${_platformString()} productId=${product.id}'); |
|
|
} else { |
|
|
} else { |
|
|
onResult?.call(true, '已发起内购,请完成支付'); |
|
|
onResult?.call(true, '已发起内购,请完成支付'); |
|
|
|
|
|
print('[IAP] 已发起内购:等待购买流回调(可能受旧交易阻塞或商店限制)'); |
|
|
} |
|
|
} |
|
|
return ok; |
|
|
return ok; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
onResult?.call(false, '发起内购失败: $e'); |
|
|
onResult?.call(false, '发起内购失败: $e'); |
|
|
|
|
|
print('[IAP] 发起内购异常: $e'); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -232,41 +251,59 @@ class PayService extends GetxService { |
|
|
_iapOnResult?.call(false, details.error?.message ?? '支付错误'); |
|
|
_iapOnResult?.call(false, details.error?.message ?? '支付错误'); |
|
|
break; |
|
|
break; |
|
|
case PurchaseStatus.purchased: |
|
|
case PurchaseStatus.purchased: |
|
|
|
|
|
final txId = details.purchaseID ?? ''; |
|
|
|
|
|
final ok = await _verifyIapWithServer(details); |
|
|
|
|
|
if (!ok) { |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP] 验证失败 purchased txId=${details.purchaseID} product=${details.productID}'); |
|
|
|
|
|
_iapOnResult?.call(false, details.error?.message ?? '支付错误'); |
|
|
|
|
|
} |
|
|
|
|
|
if (ok) { |
|
|
|
|
|
try { |
|
|
|
|
|
await _inAppPurchase.completePurchase(details); |
|
|
|
|
|
print('[IAP] 完成交易 purchased txId=${details.purchaseID}'); |
|
|
|
|
|
_iapOnResult?.call(true, details.error?.message ?? '支付成功'); |
|
|
|
|
|
} catch (e, st) { |
|
|
|
|
|
print('[IAP] 完成交易异常 purchased txId=${details.purchaseID} err=$e'); |
|
|
|
|
|
print('[IAP] 完成交易异常堆栈 purchased $st'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
break; |
|
|
case PurchaseStatus.restored: |
|
|
case PurchaseStatus.restored: |
|
|
// 完成购买并进行服务端收据验证 |
|
|
final txId = details.purchaseID ?? ''; |
|
|
try { |
|
|
final ok = await _verifyIapWithServer(details); |
|
|
await _verifyIapWithServer(details); |
|
|
if (!ok) { |
|
|
} catch (e) { |
|
|
print( |
|
|
_iapOnResult?.call(false, '收据验证失败: $e'); |
|
|
'[IAP] 验证失败 restored(silent) txId=${details.purchaseID} product=${details.productID}'); |
|
|
|
|
|
_iapOnResult?.call(false, details.error?.message ?? '支付错误'); |
|
|
|
|
|
} |
|
|
|
|
|
if (ok) { |
|
|
|
|
|
try { |
|
|
|
|
|
await _inAppPurchase.completePurchase(details); |
|
|
|
|
|
_iapOnResult?.call(true, details.error?.message ?? '支付成功'); |
|
|
|
|
|
print('[IAP] 完成交易 restored(silent) txId=${details.purchaseID}'); |
|
|
|
|
|
} catch (e, st) { |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP] 完成交易异常 restored(silent) txId=${details.purchaseID} err=$e'); |
|
|
|
|
|
print('[IAP] 完成交易异常堆栈 restored(silent) $st'); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
break; |
|
|
break; |
|
|
case PurchaseStatus.canceled: |
|
|
case PurchaseStatus.canceled: |
|
|
_iapOnResult?.call(false, '已取消支付'); |
|
|
_iapOnResult?.call(false, '已取消支付'); |
|
|
|
|
|
print('[IAP] 用户取消支付 product=${details.productID}'); |
|
|
break; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (details.pendingCompletePurchase) { |
|
|
|
|
|
try { |
|
|
|
|
|
await _inAppPurchase.completePurchase(details); |
|
|
|
|
|
} catch (_) {} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
Future<void> _verifyIapWithServer(PurchaseDetails details) async { |
|
|
Future<bool> _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 { |
|
|
try { |
|
|
final resp = await Api.payiapverify(params); |
|
|
final params = await buildIapAskParams(); |
|
|
// 兼容多种返回结构,优先拿到用户信息 |
|
|
final resp = await Api.payverifyorder(params); |
|
|
Map<String, dynamic>? user; |
|
|
Map<String, dynamic>? user; |
|
|
|
|
|
Map<String, dynamic>? order; |
|
|
if (resp is Map) { |
|
|
if (resp is Map) { |
|
|
if (resp['user'] is Map<String, dynamic>) { |
|
|
if (resp['user'] is Map<String, dynamic>) { |
|
|
user = resp['user'] as Map<String, dynamic>; |
|
|
user = resp['user'] as Map<String, dynamic>; |
|
|
@ -274,15 +311,32 @@ class PayService extends GetxService { |
|
|
(resp['data']['user'] is Map<String, dynamic>)) { |
|
|
(resp['data']['user'] is Map<String, dynamic>)) { |
|
|
user = resp['data']['user'] as Map<String, dynamic>; |
|
|
user = resp['data']['user'] as Map<String, dynamic>; |
|
|
} |
|
|
} |
|
|
|
|
|
if (resp['order'] is Map<String, dynamic>) { |
|
|
|
|
|
order = resp['order'] as Map<String, dynamic>; |
|
|
|
|
|
} else if (resp['data'] is Map && |
|
|
|
|
|
(resp['data']['order'] is Map<String, dynamic>)) { |
|
|
|
|
|
order = resp['data']['order'] as Map<String, dynamic>; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
if (user != null) { |
|
|
final int? status = order != null |
|
|
|
|
|
? (order['status'] is int |
|
|
|
|
|
? order['status'] as int |
|
|
|
|
|
: int.tryParse('${order['status']}')) |
|
|
|
|
|
: null; |
|
|
|
|
|
final bool ok = status == 1 || user != null; |
|
|
|
|
|
if (ok && user != null) { |
|
|
User.instance.updateUserInfo(user); |
|
|
User.instance.updateUserInfo(user); |
|
|
_iapOnResult?.call(true, '支付成功'); |
|
|
|
|
|
} else { |
|
|
|
|
|
_iapOnResult?.call(true, '支付成功,额度将稍后刷新'); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
if (!ok) { |
|
|
|
|
|
final code = (resp is Map) ? resp['code'] : null; |
|
|
|
|
|
final msg = (resp is Map) ? (resp['message'] ?? resp['msg']) : null; |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP] 验证失败详情 status=$status code=$code msg=$msg order_id=${params['order_id']}'); |
|
|
|
|
|
} |
|
|
|
|
|
return ok; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
_iapOnResult?.call(false, '服务器验证异常: $e'); |
|
|
print('[IAP] 服务器验证异常: $e'); |
|
|
|
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -360,6 +414,41 @@ class PayService extends GetxService { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 解析 Apple 的真实商品ID(支持环境前缀/后缀或显式映射) |
|
|
|
|
|
/// 优先级:显式映射(APP_IOS_IAP_ID_MAP JSON) > 前缀(APP_IOS_IAP_PREFIX) > 后缀(APP_IOS_IAP_SUFFIX) > 原始ID |
|
|
|
|
|
/// 示例: |
|
|
|
|
|
/// - APP_IOS_IAP_PREFIX=com.voitrans. |
|
|
|
|
|
/// - APP_IOS_IAP_ID_MAP={"ai_001":"com.voitrans.ai001","ai_002":"com.voitrans.ai002"} |
|
|
|
|
|
String resolveAppleProductId(String backendId) { |
|
|
|
|
|
final raw = backendId.trim(); |
|
|
|
|
|
Map<String, dynamic>? mapped; |
|
|
|
|
|
try { |
|
|
|
|
|
final jsonStr = dotenv.env['APP_IOS_IAP_ID_MAP']; |
|
|
|
|
|
if (jsonStr != null && jsonStr.isNotEmpty) { |
|
|
|
|
|
mapped = json.decode(jsonStr) as Map<String, dynamic>; |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
print('[IAP-ios] 解析 APP_IOS_IAP_ID_MAP 失败: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
if (mapped != null) { |
|
|
|
|
|
final m = mapped[raw]; |
|
|
|
|
|
if (m is String && m.trim().isNotEmpty) { |
|
|
|
|
|
final resolved = m.trim(); |
|
|
|
|
|
print('[IAP-ios] 使用映射ID: $raw -> $resolved'); |
|
|
|
|
|
return resolved; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
final prefix = dotenv.env['APP_IOS_IAP_PREFIX'] ?? ''; |
|
|
|
|
|
final suffix = dotenv.env['APP_IOS_IAP_SUFFIX'] ?? ''; |
|
|
|
|
|
final resolved = '${prefix}${raw}${suffix}'; |
|
|
|
|
|
if (resolved != raw) { |
|
|
|
|
|
print('[IAP-ios] 使用前后缀ID: $raw -> $resolved'); |
|
|
|
|
|
} else { |
|
|
|
|
|
print('[IAP-ios] 未配置映射/前缀,使用原始ID: $raw'); |
|
|
|
|
|
} |
|
|
|
|
|
return resolved; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/// 发起 Apple 内购 |
|
|
/// 发起 Apple 内购 |
|
|
Future<bool> processApplePayment({ |
|
|
Future<bool> processApplePayment({ |
|
|
required String productId, |
|
|
required String productId, |
|
|
@ -369,9 +458,17 @@ class PayService extends GetxService { |
|
|
onResult?.call(false, '当前平台不支持 Apple 内购'); |
|
|
onResult?.call(false, '当前平台不支持 Apple 内购'); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
// 重新初始化内购,避免早期可用性判断为 false |
|
|
if (!iapAvailable.value) { |
|
|
await _initIAP(); |
|
|
await _initIAP(); |
|
|
return _processIapPayment(productId: productId, onResult: onResult); |
|
|
if (!iapAvailable.value) { |
|
|
|
|
|
onResult?.call(false, '内购不可用'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
// 解析为 App Store 的真实商品ID |
|
|
|
|
|
final iosId = resolveAppleProductId(productId); |
|
|
|
|
|
print('[IAP-ios] 准备使用商品ID购买: backend="$productId" -> store="$iosId"'); |
|
|
|
|
|
return _processIapPayment(productId: iosId, onResult: onResult); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 发起 Google Play 内购 |
|
|
/// 发起 Google Play 内购 |
|
|
@ -383,8 +480,13 @@ class PayService extends GetxService { |
|
|
onResult?.call(false, '当前平台不支持 Google 内购'); |
|
|
onResult?.call(false, '当前平台不支持 Google 内购'); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
// 重新初始化内购,避免早期可用性判断为 false |
|
|
if (!iapAvailable.value) { |
|
|
await _initIAP(); |
|
|
await _initIAP(); |
|
|
|
|
|
if (!iapAvailable.value) { |
|
|
|
|
|
onResult?.call(false, '内购不可用'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
return _processIapPayment(productId: productId, onResult: onResult); |
|
|
return _processIapPayment(productId: productId, onResult: onResult); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|