一次大的提交

This commit is contained in:
2026-08-07 08:49:29 +08:00
parent bf2fc49c19
commit db70665ced
181 changed files with 23060 additions and 2804 deletions

View File

@@ -1,11 +1,14 @@
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';
@@ -20,6 +23,11 @@ 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';
@@ -39,7 +47,7 @@ class AuthCubit extends Cubit<AuthState> {
final ILoggerService _logger = GetIt.I<ILoggerService>();
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
// 🔥 登录验证 Completer:用于等待登录阶段的 have_logged_in 推送
Completer<bool>? _loginVerificationCompleter;
@@ -56,11 +64,57 @@ class AuthCubit extends Cubit<AuthState> {
) : 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(
@@ -70,14 +124,21 @@ class AuthCubit extends Cubit<AuthState> {
);
if (user != null) {
// 🔥 冷启动时不创建TCP连接,改为用户选择设备时再连接
// 避免发送 0x03 触发服务端残留 session 的 have_logged_in
// 2. 同步全局 App 状态
appCubit.setAuth(user);
// 3. 进入已登录状态
emit(AuthAuthenticated(user));
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态', level: 'INFO');
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());
@@ -88,6 +149,42 @@ class AuthCubit extends Cubit<AuthState> {
}
}
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);
@@ -96,33 +193,33 @@ class AuthCubit extends Cubit<AuthState> {
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; // 停留在登录页
return; // 停留在登录页
}
debugPrint('✅ [AUTH] TCP连接成功,验证通过');
_logger.logWithLevel('✅ [AUTH] TCP连接成功,验证通过', shouldLog: true);
} catch (e) {
@@ -149,6 +246,42 @@ class AuthCubit extends Cubit<AuthState> {
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 {
@@ -173,7 +306,7 @@ class AuthCubit extends Cubit<AuthState> {
try {
debugPrint('>>> [AUTH] 📢 准备显示异地登录提示弹窗');
_logger.logWithLevel('>>> [AUTH] 📢 准备显示异地登录提示弹窗', shouldLog: true);
// 使用全局 navigatorKey 显示弹窗
final context = navigatorKey.currentContext;
if (context != null) {
@@ -190,9 +323,12 @@ class AuthCubit extends Cubit<AuthState> {
);
} else {
debugPrint('>>> [AUTH] ⚠️ 无法获取 Navigator Context,将直接退出');
_logger.logWithLevel('>>> [AUTH] ⚠️ 无法获取 Navigator Context,将直接退出', shouldLog: true);
_logger.logWithLevel(
'>>> [AUTH] ⚠️ 无法获取 Navigator Context,将直接退出',
shouldLog: true,
);
}
// 🔥 延迟 2 秒后执行退出,给用户时间看到提示
Future.delayed(const Duration(seconds: 2), () {
debugPrint('>>> [AUTH] ⏰ 延迟结束,开始执行退出登录');
@@ -207,27 +343,142 @@ class AuthCubit extends Cubit<AuthState> {
}
}
/// 🔥 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 {
final devicesCubit = GetIt.I<DevicesCubit>();
final deviceStatusBloc = GetIt.I<DeviceStatusBloc>();
final siteCubit = GetIt.I<SiteCubit>();
// 1. 清空远程控制所有状态(targetDevice、权限、摇杆数据、电压电量等)
final remoteControlCubit = GetIt.I<RemoteControlCubit>();
remoteControlCubit.clearAll();
debugPrint('✅ [AUTH] 已清空 RemoteControlCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 RemoteControlCubit 状态');
// 1. 清空设备列表和选中设备
// 2. 清空设备列表和选中设备
final devicesCubit = GetIt.I<DevicesCubit>();
devicesCubit.emit(const DevicesState());
debugPrint('✅ [AUTH] 已清空 DevicesCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 DevicesCubit 状态');
// 2. 清空设备实时状态
// 3. 清空设备实时状态(图表数据等)
final deviceStatusBloc = GetIt.I<DeviceStatusBloc>();
deviceStatusBloc.add(DeviceStatusReset());
debugPrint('✅ [AUTH] 已重置 DeviceStatusBloc 状态');
_logger.logWithLevel('✅ [AUTH] 已重置 DeviceStatusBloc 状态');
// 3. 清空全局选中的场站
siteCubit.clearSelectedSite();
debugPrint('✅ [AUTH] 已清空 SiteCubit 选中状态');
_logger.logWithLevel('✅ [AUTH] 已清空 SiteCubit 选中状态');
// 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] 所有业务状态已清空');
@@ -240,15 +491,15 @@ class AuthCubit extends Cubit<AuthState> {
/// 🔥 等待登录验证结果(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;
}
@@ -290,35 +541,46 @@ class AuthCubit extends Cubit<AuthState> {
if (respond == 'have_logged_in') {
// 🔥 登录阶段策略:HTTP 已成功,TCP 认证阶段的推送视为服务端状态同步,直接放行
if (_loginVerificationCompleter != null && !_loginVerificationCompleter!.isCompleted) {
debugPrint('>>> [AUTH] 🛡️ 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入 APP');
if (_loginVerificationCompleter != null &&
!_loginVerificationCompleter!.isCompleted) {
debugPrint(
'>>> [AUTH] 🛡️ 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入 APP',
);
_logger.logWithLevel(
'🛡️ [AUTH] 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入',
shouldLog: true,
);
_loginVerificationCompleter!.complete(false); // 标记为可以进入
_loginVerificationCompleter!.complete(false); // 标记为可以进入
return;
}
// 🔥 已登录阶段策略:检查是否处于安全模式
if (_isSafeMode) {
print('>>> [AUTH] 🛡️ 安全模式下拦截 have_logged_in 推送,防止控制中断');
_logger.logWithLevel(
'🛡️ [AUTH] 安全模式下拦截异地登录推送',
level: 'WARN',
);
_logger.logWithLevel('🛡️ [AUTH] 安全模式下拦截异地登录推送', level: 'WARN');
return; // 拦截退出逻辑
}
print('>>> [AUTH] ⚠️ 已登录状态下收到 TCP 0x12 指令 respond=have_logged_in');
debugPrint('>>> [AUTH] ℹ️ 暂时忽略该推送,观察是否影响业务操作...');
_logger.logWithLevel(
'⚠️ [AUTH] 已登录状态收到 have_logged_in,暂不处理,观察业务影响',
'⚠️ [AUTH] 已登录状态收到 have_logged_in,弹出异地登录提示',
level: 'WARN',
);
// 如果你希望依然保持严格的安全策略,可以取消下面注释恢复退出逻辑:
// _showKickOutDialog();
// 🔥 关键:检查是否是自身 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",不处理');
}

View File

@@ -28,15 +28,16 @@ class LoginCubit extends Cubit<LoginState> {
// if (user != null) {
// devicesCubit.fetchAllDevices(user.username);
// }
result.fold((failure) => emit(LoginFailure(failure.message)), (user) {
result.fold((failure) => emit(LoginFailure(failure.message)), (user) async {
// 先更新全局用户状态(确保首页能获取到 token)
authCubit.appCubit.setAuth(user);
// 发出登录成功状态(触发页面跳转)
emit(LoginSuccess(user));
// 后台异步执行 TCP 连接等初始化操作(不阻塞登录流程)
authCubit.loginSuccess(user).catchError((e) {
// 🔥 先等待 TCP 连接和认证完成,确保 AuthCubit 变为 AuthAuthenticated
// 再触发页面导航,避免 GoRouter 拦截踢回登录页
await authCubit.loginSuccess(user).catchError((e) {
print('TCP 初始化失败: $e');
});
// 发出登录成功状态(触发页面跳转)
emit(LoginSuccess(user));
});
} catch (e) {
emit(LoginFailure(e.toString()));