Commit 423c2556605a8f4835b003e1bc1f88412ef7c43c
Committed by
权海
1 parent
62536c66
feat(ui): 退登添加日志诊断
Showing
9 changed files
with
306 additions
and
9 deletions
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart'; | ||
| 3 | import 'package:flutter/widgets.dart'; | 4 | import 'package:flutter/widgets.dart'; |
| 4 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 5 | 6 | ||
| @@ -51,4 +52,47 @@ abstract final class AppBootstrap { | @@ -51,4 +52,47 @@ abstract final class AppBootstrap { | ||
| 51 | unawaited(AppConfigService.to.fetch()); | 52 | unawaited(AppConfigService.to.fetch()); |
| 52 | } | 53 | } |
| 53 | } | 54 | } |
| 55 | + | ||
| 56 | + static Future<void> initForBackground() async { | ||
| 57 | + WidgetsFlutterBinding.ensureInitialized(); | ||
| 58 | + AppLogger.init(); | ||
| 59 | + | ||
| 60 | + await UserAgentProvider.init(); | ||
| 61 | + | ||
| 62 | + final local = await LocalStorage.open(); | ||
| 63 | + | ||
| 64 | + final environmentConfig = AppEnvironmentConfig(local); | ||
| 65 | + await environmentConfig.init(); | ||
| 66 | + // 1. 【核心】只读取已保存的地区与环境,不要在后台调用 _detectRegion() 重新探测并写入 SharedPreferences,防止误切海外服 | ||
| 67 | + // environmentConfig.environment.value = await local.readEnvironment(); | ||
| 68 | + // environmentConfig.region.value = | ||
| 69 | + // await local.readRegion() ?? AppRegion.china; | ||
| 70 | + | ||
| 71 | + final userPreferencesStorage = | ||
| 72 | + UserPreferencesStorage(local.sharedPreferences); | ||
| 73 | + await userPreferencesStorage.init(); | ||
| 74 | + | ||
| 75 | + final userAccountStorage = UserAccountStorage(local.sharedPreferences); | ||
| 76 | + | ||
| 77 | + InitialBinding( | ||
| 78 | + environmentConfig: environmentConfig, | ||
| 79 | + userPreferencesStorage: userPreferencesStorage, | ||
| 80 | + userAccountStorage: userAccountStorage, | ||
| 81 | + localStorage: local, | ||
| 82 | + ).dependencies(); | ||
| 83 | + | ||
| 84 | + NativeHealthDataUpdateBridge.initialize(); | ||
| 85 | + // 2. 后台无 UI,不需要初始化全局 Loading 弹窗 | ||
| 86 | + LoadingService.init(); | ||
| 87 | + // 3. 后台计算不需要重复初始化融云/分析等前台 SDK | ||
| 88 | + if (local.termsAgreed) { | ||
| 89 | + await Get.find<UserStateService>().initializeSdks(); | ||
| 90 | + unawaited(AppConfigService.to.fetch()); | ||
| 91 | + } | ||
| 92 | + AppDiagnosticLogStore.instance.log( | ||
| 93 | + category: 'initForBackground', | ||
| 94 | + action: 'success', | ||
| 95 | + details: {}, | ||
| 96 | + ); | ||
| 97 | + } | ||
| 54 | } | 98 | } |
| @@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | @@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | ||
| 8 | import 'package:flutter/material.dart'; | 8 | import 'package:flutter/material.dart'; |
| 9 | import 'package:get/get.dart'; | 9 | import 'package:get/get.dart'; |
| 10 | 10 | ||
| 11 | +import '../../../../core/logging/app_diagnostic_log_store.dart'; | ||
| 11 | import '../../../../core/logging/app_logger.dart'; | 12 | import '../../../../core/logging/app_logger.dart'; |
| 12 | import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; | 13 | import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 13 | import '../../../../core/services/raw_data_service/health_raw_models.dart'; | 14 | import '../../../../core/services/raw_data_service/health_raw_models.dart'; |
| @@ -32,6 +33,7 @@ class DeveloperOptionsItem { | @@ -32,6 +33,7 @@ class DeveloperOptionsItem { | ||
| 32 | } | 33 | } |
| 33 | 34 | ||
| 34 | enum DeveloperOptionsAction { | 35 | enum DeveloperOptionsAction { |
| 36 | + shareAppDiagnosticLog, | ||
| 35 | shareHealthRawDataDatabase, | 37 | shareHealthRawDataDatabase, |
| 36 | shareNativeAppleHealthObserverRecord, | 38 | shareNativeAppleHealthObserverRecord, |
| 37 | shareFlutterObserverRecord, | 39 | shareFlutterObserverRecord, |
| @@ -75,6 +77,10 @@ class DeveloperOptionsController extends GetxController { | @@ -75,6 +77,10 @@ class DeveloperOptionsController extends GetxController { | ||
| 75 | routeName: Routes.TOKEN_LOGIN, | 77 | routeName: Routes.TOKEN_LOGIN, |
| 76 | ), | 78 | ), |
| 77 | DeveloperOptionsItem( | 79 | DeveloperOptionsItem( |
| 80 | + title: '分享启动及登录态诊断日志', | ||
| 81 | + action: DeveloperOptionsAction.shareAppDiagnosticLog, | ||
| 82 | + ), | ||
| 83 | + DeveloperOptionsItem( | ||
| 78 | title: '分享本地计算数据库文件', | 84 | title: '分享本地计算数据库文件', |
| 79 | action: DeveloperOptionsAction.shareHealthRawDataDatabase, | 85 | action: DeveloperOptionsAction.shareHealthRawDataDatabase, |
| 80 | ), | 86 | ), |
| @@ -149,6 +155,9 @@ class DeveloperOptionsController extends GetxController { | @@ -149,6 +155,9 @@ class DeveloperOptionsController extends GetxController { | ||
| 149 | } | 155 | } |
| 150 | 156 | ||
| 151 | switch (item.action) { | 157 | switch (item.action) { |
| 158 | + case DeveloperOptionsAction.shareAppDiagnosticLog: | ||
| 159 | + await shareAppDiagnosticLog(); | ||
| 160 | + break; | ||
| 152 | case DeveloperOptionsAction.shareHealthRawDataDatabase: | 161 | case DeveloperOptionsAction.shareHealthRawDataDatabase: |
| 153 | await shareHealthRawDataDatabase(); | 162 | await shareHealthRawDataDatabase(); |
| 154 | break; | 163 | break; |
| @@ -209,6 +218,16 @@ class DeveloperOptionsController extends GetxController { | @@ -209,6 +218,16 @@ class DeveloperOptionsController extends GetxController { | ||
| 209 | } | 218 | } |
| 210 | } | 219 | } |
| 211 | 220 | ||
| 221 | + Future<void> shareAppDiagnosticLog() async { | ||
| 222 | + try { | ||
| 223 | + final bytes = await AppDiagnosticLogStore.instance.readBytes(); | ||
| 224 | + await _platformHostApi.shareFileData(bytes); | ||
| 225 | + } catch (error, stackTrace) { | ||
| 226 | + AppLogger.e('Share AppDiagnosticLog failed', error, stackTrace); | ||
| 227 | + Get.snackbar('Share failed', error.toString()); | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | + | ||
| 212 | Future<void> shareHealthRawDataDatabase() async { | 231 | Future<void> shareHealthRawDataDatabase() async { |
| 213 | try { | 232 | try { |
| 214 | final bytes = await _healthRawDataCoreService.readDatabaseFileBytes(); | 233 | final bytes = await _healthRawDataCoreService.readDatabaseFileBytes(); |
| @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_ | @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_ | ||
| 2 | import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart'; | 2 | import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart'; |
| 3 | import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dart'; | 3 | import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dart'; |
| 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart'; | ||
| 5 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 6 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 7 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 7 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; | 8 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| @@ -47,6 +48,13 @@ class HomeController extends GetxController { | @@ -47,6 +48,13 @@ class HomeController extends GetxController { | ||
| 47 | SchedulerBinding.instance.addPostFrameCallback((_) async { | 48 | SchedulerBinding.instance.addPostFrameCallback((_) async { |
| 48 | try { | 49 | try { |
| 49 | final route = await _platformHostApi.requestUnhandedUrl(); | 50 | final route = await _platformHostApi.requestUnhandedUrl(); |
| 51 | + AppDiagnosticLogStore.instance.log( | ||
| 52 | + category: 'requestUnhandedUrl', | ||
| 53 | + action: 'native_return_url', | ||
| 54 | + details: { | ||
| 55 | + 'route': route ?? '', | ||
| 56 | + }, | ||
| 57 | + ); | ||
| 50 | // final route ='doublefeel://flutter/home?tab=today&scroll_to_latest_hrv=true'; | 58 | // final route ='doublefeel://flutter/home?tab=today&scroll_to_latest_hrv=true'; |
| 51 | // final route = 'doublefeel://flutter/friend-home?user_id=131'; | 59 | // final route = 'doublefeel://flutter/friend-home?user_id=131'; |
| 52 | // final route = 'doublefeel://flutter/home?tab=trend&type=sleep'; | 60 | // final route = 'doublefeel://flutter/home?tab=trend&type=sleep'; |
| 1 | +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart'; | ||
| 1 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; | 2 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 2 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 3 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 3 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 4 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| @@ -43,6 +44,11 @@ class SplashController extends GetxController { | @@ -43,6 +44,11 @@ class SplashController extends GetxController { | ||
| 43 | final splashDelay = | 44 | final splashDelay = |
| 44 | Get.testMode ? Duration.zero : const Duration(milliseconds: 800); | 45 | Get.testMode ? Duration.zero : const Duration(milliseconds: 800); |
| 45 | await Future<void>.delayed(splashDelay); | 46 | await Future<void>.delayed(splashDelay); |
| 47 | + AppDiagnosticLogStore.instance.log( | ||
| 48 | + category: 'Splash', | ||
| 49 | + action: 'navigate_to_login_due_to_empty_token', | ||
| 50 | + details: {}, | ||
| 51 | + ); | ||
| 46 | Get.offAllNamed(AppRoutes.login); | 52 | Get.offAllNamed(AppRoutes.login); |
| 47 | return; | 53 | return; |
| 48 | } | 54 | } |
| @@ -55,6 +61,13 @@ class SplashController extends GetxController { | @@ -55,6 +61,13 @@ class SplashController extends GetxController { | ||
| 55 | } catch (_) {} | 61 | } catch (_) {} |
| 56 | await Get.offAllNamed(AppRoutes.home); | 62 | await Get.offAllNamed(AppRoutes.home); |
| 57 | } else { | 63 | } else { |
| 64 | + AppDiagnosticLogStore.instance.log( | ||
| 65 | + category: 'Splash', | ||
| 66 | + action: 'navigate_to_login_due_to_session_refresh_failed', | ||
| 67 | + details: { | ||
| 68 | + 'tokenLength': token.length, | ||
| 69 | + }, | ||
| 70 | + ); | ||
| 58 | Get.offAllNamed(AppRoutes.login); | 71 | Get.offAllNamed(AppRoutes.login); |
| 59 | } | 72 | } |
| 60 | } | 73 | } |
| @@ -77,6 +90,14 @@ class SplashController extends GetxController { | @@ -77,6 +90,14 @@ class SplashController extends GetxController { | ||
| 77 | return true; | 90 | return true; |
| 78 | 91 | ||
| 79 | case AppFailure(:final error): | 92 | case AppFailure(:final error): |
| 93 | + AppDiagnosticLogStore.instance.log( | ||
| 94 | + category: 'Splash', | ||
| 95 | + action: 'refresh_session_failure', | ||
| 96 | + details: { | ||
| 97 | + 'errorType': error.runtimeType.toString(), | ||
| 98 | + 'displayMessage': error.displayMessage, | ||
| 99 | + }, | ||
| 100 | + ); | ||
| 80 | if (error.isAccessTokenInvalid) { | 101 | if (error.isAccessTokenInvalid) { |
| 81 | return false; | 102 | return false; |
| 82 | } | 103 | } |
| 1 | import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; | 1 | import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; |
| 2 | +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart'; | ||
| 2 | import 'package:doublefeel_flutter/core/network/dio_client.dart'; | 3 | import 'package:doublefeel_flutter/core/network/dio_client.dart'; |
| 3 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | 4 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; |
| 4 | import 'package:flutter/foundation.dart'; | 5 | import 'package:flutter/foundation.dart'; |
| @@ -24,6 +25,20 @@ class AppEnvironmentConfig { | @@ -24,6 +25,20 @@ class AppEnvironmentConfig { | ||
| 24 | _isNativeDebug = await _platformHostApi.isDebugEnvoriment(); | 25 | _isNativeDebug = await _platformHostApi.isDebugEnvoriment(); |
| 25 | if (isOhos()) { | 26 | if (isOhos()) { |
| 26 | region.value = AppRegion.china; | 27 | region.value = AppRegion.china; |
| 28 | + await setRegion(AppRegion.china); | ||
| 29 | + return this; | ||
| 30 | + } | ||
| 31 | + AppDiagnosticLogStore.instance.log( | ||
| 32 | + category: 'Debug', | ||
| 33 | + action: 'detect_debug_from_native_success', | ||
| 34 | + details: { | ||
| 35 | + 'isDebug': _isNativeDebug, | ||
| 36 | + }, | ||
| 37 | + ); | ||
| 38 | + final stored = await _storage.readRegion(); | ||
| 39 | + if (stored == null) { | ||
| 40 | + final detected = await _detectRegion(); | ||
| 41 | + await setRegion(detected); | ||
| 27 | } else { | 42 | } else { |
| 28 | final stored = await _storage.readRegion(); | 43 | final stored = await _storage.readRegion(); |
| 29 | if (stored == null) { | 44 | if (stored == null) { |
| @@ -41,11 +56,27 @@ class AppEnvironmentConfig { | @@ -41,11 +56,27 @@ class AppEnvironmentConfig { | ||
| 41 | 56 | ||
| 42 | Future<AppRegion> _detectRegion() async { | 57 | Future<AppRegion> _detectRegion() async { |
| 43 | try { | 58 | try { |
| 44 | - return await _platformHostApi.isChinaRegion() | ||
| 45 | - ? AppRegion.china | ||
| 46 | - : AppRegion.global; | ||
| 47 | - } catch (_) { | ||
| 48 | - return _detectRegionFromLocale(); | 59 | + final isChina = await _platformHostApi.isChinaRegion(); |
| 60 | + AppDiagnosticLogStore.instance.log( | ||
| 61 | + category: 'Region', | ||
| 62 | + action: 'detect_region_from_native_success', | ||
| 63 | + details: { | ||
| 64 | + 'isChina': isChina, | ||
| 65 | + }, | ||
| 66 | + ); | ||
| 67 | + return isChina ? AppRegion.china : AppRegion.global; | ||
| 68 | + } catch (error, stackTrace) { | ||
| 69 | + final fallback = _detectRegionFromLocale(); | ||
| 70 | + AppDiagnosticLogStore.instance.log( | ||
| 71 | + category: 'Region', | ||
| 72 | + action: 'detect_region_from_native_failed', | ||
| 73 | + details: { | ||
| 74 | + 'error': error.toString(), | ||
| 75 | + 'stackTrace': stackTrace.toString(), | ||
| 76 | + 'fallbackResult': fallback.name, | ||
| 77 | + }, | ||
| 78 | + ); | ||
| 79 | + return fallback; | ||
| 49 | } | 80 | } |
| 50 | } | 81 | } |
| 51 | 82 | ||
| @@ -77,8 +108,17 @@ class AppEnvironmentConfig { | @@ -77,8 +108,17 @@ class AppEnvironmentConfig { | ||
| 77 | if (Get.isRegistered<DioClient>()) { | 108 | if (Get.isRegistered<DioClient>()) { |
| 78 | Get.find<DioClient>().refreshBaseUrl(); | 109 | Get.find<DioClient>().refreshBaseUrl(); |
| 79 | } | 110 | } |
| 80 | - } catch (e) { | 111 | + } catch (e, stackTrace) { |
| 81 | debugPrint('setRegion failed: $e'); | 112 | debugPrint('setRegion failed: $e'); |
| 113 | + AppDiagnosticLogStore.instance.log( | ||
| 114 | + category: 'Region', | ||
| 115 | + action: 'set_region_failed', | ||
| 116 | + details: { | ||
| 117 | + 'error': e.toString(), | ||
| 118 | + 'stackTrace': stackTrace.toString(), | ||
| 119 | + 'targetRegion': value.name, | ||
| 120 | + }, | ||
| 121 | + ); | ||
| 82 | } | 122 | } |
| 83 | } | 123 | } |
| 84 | 124 |
| @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | ||
| 2 | import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | 2 | import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; |
| 3 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 3 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 4 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | 4 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart'; | ||
| 5 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 6 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 6 | import 'package:get/get.dart'; | 7 | import 'package:get/get.dart'; |
| 7 | 8 | ||
| @@ -64,7 +65,13 @@ class AppErrorHandler { | @@ -64,7 +65,13 @@ class AppErrorHandler { | ||
| 64 | iconAsset: 'assets/images/common/ic_warning.png', | 65 | iconAsset: 'assets/images/common/ic_warning.png', |
| 65 | )); | 66 | )); |
| 66 | if (confirmResult.action != DialogAction.confirm) return; | 67 | if (confirmResult.action != DialogAction.confirm) return; |
| 67 | - | 68 | + AppDiagnosticLogStore.instance.log( |
| 69 | + category: 'AccessToken', | ||
| 70 | + action: 'invalid', | ||
| 71 | + details: { | ||
| 72 | + 'error': '401', | ||
| 73 | + }, | ||
| 74 | + ); | ||
| 68 | Get.offAllNamed(AppRoutes.login); | 75 | Get.offAllNamed(AppRoutes.login); |
| 69 | } | 76 | } |
| 70 | 77 |
| 1 | +import 'dart:convert'; | ||
| 2 | +import 'dart:io'; | ||
| 3 | +import 'dart:typed_data'; | ||
| 4 | + | ||
| 5 | +import 'package:path_provider/path_provider.dart'; | ||
| 6 | + | ||
| 7 | +import 'app_logger.dart'; | ||
| 8 | + | ||
| 9 | +/// Persists structured diagnostic events (region detection, login/user-prefs lifecycle, etc.) | ||
| 10 | +/// to a local JSON file using path_provider, which can be shared from Developer Options. | ||
| 11 | +class AppDiagnosticLogStore { | ||
| 12 | + AppDiagnosticLogStore({Directory? rootDirectory}) | ||
| 13 | + : _rootDirectory = rootDirectory; | ||
| 14 | + | ||
| 15 | + static final AppDiagnosticLogStore instance = AppDiagnosticLogStore(); | ||
| 16 | + | ||
| 17 | + static const _fileName = 'app_diagnostic_events.json'; | ||
| 18 | + static const _maxEventCount = 500; | ||
| 19 | + | ||
| 20 | + final Directory? _rootDirectory; | ||
| 21 | + Future<void> _writeQueue = Future.value(); | ||
| 22 | + | ||
| 23 | + /// Appends a diagnostic event to the local file. | ||
| 24 | + Future<void> log({ | ||
| 25 | + required String category, | ||
| 26 | + required String action, | ||
| 27 | + Map<String, Object?>? details, | ||
| 28 | + }) { | ||
| 29 | + _writeQueue = _writeQueue.then((_) async { | ||
| 30 | + try { | ||
| 31 | + final now = DateTime.now(); | ||
| 32 | + final event = <String, Object?>{ | ||
| 33 | + 'category': category, | ||
| 34 | + 'action': action, | ||
| 35 | + 'timestamp': now.toIso8601String(), | ||
| 36 | + 'timestamp_readable': _readableTime(now), | ||
| 37 | + 'timestamp_unix': now.millisecondsSinceEpoch / 1000, | ||
| 38 | + if (details != null && details.isNotEmpty) | ||
| 39 | + 'details': _sanitizeMap(details), | ||
| 40 | + }; | ||
| 41 | + | ||
| 42 | + try { | ||
| 43 | + AppLogger.d('[$category] $action details=$details'); | ||
| 44 | + } catch (_) {} | ||
| 45 | + | ||
| 46 | + final file = await _file(); | ||
| 47 | + await file.parent.create(recursive: true); | ||
| 48 | + final events = await _readEvents(file); | ||
| 49 | + events.add(event); | ||
| 50 | + | ||
| 51 | + final limited = events.length > _maxEventCount | ||
| 52 | + ? events.sublist(events.length - _maxEventCount) | ||
| 53 | + : events; | ||
| 54 | + | ||
| 55 | + const encoder = JsonEncoder.withIndent(' '); | ||
| 56 | + await file.writeAsString(encoder.convert(limited), flush: true); | ||
| 57 | + } catch (error, stackTrace) { | ||
| 58 | + try { | ||
| 59 | + AppLogger.e('AppDiagnosticLogStore log failed', error, stackTrace); | ||
| 60 | + } catch (_) {} | ||
| 61 | + } | ||
| 62 | + }); | ||
| 63 | + return _writeQueue; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /// Reads the log file as [Uint8List] bytes for sharing via platform APIs. | ||
| 67 | + Future<Uint8List> readBytes() async { | ||
| 68 | + final file = await _file(); | ||
| 69 | + if (!await file.exists()) { | ||
| 70 | + const emptyContent = '[\n {\n "message": "No diagnostic logs found"\n }\n]'; | ||
| 71 | + return Uint8List.fromList(utf8.encode(emptyContent)); | ||
| 72 | + } | ||
| 73 | + return await file.readAsBytes(); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + /// Reads the log file content as a formatted string. | ||
| 77 | + Future<String> readText() async { | ||
| 78 | + final file = await _file(); | ||
| 79 | + if (!await file.exists()) { | ||
| 80 | + return '暂无诊断日志'; | ||
| 81 | + } | ||
| 82 | + final text = await file.readAsString(); | ||
| 83 | + return text.trim().isEmpty ? '暂无诊断日志' : text; | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /// Clears the log file. | ||
| 87 | + Future<void> clear() async { | ||
| 88 | + final file = await _file(); | ||
| 89 | + if (await file.exists()) { | ||
| 90 | + await file.delete(); | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + Future<File> _file() async { | ||
| 95 | + final root = _rootDirectory ?? await getApplicationDocumentsDirectory(); | ||
| 96 | + return File('${root.path}/$_fileName'); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + Future<List<Map<String, Object?>>> _readEvents(File file) async { | ||
| 100 | + if (!await file.exists()) { | ||
| 101 | + return <Map<String, Object?>>[]; | ||
| 102 | + } | ||
| 103 | + try { | ||
| 104 | + final text = await file.readAsString(); | ||
| 105 | + if (text.trim().isEmpty) { | ||
| 106 | + return <Map<String, Object?>>[]; | ||
| 107 | + } | ||
| 108 | + final json = jsonDecode(text); | ||
| 109 | + if (json is List) { | ||
| 110 | + return json | ||
| 111 | + .whereType<Map>() | ||
| 112 | + .map((e) => _sanitizeMap(e.cast<String, Object?>())) | ||
| 113 | + .toList(); | ||
| 114 | + } | ||
| 115 | + } catch (_) {} | ||
| 116 | + return <Map<String, Object?>>[]; | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + static Map<String, Object?> _sanitizeMap(Map<String, Object?> map) { | ||
| 120 | + return map.map((key, value) => MapEntry(key, _sanitizeValue(value))); | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + static Object? _sanitizeValue(Object? value) { | ||
| 124 | + return switch (value) { | ||
| 125 | + null => null, | ||
| 126 | + String() => value, | ||
| 127 | + num() => value, | ||
| 128 | + bool() => value, | ||
| 129 | + DateTime() => value.toIso8601String(), | ||
| 130 | + List() => value.map(_sanitizeValue).toList(), | ||
| 131 | + Map() => value.map( | ||
| 132 | + (key, val) => MapEntry(key.toString(), _sanitizeValue(val)), | ||
| 133 | + ), | ||
| 134 | + _ => value.toString(), | ||
| 135 | + }; | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + static String _readableTime(DateTime time) { | ||
| 139 | + return '${time.year.toString().padLeft(4, '0')}-' | ||
| 140 | + '${time.month.toString().padLeft(2, '0')}-' | ||
| 141 | + '${time.day.toString().padLeft(2, '0')} ' | ||
| 142 | + '${time.hour.toString().padLeft(2, '0')}:' | ||
| 143 | + '${time.minute.toString().padLeft(2, '0')}:' | ||
| 144 | + '${time.second.toString().padLeft(2, '0')}'; | ||
| 145 | + } | ||
| 146 | +} |
| @@ -2,6 +2,8 @@ import 'dart:convert'; | @@ -2,6 +2,8 @@ import 'dart:convert'; | ||
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; |
| 4 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | 4 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/logging/app_diagnostic_log_store.dart'; | ||
| 6 | +import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | ||
| 5 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; | 7 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 6 | import 'package:get/get.dart'; | 8 | import 'package:get/get.dart'; |
| 7 | import 'package:shared_preferences/shared_preferences.dart'; | 9 | import 'package:shared_preferences/shared_preferences.dart'; |
| @@ -42,7 +44,17 @@ class UserPreferencesStorage { | @@ -42,7 +44,17 @@ class UserPreferencesStorage { | ||
| 42 | try { | 44 | try { |
| 43 | final map = jsonDecode(raw) as Map<String, dynamic>; | 45 | final map = jsonDecode(raw) as Map<String, dynamic>; |
| 44 | preferences.value = UserPreferences.fromJson(map); | 46 | preferences.value = UserPreferences.fromJson(map); |
| 45 | - } catch (_) { | 47 | + } catch (error, stackTrace) { |
| 48 | + AppLogger.e('UserPreferences JSON parse failed', error, stackTrace); | ||
| 49 | + AppDiagnosticLogStore.instance.log( | ||
| 50 | + category: 'UserPreferences', | ||
| 51 | + action: 'load_deserialization_failed', | ||
| 52 | + details: { | ||
| 53 | + 'error': error.toString(), | ||
| 54 | + 'stackTrace': stackTrace.toString(), | ||
| 55 | + 'rawPreview': raw.length > 200 ? '${raw.substring(0, 200)}...' : raw, | ||
| 56 | + }, | ||
| 57 | + ); | ||
| 46 | preferences.value = UserPreferences.empty; | 58 | preferences.value = UserPreferences.empty; |
| 47 | } | 59 | } |
| 48 | } | 60 | } |
| @@ -41,7 +41,7 @@ Future<void> healthBackgroundMain() async { | @@ -41,7 +41,7 @@ Future<void> healthBackgroundMain() async { | ||
| 41 | await _healthBackgroundLog(channel, 'entry.start'); | 41 | await _healthBackgroundLog(channel, 'entry.start'); |
| 42 | try { | 42 | try { |
| 43 | await _healthBackgroundLog(channel, 'bootstrap.start'); | 43 | await _healthBackgroundLog(channel, 'bootstrap.start'); |
| 44 | - await AppBootstrap.init(); | 44 | + await AppBootstrap.initForBackground(); |
| 45 | await _healthBackgroundLog(channel, 'bootstrap.success'); | 45 | await _healthBackgroundLog(channel, 'bootstrap.success'); |
| 46 | } catch (error, stackTrace) { | 46 | } catch (error, stackTrace) { |
| 47 | await _healthBackgroundLog( | 47 | await _healthBackgroundLog( |
-
Please register or login to post a comment