|
|
|
@ -1,4 +1,7 @@ |
|
|
|
import 'dart:io'; |
|
|
|
import 'dart:async'; |
|
|
|
import 'package:flutter/widgets.dart'; |
|
|
|
import 'package:flutter/services.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:get_storage/get_storage.dart'; |
|
|
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|
|
|
@ -9,6 +12,7 @@ import '../controller/message/conversation_controller.dart'; |
|
|
|
import '../controller/message/chat_controller.dart'; |
|
|
|
import '../controller/global.dart'; |
|
|
|
import '../pages/mine/login_page.dart'; |
|
|
|
import '../network/user_api.dart'; |
|
|
|
|
|
|
|
// 完整的IM管理器实现,使用实际的SDK类型和方法 |
|
|
|
class IMManager { |
|
|
|
@ -19,9 +23,11 @@ class IMManager { |
|
|
|
// 静态getter用于instance访问 |
|
|
|
static IMManager get instance => _instance; |
|
|
|
|
|
|
|
bool _isInitialized = false; |
|
|
|
bool _initialized = false; |
|
|
|
bool _listenersRegistered = false; |
|
|
|
bool _isLoggedIn = false; |
|
|
|
bool _loggedIn = false; |
|
|
|
bool _isReconnecting = false; // 是否正在重连 |
|
|
|
Completer<void>? _initCompleter; // 用于确保并发调用时只初始化一次 |
|
|
|
|
|
|
|
// 监听器标识符 |
|
|
|
static const String _connectionHandlerKey = 'im_manager_connection_handler'; |
|
|
|
@ -38,31 +44,89 @@ class IMManager { |
|
|
|
print('IMManager instance created'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 初始化IM SDK |
|
|
|
Future<bool> initialize(String appKey) async { |
|
|
|
bool get isInitialized => _initialized; |
|
|
|
bool get isLoggedIn => _loggedIn; |
|
|
|
|
|
|
|
/// 确保 IM 初始化完成(支持并发调用,只初始化一次) |
|
|
|
Future<void> ensureInitialized({required String appKey}) async { |
|
|
|
// 如果已经初始化,直接返回 |
|
|
|
if (_initialized) { |
|
|
|
debugPrint('✅ IM 已初始化,无需重复初始化'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果正在初始化,等待完成 |
|
|
|
if (_initCompleter != null) { |
|
|
|
debugPrint('🟡 IM 初始化中,等待完成...'); |
|
|
|
return _initCompleter!.future; |
|
|
|
} |
|
|
|
|
|
|
|
_initCompleter = Completer<void>(); |
|
|
|
|
|
|
|
debugPrint('🟡 IM 开始初始化'); |
|
|
|
debugPrint('📋 AppKey: $appKey'); |
|
|
|
|
|
|
|
try { |
|
|
|
if (_isInitialized) { |
|
|
|
print('IM SDK already initialized'); |
|
|
|
return true; |
|
|
|
// 确保 Flutter 绑定已初始化 |
|
|
|
WidgetsFlutterBinding.ensureInitialized(); |
|
|
|
debugPrint('✅ WidgetsFlutterBinding 已初始化'); |
|
|
|
|
|
|
|
// 验证 appKey 格式 |
|
|
|
if (appKey.isEmpty) { |
|
|
|
throw Exception('AppKey 不能为空'); |
|
|
|
} |
|
|
|
if (!appKey.contains('#')) { |
|
|
|
debugPrint('⚠️ AppKey 格式可能不正确,应为 "orgname#appname" 格式'); |
|
|
|
} |
|
|
|
|
|
|
|
// 创建EMOptions实例 |
|
|
|
final options = EMOptions( |
|
|
|
appKey: appKey, |
|
|
|
// 创建 EMOptions |
|
|
|
final options = EMOptions.withAppKey( |
|
|
|
appKey, |
|
|
|
autoLogin: false, |
|
|
|
debugMode: true, |
|
|
|
usingHttpsOnly: true, |
|
|
|
acceptInvitationAlways: false, |
|
|
|
debugMode: true |
|
|
|
); |
|
|
|
debugPrint('✅ EMOptions 创建成功'); |
|
|
|
|
|
|
|
|
|
|
|
// 初始化SDK |
|
|
|
await EMClient.getInstance.init(options); |
|
|
|
// 调用 SDK 初始化 |
|
|
|
debugPrint('🟡 调用 EMClient.getInstance.init()...'); |
|
|
|
try { |
|
|
|
await EMClient.getInstance.init(options); |
|
|
|
debugPrint('🟢 EMClient.getInstance.init() 调用完成'); |
|
|
|
} on PlatformException catch (e) { |
|
|
|
debugPrint('❌ PlatformException 捕获:'); |
|
|
|
debugPrint(' code: ${e.code}'); |
|
|
|
debugPrint(' message: ${e.message}'); |
|
|
|
debugPrint(' details: ${e.details}'); |
|
|
|
debugPrint(' stacktrace: ${e.stacktrace}'); |
|
|
|
rethrow; |
|
|
|
} catch (e, s) { |
|
|
|
debugPrint('❌ 其他异常: $e'); |
|
|
|
debugPrint('堆栈: $s'); |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
|
|
|
|
_isInitialized = true; |
|
|
|
print('IM SDK initialized successfully'); |
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
print('Failed to initialize IM SDK: $e'); |
|
|
|
return false; |
|
|
|
// 注册监听器 |
|
|
|
_registerListeners(); |
|
|
|
_initialized = true; |
|
|
|
|
|
|
|
debugPrint('✅ IM 初始化成功'); |
|
|
|
_initCompleter!.complete(); |
|
|
|
} catch (e, s) { |
|
|
|
debugPrint('❌ IM 初始化失败'); |
|
|
|
debugPrint('错误类型: ${e.runtimeType}'); |
|
|
|
debugPrint('错误信息: $e'); |
|
|
|
debugPrint('堆栈跟踪:'); |
|
|
|
debugPrint('$s'); |
|
|
|
|
|
|
|
_initialized = false; |
|
|
|
_initCompleter!.completeError(e, s); |
|
|
|
_initCompleter = null; // 允许重试 |
|
|
|
|
|
|
|
// 重新抛出异常,让调用者知道初始化失败 |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -82,17 +146,17 @@ class IMManager { |
|
|
|
_connectionHandlerKey, |
|
|
|
EMConnectionEventHandler( |
|
|
|
onConnected: () { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('✅ [IMManager] 已连接到IM服务器'); |
|
|
|
} |
|
|
|
// 重置重连状态 |
|
|
|
_isReconnecting = false; |
|
|
|
debugPrint('🔌 IM 已连接'); |
|
|
|
// 连接成功后,通知 ConversationController 加载会话列表 |
|
|
|
_refreshConversationList(); |
|
|
|
}, |
|
|
|
onDisconnected: () { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('❌ [IMManager] 与IM服务器断开连接'); |
|
|
|
} |
|
|
|
// TODO: 可以在这里添加自动重连逻辑 |
|
|
|
debugPrint('🔌 IM 已断开'); |
|
|
|
_loggedIn = false; |
|
|
|
// 自动重连逻辑 |
|
|
|
_handleDisconnected(); |
|
|
|
}, |
|
|
|
onTokenDidExpire: () { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
@ -117,9 +181,7 @@ class IMManager { |
|
|
|
_chatHandlerKey, |
|
|
|
EMChatEventHandler( |
|
|
|
onMessagesReceived: (messages) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('📨 [IMManager] 收到 ${messages.length} 条新消息'); |
|
|
|
} |
|
|
|
debugPrint('📩 收到消息数: ${messages.length}'); |
|
|
|
// 从消息扩展字段中解析用户信息并缓存 |
|
|
|
for (var message in messages) { |
|
|
|
if (message.direction == MessageDirection.RECEIVE) { |
|
|
|
@ -132,9 +194,64 @@ class IMManager { |
|
|
|
_notifyChatControllers(messages); |
|
|
|
}, |
|
|
|
onMessageContentChanged: (EMMessage message, String operatorId, int operationTime){ |
|
|
|
Get.log('-------------📨 [IMManager] -------------: ${operatorId}'); |
|
|
|
Get.log('-------------📨 [IMManager] -------------: ${message}'); |
|
|
|
Get.log('-------------📨 [IMManager] -------------: ${operationTime}'); |
|
|
|
Get.log('-------------📨 [IMManager] -------------: ${message.localTime}'); |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('📨 [IMManager] 消息内容已修改: ${message.localTime}'); |
|
|
|
} |
|
|
|
|
|
|
|
// 处理金币数值:只对接收到的消息处理金币标签 |
|
|
|
// 注意:根据实际API,金币数值可能通过 operatorId 或消息 attributes 传递 |
|
|
|
// 只处理接收到的消息(自己发送的消息不显示金币标签) |
|
|
|
if (message.direction == MessageDirection.RECEIVE) { |
|
|
|
try { |
|
|
|
double? coinValue; |
|
|
|
|
|
|
|
// 方法1:尝试从消息 attributes 中获取金币数值 |
|
|
|
if (message.attributes != null) { |
|
|
|
final coinValueStr = message.attributes!['coin_value'] as String?; |
|
|
|
if (coinValueStr != null && coinValueStr.isNotEmpty) { |
|
|
|
coinValue = double.tryParse(coinValueStr); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 方法2:如果 attributes 中没有,尝试从 operatorId 解析(如果它是数值) |
|
|
|
if (coinValue == null && operatorId.isNotEmpty) { |
|
|
|
coinValue = double.tryParse(operatorId); |
|
|
|
} |
|
|
|
|
|
|
|
// 如果获取到金币数值,确保存储到消息的 attributes 中 |
|
|
|
if (coinValue != null && coinValue > 0) { |
|
|
|
if (message.attributes == null) { |
|
|
|
message.attributes = {}; |
|
|
|
} |
|
|
|
message.attributes!['coin_value'] = coinValue.toString(); |
|
|
|
|
|
|
|
// 通知对应的 ChatController 更新消息 |
|
|
|
final fromId = message.from; |
|
|
|
if (fromId != null && fromId.isNotEmpty) { |
|
|
|
final controller = _activeChatControllers[fromId]; |
|
|
|
if (controller != null) { |
|
|
|
// 更新消息列表中的消息 |
|
|
|
final index = controller.messages.indexWhere((msg) => msg.msgId == message.msgId); |
|
|
|
if (index != -1) { |
|
|
|
controller.messages[index] = message; |
|
|
|
controller.update(); |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('✅ [IMManager] 已更新接收消息的金币数值: msgId=${message.msgId}, coinValue=$coinValue'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('⚠️ [IMManager] 处理金币数值失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
), |
|
|
|
); |
|
|
|
@ -249,7 +366,8 @@ class IMManager { |
|
|
|
final userId = presence.publisher; |
|
|
|
if (userId != null && userId.isNotEmpty) { |
|
|
|
// 使用 statusDescription 字段来判断在线状态 |
|
|
|
final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; |
|
|
|
final statusDescStr = presence.statusDescription ?? ''; |
|
|
|
final statusDesc = statusDescStr.toLowerCase(); |
|
|
|
// 判断在线状态:online、available 等表示在线 |
|
|
|
final isOnline = statusDesc == 'online' || statusDesc == 'available'; |
|
|
|
|
|
|
|
@ -292,54 +410,157 @@ class IMManager { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 登录IM服务 |
|
|
|
Future<bool> login(String token) async { |
|
|
|
/// 登录(真正判断 IM 是否可用的地方) |
|
|
|
Future<bool> loginWithToken({ |
|
|
|
required String appKey, |
|
|
|
required String userId, |
|
|
|
required String token, |
|
|
|
}) async { |
|
|
|
try { |
|
|
|
if (!_isInitialized) { |
|
|
|
print('IM SDK not initialized'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
var userId = storage.read('userId'); |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('🔐 [IMManager] 准备登录IM,userId: $userId'); |
|
|
|
} else { |
|
|
|
print('🔐 [IMManager] 准备登录IM,userId: $userId'); |
|
|
|
} |
|
|
|
await EMClient.getInstance.logout(); |
|
|
|
await ensureInitialized(appKey: appKey); |
|
|
|
|
|
|
|
// 防止脏状态 |
|
|
|
await EMClient.getInstance.logout().catchError((_) {}); |
|
|
|
|
|
|
|
await EMClient.getInstance.loginWithToken(userId, token); |
|
|
|
// 注册监听器 |
|
|
|
_registerListeners(); |
|
|
|
// 设置登录状态 |
|
|
|
_isLoggedIn = true; |
|
|
|
_loggedIn = true; |
|
|
|
|
|
|
|
// 重置重连状态 |
|
|
|
_isReconnecting = false; |
|
|
|
// 登录成功后,通知 ConversationController 刷新会话列表 |
|
|
|
_refreshConversationList(); |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('✅ [IMManager] IM登录成功,userId: $userId'); |
|
|
|
} else { |
|
|
|
print('✅ [IMManager] IM登录成功,userId: $userId'); |
|
|
|
} |
|
|
|
|
|
|
|
debugPrint('✅ IM 登录成功: $userId'); |
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('❌ [IMManager] IM登录失败: $e'); |
|
|
|
} else { |
|
|
|
print('❌ [IMManager] IM登录失败: $e'); |
|
|
|
} |
|
|
|
} catch (e, s) { |
|
|
|
_loggedIn = false; |
|
|
|
debugPrint('❌ IM 登录失败: $e'); |
|
|
|
debugPrint('$s'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 登录IM服务(向后兼容方法) |
|
|
|
Future<bool> login(String token) async { |
|
|
|
final userId = storage.read('userId'); |
|
|
|
if (userId == null) { |
|
|
|
debugPrint('❌ IM 登录失败: userId 为空'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
return await loginWithToken( |
|
|
|
appKey: '1165251016193374#demo', |
|
|
|
userId: userId, |
|
|
|
token: token, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// 登出IM服务 |
|
|
|
Future<bool> logout() async { |
|
|
|
Future<void> logout() async { |
|
|
|
try { |
|
|
|
await EMClient.getInstance.logout(); |
|
|
|
// 清除登录状态 |
|
|
|
_isLoggedIn = false; |
|
|
|
print('IM logout successful'); |
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
print('IM logout failed: $e'); |
|
|
|
return false; |
|
|
|
} finally { |
|
|
|
_loggedIn = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 处理连接断开,自动重连 |
|
|
|
void _handleDisconnected() async { |
|
|
|
// 如果正在重连,避免重复重连 |
|
|
|
if (_isReconnecting) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('⚠️ [IMManager] 正在重连中,跳过重复重连'); |
|
|
|
} |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果未初始化或未登录,不需要重连 |
|
|
|
if (!_initialized || !_loggedIn) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('⚠️ [IMManager] SDK未初始化或用户未登录,跳过重连'); |
|
|
|
} |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_isReconnecting = true; |
|
|
|
|
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('🔄 [IMManager] 开始自动重连...'); |
|
|
|
} else { |
|
|
|
print('🔄 [IMManager] 开始自动重连...'); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 延迟2秒后重连,避免频繁重连 |
|
|
|
await Future.delayed(const Duration(seconds: 2)); |
|
|
|
|
|
|
|
// 检查是否仍然需要重连(可能在延迟期间已经连接成功) |
|
|
|
if (!_loggedIn) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('⚠️ [IMManager] 用户已登出,取消重连'); |
|
|
|
} |
|
|
|
_isReconnecting = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 获取 UserApi 来获取新的 token |
|
|
|
if (!Get.isRegistered<UserApi>()) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('❌ [IMManager] UserApi 未注册,无法获取token重连'); |
|
|
|
} |
|
|
|
_isReconnecting = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
final userApi = Get.find<UserApi>(); |
|
|
|
final response = await userApi.getHxUserToken(); |
|
|
|
|
|
|
|
// 检查响应:code == 0 表示成功 |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('📡 [IMManager] 获取token响应: code=${response.data.code}, message=${response.data.message}'); |
|
|
|
} else { |
|
|
|
print('📡 [IMManager] 获取token响应: code=${response.data.code}, message=${response.data.message}'); |
|
|
|
} |
|
|
|
|
|
|
|
if (response.data.isSuccess && response.data.data != null) { |
|
|
|
final token = response.data.data!; |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('✅ [IMManager] 获取到新的token (长度: ${token.length}),开始重新登录'); |
|
|
|
} else { |
|
|
|
print('✅ [IMManager] 获取到新的token (长度: ${token.length}),开始重新登录'); |
|
|
|
} |
|
|
|
|
|
|
|
// 重新登录 |
|
|
|
final loginSuccess = await login(token); |
|
|
|
if (loginSuccess) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('✅ [IMManager] 自动重连成功'); |
|
|
|
} else { |
|
|
|
print('✅ [IMManager] 自动重连成功'); |
|
|
|
} |
|
|
|
} else { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('❌ [IMManager] 自动重连失败:登录失败'); |
|
|
|
} else { |
|
|
|
print('❌ [IMManager] 自动重连失败:登录失败'); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('❌ [IMManager] 获取token失败:code=${response.data.code}, message=${response.data.message}, data=${response.data.data}'); |
|
|
|
} else { |
|
|
|
print('❌ [IMManager] 获取token失败:code=${response.data.code}, message=${response.data.message}, data=${response.data.data}'); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e, stackTrace) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('❌ [IMManager] 自动重连异常: $e'); |
|
|
|
Get.log('堆栈跟踪: $stackTrace'); |
|
|
|
} else { |
|
|
|
print('❌ [IMManager] 自动重连异常: $e'); |
|
|
|
print('堆栈跟踪: $stackTrace'); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
_isReconnecting = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -382,9 +603,6 @@ class IMManager { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 检查是否已登录 |
|
|
|
bool get isLoggedIn => _isLoggedIn; |
|
|
|
|
|
|
|
/// 检查用户是否存在于IM系统中 |
|
|
|
Future<bool> checkUserExists(String userId) async { |
|
|
|
try { |
|
|
|
@ -676,7 +894,7 @@ class IMManager { |
|
|
|
/// [callback] 状态变化回调函数 |
|
|
|
Future<bool> subscribeUserPresence(String userId, Function(bool isOnline) callback) async { |
|
|
|
try { |
|
|
|
if (!_isLoggedIn) { |
|
|
|
if (!_loggedIn) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('⚠️ [IMManager] IM未登录,无法订阅在线状态'); |
|
|
|
} |
|
|
|
@ -730,7 +948,7 @@ class IMManager { |
|
|
|
/// [userId] 用户ID |
|
|
|
Future<bool> unsubscribeUserPresence(String userId) async { |
|
|
|
try { |
|
|
|
if (!_isLoggedIn) { |
|
|
|
if (!_loggedIn) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
@ -758,7 +976,7 @@ class IMManager { |
|
|
|
/// 返回 true 表示在线,false 表示离线,null 表示获取失败 |
|
|
|
Future<bool?> getUserPresenceStatus(String userId) async { |
|
|
|
try { |
|
|
|
if (!_isLoggedIn) { |
|
|
|
if (!_loggedIn) { |
|
|
|
if (Get.isLogEnable) { |
|
|
|
Get.log('⚠️ [IMManager] IM未登录,无法获取在线状态'); |
|
|
|
} |
|
|
|
@ -781,7 +999,8 @@ class IMManager { |
|
|
|
|
|
|
|
final presence = presences.first; |
|
|
|
// 使用 statusDescription 字段来判断在线状态 |
|
|
|
final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; |
|
|
|
final statusDescStr = presence.statusDescription ?? ''; |
|
|
|
final statusDesc = statusDescStr.toLowerCase(); |
|
|
|
// 判断在线状态:online、available 等表示在线 |
|
|
|
final isOnline = statusDesc == 'online' || statusDesc == 'available'; |
|
|
|
|
|
|
|
|