import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart'; import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart'; import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart'; import '../../../../core/app/app_user_cubit.dart'; import '../../../../core/infrastructure/logging/sentry_logger_impl.dart'; import '../../../../core/logging/i_logger_service.dart'; import '../../../../core/network/tcp/tcp_client.dart'; import '../../../../core/storage/user_storage.dart'; import '../../../devices/domain/usecases/get_user_device_usecase.dart'; import '../../../devices/domain/usecases/switch_device_usecase.dart'; import '../../../devices/presentation/bloc/devices_cubit.dart'; import '../../../devices/presentation/bloc/device_status_bloc.dart'; import '../../../devices/presentation/bloc/device_status_event.dart'; import '../../../devices/presentation/bloc/devices_state.dart'; import '../../../devices/presentation/bloc/device_task_cubit.dart'; import '../../../home/presentation/bloc/permission_request_bloc.dart'; import '../../../my/presentation/bloc/my_cubit.dart'; import '../../../remote_control/presentation/bloc/remote_control_cubit.dart'; import '../../../../core/network/mqtt/domain/interfaces/mqtt_client.dart'; import '../../../../features/v2/site/presentation/cubit/site_cubit.dart'; import '../../data/datasources/auth_tcp_datasource.dart'; import '../../data/datasources/impl/auth_tcp_datasource_impl.dart'; import 'auth_state.dart'; import '../../../../main.dart'; // 🔥 导入 main.dart 以使用 navigatorKey /// emit做的事情: /// 1.修改状态标志:通过改变类的类型(从 Initial 变成 Authenticated) /// 2.携带数据:把 user 对象塞进了状态里,让外部能拿到 /// 3.发送通知:自动触发监听(如 GoRouter 的刷新)。 class AuthCubit extends Cubit { final UserStorage storage; final TcpClient tcp; final AppUserCubit appCubit; final NetMessageDispatcher dispatcher; final AuthTcpDatasource _authTcpDatasource; final ILoggerService _logger = GetIt.I(); StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期 // 🔥 登录验证 Completer:用于等待登录阶段的 have_logged_in 推送 Completer? _loginVerificationCompleter; // 🔥 安全模式标志位:保护关键操作阶段不被异地登录踢下线 bool _isSafeMode = false; Timer? _safeModeExitTimer; AuthCubit( this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource, ) : super(AuthInitial()) { // Cubit 一启动就开始监听 TCP 的“自动逻辑” _listenToAuthResponse(); // 🔥 设置重连耗尽回调:连续4次重连失败后弹窗提示用户退出登录 tcp.onReconnectExhausted = _showReconnectFailedDialog; // 🔥 设置重连提示回调:重连中/重连成功 SnackBar tcp.onReconnectStarted = _showReconnectingToast; tcp.onReconnectSuccess = _showReconnectSuccessToast; } /// App 启动时检查本地缓存 Future appStarted() async { final logger = GetIt.I() as SentryLoggerImpl; try { final prefs = GetIt.I(); // Step 1: 先生成新的 session_id,保存旧的 final oldSessionId = prefs.getString('current_session_id'); final newSessionId = DateTime.now().millisecondsSinceEpoch.toString(); await prefs.setString('current_session_id', newSessionId); debugPrint('📱 [AUTH] 新会话: $newSessionId,旧会话: $oldSessionId'); // Step 2: 检查是否从后台被杀 final pendingKillLogout = prefs.getBool('pending_kill_logout') ?? false; final savedSessionId = prefs.getString('saved_session_id'); if (pendingKillLogout && savedSessionId != null && oldSessionId != null) { if (savedSessionId == oldSessionId) { // saved == old_current → App 在后台被杀,从未恢复过 logger.logWithLevel('🔄 [AUTH] 检测到 APP 被后台杀死,执行退出登录', level: 'INFO'); await prefs.setBool('pending_kill_logout', false); await prefs.remove('saved_session_id'); await storage.deleteUser(); emit(AuthUnauthenticated()); return; } else { // saved != old_current → App 被杀前已恢复过,清除标记 logger.logWithLevel( '📱 [AUTH] pending_kill_logout=true 但会话已恢复过,清除标记', level: 'INFO', ); await prefs.setBool('pending_kill_logout', false); await prefs.remove('saved_session_id'); } } else if (pendingKillLogout) { logger.logWithLevel( '📱 [AUTH] pending_kill_logout=true 但无会话信息,清除标记', level: 'INFO', ); await prefs.setBool('pending_kill_logout', false); await prefs.remove('saved_session_id'); } } catch (e) { logger.logWithLevel('❌ [AUTH] 检查杀后台标记失败: $e', level: 'ERROR'); } try { final user = await storage.getUser(); logger.logWithLevel( '启动时检查本地缓存', level: 'INFO', data: {'user': user != null ? '找到用户: ${user.username}' : '未找到用户'}, ); if (user != null) { final isValid = await _verifyToken(user); if (isValid) { logger.logWithLevel('✅ [AUTH] Token 有效,恢复登录状态', level: 'INFO'); appCubit.setAuth(user); emit(AuthAuthenticated(user)); logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态', level: 'INFO'); } else { logger.logWithLevel('⚠️ [AUTH] Token 已过期,清除本地缓存', level: 'WARN'); await storage.deleteUser(); emit(AuthUnauthenticated()); logger.logWithLevel( '⚠️ [AUTH] 应用启动 - Token 过期,进入未登录状态', level: 'INFO', ); } } else { logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO'); emit(AuthUnauthenticated()); } } catch (e) { logger.logWithLevel('❌ [AUTH] 应用启动检查失败: $e', level: 'ERROR'); emit(AuthUnauthenticated()); } } Future _verifyToken(UserEntity user) async { try { final verifyDio = Dio( BaseOptions( baseUrl: HttpApiConsts.baseUrl, headers: {'Authorization': 'Bearer ${user.token}'}, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), ), ); final response = await verifyDio.get( HttpApiConsts.getUserDevicesList, queryParameters: {'tenantName': user.username}, ); if (response.statusCode == 200) { final data = response.data; if (data is Map && data['code'] == 401) { return false; } return true; } return false; } on DioException catch (e) { final statusCode = e.response?.statusCode; debugPrint('>>> [AUTH] Token 验证失败: HTTP $statusCode'); if (statusCode == 401 || statusCode == 403) { return false; } return true; } catch (e) { debugPrint('>>> [AUTH] Token 验证异常: $e'); return true; } } /// 当 HTTP 登录/注册成功后调用 Future loginSuccess(UserEntity user) async { await storage.saveUser(user); // 🔥 登录后立即创建TCP连接并验证 try { debugPrint('>>> [AUTH] 登录成功,开始建立TCP连接...'); _logger.logWithLevel('>>> [AUTH] 登录成功,开始建立TCP连接...', shouldLog: true); await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); tcp.startHeartbeat(interval: const Duration(seconds: 4)); // 🔥 已注释:等待 2.5 秒看是否收到 have_logged_in 的验证逻辑 // bool isKicked = await _waitForLoginVerification(); // // if (isKicked) { // // 🔥 不能进入 APP,必须清除所有登录信息 // debugPrint('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息...'); // _logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息', shouldLog: true); // // // 1. 删除已保存的用户信息 // await storage.deleteUser(); // debugPrint('✅ [AUTH] 已删除用户信息'); // // // 2. 断开 TCP 连接 // tcp.forceDisconnect(); // debugPrint('✅ [AUTH] 已断开 TCP 连接'); // // // 3. 显示 Toast // _showLoginFailedToast("账号已在其他设备登录"); // debugPrint('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页'); // _logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页', shouldLog: true); // return; // 停留在登录页 // } debugPrint('✅ [AUTH] TCP连接成功,验证通过(已跳过等待)'); _logger.logWithLevel('✅ [AUTH] TCP连接成功,验证通过(已跳过等待)', shouldLog: true); } catch (e) { debugPrint('❌ [AUTH] TCP连接失败:$e'); _logger.logWithLevel('❌ [AUTH] TCP连接失败:$e', level: 'ERROR'); // TCP 连接失败不影响登录流程,继续执行 } appCubit.setAuth(user); emit(AuthAuthenticated(user)); } /// 退出登录 (主动或被动) Future logout() async { print('>>> [AUTH] 🚨🚨🚨 logout() 被调用!时间: ${DateTime.now()}'); print('>>> [AUTH] 调用栈:\n${StackTrace.current}'); // 🔥 关键修复:在断开TCP前,先清空所有业务状态,避免重连触发旧数据 _clearAllBusinessState(); await storage.deleteUser(); tcp.forceDisconnect(); // 🔥 修复:彻底断开连接,禁止自动重连 appCubit.clearAuth(); emit(AuthUnauthenticated()); } /// 🔥 Token 过期处理:弹出提示后退出登录 Future tokenExpired() async { _showTokenExpiredDialog(); Future.delayed(const Duration(seconds: 2), () { logout(); }); } void _showTokenExpiredDialog() { try { final context = navigatorKey.currentContext; if (context != null) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return AlertDialog( title: const Text('登录已过期'), content: const Text('账号登录状态已过期,请重新登录'), actions: [ ElevatedButton( onPressed: () { Navigator.of(dialogContext).pop(); }, child: const Text('确定'), ), ], ); }, ); } } catch (e) { debugPrint('>>> [AUTH] ❌ 显示 Token 过期弹窗失败:$e'); } } /// 🔥 新增:显示登录失败 Toast(黑色背景,和登录错误提示一致) void _showLoginFailedToast(String message) { try { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.black, duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { debugPrint('>>> [AUTH] ❌ 显示Toast失败:$e'); } } /// 🔥 新增:显示异地登录提示弹窗 void _showKickOutDialog() { try { debugPrint('>>> [AUTH] 📢 准备显示异地登录提示弹窗'); _logger.logWithLevel('>>> [AUTH] 📢 准备显示异地登录提示弹窗', shouldLog: true); // 使用全局 navigatorKey 显示弹窗 final context = navigatorKey.currentContext; if (context != null) { showDialog( context: context, barrierDismissible: false, // 不允许点击背景关闭 builder: (BuildContext dialogContext) { return AlertDialog( title: const Text('账号被顶下线'), content: const Text('您的账号在其他设备登录,即将退出当前设备...'), actions: [], ); }, ); } else { debugPrint('>>> [AUTH] ⚠️ 无法获取 Navigator Context,将直接退出'); _logger.logWithLevel( '>>> [AUTH] ⚠️ 无法获取 Navigator Context,将直接退出', shouldLog: true, ); } // 🔥 延迟 2 秒后执行退出,给用户时间看到提示 Future.delayed(const Duration(seconds: 2), () { debugPrint('>>> [AUTH] ⏰ 延迟结束,开始执行退出登录'); _logger.logWithLevel('>>> [AUTH] ⏰ 延迟结束,开始执行退出登录', shouldLog: true); logout(); }); } catch (e) { debugPrint('>>> [AUTH] ❌ 显示弹窗失败:$e'); _logger.logWithLevel('>>> [AUTH] ❌ 显示弹窗失败:$e', level: 'ERROR'); // 即使显示弹窗失败,也要执行退出 logout(); } } /// 🔥 TCP 重连耗尽弹窗:连续4次重连失败后提示用户退出登录 void _showReconnectFailedDialog() { try { debugPrint('>>> [AUTH] 📢 重连耗尽,准备显示重连失败提示弹窗'); _logger.logWithLevel('>>> [AUTH] 📢 重连耗尽,显示重连失败弹窗', shouldLog: true); final context = navigatorKey.currentContext; if (context != null) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.wifi_off_rounded, size: 48, color: Color(0xFFF53F3F)), const SizedBox(height: 12), const Text( '连接异常', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: Color(0xFF1D2129), ), ), const SizedBox(height: 8), const Text( 'TCP重连认证无效请重新登陆', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)), textAlign: TextAlign.center, ), ], ), actions: [ Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.of(dialogContext).pop(), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF86909C), side: const BorderSide(color: Color(0xFFE5E6EB)), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text('取消', style: TextStyle(fontSize: 15)), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () { Navigator.of(dialogContext).pop(); logout(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF165DFF), foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text('退出登录', style: TextStyle(fontSize: 15)), ), ), ], ), ], ); }, ); } else { debugPrint('>>> [AUTH] ⚠️ 无法获取 Navigator Context,直接退出登录'); logout(); } } catch (e) { debugPrint('>>> [AUTH] ❌ 显示重连失败弹窗失败:$e'); logout(); } } /// 🔥 重连中提示 SnackBar void _showReconnectingToast() { try { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('TCP重连中...'), backgroundColor: Color(0xFFFF7D00), duration: Duration(seconds: 4), ), ); } } catch (_) {} } /// 🔥 重连成功提示 SnackBar void _showReconnectSuccessToast() { try { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('连接成功'), backgroundColor: Color(0xFF00B42A), duration: Duration(seconds: 2), ), ); } } catch (_) {} } /// 🔥 新增:清空所有业务状态,防止数据泄露到新账户 void _clearAllBusinessState() { try { // 1. 清空远程控制所有状态(targetDevice、权限、摇杆数据、电压电量等) final remoteControlCubit = GetIt.I(); remoteControlCubit.clearAll(); debugPrint('✅ [AUTH] 已清空 RemoteControlCubit 状态'); _logger.logWithLevel('✅ [AUTH] 已清空 RemoteControlCubit 状态'); // 2. 清空设备列表和选中设备 final devicesCubit = GetIt.I(); devicesCubit.emit(const DevicesState()); debugPrint('✅ [AUTH] 已清空 DevicesCubit 状态'); _logger.logWithLevel('✅ [AUTH] 已清空 DevicesCubit 状态'); // 3. 清空设备实时状态(图表数据等) final deviceStatusBloc = GetIt.I(); deviceStatusBloc.add(DeviceStatusReset()); debugPrint('✅ [AUTH] 已重置 DeviceStatusBloc 状态'); _logger.logWithLevel('✅ [AUTH] 已重置 DeviceStatusBloc 状态'); // 4. 清空场站所有数据(列表、选中状态、_hasLoadedSites 标记) final siteCubit = GetIt.I(); siteCubit.clearAll(); debugPrint('✅ [AUTH] 已清空 SiteCubit 所有数据'); _logger.logWithLevel('✅ [AUTH] 已清空 SiteCubit 所有数据'); // 5. 清空设备任务(taskPool、currentTask、currentTaskId 等) final deviceTaskCubit = GetIt.I(); deviceTaskCubit.clearAll(); debugPrint('✅ [AUTH] 已清空 DeviceTaskCubit 状态'); _logger.logWithLevel('✅ [AUTH] 已清空 DeviceTaskCubit 状态'); // 6. 清空权限请求弹窗状态 final permissionRequestBloc = GetIt.I(); permissionRequestBloc.clearAll(); debugPrint('✅ [AUTH] 已清空 PermissionRequestBloc 状态'); _logger.logWithLevel('✅ [AUTH] 已清空 PermissionRequestBloc 状态'); // 7. 清空我的页面数据(昵称等个人信息) final myCubit = GetIt.I(); myCubit.clearAll(); debugPrint('✅ [AUTH] 已清空 MyCubit 状态'); _logger.logWithLevel('✅ [AUTH] 已清空 MyCubit 状态'); // 8. 断开 MQTT 连接(避免新用户收到上个用户的实时推送) try { final droneOsdClient = GetIt.I(instanceName: 'droneOsdClient'); if (droneOsdClient.isConnected) { droneOsdClient.disconnect(); debugPrint('✅ [AUTH] 已断开 droneOsdClient MQTT'); } } catch (_) {} try { final taskMessageClient = GetIt.I(instanceName: 'taskMessageClient'); if (taskMessageClient.isConnected) { taskMessageClient.disconnect(); debugPrint('✅ [AUTH] 已断开 taskMessageClient MQTT'); } } catch (_) {} // 9. 清除 SharedPreferences 会话相关 key final prefs = GetIt.I(); prefs.remove('current_session_id'); prefs.remove('pending_kill_logout'); prefs.remove('saved_session_id'); debugPrint('✅ [AUTH] 已清除 SharedPreferences 会话 key'); debugPrint('✅ [AUTH] 所有业务状态已清空'); _logger.logWithLevel('✅ [AUTH] 所有业务状态已清空'); } catch (e) { debugPrint('❌ [AUTH] 清空业务状态失败: $e'); _logger.logWithLevel('❌ [AUTH] 清空业务状态失败: $e', level: 'ERROR'); } } /// 🔥 等待登录验证结果(2.5 秒内看是否收到 have_logged_in) Future _waitForLoginVerification() async { _loginVerificationCompleter = Completer(); // 等待 2.5 秒 await Future.delayed(const Duration(milliseconds: 2500)); // 如果 completer 还没完成,说明没收到 have_logged_in,返回 false(可以进入) if (!_loginVerificationCompleter!.isCompleted) { _loginVerificationCompleter!.complete(false); } return _loginVerificationCompleter!.future; } /// TCP 指令监听 void _listenToAuthResponse() { debugPrint('>>> [AUTH] _listenToAuthResponse() 被调用,开始监听 0x12 指令'); _kickOutSub?.cancel(); // 防止重复监听 // 直接监听原始数据包,自己处理 JSON 解析(去掉 CRC 字节) _kickOutSub = dispatcher.onCommand(0x12).listen((packet) { debugPrint( '>>> [AUTH] 收到 0x12 原始包,payload 长度=${packet.payload.length}, 内容=${packet.payload}', ); try { // 🔥 关键:手动去掉最后 2 个 CRC 字节 String jsonString; if (packet.payload.length > 2) { jsonString = utf8.decode( packet.payload.sublist(0, packet.payload.length - 2), ); } else { jsonString = utf8.decode(packet.payload); } debugPrint('>>> [AUTH] 获取 JSON: "$jsonString"'); final jsonMap = jsonDecode(jsonString); debugPrint('>>> [AUTH] 获取 JSON Map: $jsonMap'); final respond = jsonMap['respond'] ?? ''; debugPrint('>>> [AUTH] respond 字段值: "$respond"'); _logger.logWithLevel( '>>> [AUTH] respond 字段值: "$respond"', shouldLog: true, ); debugPrint('>>> [AUTH] respond 类型: ${respond.runtimeType}'); debugPrint( '>>> [AUTH] respond == "have_logged_in": ${respond == "have_logged_in"}', ); if (respond == 'have_logged_in') { // 🔥 登录阶段策略:HTTP 已成功,TCP 认证阶段的推送视为服务端状态同步,直接放行 if (_loginVerificationCompleter != null && !_loginVerificationCompleter!.isCompleted) { debugPrint( '>>> [AUTH] 🛡️ 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入 APP', ); _logger.logWithLevel( '🛡️ [AUTH] 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入', shouldLog: true, ); _loginVerificationCompleter!.complete(false); // 标记为可以进入 return; } // 🔥 已登录阶段策略:检查是否处于安全模式 if (_isSafeMode) { print('>>> [AUTH] 🛡️ 安全模式下拦截 have_logged_in 推送,防止控制中断'); _logger.logWithLevel('🛡️ [AUTH] 安全模式下拦截异地登录推送', level: 'WARN'); return; // 拦截退出逻辑 } print('>>> [AUTH] ⚠️ 已登录状态下收到 TCP 0x12 指令 respond=have_logged_in'); _logger.logWithLevel( '⚠️ [AUTH] 已登录状态收到 have_logged_in,弹出异地登录提示', level: 'WARN', ); // 🔥 关键:检查是否是自身 TCP 重连触发的 have_logged_in // 如果是自身刚发送 0x03 认证包引起的,忽略这次推送 if (tcp.isOwnAuthTriggeredKick()) { debugPrint('>>> [AUTH] 🛡️ 检测到是自身认证触发的 have_logged_in,忽略'); _logger.logWithLevel( '🛡️ [AUTH] 自身认证触发的 have_logged_in,忽略', level: 'INFO', ); tcp.clearAuthTimestamp(); return; } // 🔥 弹出"账号被顶下线"提示,2秒后执行退出登录 _showKickOutDialog(); } else { debugPrint('>>> [AUTH] ℹ️ 收到 0x12 消息,respond="$respond",不处理'); } } catch (e) { debugPrint('>>> [AUTH] ❌ JSON 解析失败:$e'); } }); _logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'INFO'); } /// 🔥 安全模式管理:进入关键页面时调用 void enterSafeMode() { _isSafeMode = true; _safeModeExitTimer?.cancel(); // 取消之前的退出倒计时 debugPrint('>>> [AUTH] 🛡️ 已进入安全模式,异地登录推送将被拦截'); _logger.logWithLevel('>>> [AUTH] 🛡️ 已进入安全模式', shouldLog: true); _showSafeModeEnteredToast(); // 🔥 显示进入提示 } /// 🔥 安全模式管理:离开关键页面时调用 void exitSafeMode() { // 启动 90 秒倒计时 _safeModeExitTimer?.cancel(); _safeModeExitTimer = Timer(const Duration(seconds: 90), () { if (_isSafeMode) { _isSafeMode = false; debugPrint('>>> [AUTH] ⏰ 90秒无操作,已自动关闭安全模式'); _logger.logWithLevel('>>> [AUTH] ⏰ 90秒无操作,已自动关闭安全模式', shouldLog: true); _showSafeModeClosedToast(); } }); debugPrint('>>> [AUTH] 🕒 已离开安全页面,90秒后若无操作将关闭安全模式'); _showSafeModeCountdownToast(); } /// 🔥 显示“已进入安全模式”提示 void _showSafeModeEnteredToast() { try { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('已进入安全模式,防止异地登录干扰'), backgroundColor: Colors.green, duration: const Duration(seconds: 3), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { debugPrint('>>> [AUTH] ❌ 显示安全模式提示失败:$e'); } } /// 🔥 显示“90秒后关闭”提示 void _showSafeModeCountdownToast() { try { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('90秒后无操作将关闭安全模式'), backgroundColor: Colors.orange, duration: const Duration(seconds: 3), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { debugPrint('>>> [AUTH] ❌ 显示倒计时提示失败:$e'); } } /// 🔥 显示“安全模式已关闭”提示 void _showSafeModeClosedToast() { try { final context = navigatorKey.currentContext; if (context != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('安全模式已关闭,恢复异地登录检测'), backgroundColor: Colors.grey, duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { debugPrint('>>> [AUTH] ❌ 显示关闭提示失败:$e'); } } @override Future close() { _kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听 _safeModeExitTimer?.cancel(); // 清理倒计时 return super.close(); } /// 🔥 息屏/后台后恢复到前台时的重连方法 Future reconnectAfterResume() async { _logger.logWithLevel('[AUTH] 检测到应用恢复到前台,检查 TCP 连接状态...', level: 'INFO'); // 如果 TCP 未连接,则执行重连 if (!tcp.isConnected) { _logger.logWithLevel('[AUTH] TCP 未连接,开始重连...', level: 'INFO'); try { await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); tcp.startHeartbeat(interval: const Duration(seconds: 4)); _logger.logWithLevel('[AUTH] TCP 重连成功!', level: 'INFO'); } catch (e) { _logger.logWithLevel('[AUTH] TCP 重连失败:$e', level: 'ERROR'); } } else { _logger.logWithLevel('[AUTH] TCP 已连接,无需重连', level: 'INFO'); // 可选:发送一个心跳包确认连接有效 tcp.sendHeartbeat(); } } }