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.

735 lines
28 KiB

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:voitrans/data/models/user_Info.dart';
import '../../../routes/app_routes.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';
/// goods 模块基础控制器,封装通用的商品加载与支付流程
abstract class GoodsBaseController extends GetxController {
final RxList<DBGoods> items = <DBGoods>[].obs;
final RxBool loading = false.obs;
final RxString error = ''.obs;
late final PayService _payService = Get.find<PayService>();
@override
void onInit() {
super.onInit();
_init();
}
Future<void> _init() async {
loading.value = true;
error.value = '';
try {
final list = await fetchProducts();
items.assignAll(list);
} catch (e) {
error.value = e.toString();
} finally {
loading.value = false;
}
}
/// 子类实现:返回商品列表(统一使用 DBGoods)
Future<List<DBGoods>> fetchProducts();
/// 统一入口:根据后端配置决定是否弹出支付方式选择面板
Future<void> 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 = '支付失败,请重试';
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<bool> payWechat(DBGoods item,
{PaymentResultCallback? onResult}) async {
final cents = item.price; // DBGoods.price 单位:分
final serverUrl = dotenv.env['SERVER_URL'] ?? DioManager().baseUrl;
final paymentData = await _payService.createWechatPayOrder(
productId: item.id,
productName: item.name,
productDesc: item.desc,
amount: cents,
notifyUrl: '$serverUrl/api/home/pay_wechatnotify',
);
if (paymentData == null) {
onResult?.call(false, '创建微信订单失败');
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) {
await _confirmOrderAndShow(orderId,
channel: '微信', initialMessage: msg);
} else {
_showPayResultDialog(false, msg ?? '支付失败');
}
},
);
}
/// 指定支付宝支付(直接下单并拉起支付宝)
Future<bool> payAlipay(DBGoods item,
{PaymentResultCallback? onResult}) async {
final cents = item.price; // DBGoods.price 单位:分
final orderInfo = await _payService.createAlipayOrder(
productId: item.id,
productName: item.name,
productDesc: item.desc,
amount: cents,
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify',
);
if (orderInfo == null) {
onResult?.call(false, '创建支付宝订单失败');
return false;
}
final orderId = _payService.lastOrderId; // 创建订单时已缓存
return _payService.alipay(
orderInfo: orderInfo,
onResult: (success, msg) async {
onResult?.call(success, msg);
if (success) {
await _confirmOrderAndShow(orderId,
channel: '支付宝', initialMessage: msg);
} else {
_showPayResultDialog(false, msg ?? '支付失败');
}
},
);
}
/// 指定 Apple 内购(App Store IAP)
Future<bool> payAppleIAP(DBGoods item,
{PaymentResultCallback? onResult}) async {
try {
// 先创建服务端订单,记录 order_id 以便后续回查
final orderId = await _payService.createAppleIapOrder(
productId: item.id,
productName: item.name,
productDesc: item.desc,
amount: item.price,
);
if (orderId == null || orderId.isEmpty) {
onResult?.call(false, '创建 Apple 订单失败');
return false;
}
// 使用商品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);
// 仅在支付成功阶段,携带校验参数主动查询订单
_askOrderWithIapVerification();
} else {
Get.snackbar('提示', msg,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(12));
}
}
},
);
} catch (e) {
onResult?.call(false, 'Apple 内购异常: $e');
return false;
}
}
/// 指定 Google Play 内购(Play Billing IAP)
Future<bool> payGoogleIAP(DBGoods item,
{PaymentResultCallback? onResult}) async {
try {
// 先创建服务端订单,记录 order_id 以便后续回查
final orderId = await _payService.createGoogleIapOrder(
productId: item.id,
productName: item.name,
productDesc: item.desc,
amount: item.price,
);
if (orderId == null || orderId.isEmpty) {
onResult?.call(false, '创建 Google 订单失败');
return false;
}
// 使用商品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);
// 仅在支付成功阶段,携带校验参数主动查询订单
_askOrderWithIapVerification();
} else {
Get.snackbar('提示', msg,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(12));
}
}
},
);
} catch (e) {
onResult?.call(false, 'Google 内购异常: $e');
return false;
}
}
/// 调试:打印当前页面所有商品的 Google IAP 查询结果
Future<void> 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<void> _confirmOrderAndShow(String? orderId,
{required String channel, String? initialMessage}) async {
// 若无法获取订单号,直接弹成功提示但提醒稍后刷新
if (orderId == null || orderId.isEmpty) {
_showPayResultDialog(true, '${channel}支付成功,订单处理中,请稍后查看。');
return;
}
String message = initialMessage ?? '${channel}支付成功';
bool success = false;
try {
// 简单轮询回查,避免回调延迟
for (int i = 0; i < 5; i++) {
final resp = await Api.payaskorder({'order_id': orderId});
print("查询订单 resp: $resp");
final order = resp?['order'] as Map<String, dynamic>?;
final user = resp?['user'] as Map<String, dynamic>?;
if (order != null && user != null) {
final orderStatus = order["status"];
if (orderStatus == 1) {
User.instance.updateUserInfo(user);
success = true;
break;
}
}
await Future.delayed(const Duration(milliseconds: 1500));
}
// 若标记成功但未拿到用户信息,使用token登录接口回拉用户资料,确保额度刷新
if (success) {
try {
final response = await Api.tokenlogin();
if (response is Map && response['user'] is Map<String, dynamic>) {
User.instance
.updateUserInfo(response['user'] as Map<String, dynamic>);
}
} catch (_) {}
}
} catch (e) {
message = '查询支付结果异常: $e';
}
_showPayResultDialog(success, message);
}
/// Apple/Google 内购:无法监听平台回调时,主动查询订单并携带验证数据
Future<void> _askOrderWithIapVerification() async {
try {
final params = await _payService.buildIapAskParams();
final resp = await Api.payaskorder(params);
print('[IAP] 主动查询订单 resp: $resp');
final order = resp?['order'] as Map<String, dynamic>?;
final user = resp?['user'] as Map<String, dynamic>?;
if (order != null) {
final orderStatus = order['status'];
if (orderStatus == 1 && user != null) {
User.instance.updateUserInfo(user);
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<String, dynamic>) {
User.instance
.updateUserInfo(response['user'] as Map<String, dynamic>);
}
} 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,
);
}
/// 指定 PayPal 支付(创建订单并打开授权链接)
Future<bool> payPaypal(DBGoods item,
{PaymentResultCallback? onResult}) async {
final cents = item.price; // DBGoods.price 单位:分
final approvalUrl = await _payService.createPaypalOrder(
productId: item.id,
productName: item.name,
productDesc: item.desc,
amount: cents,
notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify',
);
if (approvalUrl == null) {
onResult?.call(false, '创建 PayPal 订单失败');
return false;
}
return _payService.paypalPay(approvalUrl: approvalUrl, onResult: onResult);
}
}
enum _PayMethod { wechat, alipay, paypal, apple, google }