完成封装打印和推送功能日志方法和使用

完成添加绑定设备接口的日志便于追溯
This commit is contained in:
2026-03-04 16:33:56 +08:00
parent fcfdb2c78c
commit 786d11c936
6 changed files with 239 additions and 25 deletions

View File

@@ -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';

View File

@@ -10,17 +10,26 @@ import 'package:intl/intl.dart';
class SentryLoggerImpl implements ILoggerService {
File? _localLogFile;
//调试模式开关
bool _isDebugModeEnabled = false;
@override
Future<void> 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<String, dynamic>? 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<String?> getLocalLogPath() async {

View File

@@ -6,6 +6,11 @@ abstract class ILoggerService {
bool isWarning = false,
Map<String, dynamic>? data,
});
void logWithLevel(
String message, {
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
Map<String, dynamic>? data,
});
void addBreadcrumb(String message, {String? category});
void setUser(String userId);
}

View File

@@ -1,8 +1,12 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import '../app/app_user_cubit.dart';
import '../di/injection.dart';
import '../infrastructure/logging/sentry_logger_impl.dart';
import '../logging/i_logger_service.dart';
class DioClient {
static Dio create() {
@@ -14,26 +18,110 @@ class DioClient {
headers: {'Content-Type': 'application/json'},
),
);
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 1. 从 GetIt 中获取全局的 AppUserCubit
if (kDebugMode) {
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
}
// dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
// dio.interceptors.add(
// InterceptorsWrapper(
// onRequest: (options, handler) {
// // 1. 从 GetIt 中获取全局的 AppUserCubit
// final userCubit = sl<AppUserCubit>();
//
// // 2. 从 Cubit 的状态中提取 Token
// final token = userCubit.state.user?.token;
//
// // 3. 如果 Token 存在,则添加到请求头
// if (token != null && token.isNotEmpty) {
// options.headers['Authorization'] = 'Bearer $token';
// }
//
// // 继续发送请求
// return handler.next(options);
// },
// ),
// );
// return dio;
// }
dio.interceptors.add(InterceptorsWrapper(
onError: (DioException error, ErrorInterceptorHandler handler) async {
// 捕获所有网络异常 (超时、无网络、DNS 错误等)
await _reportToSentry(error);
return handler.next(error);
},
onResponse: (Response response, ResponseInterceptorHandler handler) async {
// 捕获业务错误 (如 HTTP 4xx, 5xx)
if (response.statusCode != null && response.statusCode! >= 400) {
await _reportHttpError(response);
}
return handler.next(response);
},
onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
// 原有 Token 逻辑保持不变
try {
final userCubit = sl<AppUserCubit>();
// 2. 从 Cubit 的状态中提取 Token
final token = userCubit.state.user?.token;
// 3. 如果 Token 存在,则添加到请求头
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
} catch (e) {
// 防止获取 Token 时出错阻断请求
debugPrint('获取 Token 失败: $e');
}
return handler.next(options);
},
));
// 继续发送请求
return handler.next(options);
},
),
);
return dio;
}
// 上报 HTTP 状态码错误 (404, 500 等)
static Future<void> _reportHttpError(Response response) async {
try {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
// 记录详细的错误上下文
logger.logWithLevel(
'HTTP 请求失败: ${response.statusCode}',
level: 'ERROR',
data: {
'url': response.requestOptions.uri.toString(),
'method': response.requestOptions.method,
'statusCode': response.statusCode,
'responseData': response.data, // 注意:如果响应包含敏感信息,请脱敏
},
);
// 可选:如果希望每个 4xx/5xx 都生成一个独立 Issue,取消下面注释
// await Sentry.captureMessage(
// 'HTTP Error ${response.statusCode}: ${response.requestOptions.path}',
// level: SentryLevel.error,
// extra: {'data': response.data},
// );
} catch (e) {
debugPrint('Sentry 上报 HTTP 错误失败: $e');
}
}
// 上报网络异常 (超时、断开等)
static Future<void> _reportToSentry(DioException error) async {
try {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
logger.logWithLevel(
'网络异常: ${error.type}',
level: 'ERROR',
data: {
'url': error.requestOptions.uri.toString(),
'method': error.requestOptions.method,
'message': error.message,
'type': error.type.name,
},
);
// 捕获完整堆栈
logger.captureException(error, stackTrace: error.stackTrace);
} catch (e) {
debugPrint('Sentry 上报网络异常失败: $e');
}
}
}

View File

@@ -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';
@@ -51,7 +54,9 @@ class AuthCubit extends Cubit<AuthState> {
/// 当 HTTP 登录/注册成功后调用
Future<void> loginSuccess(UserEntity user) async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
await storage.saveUser(user);
logger.setUser(user.userId);
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳
@@ -62,6 +67,8 @@ class AuthCubit extends Cubit<AuthState> {
/// 退出登录 (主动或被动)
Future<void> logout() async {
print("退出登录");
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
logger.setUser('');// 清除用户信息
await storage.deleteUser();
tcp.disconnect();
appCubit.clearAuth();

View File

@@ -1,9 +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/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';
@@ -77,7 +80,15 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<int> switchDevice(String platform, String deviceId) async {
final logger = GetIt.I<ILoggerService>() 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},
@@ -88,16 +99,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;
}
@@ -142,9 +175,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<dynamic> getDeviceLocation(String deviceame) async {
final logger = GetIt.I<ILoggerService>() 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},
@@ -159,21 +202,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');
}