Files
feature-tenant/lib/features/auth/presentation/bloc/auth_cubit.dart
Songzex 354f7d6290 优化了tcp实时更新对UI线程的压力和对用弹窗的重复弹窗的影响。
更换了路径规划的的经纬度的字段名和取操作的名字更换。
2026-06-08 13:35:36 +08:00

218 lines
8.2 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: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) {
// 🔥 冷启动时重新初始化 TCP 连接
await tcp.initializeTcp(
host: TCPConsts.TCP_IP,
port: TCPConsts.TCP_PORT,
);
// 2. 同步全局 App 状态
appCubit.setAuth(user);
// 3. 进入已登录状态
emit(AuthAuthenticated(user));
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态并建立TCP连接', 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);
// 🔥 使用封装的TCP初始化方法:连接 + 认证 + 心跳
await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
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() {
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') {
debugPrint('>>> [AUTH] ⚠️ 检测到异地登录 (respond=have_logged_in),开始退出...');
_logger.logWithLevel(
'⚠️ [AUTH] 检测到异地登录 (respond=have_logged_in),开始退出...',
level: 'INFO',
);
logout();
} else {
debugPrint('>>> [AUTH] ℹ️ 收到 0x12 消息,respond="$respond",不处理');
}
} catch (e) {
debugPrint('>>> [AUTH] ❌ JSON 解析失败:$e');
}
});
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'INFO');
}
@override
Future<void> close() {
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
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();
}
}
}