Browse Source

上传google的购买代码测试

weicu
liwei1dao 11 months ago
parent
commit
9da7dabf63
  1. 218
      lib/data/services/pay_service.dart
  2. 149
      lib/modules/goods/controllers/goods_base_controller.dart
  3. 147
      lib/modules/goods/views/goodsai_view.dart
  4. 4
      lib/modules/settings/views/settings_view.dart
  5. 3
      pubspec.yaml

218
lib/data/services/pay_service.dart

@ -3,6 +3,9 @@ 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';
@ -22,6 +25,9 @@ class PayService extends GetxService {
StreamSubscription<List<PurchaseDetails>>? _iapSubscription;
final RxBool iapAvailable = false.obs;
PaymentResultCallback? _iapOnResult;
// 最近一次 IAP 购买上下文,用于主动查询时携带验证参数
PurchaseDetails? _lastIapPurchase;
ProductDetails? _lastIapProduct;
String? get lastOrderId => _lastOrderId;
@ -169,11 +175,35 @@ class PayService extends GetxService {
return false;
}
try {
final purchaseParam = PurchaseParam(productDetails: product);
final ok = await _inAppPurchase.buyConsumable(
purchaseParam: purchaseParam,
autoConsume: true,
);
// 记录最近一次用于购买的商品详情,便于后续构建查询参数
_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 {
@ -188,11 +218,17 @@ class PayService extends GetxService {
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:
@ -250,6 +286,66 @@ class PayService extends GetxService {
}
}
/// 构建 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) {
@ -397,7 +493,7 @@ class PayService extends GetxService {
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'payment_method': 4,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
@ -517,7 +613,6 @@ class PayService extends GetxService {
productName: description,
productDesc: description,
amount: amount,
paymentMethod: 3, // 支付宝支付
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify',
);
@ -612,7 +707,6 @@ class PayService extends GetxService {
required String productName,
String? productDesc,
required int amount,
int paymentMethod = 3, // 2表示支付宝
String? clientIp,
String? notifyUrl,
String? extraData,
@ -624,7 +718,7 @@ class PayService extends GetxService {
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'payment_method': 3,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
@ -679,7 +773,6 @@ class PayService extends GetxService {
required String productName,
String? productDesc,
required int amount,
int paymentMethod = 5, // 约定:5 表示 PayPal
String? clientIp,
String? notifyUrl,
String? extraData,
@ -690,7 +783,7 @@ class PayService extends GetxService {
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'payment_method': 5,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
@ -828,7 +921,6 @@ class PayService extends GetxService {
productName: description,
productDesc: description,
amount: amount,
paymentMethod: 5,
notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify',
);
@ -843,4 +935,106 @@ class PayService extends GetxService {
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;
}
}
}

149
lib/modules/goods/controllers/goods_base_controller.dart

@ -455,55 +455,93 @@ abstract class GoodsBaseController extends GetxController {
/// 指定 Apple 内购(App Store IAP)
Future<bool> 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);
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 {
Get.snackbar('提示', msg,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(12));
// 支付成功或额度待刷新时弹窗,其余阶段用轻提示
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 {
// 使用商品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);
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 {
Get.snackbar('提示', msg,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(12));
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 查询结果
@ -565,6 +603,37 @@ abstract class GoodsBaseController extends GetxController {
_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 =

147
lib/modules/goods/views/goodsai_view.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../../../data/models/goods.dart';
import '../../../data/models/user_Info.dart';
import '../controllers/goodsai_controller.dart';
@ -228,73 +229,83 @@ class GoodsaiView extends GetView<GoodsaiController> {
),
),
SizedBox(height: 10.h),
Wrap(
spacing: 12.w,
runSpacing: 12.h,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
children: [
_buildFeatureCard(
isDarkMode,
icon: Icons.music_note,
title: '听歌与推荐',
lines: [
'检索曲库、播放控制',
'智能推荐歌单与相似歌曲',
],
colorSeed: Colors.purple,
),
_buildFeatureCard(
isDarkMode,
icon: Icons.navigation,
title: '导航与路线',
lines: [
'路线规划与拥堵规避',
'语音播报与途径点设置',
],
colorSeed: Colors.orange,
),
_buildFeatureCard(
isDarkMode,
icon: Icons.wb_sunny,
title: '天气查询',
lines: [
'实时天气与预报',
'穿衣建议与出行提醒',
],
colorSeed: Colors.blue,
),
_buildFeatureCard(
isDarkMode,
icon: Icons.place,
title: '附近搜索',
lines: [
'餐饮/加油/停车等地点检索',
'评分、营业时间与路线直达',
],
colorSeed: Colors.green,
),
_buildFeatureCard(
isDarkMode,
icon: Icons.newspaper,
title: '新闻资讯',
lines: [
'热点快讯与专题速览',
'长文总结与要点提炼',
],
colorSeed: Colors.indigo,
),
_buildFeatureCard(
isDarkMode,
icon: Icons.call,
title: '通讯助手',
lines: [
'帮忙拨打电话与发短信',
'模板消息与日程提醒',
],
colorSeed: Colors.teal,
),
],
MasonryGridView.count(
crossAxisCount: 2,
mainAxisSpacing: 12.h,
crossAxisSpacing: 12.w,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 6,
itemBuilder: (context, index) {
switch (index) {
case 0:
return _buildFeatureCard(
isDarkMode,
icon: Icons.music_note,
title: '听歌与推荐',
lines: [
'检索曲库、播放控制',
'智能推荐歌单与相似歌曲',
],
colorSeed: Colors.purple,
);
case 1:
return _buildFeatureCard(
isDarkMode,
icon: Icons.navigation,
title: '导航与路线',
lines: [
'路线规划与拥堵规避',
'语音播报与途径点设置',
],
colorSeed: Colors.orange,
);
case 2:
return _buildFeatureCard(
isDarkMode,
icon: Icons.wb_sunny,
title: '天气查询',
lines: [
'实时天气与预报',
'穿衣建议与出行提醒',
],
colorSeed: Colors.blue,
);
case 3:
return _buildFeatureCard(
isDarkMode,
icon: Icons.place,
title: '附近搜索',
lines: [
'餐饮/加油/停车等地点检索',
'评分、营业时间与路线直达',
],
colorSeed: Colors.green,
);
case 4:
return _buildFeatureCard(
isDarkMode,
icon: Icons.newspaper,
title: '新闻资讯',
lines: [
'热点快讯与专题速览',
'长文总结与要点提炼',
],
colorSeed: Colors.indigo,
);
default:
return _buildFeatureCard(
isDarkMode,
icon: Icons.call,
title: '通讯助手',
lines: [
'帮忙拨打电话与发短信',
'模板消息与日程提醒',
],
colorSeed: Colors.teal,
);
}
},
),
SizedBox(height: 12.h),
Text(
@ -349,9 +360,7 @@ class GoodsaiView extends GetView<GoodsaiController> {
final bgColor = isDarkMode ? Colors.grey[900] : Colors.white;
final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!;
final subColor = isDarkMode ? Colors.white70 : Colors.grey[700];
final width = (Get.width - 16.w * 2 - 12.w) / 2;
return Container(
width: width,
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: bgColor,

4
lib/modules/settings/views/settings_view.dart

@ -265,7 +265,7 @@ class SettingsView extends GetView<SettingsController> {
onTap: () => Get.toNamed(Routes.goodstrade),
),
_buildCategoryItem(
icon: Icons.people_alt,
icon: Icons.file_open,
label: "Meet",
iconBg: isDarkMode
? Colors.pink[900]!.withOpacity(0.3)
@ -274,7 +274,7 @@ class SettingsView extends GetView<SettingsController> {
onTap: () => Get.toNamed(Routes.goodsmeet),
),
_buildCategoryItem(
icon: Icons.auto_awesome,
icon: Icons.smart_toy,
label: "AI",
iconBg: isDarkMode
? Colors.teal[900]!.withOpacity(0.3)

3
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
version: 1.0.18+53
version: 1.0.18+54
environment:
sdk: ">=3.3.0 <4.0.0"
@ -19,6 +19,7 @@ dependencies:
get_storage: ^2.1.1
http: ^1.3.0
flutter_screenutil: ^5.9.0
flutter_staggered_grid_view: ^0.7.0
path_provider: ^2.1.5
audio_session: ^0.1.18
flutter_markdown: ^0.7.7+1

Loading…
Cancel
Save