Files
feature-tenant/lib/features/auth/presentation/bloc/auth_cubit.dart
2026-08-10 16:26:15 +08:00

757 lines
29 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<AuthState> {
final UserStorage storage;
final TcpClient tcp;
final AppUserCubit appCubit;
final NetMessageDispatcher dispatcher;
final AuthTcpDatasource _authTcpDatasource;
final ILoggerService _logger = GetIt.I<ILoggerService>();
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
// 🔥 登录验证 Completer:用于等待登录阶段的 have_logged_in 推送
Completer<bool>? _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<void> appStarted() async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
try {
final prefs = GetIt.I<SharedPreferences>();
// 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<bool> _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<String, dynamic> && 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<void> 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<void> 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<void> 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>();
remoteControlCubit.clearAll();
debugPrint('✅ [AUTH] 已清空 RemoteControlCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 RemoteControlCubit 状态');
// 2. 清空设备列表和选中设备
final devicesCubit = GetIt.I<DevicesCubit>();
devicesCubit.emit(const DevicesState());
debugPrint('✅ [AUTH] 已清空 DevicesCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 DevicesCubit 状态');
// 3. 清空设备实时状态(图表数据等)
final deviceStatusBloc = GetIt.I<DeviceStatusBloc>();
deviceStatusBloc.add(DeviceStatusReset());
debugPrint('✅ [AUTH] 已重置 DeviceStatusBloc 状态');
_logger.logWithLevel('✅ [AUTH] 已重置 DeviceStatusBloc 状态');
// 4. 清空场站所有数据(列表、选中状态、_hasLoadedSites 标记)
final siteCubit = GetIt.I<SiteCubit>();
siteCubit.clearAll();
debugPrint('✅ [AUTH] 已清空 SiteCubit 所有数据');
_logger.logWithLevel('✅ [AUTH] 已清空 SiteCubit 所有数据');
// 5. 清空设备任务(taskPool、currentTask、currentTaskId 等)
final deviceTaskCubit = GetIt.I<DeviceTaskCubit>();
deviceTaskCubit.clearAll();
debugPrint('✅ [AUTH] 已清空 DeviceTaskCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 DeviceTaskCubit 状态');
// 6. 清空权限请求弹窗状态
final permissionRequestBloc = GetIt.I<PermissionRequestBloc>();
permissionRequestBloc.clearAll();
debugPrint('✅ [AUTH] 已清空 PermissionRequestBloc 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 PermissionRequestBloc 状态');
// 7. 清空我的页面数据(昵称等个人信息)
final myCubit = GetIt.I<MyCubit>();
myCubit.clearAll();
debugPrint('✅ [AUTH] 已清空 MyCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 MyCubit 状态');
// 8. 断开 MQTT 连接(避免新用户收到上个用户的实时推送)
try {
final droneOsdClient = GetIt.I<MqttClient>(instanceName: 'droneOsdClient');
if (droneOsdClient.isConnected) {
droneOsdClient.disconnect();
debugPrint('✅ [AUTH] 已断开 droneOsdClient MQTT');
}
} catch (_) {}
try {
final taskMessageClient = GetIt.I<MqttClient>(instanceName: 'taskMessageClient');
if (taskMessageClient.isConnected) {
taskMessageClient.disconnect();
debugPrint('✅ [AUTH] 已断开 taskMessageClient MQTT');
}
} catch (_) {}
// 9. 清除 SharedPreferences 会话相关 key
final prefs = GetIt.I<SharedPreferences>();
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<bool> _waitForLoginVerification() async {
_loginVerificationCompleter = Completer<bool>();
// 等待 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<void> close() {
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
_safeModeExitTimer?.cancel(); // 清理倒计时
return super.close();
}
/// 🔥 息屏/后台后恢复到前台时的重连方法
Future<void> 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();
}
}
}