diff --git a/ios/Podfile.lock b/ios/Podfile.lock
deleted file mode 100644
index cc533b2d6..000000000
--- a/ios/Podfile.lock
+++ /dev/null
@@ -1,201 +0,0 @@
-PODS:
- - amap_service (0.0.1):
- - AMapNavi (~> 10.0.900)
- - Flutter
- - AMapFoundation (1.8.2)
- - AMapNavi (10.0.900):
- - AMapFoundation (>= 1.8.2)
- - audio_waveforms (0.0.1):
- - Flutter
- - classic_bluetooth (0.0.1):
- - Flutter
- - FBAEMKit (18.0.0):
- - FBSDKCoreKit_Basics (= 18.0.0)
- - FBSDKCoreKit (18.0.0):
- - FBAEMKit (= 18.0.0)
- - FBSDKCoreKit_Basics (= 18.0.0)
- - FBSDKCoreKit_Basics (18.0.0)
- - FBSDKLoginKit (18.0.0):
- - FBSDKCoreKit (= 18.0.0)
- - Flutter (1.0.0)
- - flutter_facebook_auth (7.1.2):
- - FBSDKLoginKit (~> 18.0.0)
- - Flutter
- - flutter_image_compress_common (1.0.0):
- - Flutter
- - Mantle
- - SDWebImage
- - SDWebImageWebPCoder
- - flutter_local_notifications (0.0.1):
- - Flutter
- - flutter_secure_storage (6.0.0):
- - Flutter
- - flutter_tts (0.0.1):
- - Flutter
- - fluwx (0.0.1):
- - Flutter
- - fluwx/pay (= 0.0.1)
- - fluwx/pay (0.0.1):
- - Flutter
- - WechatOpenSDK-XCFramework (~> 2.0.4)
- - haptic_feedback (0.5.1):
- - Flutter
- - keyboard_height_plugin (0.0.1):
- - Flutter
- - libwebp (1.5.0):
- - libwebp/demux (= 1.5.0)
- - libwebp/mux (= 1.5.0)
- - libwebp/sharpyuv (= 1.5.0)
- - libwebp/webp (= 1.5.0)
- - libwebp/demux (1.5.0):
- - libwebp/webp
- - libwebp/mux (1.5.0):
- - libwebp/demux
- - libwebp/sharpyuv (1.5.0)
- - libwebp/webp (1.5.0):
- - libwebp/sharpyuv
- - Mantle (2.2.0):
- - Mantle/extobjc (= 2.2.0)
- - Mantle/extobjc (2.2.0)
- - ota (0.0.1):
- - Flutter
- - permission_handler_apple (9.3.0):
- - Flutter
- - QCloudCore (6.4.7):
- - QCloudCore/Default (= 6.4.7)
- - QCloudCore/Default (6.4.7):
- - QCloudTrack/Beacon (= 6.4.7)
- - QCloudCOSXML (6.4.7):
- - QCloudCOSXML/Default (= 6.4.7)
- - QCloudCOSXML/Default (6.4.7):
- - QCloudCore (= 6.4.7)
- - QCloudTrack/Beacon (6.4.7)
- - record_darwin (1.0.0):
- - Flutter
- - SDWebImage (5.21.0):
- - SDWebImage/Core (= 5.21.0)
- - SDWebImage/Core (5.21.0)
- - SDWebImageWebPCoder (0.14.6):
- - libwebp (~> 1.0)
- - SDWebImage/Core (~> 5.17)
- - spotify_sdk (0.0.1):
- - Flutter
- - tencentcloud_cos_sdk_plugin (1.2.3):
- - Flutter
- - QCloudCOSXML (= 6.4.7)
- - WechatOpenSDK-XCFramework (2.0.4)
- - wifi_iot (0.0.1):
- - Flutter
-
-DEPENDENCIES:
- - amap_service (from `.symlinks/plugins/amap_service/ios`)
- - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- - classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- - Flutter (from `Flutter`)
- - flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`)
- - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- - fluwx (from `.symlinks/plugins/fluwx/ios`)
- - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
- - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- - ota (from `.symlinks/plugins/ota/ios`)
- - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- - record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- - spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`)
- - tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`)
- - wifi_iot (from `.symlinks/plugins/wifi_iot/ios`)
-
-SPEC REPOS:
- trunk:
- - AMapFoundation
- - AMapNavi
- - FBAEMKit
- - FBSDKCoreKit
- - FBSDKCoreKit_Basics
- - FBSDKLoginKit
- - libwebp
- - Mantle
- - QCloudCore
- - QCloudCOSXML
- - QCloudTrack
- - SDWebImage
- - SDWebImageWebPCoder
- - WechatOpenSDK-XCFramework
-
-EXTERNAL SOURCES:
- amap_service:
- :path: ".symlinks/plugins/amap_service/ios"
- audio_waveforms:
- :path: ".symlinks/plugins/audio_waveforms/ios"
- classic_bluetooth:
- :path: ".symlinks/plugins/classic_bluetooth/ios"
- Flutter:
- :path: Flutter
- flutter_facebook_auth:
- :path: ".symlinks/plugins/flutter_facebook_auth/ios"
- flutter_image_compress_common:
- :path: ".symlinks/plugins/flutter_image_compress_common/ios"
- flutter_local_notifications:
- :path: ".symlinks/plugins/flutter_local_notifications/ios"
- flutter_secure_storage:
- :path: ".symlinks/plugins/flutter_secure_storage/ios"
- flutter_tts:
- :path: ".symlinks/plugins/flutter_tts/ios"
- fluwx:
- :path: ".symlinks/plugins/fluwx/ios"
- haptic_feedback:
- :path: ".symlinks/plugins/haptic_feedback/ios"
- keyboard_height_plugin:
- :path: ".symlinks/plugins/keyboard_height_plugin/ios"
- ota:
- :path: ".symlinks/plugins/ota/ios"
- permission_handler_apple:
- :path: ".symlinks/plugins/permission_handler_apple/ios"
- record_darwin:
- :path: ".symlinks/plugins/record_darwin/ios"
- spotify_sdk:
- :path: ".symlinks/plugins/spotify_sdk/ios"
- tencentcloud_cos_sdk_plugin:
- :path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios"
- wifi_iot:
- :path: ".symlinks/plugins/wifi_iot/ios"
-
-SPEC CHECKSUMS:
- amap_service: 1c3e7e07f51cbbdff03719a4a079c60d015c1c0b
- AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec
- AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd
- audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
- classic_bluetooth: 29f392f1ab7c52862a9ef036c94f2713b02e0c50
- FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c
- FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f
- FBSDKCoreKit_Basics: 017b6dc2a1862024815a8229e75661e627ac1e29
- FBSDKLoginKit: 5875762d1fe09ddcb05d03365d4f5dc34413843d
- Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- flutter_facebook_auth: d97dc09216aebd99d7ac52360186a630f67bdf18
- flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
- flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
- flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
- flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
- fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
- haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0
- keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
- libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
- Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
- ota: 20c3e522c57e7ba70ce118109e73650acc9fec76
- permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
- QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223
- QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
- QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
- record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
- SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
- SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
- spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
- tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
- WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
- wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc
-
-PODFILE CHECKSUM: e69e629768e642fde97676ea149350c61d57751e
-
-COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index a96a23c4c..c91c43c66 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 54;
+ objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
@@ -252,7 +252,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
@@ -511,6 +511,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
+ NEW_SETTING = "";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -649,6 +650,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = YES;
+ NEW_SETTING = "";
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -701,6 +703,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
+ NEW_SETTING = "";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -718,7 +721,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 537SFY28XS;
+ DEVELOPMENT_TEAM = K39FB49XKY;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Deep Voice";
@@ -797,7 +800,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 40323d9d6..ca4a8bf61 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -38,6 +38,14 @@
wx5bbb7be9324f3e2d
+
+ CFBundleURLName
+ com.saitong.voitrans.signin
+ CFBundleURLSchemes
+
+ com.saitong.voitrans
+
+
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 0286ef500..88b018abb 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -12,5 +12,9 @@
com.apple.external-accessory.wireless-configuration
+ com.apple.developer.applesignin
+
+ Default
+
diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart
index 0301daa87..08733ee66 100644
--- a/lib/data/services/network/api.dart
+++ b/lib/data/services/network/api.dart
@@ -146,10 +146,10 @@ class Api {
}
//解绑定设备
- static unbindearphone([params]) {
+ static unbinddevice([params]) {
return DioManager().request(
NWMethod.post,
- '/api/home/user_unbindearphone',
+ '/api/home/user_unbinddevice',
params: params,
);
}
diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart
index 6121ed7f8..1a1fe15a8 100644
--- a/lib/modules/devices/controllers/devices_controller.dart
+++ b/lib/modules/devices/controllers/devices_controller.dart
@@ -35,7 +35,7 @@ class DevicesController extends GetxController {
}
void unbindearphone(UserDevice device) async {
- final response = await Api.unbindearphone({
+ final response = await Api.unbinddevice({
"id": device.id,
});
if (response != null) {
diff --git a/lib/modules/login/controllers/appleLogin.dart b/lib/modules/login/controllers/appleLogin.dart
new file mode 100644
index 000000000..02c4db1f7
--- /dev/null
+++ b/lib/modules/login/controllers/appleLogin.dart
@@ -0,0 +1,267 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math';
+import 'package:flutter/foundation.dart';
+import 'package:sign_in_with_apple/sign_in_with_apple.dart';
+import 'package:crypto/crypto.dart';
+import 'package:get/get.dart';
+
+/// Apple登录控制器(纯sign_in_with_apple实现)
+class Apple {
+ String? token = '';
+ String name = '';
+ String email = '';
+ String pictureurl = '';
+ String id = '';
+
+ // 存储原始的Apple凭证信息
+ String? _identityToken;
+ String? _authorizationCode;
+ String? _userIdentifier;
+
+ /// 生成随机nonce字符串(用于安全验证)
+ String generateNonce([int length = 32]) {
+ const charset =
+ '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
+ final random = Random.secure();
+ return List.generate(length, (_) => charset[random.nextInt(charset.length)])
+ .join();
+ }
+
+ /// 返回输入字符串的sha256哈希值(十六进制表示)
+ String sha256ofString(String input) {
+ final bytes = utf8.encode(input);
+ final digest = sha256.convert(bytes);
+ return digest.toString();
+ }
+
+ /// Apple登录主方法(纯sign_in_with_apple实现)
+ Future signInWithApple() async {
+ try {
+ // 检查Apple登录可用性
+ if (!await SignInWithApple.isAvailable()) {
+ if (kDebugMode) {
+ print('Apple登录不可用');
+ }
+ return false;
+ }
+
+ if (kDebugMode) {
+ print('开始执行Apple登录流程');
+ }
+
+ // 执行Apple登录
+ final appleCredential = await _getAppleCredential();
+
+ // 提取并存储用户信息
+ _extractUserInfo(appleCredential);
+
+ // 使用identityToken作为登录token
+ token = appleCredential.identityToken;
+
+ if (kDebugMode) {
+ print('Apple登录成功');
+ print('Token: $token');
+ print('用户信息: name=$name, email=$email, id=$id');
+ }
+
+ return token != null && token!.isNotEmpty;
+ } catch (e) {
+ if (kDebugMode) {
+ print('Apple登录异常:$e');
+ }
+ return false;
+ }
+ }
+
+ /// 获取Apple登录凭证
+ Future _getAppleCredential() async {
+ // 生成nonce用于安全验证(遵循官方最佳实践)
+ final rawNonce = generateNonce();
+ final nonce = sha256ofString(rawNonce);
+
+ try {
+ // 请求Apple登录凭证(与官方示例保持一致的参数结构)
+ final appleCredential = await SignInWithApple.getAppleIDCredential(
+ scopes: [
+ AppleIDAuthorizationScopes.email,
+ AppleIDAuthorizationScopes.fullName,
+ ],
+ nonce: nonce,
+ // 可以添加state参数用于防CSRF攻击(可选)
+ state: 'voitrans-login-${DateTime.now().millisecondsSinceEpoch}',
+ );
+
+ // 存储原始凭证信息(遵循官方示例的数据提取方式)
+ _identityToken = appleCredential.identityToken;
+ _authorizationCode = appleCredential.authorizationCode;
+ _userIdentifier = appleCredential.userIdentifier;
+
+ if (kDebugMode) {
+ print('Apple凭证获取成功');
+ print('Identity Token长度: ${_identityToken?.length ?? 0}');
+ print('Authorization Code长度: ${_authorizationCode?.length ?? 0}');
+ print('User Identifier: $_userIdentifier');
+ print('State: ${appleCredential.state}');
+
+ // 调试输出用户信息(与官方示例类似)
+ if (appleCredential.givenName != null) {
+ print('Given Name: ${appleCredential.givenName}');
+ }
+ if (appleCredential.familyName != null) {
+ print('Family Name: ${appleCredential.familyName}');
+ }
+ if (appleCredential.email != null) {
+ print('Email: ${appleCredential.email}');
+ }
+ }
+
+ return appleCredential;
+ } catch (e) {
+ if (kDebugMode) {
+ print('获取Apple凭证异常:$e');
+ }
+ throw Exception('获取Apple凭证失败: $e');
+ }
+ }
+
+
+
+ /// 提取Apple返回的用户信息(遵循官方示例的处理方式)
+ void _extractUserInfo(AuthorizationCredentialAppleID credential) {
+ // 用户ID(官方示例中的主要标识符)
+ id = credential.userIdentifier ?? '';
+
+ // 邮箱(可能为空,如果用户选择隐藏邮箱)
+ email = credential.email ?? '';
+
+ // 用户姓名处理(与官方示例保持一致的逻辑)
+ // 注意:姓名信息仅在首次登录时提供
+ final givenName = credential.givenName;
+ final familyName = credential.familyName;
+
+ if (givenName != null || familyName != null) {
+ // 按照官方示例的方式组合姓名
+ final firstName = givenName ?? '';
+ final lastName = familyName ?? '';
+ name = '$firstName $lastName'.trim();
+ } else {
+ // 如果没有姓名信息,保持空值(后续登录不会提供姓名)
+ name = '';
+ }
+
+ // Apple不提供头像,使用默认值
+ pictureurl = '';
+
+ if (kDebugMode) {
+ print('=== 提取的用户信息 ===');
+ print('User Identifier: $id');
+ print('Email: ${email.isEmpty ? "未提供或隐藏" : email}');
+ print('Given Name: ${givenName ?? "未提供"}');
+ print('Family Name: ${familyName ?? "未提供"}');
+ print('Combined Name: ${name.isEmpty ? "未提供" : name}');
+ print('Picture URL: $pictureurl');
+ print('========================');
+ }
+ }
+
+ /// 退出Apple登录
+ Future signOutApple() async {
+ try {
+ // 清空用户信息
+ _clearUserInfo();
+
+ if (kDebugMode) {
+ print('Apple登录退出成功');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('Apple登录退出异常:$e');
+ }
+ }
+ }
+
+ /// 清空用户信息
+ void _clearUserInfo() {
+ token = '';
+ name = '';
+ email = '';
+ pictureurl = '';
+ id = '';
+ _identityToken = null;
+ _authorizationCode = null;
+ _userIdentifier = null;
+ }
+
+ /// 检查Apple登录环境
+ static Future checkAppleSignInEnvironment() async {
+ if (kDebugMode) {
+ print('=== Apple登录环境检查 ===');
+ print('平台: ${GetPlatform.isIOS ? "iOS" : "非iOS"}');
+ print('Apple登录可用性: ${await SignInWithApple.isAvailable()}');
+
+ // 检测设备地区
+ final locale = Get.deviceLocale;
+ final countryCode = locale?.countryCode?.toUpperCase();
+ print('设备地区: $countryCode');
+ print('认证方式: 纯Apple登录(无Firebase依赖)');
+
+ print('========================');
+ }
+ }
+
+ /// 获取认证方式名称
+ String getAuthMethodName() {
+ return 'Apple Sign-In';
+ }
+
+ /// 获取详细的凭证信息(用于调试)
+ Map getCredentialInfo() {
+ return {
+ 'identityToken': _identityToken,
+ 'authorizationCode': _authorizationCode,
+ 'userIdentifier': _userIdentifier,
+ 'name': name,
+ 'email': email,
+ 'id': id,
+ };
+ }
+
+ /// 获取用于后端验证的数据(遵循官方示例的后端集成方式)
+ ///
+ /// [deviceId] 设备ID
+ /// 返回格式化的登录数据,包含Apple凭证信息
+ Map getLoginData(String deviceId) {
+ return {
+ // 现有系统兼容的字段
+ "mail": email,
+ "phone": "",
+ "stype": 5, // Apple登录类型
+ "vcode": "",
+ "ttoken": token ?? '', // 主要使用identityToken
+ "phonemodel": "",
+ "phonemac": deviceId,
+ "name": name,
+ "avatar": pictureurl,
+ "auth_method": "apple_signin", // 标识使用纯Apple登录
+
+ // Apple特有的凭证信息(遵循官方示例)
+ "apple_credentials": {
+ "identity_token": _identityToken,
+ "authorization_code": _authorizationCode,
+ "user_identifier": _userIdentifier,
+ "email": email,
+ "name": name,
+ }
+ };
+ }
+
+ /// 获取原始Apple凭证信息(用于高级验证)
+ Map getAppleCredentials() {
+ return {
+ 'identityToken': _identityToken,
+ 'authorizationCode': _authorizationCode,
+ 'userIdentifier': _userIdentifier,
+ };
+ }
+}
diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart
index c77588c20..c17147ef4 100644
--- a/lib/modules/login/controllers/login_controller.dart
+++ b/lib/modules/login/controllers/login_controller.dart
@@ -2,12 +2,14 @@ import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
import 'package:logger/web.dart';
import '../../../routes/app_routes.dart';
import 'dart:convert';
import 'facebookLogin.dart';
import 'googleLogin.dart';
import 'weixinLogin.dart';
+import 'appleLogin.dart';
import '../../../data/services/network/api.dart';
import '../../../../data/models/user_Info.dart';
import '../../common/webview_page.dart';
@@ -50,6 +52,7 @@ class LoginController extends GetxController {
String _appVersion = ''; // APP版本号
String _countryCode = ''; // 国家代码
UserChannel _userChannel = UserChannel.OFFICIAL; // 默认渠道
+ final isChinaMainland = false.obs; //是否中国大陆地区
final _hasAcceptedPrivacy = false.obs;
bool get hasAcceptedPrivacy => _hasAcceptedPrivacy.value;
@@ -177,6 +180,21 @@ class LoginController extends GetxController {
{'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'},
];
+ // 初始化默认选择的国家代码
+ void _initDefaultCountryCode() {
+ if (isChinaMainland.value) {
+ // 中国大陆用户默认选择+86
+ _selectedCountryCode.value = '+86';
+ _selectedCountryFlag.value = '🇨🇳';
+ loginType = 1; // 设置为手机号登录类型
+ } else {
+ // 非中国大陆用户默认选择邮箱
+ _selectedCountryCode.value = '';
+ _selectedCountryFlag.value = '✉️';
+ loginType = 0; // 设置为邮箱登录类型
+ }
+ }
+
// 状态管理
final _viewState = LoginViewState.welcome.obs;
final _isLoading = false.obs;
@@ -202,6 +220,12 @@ class LoginController extends GetxController {
super.onInit();
_viewState.value = LoginViewState.welcome;
+ isChinaMainland.value =
+ Get.deviceLocale?.countryCode?.toUpperCase() == 'CN';
+
+ // 初始化默认国家代码
+ _initDefaultCountryCode();
+
_hasAcceptedPrivacy.value =
_storage.read('hasAcceptedPrivacy') ?? false; // 从本地存储中读取是否已接受隐私政策
@@ -874,6 +898,44 @@ class LoginController extends GetxController {
}
break;
}
+ case 5: //Apple登录
+ {
+ Logger().d("开始Apple登录流程");
+ final apple = Apple();
+
+ // 先检查环境(调试用)
+ if (kDebugMode) {
+ await Apple.checkAppleSignInEnvironment();
+ }
+
+ bool authResult = await apple.signInWithApple();
+
+ if (authResult) {
+ Logger().d("Apple登录成功");
+ Logger().d("认证方式: ${apple.getAuthMethodName()}");
+ Logger().d("Token: ${apple.token}");
+ Logger().d("Email: ${apple.email}");
+ Logger().d("Name: ${apple.name}");
+ Logger().d("ID: ${apple.id}");
+
+ // 获取登录数据并发送到后端
+ final loginData = apple.getLoginData(_deviceId);
+ Logger().d("发送登录数据: $loginData");
+
+ // 直接调用现有的requestLogin方法
+ requestLogin(loginData);
+ } else {
+ Logger().e("Apple登录失败");
+ Get.snackbar(
+ 'error'.tr,
+ 'Apple登录失败,请重试',
+ snackPosition: SnackPosition.TOP,
+ backgroundColor: Colors.red.withValues(alpha: 0.1),
+ colorText: Colors.red,
+ );
+ }
+ break;
+ }
case 6: //游客登录
{
requestLogin({
diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart
index 22fbc93b3..c38fae252 100644
--- a/lib/modules/login/views/login_view.dart
+++ b/lib/modules/login/views/login_view.dart
@@ -161,13 +161,14 @@ class LoginView extends GetView {
),
),
SizedBox(height: 4.h),
- Text(
- 'loginTips'.tr, // 请使用手机号或邮箱登录/注册
- style: TextStyle(
- fontSize: 14.sp,
- color: isDarkMode ? Colors.grey[400] : Colors.black54,
+ if (!controller.isChinaMainland.value)
+ Text(
+ 'loginTips'.tr, // 请使用手机号或邮箱登录/注册
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: isDarkMode ? Colors.grey[400] : Colors.black54,
+ ),
),
- ),
],
);
}
@@ -214,11 +215,12 @@ class LoginView extends GetView {
color: isDarkMode ? Colors.white : Colors.black,
),
)),
- Icon(
- Icons.arrow_drop_down,
- color: isDarkMode ? Colors.grey[400] : Colors.black54,
- size: 20.sp,
- ),
+ if (!controller.isChinaMainland.value)
+ Icon(
+ Icons.arrow_drop_down,
+ color: isDarkMode ? Colors.grey[400] : Colors.black54,
+ size: 20.sp,
+ ),
],
),
),
@@ -263,6 +265,10 @@ class LoginView extends GetView {
// 添加国家选择底部弹窗
void _showCountryPicker(bool isDarkMode) {
+ // 中国大陆用户不显示国家选择器
+ if (controller.isChinaMainland.value) {
+ return;
+ }
Get.bottomSheet(
Container(
height: 400.h,
@@ -477,13 +483,24 @@ class LoginView extends GetView {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- _buildSocialButton(FontAwesomeIcons.google, Colors.red, isDarkMode),
- SizedBox(width: 24.w),
- _buildSocialButton(
- FontAwesomeIcons.weixin, Colors.green, isDarkMode),
- SizedBox(width: 24.w),
- _buildSocialButton(
- FontAwesomeIcons.facebook, Colors.blue, isDarkMode),
+ if (controller.isChinaMainland.value) ...[
+ _buildSocialButton(
+ FontAwesomeIcons.weixin, Colors.green, isDarkMode),
+ SizedBox(width: 24.w)
+ ] else ...[
+ _buildSocialButton(
+ FontAwesomeIcons.google, Colors.red, isDarkMode),
+ SizedBox(width: 24.w),
+ ],
+
+ // Apple登录按钮(仅在iOS上显示)
+ if (GetPlatform.isIOS) ...[
+ _buildSocialButton(
+ FontAwesomeIcons.apple, Colors.black, isDarkMode),
+ ],
+ // SizedBox(width: 24.w),
+ // _buildSocialButton(
+ // FontAwesomeIcons.facebook, Colors.blue, isDarkMode),
// 根据配置显示游客登录按钮
if (controller.isGuestLoginVisible) ...[
SizedBox(width: 24.w),
@@ -541,6 +558,11 @@ class LoginView extends GetView {
controller.loginType = 2;
break;
}
+ case FontAwesomeIcons.apple: // 处理Apple登录
+ {
+ controller.loginType = 5;
+ break;
+ }
case FontAwesomeIcons.userSecret: // 处理游客登录
{
controller.loginType = 6;
diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart
index 5992d98cf..eb488a73a 100644
--- a/lib/modules/pairing/controllers/pairing_controller.dart
+++ b/lib/modules/pairing/controllers/pairing_controller.dart
@@ -89,6 +89,7 @@ class PairingController extends GetxController {
// ========== 当前设备信息 ==========
BluetoothDevice? _currentClassicDevice;
+ String? _verifiedDeviceName;
String? _verifiedDeviceAddress;
TextEditingController codeInputController = TextEditingController();
@@ -542,11 +543,11 @@ class PairingController extends GetxController {
return;
}
- Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceAddress');
+ Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceName:$_verifiedDeviceAddress');
// 调用BLE管理器的关联方法
final success = await _bleManager.startDeviceAssociation(
- deviceNamePattern: 'Voitrans', // 或者使用 _currentClassicDevice?.name
+ deviceNamePattern: _verifiedDeviceName!,
macAddress: _verifiedDeviceAddress!,
);
@@ -658,6 +659,7 @@ class PairingController extends GetxController {
_bleManager.stopScan();
deviceCode.value = code;
String? uuids = deviceInfo["id"];
+ _verifiedDeviceName = device.name;
_verifiedDeviceAddress = uuids;
print('找到匹配设备,获取到 uuids: $uuids');
//检查是否已经绑定
@@ -688,6 +690,7 @@ class PairingController extends GetxController {
if (code != "") {
_bleManager.stopScan();
deviceCode.value = code;
+ _verifiedDeviceName = device.name;
_verifiedDeviceAddress = device.address;
//检查是否已经绑定
final existingDevice = User.instance.devices.firstWhere(
@@ -719,7 +722,6 @@ class PairingController extends GetxController {
}
await Future.delayed(Duration(seconds: 1));
}
-
return 1;
}
return 0;
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 753dd87c4..29dd1d360 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
@@ -464,6 +464,7 @@ audioStream.saveAudioDataTo(data: audioData)
os_log("中断当前AI流式响应", log: logger, type: .info)
_ = interruptCurrentResponse()
}
+ stopTts();
isAiStreaming = true
os_log("设置AI流式状态为true", log: logger, type: .info)
@@ -689,7 +690,6 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
notreply = _notreply
}
metadata["notreply"] = notreply
-
// 处理卡片数据
if let cardMusic = jsonDict["card_music"] as? [String: Any] {
processMusicCard(cardMusic)
@@ -958,6 +958,7 @@ class ChatApiStreamCallback: StreamCallback {
}
func onToken(_ sessionId:String,_ token: String) {
do {
+// print("liwei--------------- AI Call onToken! sessionId:\(sessionId) token:\(token)")
guard let agentService = try agentService else { return }
if (sessionId != agentService.currsessionId) {
return
@@ -1044,18 +1045,12 @@ class ChatApiStreamCallback: StreamCallback {
let name = functionCall["name"] as? String ?? ""
os_log("收到函数调用: %{public}@", log: agentService.logger, type: .info, name)
print("onFunctionCall=functionCall\(functionCall)")
- // try agentService.audioPlayer?.playCallingSound()
-
+
try agentService.sendEvent(name: "function_call", data: [
"name": name,
"arguments": functionCall
])
-
- if name == "exit_interaction" {
- os_log("执行退出交互", log: agentService.logger, type: .info)
- agentService.stopRecognition()
- }
-
+
}catch{
print("liwei--------------- AI Call onFunctionCall 异常: \(error)")
}
@@ -1067,20 +1062,22 @@ class ChatApiStreamCallback: StreamCallback {
guard let agentService = agentService else { return }
let name = functionCall["name"] as? String ?? ""
- os_log("收到函数调用结果: %{public}@", log: agentService.logger, type: .info, functionCallResult)
+
// agentService.audioPlayer?.stopCallingSound()
let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult)
metadata = resultmap["meta"] as? String ?? ""
broadcast = resultmap["broadcast"] as? Bool ?? true
reply = !(resultmap["notreply"] as? Bool ?? true)
- os_log("卡片数据: %{public}@", log: agentService.logger, type: .info, metadata)
agentService.sendEvent(name: "function_call_result", data: [
"function_call": functionCall,
"result": functionCallResult,
"meta":metadata,
])
- os_log("处理函数调用结果完成", log: agentService.logger, type: .info)
+ os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@",
+ log: agentService.logger,
+ type: .info,
+ "\(broadcast)", "\(reply)")
}catch{
print("liwei--------------- AI Call onFunctionCallResult 异常: \(error)")
}
diff --git a/local_plugins/amap_service/ios/Classes/AmapService.swift b/local_plugins/amap_service/ios/Classes/AmapService.swift
index 30f6d1308..c962cc002 100644
--- a/local_plugins/amap_service/ios/Classes/AmapService.swift
+++ b/local_plugins/amap_service/ios/Classes/AmapService.swift
@@ -24,7 +24,7 @@ public class AmapService: NSObject, AMapNaviDriveManagerDelegate {
// 设置高德地图服务
// 1. 设置隐私合规
AMapServices.shared()?.enableHTTPS = true
- AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03"
+ AMapServices.shared()?.apiKey = "0b704e172b7ade5a7a18728e8f517a05"
}
public func startNavigation(from start: String, to end: String) {
diff --git a/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift b/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift
index 05e7c0ec4..4be2e55a8 100644
--- a/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift
+++ b/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift
@@ -22,7 +22,7 @@ public class AmapServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
// 高德SDK初始化
DispatchQueue.main.async {
- AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03"
+ AMapServices.shared()?.apiKey = "0b704e172b7ade5a7a18728e8f517a05"
AMapServices.shared()?.enableHTTPS = true
if #available(iOS 13.0, *) {
@@ -101,4 +101,4 @@ public class AmapServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
result(FlutterMethodNotImplemented)
}
}
-}
\ No newline at end of file
+}
diff --git a/local_plugins/amap_service/ios/amap_service.podspec b/local_plugins/amap_service/ios/amap_service.podspec
index 5e90480f9..b33af0daa 100644
--- a/local_plugins/amap_service/ios/amap_service.podspec
+++ b/local_plugins/amap_service/ios/amap_service.podspec
@@ -22,6 +22,6 @@ A new Flutter project.
'DEFINES_MODULE' => 'YES',
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386'
}
- s.platform = :ios, '12.0'
+ s.platform = :ios, '16.0'
s.swift_version = '5.0'
end
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
index e45b51003..24b60e444 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
@@ -576,7 +576,7 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return }
while self.isRunning {
- print("是否写入: \(self.isWriting)")
+ // print("是否写入: \(self.isWriting)")
if !self.isWriting {
usleep(10_000)
continue
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
index 41d31caa7..b0c76fa81 100644
--- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
@@ -24,6 +24,12 @@ class BleConst {
/** 客户端特征配置描述符UUID */
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb")
+ /** 扫描服务UUID - 用于后台扫描过滤 */
+ static let SERVICE_UUID = CBUUID(string: "abc0")
+
+ /** 扫描服务UUID1 - 用于后台扫描过滤 */
+ static let SERVICE_UUID1 = CBUUID(string: "fd7b")
+
/** BLE扫描Action */
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN"
@@ -90,4 +96,4 @@ class BleConst {
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02
/** 立体声 */
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
index 6647a5e4a..66b8d73e2 100644
--- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
@@ -238,11 +238,7 @@ private var cmdReplyType: UInt8 = 0
manager.scanForPeripherals(withServices: nil, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据
])
- // let serviceUUIDs = [BleConst.SERVICE_UUID]
- // manager.scanForPeripherals(withServices: serviceUUIDs, options: [
- // CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据
- // ])
- // os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout)
+ os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout)
// 设置扫描超时
scanTimer?.invalidate()
@@ -391,6 +387,7 @@ private var cmdReplyType: UInt8 = 0
// 遍历所有已发现的设备,查找匹配的设备名
for (deviceId, deviceInfo) in discoveredDevices {
+ print("deviceInfo: \(deviceInfo)")
if let name = deviceInfo["name"] as? String, name == deviceName {
// 获取对应的广播数据
var deviceData = deviceInfo
@@ -1224,14 +1221,76 @@ extension BleService: CBCentralManagerDelegate {
os_log("📱 设备RSSI变化较大 - 名称: %{public}@, UUID: %{public}@, RSSI: %d → %d", log: logger, type: .debug, deviceName, deviceId, oldRssi, newRssi)
}
} else {
- // 新设备,需要上报
+ // 新设备,需要上报并打印详细信息
shouldNotify = true
- os_log("📱 发现新设备 - 名称: %{public}@, UUID: %{public}@", log: logger, type: .info, deviceName, deviceId)
- // 打印 hex 数据 - 直接从 deviceInfo 获取
- if let hex = deviceInfo["hex"] as? String {
- os_log("📡 设备 hex 数据: %{public}@", log: logger, type: .info, hex)
+ // 打印详细的设备信息
+ os_log("📱 ===== 发现新BLE设备 =====", log: logger, type: .info)
+ os_log("📱 设备名称: %{public}@", log: logger, type: .info, deviceName)
+ os_log("📱 设备UUID(MAC地址): %{public}@", log: logger, type: .info, deviceId)
+ os_log("📱 RSSI信号强度: %d dBm", log: logger, type: .info, RSSI.intValue)
+ os_log("📱 发现时间: %{public}@", log: logger, type: .info, Date().description)
+
+ // 打印服务UUID信息
+ if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
+ var uuidDetails: [String] = []
+ for uuid in serviceUUIDs {
+ let uuidString = uuid.uuidString
+ // 检查是否为16位UUID(通常显示为4个字符)
+ if uuidString.count == 4 {
+ let fullUUID = "0000\(uuidString)-0000-1000-8000-00805F9B34FB"
+ uuidDetails.append("\(uuidString) (完整: \(fullUUID))")
+ } else {
+ uuidDetails.append(uuidString)
+ }
+ }
+ os_log("📱 服务UUIDs: %{public}@", log: logger, type: .info, uuidDetails.joined(separator: ", "))
+ os_log("📱 服务UUID数量: %d", log: logger, type: .info, serviceUUIDs.count)
+ } else {
+ os_log("📱 服务UUIDs: 无", log: logger, type: .info)
+ }
+
+ // 打印制造商数据
+ if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
+ let hexString = manufacturerData.map { String(format: "%02X", $0) }.joined(separator: " ")
+ os_log("📱 制造商数据: %{public}@", log: logger, type: .info, hexString)
+ } else {
+ os_log("📱 制造商数据: 无", log: logger, type: .info)
+ }
+
+ // 打印服务数据
+ if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
+ for (serviceUUID, data) in serviceData {
+ let hexString = data.map { String(format: "%02X", $0) }.joined(separator: " ")
+ os_log("📱 服务数据[%{public}@]: %{public}@", log: logger, type: .info, serviceUUID.uuidString, hexString)
+ }
+ } else {
+ os_log("📱 服务数据: 无", log: logger, type: .info)
+ }
+
+ // 打印本地名称
+ if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
+ os_log("📱 本地名称: %{public}@", log: logger, type: .info, localName)
+ }
+
+ // 打印TX功率等级
+ if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
+ os_log("📱 TX功率等级: %d dBm", log: logger, type: .info, txPowerLevel.intValue)
}
+
+ // 打印是否可连接
+ if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber {
+ os_log("📱 可连接状态: %{public}@", log: logger, type: .info, isConnectable.boolValue ? "是" : "否")
+ }
+
+ // 打印完整的原始广播数据(十六进制)
+ if let hex = deviceInfo["hex"] as? String, !hex.isEmpty {
+ os_log("📱 原始广播数据(HEX): %{public}@", log: logger, type: .info, hex)
+ } else {
+ os_log("📱 原始广播数据(HEX): 无", log: logger, type: .info)
+ }
+
+ os_log("📱 =========================", log: logger, type: .info)
}
// 更新缓存(无论是否上报都要更新)
diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
index ad6b53219..cdf5cfa3a 100644
--- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
+++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
@@ -67,12 +67,24 @@ public class ChatApiService: NSObject {
// MARK: - 公共方法
/// 设置流式回调
- public func setStreamCallback(_ sessionId:String,_ callback: StreamCallback) {
+ public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) {
+ print("[ChatApiService] 设置回调 for sessionId: \(sessionId)")
+ print("[ChatApiService] 回调对象: \(callback)")
self.sessionCallbacks[sessionId] = callback
+ print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)")
}
- public func getSessionCallback(_ sessionId:String)->StreamCallback?{
- return self.sessionCallbacks[sessionId]
+ public func getSessionCallback(_ sessionId: String) -> StreamCallback? {
+ let callback = self.sessionCallbacks[sessionId]
+ print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)")
+ print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)")
+ print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))")
+ print("[ChatApiService] callback exists: \(callback != nil)")
+ return callback
+ }
+ //移除回调函数
+ public func removeSessionCallback(_ sessionId: String){
+ self.sessionCallbacks.removeValue(forKey: sessionId)
}
/// 初始化ChatAPI服务
@@ -289,6 +301,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
let query: ChatQuery
if !tools.isEmpty {
let chatTools = self.convertToOpenAITools(tools)
+
query = ChatQuery(
messages: chatMessages,
model: currentModel,
@@ -296,6 +309,8 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
temperature: 0.7,
tools: chatTools
)
+
+ print("[ChatApiService] 发起AI问答 问题:\(messages[0]) 工具数量: \(chatTools.count)")
} else {
query = ChatQuery(
messages: chatMessages,
@@ -319,7 +334,17 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
// 处理文本内容
if let content = delta.content {
replyContext += content
- self.getSessionCallback(sessionId)?.onToken(sessionId, content)
+ print("[ChatApiService] 收到内容: \(content)")
+ print("[ChatApiService] 当前sessionId: \(sessionId)")
+
+ let callback = self.getSessionCallback(sessionId)
+ if let callback = callback {
+ print("[ChatApiService] 找到回调,准备调用 onToken")
+ callback.onToken(sessionId, content)
+ print("[ChatApiService] onToken 调用完成")
+ } else {
+ print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调")
+ }
}
// 收集工具调用信息
@@ -690,11 +715,10 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
currentStreamTask = nil
// 2. 通知旧会话被中止
- getSessionCallback(oldSessionId)?.onComplete(currSessionId)
+// getSessionCallback(oldSessionId)?.onComplete(currSessionId)
// 3. 清理状态
currSessionId = ""
- sessionCallbacks.removeValue(forKey: oldSessionId)
toolCalls.removeAll()
}
}
@@ -703,7 +727,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
private func clearCurrentSession() {
let oldSessionId = currSessionId
currSessionId = ""
- sessionCallbacks.removeValue(forKey: oldSessionId)
+ self.sessionCallbacks.removeValue(forKey: oldSessionId)
currentStreamTask = nil
toolCalls.removeAll()
}
@@ -876,14 +900,17 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
var chatTools: [ChatQuery.ChatCompletionToolParam] = []
for tool in tools {
+
guard let type = tool["type"] as? String,
type == "function",
let function = tool["function"] as? [String: Any],
let name = function["name"] as? String,
let description = function["description"] as? String,
let parameters: [String: Any] = function["parameters"] as? [String: Any] else {
+ print("无法将 parameters 转换为 JSON 数据")
continue
}
+
// 将字典转换为 JSON 数据
guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) else {
print("无法将 parameters 转换为 JSON 数据")
@@ -904,6 +931,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
// 创建工具参数
let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition)
+ print("解析工具 \(name):\(schema)")
chatTools.append(toolParam)
}
@@ -922,4 +950,4 @@ private class LocalFunctionHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
return "LOCAL_FUNCTION:\(functionName)"
}
-}
+}
diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift
index 2b9f58eb6..e5da6b689 100644
--- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift
+++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift
@@ -17,6 +17,9 @@ public actor CustomSseClientTransport: Transport {
private let reconnectionTime: TimeInterval
private let session: URLSession
+ // 断线回调
+ private let onConnectionLost: (() -> Void)?
+
// URL解析结果
private var hostPart: String = ""
private var pathPart: String = ""
@@ -26,6 +29,7 @@ public actor CustomSseClientTransport: Transport {
// 连接状态
private var isConnected = false
private var isInitialized = false
+ private var connectionMonitorTask: Task?
// 消息流
private let messageStream: AsyncThrowingStream
@@ -57,11 +61,13 @@ public actor CustomSseClientTransport: Transport {
urlString: String,
reconnectionTime: TimeInterval = 1.0,
session: URLSession = .shared,
- logger: Logger? = nil
+ logger: Logger? = nil,
+ onConnectionLost: (() -> Void)? = nil
) {
self.originalUrlString = urlString
self.reconnectionTime = reconnectionTime
self.session = session
+ self.onConnectionLost = onConnectionLost
self.logger = logger ?? Logger(
label: "com.yunqiinnovation.chat_api.CustomSseClientTransport",
factory: { _ in SwiftLogNoOpLogHandler() }
@@ -93,6 +99,9 @@ public actor CustomSseClientTransport: Transport {
_ = try await waitForEndpoint()
isConnected = true
+
+ // 启动连接监控
+ startConnectionMonitoring()
}
/// 断开传输连接
@@ -104,6 +113,10 @@ public actor CustomSseClientTransport: Transport {
isConnected = false
isInitialized = false
+ // 停止连接监控
+ connectionMonitorTask?.cancel()
+ connectionMonitorTask = nil
+
// 取消SSE任务
sseTask?.cancel()
sseTask = nil
@@ -120,6 +133,11 @@ public actor CustomSseClientTransport: Transport {
endpointContinuation = nil
}
+ /// 检查连接是否活跃
+ public func isConnectionActive() -> Bool {
+ return isConnected && isInitialized && eventSource != nil
+ }
+
/// 发送数据
public func send(_ data: Data) async throws {
guard isConnected, let endpointUrl = endpointUrl else {
@@ -329,10 +347,17 @@ public actor CustomSseClientTransport: Transport {
/// 处理SSE错误
private func handleSseError(_ error: String) {
+ logger.error("SSE错误: \(error)")
+
let mcpError = MCPError.internalError("SSE error: \(error)")
endpointContinuation?.resume(throwing: mcpError)
endpointContinuation = nil
messageContinuation.finish(throwing: mcpError)
+
+ // 触发连接断开处理
+ Task {
+ await handleConnectionLost()
+ }
}
/// 处理SSE连接打开
@@ -382,4 +407,73 @@ public actor CustomSseClientTransport: Transport {
private func handleOtherEvent(_ data: String) {
safeParseMessage(data)
}
-}
\ No newline at end of file
+
+ // MARK: - 连接监控和错误处理
+
+ /// 启动连接监控
+ private func startConnectionMonitoring() {
+ connectionMonitorTask = Task { [weak self] in
+ await self?.monitorConnectionHealth()
+ }
+ }
+
+ /// 监控连接健康状态
+ private func monitorConnectionHealth() async {
+ while isInitialized && !Task.isCancelled {
+ // 检查连接状态
+ if !isConnectionActive() {
+ await handleConnectionLost()
+ break
+ }
+
+ // 每5秒检查一次连接状态
+ try? await Task.sleep(nanoseconds: 5_000_000_000)
+ }
+ }
+
+ /// 处理连接丢失 - 简化版本,只重置状态
+ private func handleConnectionLost() async {
+ guard isConnected else { return }
+
+ logger.warning("SSE连接丢失,重置连接状态")
+
+ // 重置连接状态
+ await resetConnectionState()
+
+ // 通知外部连接丢失
+ onConnectionLost?()
+ }
+
+ /// 重置连接状态
+ private func resetConnectionState() async {
+ // 标记连接已断开
+ isConnected = false
+
+ // 停止连接监控
+ connectionMonitorTask?.cancel()
+ connectionMonitorTask = nil
+
+ // 清理连接资源
+ await cleanupConnection()
+
+ // 重置端点信息
+ endpointUrl = nil
+
+ // 如果有等待中的端点请求,取消它
+ endpointContinuation?.resume(throwing: MCPError.internalError("Connection lost"))
+ endpointContinuation = nil
+
+ logger.info("连接状态已重置,等待下次工具调用时重连")
+ }
+
+ /// 清理连接资源
+ private func cleanupConnection() async {
+ // 取消SSE任务
+ sseTask?.cancel()
+ sseTask = nil
+
+ // 关闭EventSource
+ await eventSource?.close()
+ eventSource = nil
+ }
+}
diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
index aba509fc5..b42708dca 100644
--- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
+++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
@@ -39,38 +39,71 @@ public class MCPSubClient {
private var toolMaps: [[String: Any]] = []
private var isConnectedFlag = false
- public init(serverId: String, serverUrl: String,filterTools:String) {
+ // 重连配置
+ private let maxRetryAttempts = 3
+ private let initialReconnectDelay: TimeInterval = 1.0
+ private let maxReconnectDelay: TimeInterval = 30.0
+ private var currentReconnectDelay: TimeInterval = 1.0
+ private var retryCount = 0
+ private var reconnectTask: Task?
+
+ // 连接状态锁
+ private let connectionLock = NSLock()
+
+ public init(serverId: String, serverUrl: String, filterTools: String) {
self.serverId = serverId
self.serverUrl = serverUrl
self.filterTools = filterTools
+ self.currentReconnectDelay = initialReconnectDelay
}
public func connect() async -> Bool {
+ return await connectionLock.withLock {
+ await performConnect()
+ }
+ }
+
+ private func performConnect() async -> Bool {
+ if isConnectedFlag {
+ return true
+ }
+
do {
// 1. 创建MCP客户端
- mcpClient = Client(name: "deep_voice_\(serverId)", version: "1.0.0")
+ self.mcpClient = Client(name: "deep_voice_\(serverId)", version: "1.0.0")
- // 2. 创建自定义SSE传输层
+ // 2. 创建自定义SSE传输层,添加断线回调
let swiftLogger = Logger(label: "mcp.transport.custom_sse.\(serverId)")
let transport = CustomSseClientTransport(
urlString: serverUrl,
reconnectionTime: 1.0,
- logger: swiftLogger
+ logger: swiftLogger,
+ onConnectionLost: { [weak self] in
+ Task {
+ await self?.handleConnectionLost()
+ }
+ }
)
- // 3. 连接到服务器
- _ = try await mcpClient!.connect(transport: transport)
+ // 3. 连接到服务器(添加超时)
+ try await withTimeout(seconds: 10) {
+ try await self.mcpClient!.connect(transport: transport)
+ }
// 4. 获取工具列表
- let (toolList, _) = try await mcpClient!.listTools()
+ let (toolList, _) = try await self.mcpClient!.listTools()
// 5. 处理工具列表
processTools(toolList)
isConnectedFlag = true
+ retryCount = 0 // 重置重试计数
+ currentReconnectDelay = initialReconnectDelay // 重置延迟
+
return true
} catch {
+ await handleConnectionError(error as! Error)
return false
}
}
@@ -130,7 +163,32 @@ public class MCPSubClient {
]
}
+ /// 检查连接状态
+ /// 检查连接状态
+ public func checkConnection() async -> Bool {
+ if !isConnectedFlag {
+ return await connect()
+ }
+
+ // 检查传输层连接状态
+ // 这里可以添加ping测试或其他连接验证
+
+ return isConnectedFlag
+ }
+
+ /// 调用工具前检查连接
public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? {
+ // 先检查连接状态
+ if !(await checkConnection()) {
+ return [
+ "content": [[
+ "type": "text",
+ "text": "Tool call failed: mcp: service not connected"
+ ]],
+ "isError": true
+ ]
+ }
+
guard isConnectedFlag, let client = mcpClient else {
return createErrorResult("Not connected to MCP server")
}
@@ -227,6 +285,74 @@ public class MCPSubClient {
}
isConnectedFlag = false
}
+
+ /// 处理连接断开
+ private func handleConnectionLost() async {
+ await connectionLock.withLock {
+ if isConnectedFlag {
+ print("[\(serverId)] 连接已断开,更新状态")
+ isConnectedFlag = false
+
+ // 启动重连逻辑
+ startReconnection()
+ }
+ }
+ }
+
+ /// 启动重连
+ private func startReconnection() {
+ // 取消之前的重连任务
+ reconnectTask?.cancel()
+
+ reconnectTask = Task { [weak self] in
+ await self?.performReconnection()
+ }
+ }
+
+ /// 执行重连
+ private func performReconnection() async {
+ while retryCount < maxRetryAttempts && !Task.isCancelled {
+ retryCount += 1
+
+ print("[\(serverId)] 尝试重连 (\(retryCount)/\(maxRetryAttempts)),延迟 \(currentReconnectDelay)秒")
+
+ try? await Task.sleep(for: .seconds(currentReconnectDelay))
+
+ if await connect() {
+ print("[\(serverId)] 重连成功")
+ return
+ }
+
+ // 指数退避
+ currentReconnectDelay = min(currentReconnectDelay * 2, maxReconnectDelay)
+ }
+
+ if retryCount >= maxRetryAttempts {
+ print("[\(serverId)] 已达到最大重试次数,停止重连")
+ }
+ }
+
+ /// 处理连接错误
+ private func handleConnectionError(_ error: Error) async {
+ print("[\(serverId)] 连接错误: \(error.localizedDescription)")
+ isConnectedFlag = false
+
+ // 如果是MCP特定错误,可以进行特殊处理
+ if let mcpError = error as? MCPError {
+ // 处理MCP特定错误
+ print("[\(serverId)] MCP错误类型: \(mcpError)")
+ }
+
+ // 如果还有重试机会,启动重连
+ // if retryCount < maxRetryAttempts {
+ // startReconnection()
+ // }
+ }
+
+ /// 获取连接状态
+ public func getConnectionStatus() -> Bool {
+ return isConnectedFlag
+ }
}
/// MCP客户端
@@ -285,6 +411,25 @@ public class MCPClient {
}
}
+ // 添加连接成功后的工具列表汇总输出
+ if connectedCount > 0 {
+ print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器")
+ print("[MCPClient] 📋 所有可用工具汇总:")
+
+ let allTools = getToolMaps()
+ for (index, toolMap) in allTools.enumerated() {
+ if let function = toolMap["function"] as? [String: Any],
+ let name = function["name"] as? String,
+ let description = function["description"] as? String {
+ print("[MCPClient] \(index + 1). \(name) - \(description)")
+ }
+ }
+
+ print("[MCPClient] 📊 工具总数: \(allTools.count)")
+ print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个")
+ print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个")
+ }
+
return connectedCount > 0
}
@@ -298,6 +443,7 @@ public class MCPClient {
parameters: Any,
handler: FunctionHandler
) -> Bool {
+ print("[MCPClient] 注册本地函数: \(name)")
localFunctions[name] = handler
let functionDef: [String: Any] = [
@@ -307,6 +453,9 @@ public class MCPClient {
]
localFunctionDefs[name] = functionDef
+ print("[MCPClient] 本地函数 \(name) 注册成功")
+ print("[MCPClient] 当前本地函数列表: \(Array(localFunctions.keys))")
+
return true
}
@@ -329,43 +478,69 @@ public class MCPClient {
}
for client in subClients.values {
- allToolMaps.append(contentsOf: client.getToolMaps())
+ let tools = client.getToolMaps()
+ print("[MCPClient] 读取工具列表: \(tools)")
+ allToolMaps.append(contentsOf: tools)
}
return allToolMaps
}
public func getToolType(name: String) -> ToolType? {
+ print("[MCPClient] 检查工具类型: \(name)")
+ print("[MCPClient] 本地函数列表: \(Array(localFunctions.keys))")
+
if localFunctions.keys.contains(name) {
+ print("[MCPClient] \(name) 是本地函数")
return .localFunction
}
if subClients.values.contains(where: { $0.containsTool(name: name) }) {
+ print("[MCPClient] \(name) 是 MCP 工具")
return .mcpTool
}
+ print("[MCPClient] \(name) 未找到")
return nil
}
public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? {
+ print("[MCPClient] 开始调用工具: \(name)")
+ print("[MCPClient] 参数: \(arguments)")
+ print("[MCPClient] 本地函数列表: \(Array(localFunctions.keys))")
+ print("[MCPClient] 子客户端数量: \(subClients.count)")
+
+ // 检查本地函数
if let handler = localFunctions[name] {
+ print("[MCPClient] 找到本地函数: \(name),开始执行")
do {
let result = try await handler.handle(arguments: arguments)
+ print("[MCPClient] 本地函数 \(name) 执行成功")
return ["context": result]
} catch {
+ print("[MCPClient] 本地函数 \(name) 执行失败: \(error.localizedDescription)")
return [
"content": [["type": "text", "text": "Function call failed: \(error.localizedDescription)"]],
"isError": true
]
}
+ } else {
+ print("[MCPClient] 未找到本地函数: \(name)")
}
- for client in subClients.values {
+ // 检查MCP工具
+ print("[MCPClient] 开始检查 MCP 工具")
+ for (serverId, client) in subClients {
+ print("[MCPClient] 检查服务器 \(serverId) 是否包含工具 \(name)")
if client.containsTool(name: name) {
- return await client.callTool(name: name, arguments: arguments)
+ print("[MCPClient] 在服务器 \(serverId) 中找到工具 \(name),开始远程调用")
+ let result = await client.callTool(name: name, arguments: arguments)
+ print("[MCPClient] 远程工具 \(name) 调用完成")
+ return result
}
}
+ print("[MCPClient] 工具 \(name) 未找到")
return [
"content": [["type": "text", "text": "Tool not found: \(name)"]],
"isError": true
@@ -386,7 +561,7 @@ public class MCPClient {
}
public func isConnected() -> Bool {
- return !subClients.isEmpty
+ return !subClients.isEmpty && subClients.values.allSatisfy { $0.getConnectionStatus() }
}
public func disconnectAll() async {
@@ -395,4 +570,40 @@ public class MCPClient {
}
subClients.removeAll()
}
-}
\ No newline at end of file
+}
+
+// 扩展NSLock以支持async/await
+extension NSLock {
+ func withLock(_ body: () async throws -> T) async rethrows -> T {
+ lock()
+ defer { unlock() }
+ return try await body()
+ }
+}
+
+// 添加超时支持
+func withTimeout(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T {
+ return try await withThrowingTaskGroup(of: T.self) { group in
+ group.addTask {
+ try await operation()
+ }
+
+ group.addTask {
+ try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
+ throw TimeoutError()
+ }
+
+ guard let result = try await group.next() else {
+ throw TimeoutError()
+ }
+
+ group.cancelAll()
+ return result
+ }
+}
+
+struct TimeoutError: Swift.Error {
+ var localizedDescription: String {
+ return "操作超时"
+ }
+}
diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
index 41c3df391..6a553d692 100644
--- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
+++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
@@ -312,58 +312,186 @@ public class SystemFunctionHandler: NSObject {
// MARK: - 功能处理器实现
/// 获取当前位置处理器
+// 移除这个重复的函数定义(第454-470行)
+// private func withTimeout(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T {
+// ...
+// }
+
+// 在 GetCurrentLocationHandler 中修改超时处理
private class GetCurrentLocationHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
- // 检查位置权限
- guard LocationService.shared.hasLocationPermission() else {
- let errorResult: [String: Any] = [
- "success": false,
- "message": "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限"
- ]
- let jsonData = try JSONSerialization.data(withJSONObject: errorResult)
- return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}"
- }
+ print("[GetCurrentLocationHandler] 开始处理位置请求")
- // 先尝试获取缓存的位置数据
- if let cachedLocation = LocationService.shared.getCachedLocationData() {
- let message = "当前位置:\(cachedLocation.address ?? "未知地址")(纬度 \(cachedLocation.latitude),经度 \(cachedLocation.longitude))"
+ return await withCheckedContinuation { continuation in
+ // 设置超时
+ let timeoutTask = Task {
+ try await Task.sleep(nanoseconds: 15_000_000_000) // 15秒
+ continuation.resume(returning: self.createErrorResponse(message: "位置获取超时,请检查网络连接后重试"))
+ }
- // 异步触发最新定位(不等待结果)
- Task {
- do {
- _ = try await LocationService.shared.getCurrentLocation()
- } catch {
- print("后台更新位置失败: \(error)")
+ LocationService.shared.getCurrentLocation { result in
+ timeoutTask.cancel()
+
+ switch result {
+ case .success(let locationInfo):
+ print("[GetCurrentLocationHandler] 位置获取成功")
+
+ // 解析位置数据
+ guard let latStr = locationInfo["user_lat"] as? String,
+ let lngStr = locationInfo["user_lng"] as? String,
+ let latitude = Double(latStr),
+ let longitude = Double(lngStr),
+ let address = locationInfo["user_location"] as? String else {
+ print("[GetCurrentLocationHandler] 位置数据格式错误")
+ continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试"))
+ return
+ }
+
+ print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)")
+
+ let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))"
+ let successResponse = self.createSuccessResponse(message: message, latitude: latitude, longitude: longitude, address: address)
+
+ print("[GetCurrentLocationHandler] 返回位置结果")
+ continuation.resume(returning: successResponse)
+
+ case .failure(let error):
+ print("[GetCurrentLocationHandler] 位置获取失败: \(error.localizedDescription)")
+ let errorMessage = self.getUserFriendlyErrorMessage(error)
+ continuation.resume(returning: self.createErrorResponse(message: errorMessage))
}
}
-
- let result: [String: Any] = [
- "success": true,
- "message": message
- ]
- let jsonData = try JSONSerialization.data(withJSONObject: result)
- return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}"
}
+ }
+
+ private func createSuccessResponse(message: String, latitude: Double, longitude: Double, address: String) -> String {
+ let result: [String: Any] = [
+ "success": true,
+ "message": message,
+ "data": [
+ "latitude": latitude,
+ "longitude": longitude,
+ "address": address,
+ "timestamp": Date().timeIntervalSince1970 * 1000
+ ]
+ ]
- // 如果没有缓存数据,同步获取位置
do {
- let locationData = try await LocationService.shared.getCurrentLocation()
- let message = "当前位置:\(locationData.address ?? "未知地址")(纬度 \(locationData.latitude),经度 \(locationData.longitude))"
-
- let result: [String: Any] = [
- "success": true,
- "message": message
- ]
let jsonData = try JSONSerialization.data(withJSONObject: result)
return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}"
} catch {
- let errorResult: [String: Any] = [
- "success": false,
- "message": "获取位置失败:\(error.localizedDescription)"
- ]
+ return "{\"error\": \"JSON序列化失败\"}"
+ }
+ }
+
+ private func createErrorResponse(message: String) -> String {
+ let errorResult: [String: Any] = [
+ "success": false,
+ "message": message
+ ]
+
+ do {
let jsonData = try JSONSerialization.data(withJSONObject: errorResult)
return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}"
+ } catch {
+ return "{\"error\": \"JSON序列化失败\"}"
+ }
+ }
+
+ private func getUserFriendlyErrorMessage(_ error: Error) -> String {
+ let errorDescription = error.localizedDescription.lowercased()
+
+ if errorDescription.contains("permission") || errorDescription.contains("权限") {
+ return "需要位置权限才能获取当前位置,请在设置中开启位置权限"
+ } else if errorDescription.contains("network") || errorDescription.contains("网络") {
+ return "网络连接异常,请检查网络后重试"
+ } else if errorDescription.contains("service") || errorDescription.contains("服务") {
+ return "位置服务暂时不可用,请稍后重试"
+ } else {
+ return "获取位置失败:\(error.localizedDescription)"
+ }
+ }
+}
+
+// MARK: - 支持结构体
+
+private struct LocationResult: Codable {
+ let success: Bool
+ let message: String
+ let data: LocationResultData?
+ let error: LocationErrorInfo?
+ let executionTime: TimeInterval
+ let timestamp: Double
+
+ init(success: Bool, message: String, data: LocationResultData? = nil, error: LocationErrorInfo? = nil, executionTime: TimeInterval) {
+ self.success = success
+ self.message = message
+ self.data = data
+ self.error = error
+ self.executionTime = executionTime
+ self.timestamp = Date().timeIntervalSince1970 * 1000
+ }
+
+ func toJSONString() throws -> String {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let jsonData = try encoder.encode(self)
+ return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}"
+ }
+}
+
+private struct LocationResultData: Codable {
+ let latitude: Double
+ let longitude: Double
+ let address: String?
+ let timestamp: Double
+ let executionTime: TimeInterval
+}
+
+private struct LocationErrorInfo: Codable {
+ let code: String
+ let type: String
+ let description: String
+}
+
+
+// MARK: - 自定义错误类型
+
+private enum LocationError: Error {
+ case timeout
+ case permissionDenied
+ case serviceUnavailable
+ case invalidData
+ case networkError
+
+ var code: String {
+ switch self {
+ case .timeout: return "TIMEOUT"
+ case .permissionDenied: return "PERMISSION_DENIED"
+ case .serviceUnavailable: return "SERVICE_UNAVAILABLE"
+ case .invalidData: return "INVALID_DATA"
+ case .networkError: return "NETWORK_ERROR"
+ }
+ }
+
+ var type: String {
+ switch self {
+ case .timeout: return "timeout"
+ case .permissionDenied: return "permission"
+ case .serviceUnavailable: return "service"
+ case .invalidData: return "data"
+ case .networkError: return "network"
+ }
+ }
+
+ var userFriendlyMessage: String {
+ switch self {
+ case .timeout: return "位置获取超时,请检查网络连接后重试"
+ case .permissionDenied: return "需要位置权限才能获取当前位置,请在设置中开启位置权限"
+ case .serviceUnavailable: return "位置服务暂时不可用,请稍后重试"
+ case .invalidData: return "位置数据格式错误,请重试"
+ case .networkError: return "网络连接异常,请检查网络后重试"
}
}
}
@@ -587,3 +715,8 @@ private class OpenRecorderHandler: FunctionHandler {
return "{\"result\": \"无法打开录音机应用\"}"
}
}
+
+// 同时移除 LocationError 枚举定义(第472-510行),因为它没有被使用
+// private enum LocationError: Error {
+// ...
+// }
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 04c991abc..2312b1fc4 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
@@ -9,349 +9,368 @@ public extension Notification.Name {
public class LocationService: NSObject {
private static let TAG = "LocationService"
- private static let CACHE_PREFS_NAME = "location_cache"
- private static let PERMISSION_REQUEST_CODE = 1001
public static let shared = LocationService()
private var locationManager: CLLocationManager?
- private var initialized = false
- private var permissionRetryTimer: Timer?
-
- // 存储当前的定位请求回调(Flutter用)
+ private var cachedLocationData: LocationData?
private var currentLocationCallback: ((Result<[String: Any], Error>) -> Void)?
- // 存储权限请求回调(Flutter用)
- private var permissionRequestCallback: ((Result<[String: Bool], Error>) -> Void)?
- // 存储服务间调用的回调
- private var serviceCallbacks: [LocationCallback] = []
+ // 缓存有效期(5分钟)
+ private let cacheValidityDuration: TimeInterval = 5 * 60
- // 缓存的位置数据
- private var cachedLocationData: LocationData?
+ // 权限状态缓存
+ private var authorizationStatus: CLAuthorizationStatus = .notDetermined
+ private var isLocationServicesEnabled: Bool = false
private override init() {
super.init()
- }
-
- /// 检查应用是否在前台运行
- public func isAppInForeground() -> Bool {
- return UIApplication.shared.applicationState == .active
- }
-
- /// 初始化定位服务
- public func initialize() {
- if initialized { return }
-
- if !checkPermissions() {
- print("\(LocationService.TAG): 没有定位权限,等待用户授权中...")
- startPermissionMonitor()
- return
- }
-
setupLocationManager()
- initialized = true
- print("\(LocationService.TAG): 定位服务初始化完成")
}
private func setupLocationManager() {
- locationManager = CLLocationManager()
- locationManager?.delegate = self
- locationManager?.desiredAccuracy = kCLLocationAccuracyBest
- locationManager?.distanceFilter = 10
- }
-
- private func startPermissionMonitor() {
- permissionRetryTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
- guard let self = self else { return }
- let hasPermission = self.checkPermissions()
- if hasPermission && !self.initialized {
- print("\(LocationService.TAG): 权限已授予,重新初始化定位服务")
- self.initialize()
- self.stopPermissionMonitor()
- }
+ DispatchQueue.main.async { [weak self] in
+ self?.locationManager = CLLocationManager()
+ self?.locationManager?.delegate = self
+ self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
+ self?.locationManager?.distanceFilter = 10
+
+ // 初始化时获取当前状态
+ self?.authorizationStatus = CLLocationManager.authorizationStatus()
+ self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled()
+
+ print("[LocationService] LocationManager 初始化完成")
+ print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)")
+ print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)")
}
}
- private func stopPermissionMonitor() {
- permissionRetryTimer?.invalidate()
- permissionRetryTimer = nil
- }
-
- /// 检查定位权限
- private func checkPermissions() -> Bool {
- return hasForegroundLocationPermission()
- }
-
- private func hasForegroundLocationPermission() -> Bool {
- let status = CLLocationManager.authorizationStatus()
- return status == .authorizedWhenInUse || status == .authorizedAlways
- }
-
- private func hasBackgroundLocationPermission() -> Bool {
- return CLLocationManager.authorizationStatus() == .authorizedAlways
- }
-
- /// 检查定位权限状态(Flutter用)
- public func checkLocationPermission(completion: @escaping (Result<[String: Bool], Error>) -> Void) {
- let hasPermission = checkPermissions()
- let result: [String: Bool] = [
- "hasPermission": hasPermission,
- "hasForegroundPermission": hasForegroundLocationPermission(),
- "hasBackgroundPermission": hasBackgroundLocationPermission()
- ]
- completion(.success(result))
- }
-
- /// 请求定位权限(Flutter用)
- public func requestLocationPermission(completion: @escaping (Result<[String: Bool], Error>) -> Void) {
- if checkPermissions() {
- completion(.success(["granted": true]))
+ /// 获取当前位置 - 主要接口
+ public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
+ print("[LocationService] 开始获取当前位置")
+
+ // 1. 检查缓存数据是否存在且有效
+ if let cachedData = getCachedLocationIfValid() {
+ print("[LocationService] 返回缓存位置数据")
+ let locationInfo = formatLocationData(cachedData)
+ completion(.success(locationInfo))
+
+ // 异步更新缓存
+ print("[LocationService] 异步更新位置缓存")
+ asyncUpdateLocation()
return
}
- permissionRequestCallback = completion
- locationManager?.requestWhenInUseAuthorization()
+ // 2. 缓存不存在或已过期,检查权限并同步获取位置
+ print("[LocationService] 缓存无效,开始同步获取位置")
+ syncGetLocation(completion: completion)
}
- /// 按需获取当前位置(Flutter用)
- public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
- if !checkPermissions() {
- completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "需要定位权限"])))
- return
+ /// 检查缓存是否有效
+ private func getCachedLocationIfValid() -> LocationData? {
+ guard let cached = cachedLocationData else {
+ print("[LocationService] 无缓存数据")
+ return nil
}
- if !initialized {
- completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "定位服务未初始化"])))
- return
- }
+ let currentTime = Date().timeIntervalSince1970 * 1000
+ let cacheAge = currentTime - cached.timestamp
- if currentLocationCallback != nil {
- completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "正在定位中,请稍后再试"])))
- return
+ if cacheAge < cacheValidityDuration * 1000 {
+ print("[LocationService] 缓存有效,年龄: \(cacheAge/1000)秒")
+ return cached
+ } else {
+ print("[LocationService] 缓存已过期,年龄: \(cacheAge/1000)秒")
+ return nil
}
-
- // 保存回调,开始定位
- currentLocationCallback = completion
- locationManager?.requestLocation()
}
- // MARK: - 供其他iOS服务使用的接口
-
- /// 检查是否有定位权限(供其他服务使用)
- public func hasLocationPermission() -> Bool {
- return checkPermissions()
+ /// 异步更新位置缓存
+ private func asyncUpdateLocation() {
+ // 检查权限
+ guard checkLocationPermission() else {
+ print("[LocationService] 异步更新失败:无位置权限")
+ return
+ }
+
+ // 异步请求位置更新
+ DispatchQueue.global(qos: .background).async { [weak self] in
+ self?.requestLocationUpdate { result in
+ switch result {
+ case .success(let locationData):
+ print("[LocationService] 异步更新位置缓存成功")
+ self?.cachedLocationData = locationData
+ case .failure(let error):
+ print("[LocationService] 异步更新位置缓存失败: \(error.localizedDescription)")
+ }
+ }
+ }
}
- /// 异步获取当前位置(供其他iOS服务使用)
- public func getCurrentLocation() async throws -> LocationData {
- return try await withCheckedThrowingContinuation { continuation in
- getCurrentLocation { result in
+ /// 同步获取位置
+ private func syncGetLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
+ // 检查权限
+ guard checkLocationPermission() else {
+ print("[LocationService] 无位置权限,尝试请求权限")
+ requestLocationPermission { [weak self] result in
switch result {
- case .success(let locationInfo):
- // 从返回的字典中提取位置信息
- if let latStr = locationInfo["user_lat"] as? String,
- let lngStr = locationInfo["user_lng"] as? String,
- let latitude = Double(latStr),
- let longitude = Double(lngStr),
- let address = locationInfo["user_location"] as? String {
-
- let locationData = LocationData(
- latitude: latitude,
- longitude: longitude,
- address: address,
- timestamp: Date().timeIntervalSince1970 * 1000
- )
- continuation.resume(returning: locationData)
+ case .success(let granted):
+ if granted {
+ print("[LocationService] 权限获取成功,重新获取位置")
+ self?.syncGetLocation(completion: completion)
} else {
- continuation.resume(throwing: NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"]))
+ completion(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "位置权限被拒绝"])))
}
case .failure(let error):
- continuation.resume(throwing: error)
+ completion(.failure(error))
}
}
- }
- }
-
- /// 获取当前位置(供其他iOS服务使用)
- public func requestLocation(callback: LocationCallback) {
- if !checkPermissions() {
- callback.onLocationError(errorCode: -2, errorMessage: "没有定位权限")
- return
- }
-
- if !initialized {
- callback.onLocationError(errorCode: -3, errorMessage: "定位服务未初始化")
return
}
- // 添加到回调列表
- serviceCallbacks.append(callback)
-
- // 如果没有正在进行的定位请求,开始定位
- if currentLocationCallback == nil && serviceCallbacks.count == 1 {
- locationManager?.requestLocation()
+ // 有权限,直接获取位置
+ requestLocationUpdate { [weak self] result in
+ switch result {
+ case .success(let locationData):
+ print("[LocationService] 同步获取位置成功")
+ self?.cachedLocationData = locationData
+ let locationInfo = self?.formatLocationData(locationData) ?? [:]
+ completion(.success(locationInfo))
+ case .failure(let error):
+ print("[LocationService] 同步获取位置失败: \(error.localizedDescription)")
+ completion(.failure(error))
+ }
}
}
- /// 同步获取缓存的位置信息(供其他服务使用)
- /// 注意:这个方法返回的可能是过期的位置信息
- public func getCachedLocationData() -> LocationData? {
- // 优先返回内存中的缓存
- if let cached = cachedLocationData {
- return cached
- }
-
- // 从UserDefaults中读取缓存
- return loadCachedLocationFromUserDefaults()
+ /// 检查位置权限 - 使用缓存的状态
+ private func checkLocationPermission() -> Bool {
+ return authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways
}
- private func saveCachedLocationToUserDefaults(_ locationData: LocationData) {
- do {
- let encoder = JSONEncoder()
- let data = try encoder.encode(locationData)
- UserDefaults.standard.set(data, forKey: "latitude")
- UserDefaults.standard.set(String(locationData.latitude), forKey: "latitude")
- UserDefaults.standard.set(String(locationData.longitude), forKey: "longitude")
- UserDefaults.standard.set(locationData.address, forKey: "address")
- UserDefaults.standard.set(Double(locationData.timestamp), forKey: "timestamp")
- UserDefaults.standard.synchronize()
- } catch {
- print("\(LocationService.TAG): 保存位置缓存失败: \(error.localizedDescription)")
+ /// 请求位置权限 - 异步处理
+ private func requestLocationPermission(completion: @escaping (Result) -> Void) {
+ print("[LocationService] 当前缓存的权限状态: \(authorizationStatus.rawValue)")
+
+ switch authorizationStatus {
+ case .notDetermined:
+ print("[LocationService] 请求位置权限")
+ // 保存回调,等待权限结果
+ permissionCallback = completion
+ DispatchQueue.main.async { [weak self] in
+ self?.locationManager?.requestWhenInUseAuthorization()
+ }
+ case .denied, .restricted:
+ print("[LocationService] 位置权限被拒绝或受限")
+ completion(.success(false))
+ case .authorizedWhenInUse, .authorizedAlways:
+ print("[LocationService] 已有位置权限")
+ completion(.success(true))
+ @unknown default:
+ completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"])))
}
}
- private func loadCachedLocationFromUserDefaults() -> LocationData? {
- do {
- guard let latitudeStr = UserDefaults.standard.string(forKey: "latitude"),
- let longitudeStr = UserDefaults.standard.string(forKey: "longitude"),
- let latitude = Double(latitudeStr),
- let longitude = Double(longitudeStr) else {
- return nil
+ /// 请求位置更新 - 优化版本
+ private func requestLocationUpdate(completion: @escaping (Result) -> Void) {
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ guard let locationManager = self.locationManager else {
+ print("[LocationService] 错误:LocationManager 未初始化")
+ completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "LocationManager 未初始化"])))
+ return
}
- let address = UserDefaults.standard.string(forKey: "address") ?? "未知地址"
- let timestampDouble = UserDefaults.standard.double(forKey: "timestamp")
+ // 检查是否已有请求在进行
+ if self.currentLocationCallback != nil {
+ print("[LocationService] 错误:位置请求正在进行中,拒绝新请求")
+ completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "位置请求正在进行中"])))
+ return
+ }
- if timestampDouble == 0 { return nil }
+ // 使用缓存的状态信息
+ print("[LocationService] 开始位置请求")
+ print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)")
+ print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)")
+ print("[LocationService] 当前线程: \(Thread.current)")
+ print("[LocationService] 是否主线程: \(Thread.isMainThread)")
- return LocationData(
- latitude: latitude,
- longitude: longitude,
- address: address,
- timestamp: TimeInterval(timestampDouble)
- )
- } catch {
- print("\(LocationService.TAG): 读取位置缓存失败: \(error.localizedDescription)")
- return nil
- }
- }
-
- /// 停止定位服务
- public func stop() {
- locationManager?.stopUpdatingLocation()
- locationManager = nil
- currentLocationCallback = nil
- permissionRequestCallback = nil
- serviceCallbacks.removeAll()
- initialized = false
- stopPermissionMonitor()
- }
-
- private func handleLocationResult(_ location: CLLocation?, error: Error?) {
- // 处理Flutter回调
- let flutterCallback = currentLocationCallback
- currentLocationCallback = nil
-
- if let location = location, error == nil {
- print("\(LocationService.TAG): 定位成功: \(location.coordinate.latitude), \(location.coordinate.longitude)")
+ // 验证权限状态
+ if !self.checkLocationPermission() {
+ print("[LocationService] 错误:无位置权限")
+ completion(.failure(NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "无位置权限"])))
+ return
+ }
- // 进行逆地理编码获取地址
- let geocoder = CLGeocoder()
- geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, geocodeError in
- guard let self = self else { return }
-
- let address = placemarks?.first?.name ?? "未知地址"
-
- // 缓存位置数据
- let locationData = LocationData(
- latitude: location.coordinate.latitude,
- longitude: location.coordinate.longitude,
- address: address,
- timestamp: TimeInterval(Date().timeIntervalSince1970) * 1000
- )
- self.cachedLocationData = locationData
- self.saveCachedLocationToUserDefaults(locationData)
-
- // 通知Flutter
- if let callback = flutterCallback {
- let result: [String: Any] = [
- "user_location": address,
- "user_lat": String(location.coordinate.latitude),
- "user_lng": String(location.coordinate.longitude)
- ]
- callback(.success(result))
- }
-
- // 通知其他iOS服务
- self.serviceCallbacks.forEach { callback in
- callback.onLocationSuccess(
- latitude: location.coordinate.latitude,
- longitude: location.coordinate.longitude,
- address: address
- )
- }
-
- // 清空服务回调列表(单次定位)
- self.serviceCallbacks.removeAll()
+ if !self.isLocationServicesEnabled {
+ print("[LocationService] 错误:位置服务未启用")
+ completion(.failure(NSError(domain: "LocationService", code: -6, userInfo: [NSLocalizedDescriptionKey: "位置服务未启用"])))
+ return
}
- } else {
- let errorMessage = error?.localizedDescription ?? "未知错误"
- print("\(LocationService.TAG): 定位失败: \(errorMessage)")
- // 通知Flutter
- if let callback = flutterCallback {
- callback(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "定位失败: \(errorMessage)"])))
+ // 强制重新设置 delegate
+ if locationManager.delegate == nil {
+ print("[LocationService] 警告:Delegate 为空,重新设置")
+ locationManager.delegate = self
}
- // 通知其他iOS服务
- serviceCallbacks.forEach { callback in
- callback.onLocationError(errorCode: -1, errorMessage: errorMessage)
+ // 验证 delegate
+ if locationManager.delegate === self {
+ print("[LocationService] Delegate 验证成功")
+ } else {
+ print("[LocationService] 错误:Delegate 设置失败")
+ completion(.failure(NSError(domain: "LocationService", code: -8, userInfo: [NSLocalizedDescriptionKey: "Delegate 设置失败"])))
+ return
}
- // 清空服务回调列表
- serviceCallbacks.removeAll()
+ // 设置回调
+ self.currentLocationCallback = { result in
+ switch result {
+ case .success(let locationInfo):
+ print("[LocationService] 位置回调成功,开始解析数据")
+ if let latStr = locationInfo["user_lat"] as? String,
+ let lngStr = locationInfo["user_lng"] as? String,
+ let latitude = Double(latStr),
+ let longitude = Double(lngStr) {
+
+ print("[LocationService] 位置数据解析成功: lat=\(latitude), lng=\(longitude)")
+ let locationData = LocationData(
+ latitude: latitude,
+ longitude: longitude,
+ address: "",
+ timestamp: Date().timeIntervalSince1970 * 1000
+ )
+ completion(.success(locationData))
+ } else {
+ print("[LocationService] 错误:位置数据格式错误,原始数据: \(locationInfo)")
+ completion(.failure(NSError(domain: "LocationService", code: -7, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"])))
+ }
+ case .failure(let error):
+ print("[LocationService] 位置回调失败: \(error.localizedDescription)")
+ completion(.failure(error))
+ }
+ }
+
+ print("[LocationService] 调用 locationManager.requestLocation()")
+ locationManager.requestLocation()
+ print("[LocationService] requestLocation() 调用完成,等待回调")
+
+ // 30秒超时机制
+ DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
+ if self.currentLocationCallback != nil {
+ print("[LocationService] 位置请求超时")
+ let callback = self.currentLocationCallback
+ self.currentLocationCallback = nil
+ callback?(.failure(NSError(domain: "LocationService", code: -9, userInfo: [NSLocalizedDescriptionKey: "位置请求超时"])))
+ }
+ }
}
}
+
+ /// 格式化位置数据
+ private func formatLocationData(_ locationData: LocationData) -> [String: Any] {
+ return [
+ "user_lat": String(locationData.latitude),
+ "user_lng": String(locationData.longitude),
+ "user_location": locationData.address,
+ "timestamp": locationData.timestamp
+ ]
+ }
+
+ // MARK: - 权限回调
+ private var permissionCallback: ((Result) -> Void)?
}
// MARK: - CLLocationManagerDelegate
extension LocationService: CLLocationManagerDelegate {
+ // 替换 didUpdateLocations 方法中的逆地理编码部分
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
- guard let location = locations.last else { return }
- handleLocationResult(location, error: nil)
+ print("[LocationService] didUpdateLocations 被调用,位置数量: \(locations.count)")
+
+ guard let location = locations.last else {
+ print("[LocationService] 错误:locations 数组为空")
+ return
+ }
+
+ print("[LocationService] 位置更新成功: lat=\(location.coordinate.latitude), lng=\(location.coordinate.longitude)")
+ print("[LocationService] 位置精度: \(location.horizontalAccuracy)m")
+
+ // 直接使用经纬度,不进行逆地理编码
+ let locationInfo: [String: Any] = [
+ "user_lat": String(location.coordinate.latitude),
+ "user_lng": String(location.coordinate.longitude),
+ "timestamp": Date().timeIntervalSince1970 * 1000
+ ]
+
+ print("[LocationService] 准备调用位置回调")
+ // 调用回调
+ let callback = currentLocationCallback
+ currentLocationCallback = nil
+ callback?(.success(locationInfo))
+ print("[LocationService] 位置回调已调用")
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
- handleLocationResult(nil, error: error)
+ print("[LocationService] didFailWithError 被调用")
+ print("[LocationService] 位置获取失败: \(error.localizedDescription)")
+ print("[LocationService] 错误代码: \((error as NSError).code)")
+ print("[LocationService] 错误域: \((error as NSError).domain)")
+
+ let callback = currentLocationCallback
+ currentLocationCallback = nil
+ print("[LocationService] 准备调用错误回调")
+ callback?(.failure(error))
+ print("[LocationService] 错误回调已调用")
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
- print("\(LocationService.TAG): 定位权限状态变更: \(status.rawValue)")
+ print("[LocationService] didChangeAuthorization 被调用")
+ print("[LocationService] 权限状态变更: \(status.rawValue) (\(authorizationStatusString(status)))")
+
+ // 更新缓存的状态
+ authorizationStatus = status
+ isLocationServicesEnabled = CLLocationManager.locationServicesEnabled()
+
+ print("[LocationService] 更新缓存状态 - 权限: \(authorizationStatus.rawValue), 服务启用: \(isLocationServicesEnabled)")
+
+ guard let callback = permissionCallback else {
+ print("[LocationService] 无权限回调,忽略权限变更")
+ return
+ }
switch status {
case .authorizedWhenInUse, .authorizedAlways:
- if !initialized {
- initialize()
- }
- permissionRequestCallback?(.success(["granted": true]))
- permissionRequestCallback = nil
+ print("[LocationService] 权限获取成功")
+ permissionCallback = nil
+ callback(.success(true))
case .denied, .restricted:
- permissionRequestCallback?(.success(["granted": false]))
- permissionRequestCallback = nil
+ print("[LocationService] 权限被拒绝或受限")
+ permissionCallback = nil
+ callback(.success(false))
case .notDetermined:
- break
+ print("[LocationService] 权限状态仍未确定,继续等待")
+ // 继续等待用户选择
+ permissionCallback = callback
@unknown default:
- break
+ print("[LocationService] 未知权限状态")
+ permissionCallback = nil
+ callback(.failure(NSError(domain: "LocationService", code: -10, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"])))
+ }
+ }
+
+ // 辅助方法:将权限状态转换为可读字符串
+ private func authorizationStatusString(_ status: CLAuthorizationStatus) -> String {
+ switch status {
+ case .notDetermined: return "notDetermined"
+ case .restricted: return "restricted"
+ case .denied: return "denied"
+ case .authorizedAlways: return "authorizedAlways"
+ case .authorizedWhenInUse: return "authorizedWhenInUse"
+ @unknown default: return "unknown"
}
}
}
+
+// Remove the duplicate LocationData definition from here
+// The LocationData struct is already defined in LocationData.swift
diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift
index 80b383126..ba8222b63 100644
--- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift
+++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift
@@ -13,16 +13,10 @@ public class LocationServicePlugin: NSObject, FlutterPlugin {
registrar.addMethodCallDelegate(instance, channel: channel)
self.channel = channel
- instance.addStartLocationMonitor()
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
- case "location":
- result(nil)
- case "initialize":
- LocationService.shared.initialize()
- result(nil)
case "getCurrentLocation":
LocationService.shared.getCurrentLocation { locationResult in
switch locationResult {
@@ -32,41 +26,9 @@ public class LocationServicePlugin: NSObject, FlutterPlugin {
result(FlutterError(code: "LOCATION_ERROR", message: error.localizedDescription, details: nil))
}
}
- case "checkPermission":
- LocationService.shared.checkLocationPermission { permissionResult in
- switch permissionResult {
- case .success(let permissions):
- result(permissions)
- case .failure(let error):
- result(FlutterError(code: "PERMISSION_ERROR", message: error.localizedDescription, details: nil))
- }
- }
- case "requestPermission":
- LocationService.shared.requestLocationPermission { permissionResult in
- switch permissionResult {
- case .success(let permissions):
- result(permissions)
- case .failure(let error):
- result(FlutterError(code: "PERMISSION_ERROR", message: error.localizedDescription, details: nil))
- }
- }
- case "stop":
- LocationService.shared.stop()
- result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
-
- private func addStartLocationMonitor() {
- NotificationCenter.default.addObserver(
- forName: .locationStartEvent,
- object: nil,
- queue: .main
- ) { notification in
- print("📥 收到启动定位服务事件")
- LocationService.shared.initialize()
- }
- }
}
-#endif
\ No newline at end of file
+#endif
diff --git a/local_plugins/music_service/ios/music_service.podspec b/local_plugins/music_service/ios/music_service.podspec
index d9aa1dcb3..61019dd4b 100644
--- a/local_plugins/music_service/ios/music_service.podspec
+++ b/local_plugins/music_service/ios/music_service.podspec
@@ -15,7 +15,7 @@ A new Flutter project.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
- s.platform = :ios, '12.0'
+ s.platform = :ios, '16.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
diff --git a/local_plugins/ota/ios/ota.podspec b/local_plugins/ota/ios/ota.podspec
index 39c28fd0a..b4e84fa29 100644
--- a/local_plugins/ota/ios/ota.podspec
+++ b/local_plugins/ota/ios/ota.podspec
@@ -15,7 +15,7 @@ A new Flutter project.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
- s.platform = :ios, '12.0'
+ s.platform = :ios, '16.0'
s.vendored_frameworks = [
'Frameworks/DFUnits.framework',
'Frameworks/JL_AdvParse.framework',
diff --git a/local_plugins/spotify_sdk/ios/spotify_sdk.podspec b/local_plugins/spotify_sdk/ios/spotify_sdk.podspec
index 7be779eb9..dbb91fa4b 100644
--- a/local_plugins/spotify_sdk/ios/spotify_sdk.podspec
+++ b/local_plugins/spotify_sdk/ios/spotify_sdk.podspec
@@ -15,8 +15,8 @@ Unofficial Spotify Flutter SDK.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
- s.platform = :ios, '12.0'
- s.ios.deployment_target = '12.0'
+ s.platform = :ios, '16.0'
+ s.ios.deployment_target = '16.0'
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.swift_version = '5.0'
diff --git a/local_plugins/user_portrait/ios/user_portrait/Package.swift b/local_plugins/user_portrait/ios/user_portrait/Package.swift
index 4d8e0e3fc..3519e97aa 100644
--- a/local_plugins/user_portrait/ios/user_portrait/Package.swift
+++ b/local_plugins/user_portrait/ios/user_portrait/Package.swift
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "user_portrait",
platforms: [
- .iOS("12.0")
+ .iOS("16.0")
],
products: [
.library(name: "user-portrait", targets: ["user_portrait"])
diff --git a/local_plugins/volcano_speech/ios/volcano_speech.podspec b/local_plugins/volcano_speech/ios/volcano_speech.podspec
index 9a70baed1..42b24bd77 100644
--- a/local_plugins/volcano_speech/ios/volcano_speech.podspec
+++ b/local_plugins/volcano_speech/ios/volcano_speech.podspec
@@ -15,7 +15,7 @@ A Flutter plugin for Volcano Speech services.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
- s.platform = :ios, '12.0'
+ s.platform = :ios, '16.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
diff --git a/pubspec.yaml b/pubspec.yaml
index 1abb24202..db31dc5c8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -65,6 +65,7 @@ dependencies:
firebase_auth: ^5.5.1
google_sign_in: ^6.2.2
flutter_facebook_auth: ^7.1.1
+ sign_in_with_apple: ^7.0.1
flutter_image_compress: ^2.4.0
common_utils: ^2.1.0
image_gallery_saver_plus: ^4.0.1