Files
flutterApp/lib/features/auth/presentation/bloc/auth_cubit.dart
Songzex 335c468aac 修复版本管理整包更新无法弹出的问题
完成集成新的登录接口的更换使用
完成集成用户的场站列表接口
完成开发设计选择场站项为全局属性
完成我的页面的个人信息的集成和详情页面的开发
完成设备页面的中无人机机场的列表接口对接入和个项页面的布局的优化
完成设备页面的中无人机机场的详情接口的对接和使用和页面的更新布局
2026-05-29 14:13:34 +08:00

196 lines
8.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

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:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.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 '../../../../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';
/// 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; // 新增:用于管理监听生命周期
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource) : super(AuthInitial()) {
// Cubit 一启动就开始监听 TCP 的“自动逻辑”
_listenToAuthResponse();
}
/// App 启动时检查本地缓存
Future<void> appStarted() async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
try {
final user = await storage.getUser();
logger.logWithLevel(
'启动时检查本地缓存',
level: 'INFO',
data: {'user': user != null ? '找到用户: ${user.username}' : '未找到用户'}
);
if (user != null) {
// await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
// await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
// tcp.startHeartbeat(interval: const Duration(seconds: 4));
// 2. 同步全局 App 状态
appCubit.setAuth(user);
// 3. 进入已登录状态
emit(AuthAuthenticated(user));
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态', level: 'INFO');
} else {
logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO');
emit(AuthUnauthenticated());
}
} catch (e) {
logger.logWithLevel('❌ [AUTH] 应用启动检查失败: $e', level: 'ERROR');
emit(AuthUnauthenticated());
}
}
/// 当 HTTP 登录/注册成功后调用
Future<void> loginSuccess(UserEntity user) async {
await storage.saveUser(user);
// await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
// await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数
//tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳
appCubit.setAuth(user);
emit(AuthAuthenticated(user));
}
/// 退出登录 (主动或被动)
Future<void> logout() async {
//print("退出登录");
// 🔥 关键修复:在断开TCP前,先清空所有业务状态,避免重连触发旧数据
_clearAllBusinessState();
await storage.deleteUser();
tcp.disconnect();
appCubit.clearAuth();
emit(AuthUnauthenticated());
}
/// 🔥 新增:清空所有业务状态,防止数据泄露到新账户
void _clearAllBusinessState() {
try {
final devicesCubit = GetIt.I<DevicesCubit>();
final deviceStatusBloc = GetIt.I<DeviceStatusBloc>();
final siteCubit = GetIt.I<SiteCubit>();
// 1. 清空设备列表和选中设备
devicesCubit.emit(const DevicesState());
debugPrint('✅ [AUTH] 已清空 DevicesCubit 状态');
_logger.logWithLevel('✅ [AUTH] 已清空 DevicesCubit 状态');
// 2. 清空设备实时状态
deviceStatusBloc.add(DeviceStatusReset());
debugPrint('✅ [AUTH] 已重置 DeviceStatusBloc 状态');
_logger.logWithLevel('✅ [AUTH] 已重置 DeviceStatusBloc 状态');
// 3. 清空全局选中的场站
siteCubit.clearSelectedSite();
debugPrint('✅ [AUTH] 已清空 SiteCubit 选中状态');
_logger.logWithLevel('✅ [AUTH] 已清空 SiteCubit 选中状态');
debugPrint('✅ [AUTH] 所有业务状态已清空');
_logger.logWithLevel('✅ [AUTH] 所有业务状态已清空');
} catch (e) {
debugPrint('❌ [AUTH] 清空业务状态失败: $e');
_logger.logWithLevel('❌ [AUTH] 清空业务状态失败: $e', level: 'ERROR');
}
}
/// TCP 指令监听
void _listenToAuthResponse() {
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'DEBUG');
_kickOutSub?.cancel(); // 防止重复监听
// 直接监听原始数据包,自己处理 JSON 解析(去掉 CRC 字节)
_kickOutSub = dispatcher.onCommand(0x12).listen((packet) {
_logger.logWithLevel('[AUTH] 获取原始包:$packet', level: 'DEBUG');
_logger.logWithLevel('>>> [AUTH] 收到 0x12 原始包,payload 长度=${packet.payload.length}, 内容=${packet.payload}', level: 'DEBUG');
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);
}
_logger.logWithLevel('[AUTH] 获取 JSON: $jsonString', level: 'DEBUG');
final jsonMap = jsonDecode(jsonString);
_logger.logWithLevel('[AUTH] 获取 JSON Map: $jsonMap', level: 'DEBUG');
final respond = jsonMap['respond'] ?? '';
if (respond == 'have_logged_in') {
_logger.logWithLevel('[AUTH] ⚠️ 检测到异地登录 (respond=have_logged_in),开始退出...', level: 'WARN');
logout();
} else {
_logger.logWithLevel('[AUTH] ⚠️ 检测到异地登录 (respond=$respond),开始退出...', level: 'WARN');
}
} catch (e) {
_logger.logWithLevel('[AUTH] ❌ JSON 解析失败:$e', level: 'ERROR');
}
});
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'DEBUG');
}
@override
Future<void> close() {
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
return super.close();
}
/// 🔥 息屏/后台后恢复到前台时的重连方法
Future<void> reconnectAfterResume() async {
_logger.logWithLevel('[AUTH] 检测到应用恢复到前台,检查 TCP 连接状态...', level: 'DEBUG');
// 如果 TCP 未连接,则执行重连
if (!tcp.isConnected) {
_logger.logWithLevel('[AUTH] TCP 未连接,开始重连...',
level: 'DEBUG');
try {
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
tcp.startHeartbeat(interval: const Duration(seconds: 4));
_logger.logWithLevel('[AUTH] TCP 重连成功!', level: 'DEBUG');
} catch (e) {
_logger.logWithLevel('[AUTH] TCP 重连失败:$e', level: 'ERROR');
}
} else {
_logger.logWithLevel('[AUTH] TCP 已连接,无需重连', level: 'DEBUG');
// 可选:发送一个心跳包确认连接有效
tcp.sendHeartbeat();
}
}
}