diff --git a/lib/core/env/env_config.dart b/lib/core/env/env_config.dart index 5859fc23..6a682e47 100644 --- a/lib/core/env/env_config.dart +++ b/lib/core/env/env_config.dart @@ -7,9 +7,9 @@ class EnvConfig { static String get sentryDsn { // 根据环境切换 DSN(去 Sentry 后台创建两个项目:Test 和 Prod) if (environment == 'prod') { - return 'https://your_prod_dsn@sentry.io/123'; + return 'https://d5fdda78335793935593efa3f4ed11bb@o4510984107261952.ingest.de.sentry.io/4510984527937616'; } - return 'https://your_test_dsn@sentry.io/456'; + return 'https://1a72051f8ac6c21cde96ded18a318217@o4510984107261952.ingest.de.sentry.io/4510984468824144'; } static bool get isProduction => environment == 'prod'; diff --git a/lib/core/infrastructure/logging/sentry_logger_impl.dart b/lib/core/infrastructure/logging/sentry_logger_impl.dart index 91ae2199..14bcee3a 100644 --- a/lib/core/infrastructure/logging/sentry_logger_impl.dart +++ b/lib/core/infrastructure/logging/sentry_logger_impl.dart @@ -10,17 +10,26 @@ import 'package:intl/intl.dart'; class SentryLoggerImpl implements ILoggerService { File? _localLogFile; + //调试模式开关 + bool _isDebugModeEnabled = false; @override Future init() async { - // await SentryFlutter.init((options) { - // options.dsn = EnvConfig.sentryDsn; - // options.environment = EnvConfig.environment; - // // 生产环境只采样 20% 的性能数据,测试环境全开 - // options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0; - // }); - await _collectDeviceInfo(); + _isDebugModeEnabled = false; + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.environment = EnvConfig.environment; + // 生产环境只采样 20% 的性能数据,测试环境全开 + options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0; + // 动态设置 Sentry 接收的日志级别 + // 如果开启调试模式,接收 DEBUG;否则生产环境只接收 INFO 以上 + + options.debug = _isDebugModeEnabled; // 开启 SDK 内部调试 + + }); + + await _collectDeviceInfo(); await _prepareLocalFile(); } @@ -93,6 +102,54 @@ class SentryLoggerImpl implements ILoggerService { // 本地写入失败不应崩溃 } } + @override + void logWithLevel( + String message, { + String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR' + Map? data, + }) { + final timestamp = DateTime.now().toIso8601String(); + final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n"; + + // --- 策略判断 --- + // 1. 开发环境:全部允许 + // 2. 生产环境: + // - 如果是 DEBUG 级别:只有 _isDebugModeEnabled 为 true 时才允许 + // - 其他级别:始终允许 + bool shouldProcess = !EnvConfig.isProduction || + level != 'DEBUG' || + _isDebugModeEnabled; + + if (shouldProcess) { + // 写入本地文件 + _writeToLocalFile(logEntry); + + // 上报 Sentry + SentryLevel sentryLevel; + switch (level) { + case 'DEBUG': sentryLevel = SentryLevel.debug; break; + case 'WARNING': sentryLevel = SentryLevel.warning; break; + case 'ERROR': sentryLevel = SentryLevel.error; break; + default: sentryLevel = SentryLevel.info; + } + + // 使用 addBreadcrumb 记录轨迹 + Sentry.addBreadcrumb(Breadcrumb( + message: message, + level: sentryLevel, + data: data, + )); + + // 如果是 ERROR,额外捕获为一个事件 + if (level == 'ERROR') { + Sentry.captureMessage(message, level: sentryLevel); + } + } + + if (kDebugMode) { + debugPrint(logEntry); + } + } // 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能 Future getLocalLogPath() async { diff --git a/lib/core/logging/i_logger_service.dart b/lib/core/logging/i_logger_service.dart index 6314cb86..3f23e0da 100644 --- a/lib/core/logging/i_logger_service.dart +++ b/lib/core/logging/i_logger_service.dart @@ -6,6 +6,11 @@ abstract class ILoggerService { bool isWarning = false, Map? data, }); + void logWithLevel( + String message, { + String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR' + Map? data, + }); void addBreadcrumb(String message, {String? category}); void setUser(String userId); } diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 898cb898..22c2ae85 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -9,8 +9,8 @@ class DioClient { final dio = Dio( BaseOptions( baseUrl: HttpApiConsts.baseUrl, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 10), + connectTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 5), headers: {'Content-Type': 'application/json'}, ), ); diff --git a/lib/core/network/net_message_dispatcher.dart b/lib/core/network/net_message_dispatcher.dart index 3277e87f..a6658841 100644 --- a/lib/core/network/net_message_dispatcher.dart +++ b/lib/core/network/net_message_dispatcher.dart @@ -81,24 +81,6 @@ class NetMessageDispatcher { if (fullData.length > 5 && fullData[5] == 0x01) { debugPrint('✅ [Dispatcher] 状态位验证通过:0x01 - 可以发送下一个指令'); ///取全局的路径规划发送实体 queue - - - // if (isStart && locationQueue.isEmpty) { - // isStart = false; - // print("[track]"); - // tcpClient.disconnect(); - // return; - // } - // - // final entity = locationQueue.removeFirst(); // 类型:DeviceAddPathPointModel - // final routePlanSendEntity = RoutePlanSendEntity( - // commandType: 0x01, - // pointCounts: 1, - // targetLatitude: entity.latitude, - // targetLongitude: entity.longitude, - // speed: 1000, - // ); - /// 发送下一个指令 移除上一条数据 var queue = _pathPlanningService.getQueue(); //去除第一条数据 diff --git a/lib/features/auth/data/datasources/impl/auth_tcp_datasource_impl.dart b/lib/features/auth/data/datasources/impl/auth_tcp_datasource_impl.dart index bd5cacba..173de890 100644 --- a/lib/features/auth/data/datasources/impl/auth_tcp_datasource_impl.dart +++ b/lib/features/auth/data/datasources/impl/auth_tcp_datasource_impl.dart @@ -28,7 +28,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource { @override Future sendAuthPacket() async { if (!_tcpClient.isConnected) { - throw Exception('TCP 未连接,无法发送认证包'); + throw Exception('TCP 未连接,无法发送认证包sendAuthPacket'); } final user = await _userStorage.getUser(); @@ -40,7 +40,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource { final token = user.token!; final authString = '$username:app:$token'; - debugPrint('🔑 [AuthTcp] 准备发送认证包:$authString'); + debugPrint('🔑 sendAuthPacket[AuthTcp] 准备发送认证包:$authString'); final authBytes = utf8.encode(authString); @@ -57,7 +57,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource { ..addByte(0xAB); _tcpClient.sendRawBytes(builder.takeBytes()); // 假设 TcpClient 增加了此方法,或用 socket.add - debugPrint('✅ [AuthTcp] 认证包已发送'); + debugPrint('✅ sendAuthPacket[AuthTcp] 认证包已发送'); // 获取用户设备列表 try { @@ -70,36 +70,36 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource { await eitherResult.fold( (failure) { // 处理失败:打印日志或抛出异常 - debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure'); + debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure'); throw Exception('获取设备列表失败:$failure'); }, (devices) async { // 处理成功:devices 现在是真正的 List if (devices.isEmpty) { - debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤'); + debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤'); return; } // 取第一个设备 final DeviceEntity targetDevice = devices.first; - debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}'); + debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}'); // 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理) final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName); await switchResult.fold( (failure) { - debugPrint('❌ [AuthTcp] 切换设备失败:$failure'); + debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure'); throw Exception('切换设备失败:$failure'); }, (success) { - debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据'); + debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据'); }, ); }, ); } catch (e) { - debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e'); + debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e'); rethrow; } } diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 86ce5d54..6affdfba 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -1,11 +1,14 @@ import 'dart:async'; 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'; @@ -34,16 +37,27 @@ class AuthCubit extends Cubit { /// App 启动时检查本地缓存 Future appStarted() async { + final logger = GetIt.I() as SentryLoggerImpl; + // print("App 启动时检查本地缓存"); final user = await storage.getUser(); + logger.logWithLevel( + '启动时检查本地缓存', + level: 'INFO', + data: {'user': user, 'data': user} + ); if (user != null) { - // 1. 建立 TCP 连接 (远程控制) + // print("App 启动时检查本地缓存user != null) "); + // 1. print("App 启动时检查本地缓存user != null) "); + // print("App 启动时检查本地缓存 tcp.connect) "); tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); - //await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数 + + await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数 // 2. 同步全局 App 状态 appCubit.setAuth(user); // 3. 进入已登录状态 emit(AuthAuthenticated(user)); } else { + // print("App 启动时检查本地缓存user=null) "); emit(AuthUnauthenticated()); } } diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index 47021418..11f4204a 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -1,10 +1,12 @@ import 'package:dio/dio.dart'; import 'package:flutter/widgets.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/network/tcp/tcp_client.dart'; import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart'; +import '../../../../../core/infrastructure/logging/sentry_logger_impl.dart'; +import '../../../../../core/logging/i_logger_service.dart'; import '../../../../../core/storage/user_storage.dart'; import '../../../domain/entities/device_location_entity.dart'; @@ -24,17 +26,43 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future bindDevice(String deviceId, String deviceAlias) async { + final logger = GetIt.I() as SentryLoggerImpl; + logger.logWithLevel('用户点击bindDevice方法开始触发', level: 'INFO'); + logger.logWithLevel( + '用户点击bindDevice方法API 请求开始', + level: 'INFO', + data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'} + ); var response = await dio.post( HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias}, ); + logger.logWithLevel( + '请求详细参数', + level: 'DEBUG', + data: { + 'url': 'https://serviceri.satabot.com/iot/device/bindDevice', + 'deviceId': deviceId, + 'deviceAlias': deviceAlias + } + ); if (response.statusCode != 200) { + logger.logWithLevel( + 'API 响应失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; if (responseData['code'] != 200 || responseData['data'] != true) { + logger.logWithLevel( + 'API 响应失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception(responseData['msg'] ?? '业务异常'); } @@ -78,7 +106,15 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future switchDevice(String platform, String deviceId) async { + final logger = GetIt.I() as SentryLoggerImpl; final token = await _getToken(); + print('用户点击switchDevice方法开始触发: $token'); + logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG'); + logger.logWithLevel( + '用户点击switchDevice方法API 请求开始', + level: 'INFO', + data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'} + ); var response = await dio.post( HttpApiConsts.switchDevice, data: {'platform': platform, 'deviceId': deviceId}, @@ -89,16 +125,38 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { }, ), ); + logger.logWithLevel( + '请求详细参数', + level: 'DEBUG', + data: { + 'url': 'https://serviceri.satabot.com/iot/device/switchDevice', + 'platform': platform, + 'deviceId': deviceId + } + ); if (response.statusCode != 200) { + logger.logWithLevel( + 'API 请求失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; - + logger.logWithLevel( + 'API 响应成功', + level: 'INFO', + data: {'statusCode': response.statusCode, 'data': response.data} + ); if (responseData['code'] != 200 || responseData['data'] != true) { + logger.logWithLevel( + 'API 响应失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception(responseData['msg'] ?? '业务异常'); } - return 1; } @@ -143,9 +201,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future getDeviceLocation(String deviceame) async { + final logger = GetIt.I() as SentryLoggerImpl; // TODO: implement getDeviceLocation try { // 发起 GET 请求 + logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO'); + logger.logWithLevel( + '请求详细参数', + level: 'DEBUG', + data: { + 'url': 'https://serviceri.satabot.com/iot/device/userDevice', + 'tenantName': deviceame + } + ); final response = await dio.get( 'https://serviceri.satabot.com/iot/device/userDevice', queryParameters: {'tenantName': deviceame}, @@ -160,21 +228,35 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { // 检查响应状态码 if (response.statusCode == 200) { final data = response.data; + logger.logWithLevel( + 'API 响应接收', + level: 'DEBUG', + data: {'statusCode': response.statusCode, 'data': response.data} + ); if (data['code'] == 200) { //return data['data']; // 返回设备数据 final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化 + logger.logWithLevel( + '成功获取设备位置', + level: 'INFO', + data: {'lat': locationData['latitude'], 'lng': locationData['longitude']} + ); return DeviceLocationEntity( deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude'], ); } else { + logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR'); throw Exception(data['msg'] ?? '业务异常'); } } else { + logger.logWithLevel('网络请求失败: ${response.statusCode}', level: 'ERROR'); throw Exception('网络请求失败: ${response.statusCode}'); } } catch (e) { + logger.logWithLevel('获取设备位置失败: $e', level: 'ERROR'); + logger.captureException(e, stackTrace: e); throw Exception('获取设备位置失败: $e'); }