You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1040 lines
33 KiB
1040 lines
33 KiB
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:in_app_purchase_android/in_app_purchase_android.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../../data/services/network/api.dart';
|
|
import 'package:tobias/tobias.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import '../../data/models/user_Info.dart';
|
|
|
|
import '../../modules/login/controllers/weixinLogin.dart';
|
|
|
|
typedef PaymentResultCallback = void Function(bool success, String message);
|
|
|
|
/// 支付服务
|
|
class PayService extends GetxService {
|
|
late FlWX _flwx;
|
|
late Tobias _tobias; // 添加 Tobias 实例
|
|
String? _lastOrderId; // 最近一次创建的订单号
|
|
// 内购
|
|
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
|
|
StreamSubscription<List<PurchaseDetails>>? _iapSubscription;
|
|
final RxBool iapAvailable = false.obs;
|
|
PaymentResultCallback? _iapOnResult;
|
|
// 最近一次 IAP 购买上下文,用于主动查询时携带验证参数
|
|
PurchaseDetails? _lastIapPurchase;
|
|
ProductDetails? _lastIapProduct;
|
|
|
|
String? get lastOrderId => _lastOrderId;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
//微信
|
|
FlWX().register(
|
|
appSecret: '9ceb39df312b3ba95a229b2aa0d15496',
|
|
appId: 'wx4b5ec8957ad5e425',
|
|
partnerId: '1719983673',
|
|
universalLink: 'https://voitrans.net/app/',
|
|
params: FlWXCallbackParams(onHttp: (url) async {
|
|
return '';
|
|
}));
|
|
_flwx = FlWX();
|
|
|
|
// 初始化支付宝SDK
|
|
_tobias = Tobias(); // 初始化 Tobias 实例
|
|
_initAlipay();
|
|
_initIAP();
|
|
}
|
|
|
|
/// 初始化支付宝SDK
|
|
Future<void> _initAlipay() async {
|
|
try {
|
|
// 检查支付宝是否安装 - 修复用法
|
|
final isInstalled = await _tobias.isAliPayInstalled;
|
|
print('支付宝是否安装: $isInstalled');
|
|
} catch (e) {
|
|
print('初始化支付宝SDK失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 初始化内购(Apple/Google)
|
|
Future<void> _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<void> debugQueryProducts(Set<String> 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<ProductDetails?> _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<bool> _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 {
|
|
// 记录最近一次用于购买的商品详情,便于后续构建查询参数
|
|
_lastIapProduct = product;
|
|
final isAndroid =
|
|
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
|
bool ok;
|
|
if (isAndroid && product is GooglePlayProductDetails) {
|
|
// 通过环境变量决定订阅或一次性商品路径,避免访问不兼容的属性
|
|
final isSubscription =
|
|
_getEnvInt('APP_IAP_GOOGLE_IS_SUBSCRIPTION', defaultValue: 0) == 1;
|
|
if (isSubscription) {
|
|
print('[IAP-android] 订阅商品(ENV),使用 buyNonConsumable');
|
|
final gpParam = GooglePlayPurchaseParam(productDetails: product);
|
|
ok = await _inAppPurchase.buyNonConsumable(purchaseParam: gpParam);
|
|
} else {
|
|
// 普通一次性/消耗型商品,使用 buyConsumable
|
|
final purchaseParam = PurchaseParam(productDetails: product);
|
|
ok = await _inAppPurchase.buyConsumable(
|
|
purchaseParam: purchaseParam,
|
|
autoConsume: true,
|
|
);
|
|
}
|
|
} else {
|
|
// iOS 或非订阅商品,按消耗型处理(如需支持 iOS 订阅,可通过 ENV 切换调用 buyNonConsumable)
|
|
final purchaseParam = PurchaseParam(productDetails: product);
|
|
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<void> _handlePurchaseUpdates(List<PurchaseDetails> detailsList) async {
|
|
for (final details in detailsList) {
|
|
// 记录最近一次 IAP 购买详情
|
|
_lastIapPurchase = details;
|
|
print(
|
|
'[IAP] 更新: status=${details.status} product=${details.productID} purchaseID=${details.purchaseID} error=${details.error}');
|
|
switch (details.status) {
|
|
case PurchaseStatus.pending:
|
|
_iapOnResult?.call(true, '支付处理中…');
|
|
break;
|
|
case PurchaseStatus.error:
|
|
print(
|
|
'[IAP] 错误: code=${details.error?.code} message=${details.error?.message}');
|
|
_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<void> _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<String, dynamic>? user;
|
|
if (resp is Map) {
|
|
if (resp['user'] is Map<String, dynamic>) {
|
|
user = resp['user'] as Map<String, dynamic>;
|
|
} else if (resp['data'] is Map &&
|
|
(resp['data']['user'] is Map<String, dynamic>)) {
|
|
user = resp['data']['user'] as Map<String, dynamic>;
|
|
}
|
|
}
|
|
if (user != null) {
|
|
User.instance.updateUserInfo(user);
|
|
_iapOnResult?.call(true, '支付成功');
|
|
} else {
|
|
_iapOnResult?.call(true, '支付成功,额度将稍后刷新');
|
|
}
|
|
} catch (e) {
|
|
_iapOnResult?.call(false, '服务器验证异常: $e');
|
|
}
|
|
}
|
|
|
|
/// 构建 Apple/Google 主动查询(pay_askorder)所需的验证参数
|
|
/// 返回与后端 PayAskOrCancelReq 对应的 Map
|
|
Future<Map<String, dynamic>> buildIapAskParams() async {
|
|
final params = <String, dynamic>{};
|
|
if (_lastOrderId != null && _lastOrderId!.isNotEmpty) {
|
|
params['order_id'] = _lastOrderId;
|
|
}
|
|
|
|
final platform = _platformString();
|
|
if (platform == 'ios') {
|
|
final receiptOrData =
|
|
_lastIapPurchase?.verificationData.serverVerificationData ?? '';
|
|
final txId = _lastIapPurchase?.purchaseID ?? '';
|
|
// 是否按交易ID校验,默认 false,可通过环境变量开启
|
|
final useTxIdEnv = _getEnvBool('APP_IAP_USE_TXID', defaultValue: false);
|
|
// 沙箱标识,默认在非发布模式下认为是沙箱;支持环境变量覆盖
|
|
final sandbox =
|
|
_getEnvBool('APP_IAP_SANDBOX', defaultValue: !kReleaseMode);
|
|
params['apple_receipt_data'] = useTxIdEnv ? txId : receiptOrData;
|
|
params['apple_use_transaction_id'] = useTxIdEnv;
|
|
params['apple_sandbox'] = sandbox;
|
|
} else if (platform == 'android') {
|
|
final packageInfo = await PackageInfo.fromPlatform();
|
|
final pkg = packageInfo.packageName;
|
|
final token =
|
|
_lastIapPurchase?.verificationData.serverVerificationData ?? '';
|
|
final productId =
|
|
_lastIapPurchase?.productID ?? _lastIapProduct?.id ?? '';
|
|
// 订阅标识,默认 false;如需严格区分,可在商品元数据或环境变量中配置
|
|
final isSub =
|
|
_getEnvBool('APP_IAP_GOOGLE_IS_SUBSCRIPTION', defaultValue: false);
|
|
params['google_package_name'] = pkg;
|
|
params['google_product_id'] = productId;
|
|
params['google_purchase_token'] = token;
|
|
params['google_is_subscription'] = isSub;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
bool _getEnvBool(String key, {required bool defaultValue}) {
|
|
try {
|
|
final v = dotenv.env[key];
|
|
if (v != null && v.isNotEmpty) {
|
|
final low = v.toLowerCase();
|
|
return low == 'true' || low == '1' || low == 'yes';
|
|
}
|
|
} catch (_) {}
|
|
return defaultValue;
|
|
}
|
|
|
|
int _getEnvInt(String key, {required int defaultValue}) {
|
|
try {
|
|
final v = dotenv.env[key];
|
|
if (v != null && v.isNotEmpty) {
|
|
return int.tryParse(v.trim()) ?? defaultValue;
|
|
}
|
|
} catch (_) {}
|
|
return defaultValue;
|
|
}
|
|
|
|
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<bool> 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<bool> 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
|
|
/// 返回支付状态:success, failed, pending
|
|
Future<String?> queryPaymentStatus(String orderId) async {
|
|
try {
|
|
// TODO: 调用后端API查询支付状态
|
|
// final response = await Api.queryPaymentStatus({'orderId': orderId});
|
|
// return response['status'] as String?;
|
|
|
|
// 临时返回,实际使用时需要删除
|
|
return 'pending';
|
|
} catch (e) {
|
|
print('查询支付状态失败: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 微信支付
|
|
///
|
|
/// [paymentData] 支付参数,包含以下字段:
|
|
/// - appId: 应用ID
|
|
/// - partnerId: 商户号
|
|
/// - prepayId: 预支付交易会话ID
|
|
/// - packageValue: 扩展字段,固定值"Sign=WXPay"
|
|
/// - nonceStr: 随机字符串
|
|
/// - timestamp: 时间戳
|
|
/// - sign: 签名
|
|
///
|
|
/// [onResult] 支付结果回调
|
|
Future<bool> wechatPay({
|
|
required Map<String, dynamic> paymentData,
|
|
PaymentResultCallback? onResult,
|
|
}) async {
|
|
try {
|
|
// 检查微信是否安装
|
|
if (!await _flwx.isInstalled) {
|
|
onResult?.call(false, '微信未安装');
|
|
return false;
|
|
}
|
|
|
|
// 将支付数据转换为Payment对象
|
|
final payment = paymentData.toPayment();
|
|
if (payment == null) {
|
|
onResult?.call(false, '支付参数格式错误');
|
|
return false;
|
|
}
|
|
|
|
// 调用微信支付
|
|
final result = await _flwx.pay(
|
|
payment,
|
|
onSuccess: () {
|
|
onResult?.call(true, '支付成功');
|
|
},
|
|
onResponse: (response) {
|
|
if (response.errCode == 0) {
|
|
onResult?.call(true, '支付成功');
|
|
} else if (response.errCode == -2) {
|
|
onResult?.call(false, '用户取消支付');
|
|
} else {
|
|
onResult?.call(false, '支付失败: ${response.errStr}');
|
|
}
|
|
},
|
|
);
|
|
|
|
return result;
|
|
} catch (e) {
|
|
onResult?.call(false, '支付异常: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 创建微信支付订单
|
|
///
|
|
/// [productId] 商品ID
|
|
/// [productName] 商品名称
|
|
/// [productDesc] 商品描述
|
|
/// [amount] 支付金额(分)
|
|
/// [paymentMethod] 支付方式(1=微信支付)
|
|
/// [clientIp] 客户端IP(可选)
|
|
/// [notifyUrl] 通知URL(可选)
|
|
/// [extraData] 扩展数据(可选)
|
|
///
|
|
/// 返回支付参数,用于调用 [wechatPay] 方法
|
|
Future<Map<String, dynamic>?> createWechatPayOrder({
|
|
required String productId,
|
|
required String productName,
|
|
String? productDesc,
|
|
required int amount,
|
|
int paymentMethod = 4, // 1表示微信支付
|
|
String? clientIp,
|
|
String? notifyUrl,
|
|
String? extraData,
|
|
}) async {
|
|
try {
|
|
// 调用后端API创建支付订单
|
|
final response = await Api.paycreateorder({
|
|
'product_id': productId,
|
|
'product_name': productName,
|
|
'product_desc': productDesc ?? '',
|
|
'amount': amount,
|
|
'payment_method': 4,
|
|
'client_ip': clientIp ?? '',
|
|
'notify_url': notifyUrl ?? '',
|
|
'extra_data': extraData ?? '',
|
|
});
|
|
print('创建支付订单: $response');
|
|
// 检查响应是否成功
|
|
if (response != null) {
|
|
// 尝试提取订单号,兼容多种返回字段
|
|
String? orderId;
|
|
final candidates = [
|
|
'order_id',
|
|
'orderId',
|
|
'out_trade_no',
|
|
'outTradeNo',
|
|
];
|
|
for (final key in candidates) {
|
|
final v = response[key];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
if (orderId == null && response['data'] is Map<String, dynamic>) {
|
|
final data = response['data'] as Map<String, dynamic>;
|
|
for (final key in candidates) {
|
|
final v = data[key];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_lastOrderId = orderId;
|
|
|
|
final pay_params = response["pay_params"] as Map<String, dynamic>;
|
|
// 返回微信支付所需的参数
|
|
final result = {
|
|
'appId': pay_params['appid'],
|
|
'partnerId': pay_params['partnerid'],
|
|
'prepayId': pay_params['prepayid'],
|
|
'packageValue': pay_params['package'] ?? 'Sign=WXPay',
|
|
'nonceStr': pay_params['noncestr'],
|
|
'timestamp': pay_params['timestamp'],
|
|
'sign': pay_params['sign'],
|
|
};
|
|
if (orderId != null) {
|
|
result['orderId'] = orderId;
|
|
}
|
|
return result;
|
|
} else {
|
|
print('创建支付订单失败: ${response?['message'] ?? '未知错误'}');
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
print('创建支付订单异常: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<bool> processWechatPayment({
|
|
required String productId,
|
|
required int amount,
|
|
required String description,
|
|
String? userId,
|
|
PaymentResultCallback? onResult,
|
|
}) async {
|
|
try {
|
|
// 1. 创建支付订单
|
|
final paymentData = await createWechatPayOrder(
|
|
productId: productId,
|
|
productName: '会员充值',
|
|
productDesc: '1个月会员服务',
|
|
amount: 1, // 29.99元,单位:分
|
|
paymentMethod: 4, // 微信支付
|
|
notifyUrl: 'https://api.ideapsound.com/api/home/pay_wechatnotify',
|
|
);
|
|
|
|
if (paymentData == null) {
|
|
onResult?.call(false, '创建支付订单失败');
|
|
return false;
|
|
}
|
|
|
|
// 2. 调用微信支付
|
|
return await wechatPay(
|
|
paymentData: paymentData,
|
|
onResult: onResult,
|
|
);
|
|
} catch (e) {
|
|
onResult?.call(false, '支付流程异常: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 完整的支付宝支付流程
|
|
///
|
|
/// [productId] 产品ID
|
|
/// [amount] 支付金额(分)
|
|
/// [description] 商品描述
|
|
/// [onResult] 支付结果回调
|
|
Future<bool> processAlipayPayment({
|
|
required String productId,
|
|
required int amount,
|
|
required String description,
|
|
PaymentResultCallback? onResult,
|
|
}) async {
|
|
try {
|
|
// 检查支付宝是否安装
|
|
final isInstalled = await _tobias.isAliPayInstalled;
|
|
if (!isInstalled) {
|
|
onResult?.call(false, '支付宝未安装');
|
|
return false;
|
|
}
|
|
|
|
// 调用现有的 createAlipayOrder 方法创建订单
|
|
final orderInfo = await createAlipayOrder(
|
|
productId: productId,
|
|
productName: description,
|
|
productDesc: description,
|
|
amount: amount,
|
|
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify',
|
|
);
|
|
|
|
if (orderInfo == null) {
|
|
onResult?.call(false, '创建支付订单失败');
|
|
return false;
|
|
}
|
|
print('支付宝支付订单: $orderInfo');
|
|
// 调用支付宝支付
|
|
final result = await _tobias.pay(orderInfo);
|
|
|
|
print('支付宝支付结果: $result');
|
|
// 处理支付结果
|
|
if (result != null) {
|
|
final resultStatus = result['resultStatus'] as String?;
|
|
final memo = result['memo'] as String?;
|
|
|
|
if (resultStatus == '9000') {
|
|
onResult?.call(true, '支付成功');
|
|
return true;
|
|
} else {
|
|
onResult?.call(false, memo ?? '支付失败');
|
|
return false;
|
|
}
|
|
} else {
|
|
onResult?.call(false, '支付结果为空');
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
onResult?.call(false, '支付异常: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 支付宝支付
|
|
///
|
|
/// [orderInfo] 支付宝订单信息字符串
|
|
/// [onResult] 支付结果回调
|
|
Future<bool> alipay({
|
|
required String orderInfo,
|
|
PaymentResultCallback? onResult,
|
|
}) async {
|
|
try {
|
|
// 检查支付宝是否安装 - 使用实例方法
|
|
final isInstalled = await _tobias.isAliPayInstalled;
|
|
if (!isInstalled) {
|
|
onResult?.call(false, '支付宝未安装');
|
|
return false;
|
|
}
|
|
|
|
// 调用支付宝支付 - 使用实例方法
|
|
final result = await _tobias.pay(orderInfo);
|
|
|
|
if (result != null) {
|
|
// 解析支付结果
|
|
final resultStatus = result['resultStatus'] as String?;
|
|
final memo = result['memo'] as String?;
|
|
|
|
if (resultStatus == '9000') {
|
|
// 支付成功
|
|
onResult?.call(true, '支付成功');
|
|
return true;
|
|
} else {
|
|
// 支付失败或取消
|
|
onResult?.call(false, memo ?? '支付失败');
|
|
return false;
|
|
}
|
|
} else {
|
|
onResult?.call(false, '支付结果为空');
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
onResult?.call(false, '支付异常: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 创建支付宝支付订单
|
|
///
|
|
/// [productId] 商品ID
|
|
/// [productName] 商品名称
|
|
/// [productDesc] 商品描述
|
|
/// [amount] 支付金额(分)
|
|
/// [paymentMethod] 支付方式(2=支付宝)
|
|
/// [clientIp] 客户端IP(可选)
|
|
/// [notifyUrl] 通知URL(可选)
|
|
/// [extraData] 扩展数据(可选)
|
|
///
|
|
/// 返回支付宝订单信息字符串,用于调用 [alipay] 方法
|
|
Future<String?> createAlipayOrder({
|
|
required String productId,
|
|
required String productName,
|
|
String? productDesc,
|
|
required int amount,
|
|
String? clientIp,
|
|
String? notifyUrl,
|
|
String? extraData,
|
|
}) async {
|
|
try {
|
|
// 调用后端API创建支付订单
|
|
final response = await Api.paycreateorder({
|
|
'product_id': productId,
|
|
'product_name': productName,
|
|
'product_desc': productDesc ?? '',
|
|
'amount': amount,
|
|
'payment_method': 3,
|
|
'client_ip': clientIp ?? '',
|
|
'notify_url': notifyUrl ?? '',
|
|
'extra_data': extraData ?? '',
|
|
});
|
|
|
|
print('创建支付宝订单: $response');
|
|
|
|
// 检查响应是否成功
|
|
if (response != null && response['alipay_pay_params'] != null) {
|
|
// 解析订单号并缓存
|
|
String? orderId;
|
|
final candidates = [
|
|
'order_id',
|
|
'orderId',
|
|
'out_trade_no',
|
|
'outTradeNo',
|
|
];
|
|
for (final key in candidates) {
|
|
final v = response[key];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
if (orderId == null && response['data'] is Map<String, dynamic>) {
|
|
final data = response['data'] as Map<String, dynamic>;
|
|
for (final key in candidates) {
|
|
final v = data[key];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_lastOrderId = orderId;
|
|
// 返回支付宝订单信息字符串
|
|
return response['alipay_pay_params'] as String?;
|
|
} else {
|
|
print('创建支付宝订单失败: ${response?['message'] ?? '未知错误'}');
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
print('创建支付宝订单异常: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 创建 PayPal 支付订单
|
|
/// 返回需要打开的支付授权链接(approval_url)或支付页面URL
|
|
Future<String?> createPaypalOrder({
|
|
required String productId,
|
|
required String productName,
|
|
String? productDesc,
|
|
required int amount,
|
|
String? clientIp,
|
|
String? notifyUrl,
|
|
String? extraData,
|
|
}) async {
|
|
try {
|
|
final response = await Api.paycreateorder({
|
|
'product_id': productId,
|
|
'product_name': productName,
|
|
'product_desc': productDesc ?? '',
|
|
'amount': amount,
|
|
'payment_method': 5,
|
|
'client_ip': clientIp ?? '',
|
|
'notify_url': notifyUrl ?? '',
|
|
'extra_data': extraData ?? '',
|
|
});
|
|
|
|
if (response != null) {
|
|
// 1) 提取并缓存平台订单号(用于回查)
|
|
String? appOrderId;
|
|
const candidates = [
|
|
'order_id',
|
|
'orderId',
|
|
'out_trade_no',
|
|
'outTradeNo',
|
|
];
|
|
for (final key in candidates) {
|
|
final v = response[key];
|
|
if (v is String && v.isNotEmpty) {
|
|
appOrderId = v;
|
|
break;
|
|
}
|
|
}
|
|
final data = response['data'];
|
|
if (appOrderId == null && data is Map<String, dynamic>) {
|
|
for (final key in candidates) {
|
|
final v = data[key];
|
|
if (v is String && v.isNotEmpty) {
|
|
appOrderId = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_lastOrderId = appOrderId; // 记录APP侧订单号(如 APP2025...),用于 pay_askorder
|
|
|
|
// 2) 优先解析 paypal_pay_params 字段(后端返回JSON字符串,含 links)
|
|
String? rawParams;
|
|
final topParams = response['paypal_pay_params'];
|
|
final dataParams =
|
|
(data is Map<String, dynamic>) ? data['paypal_pay_params'] : null;
|
|
if (topParams is String && topParams.isNotEmpty) {
|
|
rawParams = topParams;
|
|
} else if (dataParams is String && dataParams.isNotEmpty) {
|
|
rawParams = dataParams;
|
|
}
|
|
|
|
if (rawParams != null) {
|
|
try {
|
|
final parsed = jsonDecode(rawParams);
|
|
if (parsed is Map<String, dynamic>) {
|
|
final links = parsed['links'];
|
|
if (links is List) {
|
|
for (final item in links) {
|
|
if (item is Map) {
|
|
final rel = item['rel']?.toString();
|
|
final hrefRaw = item['href']?.toString();
|
|
if (rel == 'approve' &&
|
|
hrefRaw != null &&
|
|
hrefRaw.isNotEmpty) {
|
|
// 清理日志中可能出现的反引号等噪音字符
|
|
final href = hrefRaw.replaceAll('`', '').trim();
|
|
return href;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('解析 paypal_pay_params 失败: $e');
|
|
}
|
|
}
|
|
|
|
// 3) 回退:尝试从常见字段直接取URL
|
|
final urlKeys = [
|
|
'paypal_pay_url',
|
|
'approval_url',
|
|
'paypalApprovalUrl',
|
|
'pay_url',
|
|
'payment_url',
|
|
'url',
|
|
];
|
|
for (final key in urlKeys) {
|
|
final url = response[key];
|
|
if (url is String && url.isNotEmpty) {
|
|
return url;
|
|
}
|
|
}
|
|
if (data is Map<String, dynamic>) {
|
|
for (final key in urlKeys) {
|
|
final url = data[key];
|
|
if (url is String && url.isNotEmpty) {
|
|
return url;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
print('创建 PayPal 订单失败或未返回有效URL: $response');
|
|
return null;
|
|
} catch (e) {
|
|
print('创建 PayPal 订单异常: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 拉起 PayPal 支付(打开授权链接)。
|
|
/// 注意:该方法仅负责跳转至 PayPal 页面,结果需要后端回调通知。
|
|
Future<bool> paypalPay({
|
|
required String approvalUrl,
|
|
PaymentResultCallback? onResult,
|
|
}) async {
|
|
try {
|
|
final uri = Uri.parse(approvalUrl);
|
|
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
if (ok) {
|
|
onResult?.call(true, '已打开 PayPal,请完成支付');
|
|
} else {
|
|
onResult?.call(false, '无法打开 PayPal 支付页面');
|
|
}
|
|
return ok;
|
|
} catch (e) {
|
|
onResult?.call(false, '打开 PayPal 异常: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 一体化的 PayPal 支付流程:创建订单 -> 打开授权链接
|
|
Future<bool> processPaypalPayment({
|
|
required String productId,
|
|
required int amount,
|
|
required String description,
|
|
PaymentResultCallback? onResult,
|
|
}) async {
|
|
try {
|
|
final approvalUrl = await createPaypalOrder(
|
|
productId: productId,
|
|
productName: description,
|
|
productDesc: description,
|
|
amount: amount,
|
|
notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify',
|
|
);
|
|
|
|
if (approvalUrl == null) {
|
|
onResult?.call(false, '创建 PayPal 订单失败');
|
|
return false;
|
|
}
|
|
|
|
return await paypalPay(approvalUrl: approvalUrl, onResult: onResult);
|
|
} catch (e) {
|
|
onResult?.call(false, 'PayPal 支付流程异常: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 创建 Apple 内购订单,仅用于在服务端生成并记录订单号,后续回查使用
|
|
/// 返回服务端生成的 `order_id`,若失败则返回 null
|
|
Future<String?> createAppleIapOrder({
|
|
required String productId,
|
|
required String productName,
|
|
String? productDesc,
|
|
required int amount,
|
|
}) async {
|
|
try {
|
|
final resp = await Api.paycreateorder({
|
|
'product_id': productId,
|
|
'product_name': productName,
|
|
'product_desc': productDesc ?? '',
|
|
'amount': amount,
|
|
'payment_method': 6,
|
|
});
|
|
if (resp != null) {
|
|
String? orderId;
|
|
const candidates = [
|
|
'order_id',
|
|
'orderId',
|
|
'out_trade_no',
|
|
'outTradeNo'
|
|
];
|
|
for (final k in candidates) {
|
|
final v = resp[k];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
if (orderId == null && resp['data'] is Map<String, dynamic>) {
|
|
final data = resp['data'] as Map<String, dynamic>;
|
|
for (final k in candidates) {
|
|
final v = data[k];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_lastOrderId = orderId;
|
|
return orderId;
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
print('创建 Apple IAP 订单异常: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 创建 Google 内购订单,仅用于在服务端生成并记录订单号,后续回查使用
|
|
/// 返回服务端生成的 `order_id`,若失败则返回 null
|
|
Future<String?> createGoogleIapOrder({
|
|
required String productId,
|
|
required String productName,
|
|
String? productDesc,
|
|
required int amount,
|
|
}) async {
|
|
try {
|
|
final resp = await Api.paycreateorder({
|
|
'product_id': productId,
|
|
'product_name': productName,
|
|
'product_desc': productDesc ?? '',
|
|
'amount': amount,
|
|
'payment_method': 7,
|
|
});
|
|
if (resp != null) {
|
|
String? orderId;
|
|
const candidates = [
|
|
'order_id',
|
|
'orderId',
|
|
'out_trade_no',
|
|
'outTradeNo'
|
|
];
|
|
for (final k in candidates) {
|
|
final v = resp[k];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
if (orderId == null && resp['data'] is Map<String, dynamic>) {
|
|
final data = resp['data'] as Map<String, dynamic>;
|
|
for (final k in candidates) {
|
|
final v = data[k];
|
|
if (v is String && v.isNotEmpty) {
|
|
orderId = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_lastOrderId = orderId;
|
|
return orderId;
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
print('创建 Google IAP 订单异常: $e');
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|