703 lines
27 KiB
Dart
703 lines
27 KiB
Dart
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;
|
||
}
|
||
|
||
/// 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: [
|
||
SizedBox(
|
||
width: double.infinity,
|
||
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();
|
||
}
|
||
}
|
||
|
||
/// 🔥 新增:清空所有业务状态,防止数据泄露到新账户
|
||
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();
|
||
}
|
||
}
|
||
}
|