From 174c4fb90b429fe33f421dcd4702ddce4044bbb1 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 26 Sep 2025 11:57:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/bindings/initial_binding.dart | 1 - lib/data/services/pay_service.dart | 219 +++++++++++-- .../controllers/payment_controller.dart | 76 ++++- lib/modules/payment/views/payment_view.dart | 293 +++++++++++++----- .../agent_service/AgentServiceImpl.swift | 98 +++--- .../chat_api/SystemFunctionHandler.kt | 190 +++++++++++- .../location_service/LocationService.swift | 2 +- pubspec.yaml | 5 + 8 files changed, 726 insertions(+), 158 deletions(-) diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 2b7c71a77..ae0534031 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -92,7 +92,6 @@ class InitialBinding extends Bindings { // 悬浮窗管理器 Get.lazyPut(() => FloatingWindowManager(), fenix: true); - // 支付服务 Get.lazyPut(() => PayService(), fenix: true); diff --git a/lib/data/services/pay_service.dart b/lib/data/services/pay_service.dart index 1a398dc03..261feabb0 100644 --- a/lib/data/services/pay_service.dart +++ b/lib/data/services/pay_service.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:voitrans/data/services/network/api.dart'; +import 'package:tobias/tobias.dart'; import '../../modules/login/controllers/weixinLogin.dart'; @@ -8,6 +9,7 @@ typedef PaymentResultCallback = void Function(bool success, String message); /// 支付服务 class PayService extends GetxService { late FlWX _flwx; + late Tobias _tobias; // 添加 Tobias 实例 @override void onInit() { @@ -22,6 +24,39 @@ class PayService extends GetxService { return ''; })); _flwx = FlWX(); + + // 初始化支付宝SDK + _tobias = Tobias(); // 初始化 Tobias 实例 + _initAlipay(); + } + + /// 初始化支付宝SDK + Future _initAlipay() async { + try { + // 检查支付宝是否安装 - 修复用法 + final isInstalled = await _tobias.isAliPayInstalled; + print('支付宝是否安装: $isInstalled'); + } catch (e) { + print('初始化支付宝SDK失败: $e'); + } + } + + /// 查询支付结果 + /// + /// [orderId] 订单ID + /// 返回支付状态:success, failed, pending + Future 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; + } } /// 微信支付 @@ -136,33 +171,8 @@ class PayService extends GetxService { } } - /// 查询支付结果 - /// - /// [orderId] 订单ID - /// 返回支付状态:success, failed, pending - Future 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; - } - } - - /// 完整的支付流程 - /// - /// [orderId] 订单ID - /// [amount] 支付金额(分) - /// [description] 商品描述 - /// [userId] 用户ID - /// [onResult] 支付结果回调 Future processWechatPayment({ - required String orderId, + required String productId, required int amount, required String description, String? userId, @@ -171,7 +181,7 @@ class PayService extends GetxService { try { // 1. 创建支付订单 final paymentData = await createWechatPayOrder( - productId: 'prod_001', + productId: productId, productName: '会员充值', productDesc: '1个月会员服务', amount: 1, // 29.99元,单位:分 @@ -194,4 +204,159 @@ class PayService extends GetxService { return false; } } + + /// 完整的支付宝支付流程 + /// + /// [productId] 产品ID + /// [amount] 支付金额(分) + /// [description] 商品描述 + /// [onResult] 支付结果回调 + Future 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, + paymentMethod: 3, // 支付宝支付 + 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 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 createAlipayOrder({ + required String productId, + required String productName, + String? productDesc, + required int amount, + int paymentMethod = 2, // 2表示支付宝 + 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': paymentMethod, + 'client_ip': clientIp ?? '', + 'notify_url': notifyUrl ?? '', + 'extra_data': extraData ?? '', + }); + + print('创建支付宝订单: $response'); + + // 检查响应是否成功 + if (response != null && response['alipay_pay_params'] != null) { + // 返回支付宝订单信息字符串 + return response['alipay_pay_params'] as String?; + } else { + print('创建支付宝订单失败: ${response?['message'] ?? '未知错误'}'); + return null; + } + } catch (e) { + print('创建支付宝订单异常: $e'); + return null; + } + } } diff --git a/lib/modules/payment/controllers/payment_controller.dart b/lib/modules/payment/controllers/payment_controller.dart index f7b4178ff..886a8d7f4 100644 --- a/lib/modules/payment/controllers/payment_controller.dart +++ b/lib/modules/payment/controllers/payment_controller.dart @@ -10,26 +10,29 @@ class PaymentController extends GetxController { final RxString errorMessage = ''.obs; // 测试订单信息 - final RxString testOrderId = ''.obs; + final RxString testProductId = 'vip_101'.obs; final RxDouble testAmount = 0.01.obs; // 测试金额 1分钱 + // 支付方式选择 + final RxInt selectedPaymentMethod = 1.obs; // 1=微信支付, 2=支付宝支付 + @override void onInit() { super.onInit(); // 生成测试订单ID - testOrderId.value = 'test_${DateTime.now().millisecondsSinceEpoch}'; + // testProductId.value = 'test_${DateTime.now().millisecondsSinceEpoch}'; } /// 测试微信支付 Future testWechatPayment() async { try { isLoading.value = true; - paymentStatus.value = '正在发起支付...'; + paymentStatus.value = '正在发起微信支付...'; errorMessage.value = ''; // 调用支付服务,使用回调处理结果 final launched = await _payService.processWechatPayment( - orderId: testOrderId.value, + productId: testProductId.value, amount: (testAmount.value * 100).toInt(), // 转换为分 description: '微信支付测试', onResult: (success, message) { @@ -37,7 +40,7 @@ class PaymentController extends GetxController { isLoading.value = false; if (success) { - paymentStatus.value = '支付成功!'; + paymentStatus.value = '微信支付成功!'; errorMessage.value = ''; Get.snackbar( '支付成功', @@ -45,7 +48,7 @@ class PaymentController extends GetxController { snackPosition: SnackPosition.TOP, ); } else { - paymentStatus.value = '支付失败'; + paymentStatus.value = '微信支付失败'; errorMessage.value = message ?? '未知错误'; Get.snackbar( '支付失败', @@ -59,23 +62,78 @@ class PaymentController extends GetxController { if (!launched) { // 如果支付没有成功启动 isLoading.value = false; - paymentStatus.value = '支付启动失败'; + paymentStatus.value = '微信支付启动失败'; errorMessage.value = '无法启动微信支付'; Get.snackbar('支付启动失败', '无法启动微信支付'); } } catch (e) { isLoading.value = false; - paymentStatus.value = '支付异常'; + paymentStatus.value = '微信支付异常'; errorMessage.value = e.toString(); Get.snackbar('支付异常', e.toString()); } } + /// 测试支付宝支付 + Future testAlipayPayment() async { + try { + isLoading.value = true; + paymentStatus.value = '正在发起支付宝支付...'; + errorMessage.value = ''; + + // 调用支付宝支付服务 + final launched = await _payService.processAlipayPayment( + productId: testProductId.value, // 这里传递的是产品ID + amount: (testAmount.value * 100).toInt(), // 转换为分 + description: '支付宝支付测试', + onResult: (success, message) { + // 支付结果回调 + isLoading.value = false; + + if (success) { + paymentStatus.value = '支付宝支付成功!'; + errorMessage.value = ''; + } else { + paymentStatus.value = '支付宝支付失败'; + errorMessage.value = message; + } + }, + ); + + if (!launched) { + // 如果支付没有成功启动 + isLoading.value = false; + paymentStatus.value = '支付宝支付启动失败'; + errorMessage.value = '无法启动支付宝支付'; + Get.snackbar('支付启动失败', '无法启动支付宝支付'); + } + } catch (e) { + isLoading.value = false; + paymentStatus.value = '支付宝支付异常'; + errorMessage.value = e.toString(); + Get.snackbar('支付异常', e.toString()); + } + } + + /// 根据选择的支付方式进行支付 + Future processPayment() async { + if (selectedPaymentMethod.value == 1) { + await testWechatPayment(); + } else if (selectedPaymentMethod.value == 2) { + await testAlipayPayment(); + } + } + + /// 选择支付方式 + void selectPaymentMethod(int method) { + selectedPaymentMethod.value = method; + } + /// 重置状态 void resetStatus() { paymentStatus.value = ''; errorMessage.value = ''; - testOrderId.value = 'test_${DateTime.now().millisecondsSinceEpoch}'; + // testProductId.value = 'test_${DateTime.now().millisecondsSinceEpoch}'; } /// 更新测试金额 diff --git a/lib/modules/payment/views/payment_view.dart b/lib/modules/payment/views/payment_view.dart index 04a1a7480..9f1bac599 100644 --- a/lib/modules/payment/views/payment_view.dart +++ b/lib/modules/payment/views/payment_view.dart @@ -9,14 +9,14 @@ class PaymentView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: AppBar( backgroundColor: isDarkMode ? Colors.black : Colors.white, elevation: 0.5, title: Text( - '微信支付测试', + '支付测试', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -39,6 +39,8 @@ class PaymentView extends GetView { children: [ _buildTestInfoCard(isDarkMode), SizedBox(height: 20.h), + _buildPaymentMethodSelector(isDarkMode), + SizedBox(height: 20.h), _buildAmountSelector(isDarkMode), SizedBox(height: 20.h), _buildPaymentButton(isDarkMode), @@ -51,7 +53,7 @@ class PaymentView extends GetView { ), ); } - + /// 测试信息卡片 Widget _buildTestInfoCard(bool isDarkMode) { return Container( @@ -79,16 +81,20 @@ class PaymentView extends GetView { ), ), SizedBox(height: 12.h), - Obx(() => _buildInfoRow('订单ID', controller.testOrderId.value, isDarkMode)), + Obx(() => _buildInfoRow( + '商品ID', controller.testProductId.value, isDarkMode)), SizedBox(height: 8.h), _buildInfoRow('商品描述', '微信支付测试', isDarkMode), SizedBox(height: 8.h), - Obx(() => _buildInfoRow('支付金额', '¥${controller.testAmount.value.toStringAsFixed(2)}', isDarkMode)), + Obx(() => _buildInfoRow( + '支付金额', + '¥${controller.testAmount.value.toStringAsFixed(2)}', + isDarkMode)), ], ), ); } - + /// 信息行 Widget _buildInfoRow(String label, String value, bool isDarkMode) { return Row( @@ -116,11 +122,11 @@ class PaymentView extends GetView { ], ); } - + /// 金额选择器 Widget _buildAmountSelector(bool isDarkMode) { final amounts = [0.01, 0.10, 1.00, 10.00]; - + return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( @@ -151,95 +157,230 @@ class PaymentView extends GetView { runSpacing: 8.h, children: amounts.map((amount) { return Obx(() => GestureDetector( - onTap: () => controller.updateTestAmount(amount), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - decoration: BoxDecoration( - color: controller.testAmount.value == amount - ? Colors.blue[600] - : (isDarkMode ? Colors.grey[800] : Colors.grey[200]), - borderRadius: BorderRadius.circular(20.r), - ), - child: Text( - '¥${amount.toStringAsFixed(2)}', - style: TextStyle( - fontSize: 14.sp, - color: controller.testAmount.value == amount - ? Colors.white - : (isDarkMode ? Colors.white : Colors.black), + onTap: () => controller.updateTestAmount(amount), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + decoration: BoxDecoration( + color: controller.testAmount.value == amount + ? Colors.blue[600] + : (isDarkMode + ? Colors.grey[800] + : Colors.grey[200]), + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + '¥${amount.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 14.sp, + color: controller.testAmount.value == amount + ? Colors.white + : (isDarkMode ? Colors.white : Colors.black), + ), + ), ), - ), - ), - )); + )); }).toList(), ), ], ), ); } - - /// 支付按钮 - Widget _buildPaymentButton(bool isDarkMode) { - return Obx(() => SizedBox( - width: double.infinity, - height: 50.h, - child: ElevatedButton( - onPressed: controller.isLoading.value ? null : controller.testWechatPayment, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[600], - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), + + /// 支付方式选择器 + Widget _buildPaymentMethodSelector(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), ), - elevation: 2, - ), - child: controller.isLoading.value - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 20.w, - height: 20.w, - child: const CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - SizedBox(width: 12.w), - const Text('处理中...'), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.payment, size: 20.w), - SizedBox(width: 8.w), - Text( - '发起微信支付', + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '选择支付方式', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 16.h), + // 移除外层的 Obx,因为内部的 _buildPaymentMethodOption 已经有 Obx 了 + Column( + children: [ + _buildPaymentMethodOption( + method: 1, + title: '微信支付', + icon: Icons.wechat, + iconColor: Colors.green, + isDarkMode: isDarkMode, + ), + SizedBox(height: 12.h), + _buildPaymentMethodOption( + method: 2, + title: '支付宝支付', + icon: Icons.account_balance_wallet, + iconColor: Colors.blue, + isDarkMode: isDarkMode, + ), + ], + ), + ], + ), + ); + } + + /// 支付方式选项 + Widget _buildPaymentMethodOption({ + required int method, + required String title, + required IconData icon, + required Color iconColor, + required bool isDarkMode, + }) { + return Obx(() => GestureDetector( + onTap: () => controller.selectPaymentMethod(method), + child: Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: controller.selectedPaymentMethod.value == method + ? (isDarkMode + ? Colors.blue.withOpacity(0.2) + : Colors.blue.withOpacity(0.1)) + : (isDarkMode ? Colors.grey[800] : Colors.grey[100]), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: controller.selectedPaymentMethod.value == method + ? Colors.blue + : Colors.transparent, + width: 1.5, + ), + ), + child: Row( + children: [ + Icon( + icon, + color: iconColor, + size: 24.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + title, style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black, ), ), - ], + ), + if (controller.selectedPaymentMethod.value == method) + Icon( + Icons.check_circle, + color: Colors.blue, + size: 20.sp, + ), + ], + ), + ), + )); + } + + /// 支付按钮 + Widget _buildPaymentButton(bool isDarkMode) { + return Obx(() => Container( + width: double.infinity, + height: 50.h, + child: ElevatedButton( + onPressed: controller.isLoading.value + ? null + : () { + controller.processPayment(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: controller.isLoading.value + ? Colors.grey + : (controller.selectedPaymentMethod.value == 1 + ? Colors.green + : Colors.blue), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), ), - ), - )); + elevation: 2, + ), + child: controller.isLoading.value + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ), + SizedBox(width: 12.w), + Text( + '支付中...', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + controller.selectedPaymentMethod.value == 1 + ? Icons.wechat + : Icons.account_balance_wallet, + color: Colors.white, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + controller.selectedPaymentMethod.value == 1 + ? '微信支付 ¥${controller.testAmount.value.toStringAsFixed(2)}' + : '支付宝支付 ¥${controller.testAmount.value.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ), + )); } - + /// 状态卡片 Widget _buildStatusCard(bool isDarkMode) { return Obx(() { if (controller.paymentStatus.value.isEmpty) { return const SizedBox.shrink(); } - + final isError = controller.errorMessage.value.isNotEmpty; - + return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( - color: isError + color: isError ? (isDarkMode ? Colors.red[900] : Colors.red[50]) : (isDarkMode ? Colors.green[900] : Colors.green[50]), borderRadius: BorderRadius.circular(12.r), @@ -310,7 +451,7 @@ class PaymentView extends GetView { ); }); } - + /// 使用说明 Widget _buildInstructions(bool isDarkMode) { return Container( @@ -362,4 +503,4 @@ class PaymentView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 5f8eedde6..caabbed76 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -1077,17 +1077,20 @@ private func jsonToString(_ json: [String: Any]) -> String? { print("百度导航链接: \(url.absoluteString)") - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) { success in - if success { - print("成功打开百度地图") - } else { - print("打开百度地图失败") + // 确保在主线程上执行UI操作 + DispatchQueue.main.async { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) { success in + if success { + print("成功打开百度地图") + } else { + print("打开百度地图失败") + } } + } else { + print("百度地图未安装") + // 可以在这里添加提示用户安装百度地图的逻辑 } - } else { - print("百度地图未安装") - // 可以在这里添加提示用户安装百度地图的逻辑 } } @@ -1135,17 +1138,20 @@ private func jsonToString(_ json: [String: Any]) -> String? { print("高德导航链接: \(url.absoluteString)") - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) { success in - if success { - print("成功打开高德地图") - } else { - print("打开高德地图失败") + // 确保在主线程上执行UI操作 + DispatchQueue.main.async { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) { success in + if success { + print("成功打开高德地图") + } else { + print("打开高德地图失败") + } } + } else { + print("高德地图未安装") + // 可以在这里添加提示用户安装高德地图的逻辑 } - } else { - print("高德地图未安装") - // 可以在这里添加提示用户安装高德地图的逻辑 } } @@ -1160,25 +1166,28 @@ private func jsonToString(_ json: [String: Any]) -> String? { URLQueryItem(name: "directionsmode", value: mode) ] - if let appUrl = components.url, UIApplication.shared.canOpenURL(appUrl) { - UIApplication.shared.open(appUrl, options: [:]) { success in - if success { - print("成功打开谷歌地图应用") - } else { - print("打开谷歌地图应用失败") + // 确保在主线程上执行UI操作 + DispatchQueue.main.async { + if let appUrl = components.url, UIApplication.shared.canOpenURL(appUrl) { + UIApplication.shared.open(appUrl, options: [:]) { success in + if success { + print("成功打开谷歌地图应用") + } else { + print("打开谷歌地图应用失败") + } } + return } - return - } - - // 方案2: 使用网页版谷歌地图 - let webUrlString = "https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=\(destination)&travelmode=\(mode)" - if let webUrl = URL(string: webUrlString) { - UIApplication.shared.open(webUrl, options: [:]) { success in - if success { - print("成功打开谷歌地图网页版") - } else { - print("打开谷歌地图网页版失败") + + // 方案2: 使用网页版谷歌地图 + let webUrlString = "https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=\(destination)&travelmode=\(mode)" + if let webUrl = URL(string: webUrlString) { + UIApplication.shared.open(webUrl, options: [:]) { success in + if success { + print("成功打开谷歌地图网页版") + } else { + print("打开谷歌地图网页版失败") + } } } } @@ -1198,16 +1207,19 @@ private func jsonToString(_ json: [String: Any]) -> String? { return } - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) { success in - if success { - print("成功打开苹果地图") - } else { - print("打开苹果地图失败") + // 确保在主线程上执行UI操作 + DispatchQueue.main.async { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) { success in + if success { + print("成功打开苹果地图") + } else { + print("打开苹果地图失败") + } } + } else { + print("苹果地图不可用") } - } else { - print("苹果地图不可用") } } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index e82e77bd2..6da97f176 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -81,10 +81,32 @@ class SystemFunctionHandler(private val context: Context? = null) { handler = SendTextMessageHandler(context) ) + // 注册获取联系人列表函数 + client.registerLocalFunction( + name = "get_contacts_list", + description = "获取用户通信录联系人列表", + parameters = mapOf( + "type" to "object", + "properties" to mapOf( + "limit" to mapOf( + "type" to "integer", + "description" to "返回联系人数量限制,默认20个", + "default" to 20 + ), + "search" to mapOf( + "type" to "string", + "description" to "搜索关键词,可选参数" + ) + ), + "required" to emptyList() + ), + handler = GetContactsListHandler(context) + ) + // 注册拨打电话函数 client.registerLocalFunction( name = "make_phone_call", - description = "拨打电话", + description = "拨打电话给指定联系人", parameters = mapOf( "type" to "object", "properties" to mapOf( @@ -1328,6 +1350,172 @@ private class MaxVolumeHandler(private val context: Context?) : FunctionHandler } +/** + * 获取联系人列表处理器 + */ +private class GetContactsListHandler(private val context: Context?) : FunctionHandler { + + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + val handler = SystemFunctionHandler(context) + + // 检查读取联系人权限 + if (!handler.checkPermissionAndGuide(Manifest.permission.READ_CONTACTS, "读取联系人", "获取联系人列表")) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "permission_denied") + put("message", "需要联系人权限才能获取通信录,请在权限管理中开启联系人权限") + }) + }.toString() + } + + return try { + val limit = (arguments["limit"] as? Number)?.toInt() ?: 20 + val searchKeyword = arguments["search"] as? String + + val contacts = getContactsList(context, limit, searchKeyword) + + JSONObject().apply { + put("result", "成功获取联系人列表") + put("count", contacts.size) + put("contacts", contacts) + }.toString() + } catch (e: Exception) { + Log.e(SystemFunctionHandler.TAG, "获取联系人列表失败: ${e.message}", e) + JSONObject().apply { + put("error", JSONObject().apply { + put("code", "fetch_failed") + put("message", "获取联系人列表失败: ${e.message}") + }) + }.toString() + } + } + + /** + * 获取联系人列表 + * @param context 上下文 + * @param limit 返回数量限制 + * @param searchKeyword 搜索关键词 + * @return 联系人列表 + */ + private fun getContactsList(context: Context, limit: Int, searchKeyword: String?): List> { + val contacts = mutableListOf>() + + try { + val contentResolver = context.contentResolver + + // 构建查询条件 + val selection = if (!searchKeyword.isNullOrBlank()) { + "${ContactsContract.Contacts.DISPLAY_NAME} LIKE ?" + } else { + null + } + + val selectionArgs = if (!searchKeyword.isNullOrBlank()) { + arrayOf("%$searchKeyword%") + } else { + null + } + + val projection = arrayOf( + ContactsContract.Contacts._ID, + ContactsContract.Contacts.DISPLAY_NAME, + ContactsContract.Contacts.HAS_PHONE_NUMBER + ) + + // 按姓名排序,限制数量 + val sortOrder = "${ContactsContract.Contacts.DISPLAY_NAME} ASC LIMIT $limit" + + contentResolver.query( + ContactsContract.Contacts.CONTENT_URI, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + + val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID) + val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME) + val hasPhoneColumn = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER) + + while (cursor.moveToNext() && contacts.size < limit) { + val contactId = cursor.getString(idColumn) + val displayName = cursor.getString(nameColumn) ?: "未知联系人" + val hasPhoneNumber = cursor.getInt(hasPhoneColumn) > 0 + + if (hasPhoneNumber) { + // 获取该联系人的电话号码 + val phoneNumbers = getPhoneNumbersForContact(context, contactId) + + if (phoneNumbers.isNotEmpty()) { + contacts.add(mapOf( + "id" to contactId, + "name" to displayName, + "phone" to phoneNumbers.first(), // 使用第一个电话号码 + "allPhones" to phoneNumbers.joinToString(", ") // 所有电话号码 + )) + } + } + } + } + } catch (e: Exception) { + Log.e(SystemFunctionHandler.TAG, "查询联系人失败: ${e.message}", e) + } + + return contacts + } + + /** + * 获取指定联系人的所有电话号码 + * @param context 上下文 + * @param contactId 联系人ID + * @return 电话号码列表 + */ + private fun getPhoneNumbersForContact(context: Context, contactId: String): List { + val phoneNumbers = mutableListOf() + + try { + val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI + val phoneProjection = arrayOf( + ContactsContract.CommonDataKinds.Phone.NUMBER, + ContactsContract.CommonDataKinds.Phone.TYPE + ) + val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?" + val phoneSelectionArgs = arrayOf(contactId) + + context.contentResolver.query( + phoneUri, + phoneProjection, + phoneSelection, + phoneSelectionArgs, + null + )?.use { phoneCursor -> + + val numberColumn = phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + + while (phoneCursor.moveToNext()) { + val phoneNumber = phoneCursor.getString(numberColumn) + if (!phoneNumber.isNullOrBlank()) { + phoneNumbers.add(phoneNumber) + } + } + } + } catch (e: Exception) { + Log.e(SystemFunctionHandler.TAG, "获取联系人电话号码失败: ${e.message}", e) + } + + return phoneNumbers + } +} + /** * 更新用户画像 */ diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift index b55764459..e9640d020 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift @@ -323,7 +323,7 @@ extension LocationService: CLLocationManagerDelegate { // 过滤过旧的位置(超过5秒) let locationAge = -location.timestamp.timeIntervalSinceNow - guard locationAge < 5.0 else { + guard locationAge < 360 else { print("[LocationService] 位置数据过旧(\(locationAge)秒),忽略") return } diff --git a/pubspec.yaml b/pubspec.yaml index d8cd1e987..fdfec6bd2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: lottie: ^3.3.1 app_settings: 5.2.0 fluwx: ^5.7.1 + tobias: ^5.2.0 # 支付宝SDK url_launcher: ^6.3.1 firebase_core: ^4.0.0 firebase_auth: ^6.0.1 @@ -142,3 +143,7 @@ flutter: - assets/images/music.png - .env - .mcp.json + +# 添加 tobias 配置 +tobias: + url_scheme: voitrans # 你的应用的 URL Scheme