Commit da29459c6a41307422ba8489db6ac9db57a5f389

Authored by 权海
1 parent b73c5efd

feat(ui):开放线上隐藏的日志入口

@@ -31,10 +31,8 @@ class DeveloperOptionsItem { @@ -31,10 +31,8 @@ class DeveloperOptionsItem {
31 31
32 enum DeveloperOptionsAction { 32 enum DeveloperOptionsAction {
33 shareHealthRawDataDatabase, 33 shareHealthRawDataDatabase,
34 - shareHealthRawUploadLog,  
35 shareNativeAppleHealthObserverRecord, 34 shareNativeAppleHealthObserverRecord,
36 shareFlutterObserverRecord, 35 shareFlutterObserverRecord,
37 - shareUploadTaskRecord,  
38 shareLocalNotificationDebugRecord, 36 shareLocalNotificationDebugRecord,
39 sendTestHrvLocalNotification, 37 sendTestHrvLocalNotification,
40 clearHealthRawDataDatabaseAndUploadLog, 38 clearHealthRawDataDatabaseAndUploadLog,
@@ -52,61 +50,84 @@ class DeveloperOptionsController extends GetxController { @@ -52,61 +50,84 @@ class DeveloperOptionsController extends GetxController {
52 final AppPlatformHostApi _platformHostApi = AppPlatformHostApi(); 50 final AppPlatformHostApi _platformHostApi = AppPlatformHostApi();
53 final TextEditingController tokenController = TextEditingController(); 51 final TextEditingController tokenController = TextEditingController();
54 52
55 - final List<DeveloperOptionsItem> options = const [  
56 - DeveloperOptionsItem(  
57 - title: 'User Information',  
58 - routeName: Routes.USER_INFORMATION,  
59 - ),  
60 - DeveloperOptionsItem(  
61 - title: 'Route List',  
62 - routeName: Routes.ROUTE_LIST,  
63 - ),  
64 - DeveloperOptionsItem(  
65 - title: 'Token Login',  
66 - routeName: Routes.TOKEN_LOGIN,  
67 - ),  
68 - DeveloperOptionsItem(  
69 - title: 'Share HealthRawData Database',  
70 - action: DeveloperOptionsAction.shareHealthRawDataDatabase,  
71 - ),  
72 - DeveloperOptionsItem(  
73 - title: '本地上传日志',  
74 - action: DeveloperOptionsAction.shareHealthRawUploadLog,  
75 - ),  
76 - DeveloperOptionsItem(  
77 - title: '分享原生AppleHealthObserver记录',  
78 - action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,  
79 - ),  
80 - DeveloperOptionsItem(  
81 - title: '分享Flutter Observer-Caculate记录',  
82 - action: DeveloperOptionsAction.shareFlutterObserverRecord,  
83 - ),  
84 - DeveloperOptionsItem(  
85 - title: '分享上传Task日志',  
86 - action: DeveloperOptionsAction.shareUploadTaskRecord,  
87 - ),  
88 - DeveloperOptionsItem(  
89 - title: '分享本地推送诊断日志',  
90 - action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,  
91 - ),  
92 - DeveloperOptionsItem(  
93 - title: '测试本地推送 HRV',  
94 - action: DeveloperOptionsAction.sendTestHrvLocalNotification,  
95 - ),  
96 - DeveloperOptionsItem(  
97 - title: '清除本地数据库、上传日志',  
98 - action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,  
99 - ),  
100 - DeveloperOptionsItem(  
101 - title: 'Global Env',  
102 - action: DeveloperOptionsAction.globalEnv,  
103 - ),  
104 - ]; 53 + late final List<DeveloperOptionsItem> options;
105 54
106 @override 55 @override
107 void onInit() { 56 void onInit() {
108 super.onInit(); 57 super.onInit();
109 tokenController.text = _userPreferencesStorage.accessToken; 58 tokenController.text = _userPreferencesStorage.accessToken;
  59 + final config = Get.find<AppEnvironmentConfig>();
  60 + if (config.isDebug) {
  61 + options = const [
  62 + DeveloperOptionsItem(
  63 + title: 'User Information',
  64 + routeName: Routes.USER_INFORMATION,
  65 + ),
  66 + DeveloperOptionsItem(
  67 + title: 'Route List',
  68 + routeName: Routes.ROUTE_LIST,
  69 + ),
  70 + DeveloperOptionsItem(
  71 + title: 'Token Login',
  72 + routeName: Routes.TOKEN_LOGIN,
  73 + ),
  74 + DeveloperOptionsItem(
  75 + title: '分享本地计算数据库文件',
  76 + action: DeveloperOptionsAction.shareHealthRawDataDatabase,
  77 + ),
  78 + DeveloperOptionsItem(
  79 + title: '分享原生响应AppleHealth记录',
  80 + action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,
  81 + ),
  82 + DeveloperOptionsItem(
  83 + title: '分享Flutter 响应AppleHealth回调记录',
  84 + action: DeveloperOptionsAction.shareFlutterObserverRecord,
  85 + ),
  86 + DeveloperOptionsItem(
  87 + title: '分享本地推送诊断日志',
  88 + action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
  89 + ),
  90 + DeveloperOptionsItem(
  91 + title: '测试本地推送 HRV',
  92 + action: DeveloperOptionsAction.sendTestHrvLocalNotification,
  93 + ),
  94 + DeveloperOptionsItem(
  95 + title: '清除本地数据库、上传日志',
  96 + action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
  97 + ),
  98 + DeveloperOptionsItem(
  99 + title: 'Global Env',
  100 + action: DeveloperOptionsAction.globalEnv,
  101 + ),
  102 + ];
  103 + } else {
  104 + options = const [
  105 + DeveloperOptionsItem(
  106 + title: 'User Information',
  107 + routeName: Routes.USER_INFORMATION,
  108 + ),
  109 + DeveloperOptionsItem(
  110 + title: '分享本地计算数据库文件',
  111 + action: DeveloperOptionsAction.shareHealthRawDataDatabase,
  112 + ),
  113 + DeveloperOptionsItem(
  114 + title: '分享原生响应AppleHealth记录',
  115 + action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,
  116 + ),
  117 + DeveloperOptionsItem(
  118 + title: '分享Flutter 响应AppleHealth回调记录',
  119 + action: DeveloperOptionsAction.shareFlutterObserverRecord,
  120 + ),
  121 + DeveloperOptionsItem(
  122 + title: '分享本地推送诊断日志',
  123 + action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
  124 + ),
  125 + DeveloperOptionsItem(
  126 + title: '清除本地数据库、上传日志',
  127 + action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
  128 + ),
  129 + ];
  130 + }
110 } 131 }
111 132
112 Future<void> optionItemOnTap(DeveloperOptionsItem item) async { 133 Future<void> optionItemOnTap(DeveloperOptionsItem item) async {
@@ -120,18 +141,12 @@ class DeveloperOptionsController extends GetxController { @@ -120,18 +141,12 @@ class DeveloperOptionsController extends GetxController {
120 case DeveloperOptionsAction.shareHealthRawDataDatabase: 141 case DeveloperOptionsAction.shareHealthRawDataDatabase:
121 await shareHealthRawDataDatabase(); 142 await shareHealthRawDataDatabase();
122 break; 143 break;
123 - case DeveloperOptionsAction.shareHealthRawUploadLog:  
124 - await shareHealthRawUploadLog();  
125 - break;  
126 case DeveloperOptionsAction.shareNativeAppleHealthObserverRecord: 144 case DeveloperOptionsAction.shareNativeAppleHealthObserverRecord:
127 await shareNativeAppleHealthObserverRecord(); 145 await shareNativeAppleHealthObserverRecord();
128 break; 146 break;
129 case DeveloperOptionsAction.shareFlutterObserverRecord: 147 case DeveloperOptionsAction.shareFlutterObserverRecord:
130 await shareFlutterObserverRecord(); 148 await shareFlutterObserverRecord();
131 break; 149 break;
132 - case DeveloperOptionsAction.shareUploadTaskRecord:  
133 - await shareUploadTaskRecord();  
134 - break;  
135 case DeveloperOptionsAction.shareLocalNotificationDebugRecord: 150 case DeveloperOptionsAction.shareLocalNotificationDebugRecord:
136 await shareLocalNotificationDebugRecord(); 151 await shareLocalNotificationDebugRecord();
137 break; 152 break;
@@ -15,6 +15,9 @@ import '../models/watch_theme_models.dart'; @@ -15,6 +15,9 @@ import '../models/watch_theme_models.dart';
15 class WatchThemeController extends GetxController { 15 class WatchThemeController extends GetxController {
16 WatchThemeController(this._themeApi, this._vipApi); 16 WatchThemeController(this._themeApi, this._vipApi);
17 17
  18 + static const int _developerOptionsTapCount = 8;
  19 + static const Duration _developerOptionsTapWindow = Duration(seconds: 3);
  20 +
18 final ThemeApi _themeApi; 21 final ThemeApi _themeApi;
19 final VipApi _vipApi; 22 final VipApi _vipApi;
20 23
@@ -23,6 +26,8 @@ class WatchThemeController extends GetxController { @@ -23,6 +26,8 @@ class WatchThemeController extends GetxController {
23 final officialThemeItems = <WatchThemeItem>[].obs; 26 final officialThemeItems = <WatchThemeItem>[].obs;
24 final customThemeItems = <WatchThemeItem>[].obs; 27 final customThemeItems = <WatchThemeItem>[].obs;
25 final selectedThemeItem = Rx<WatchThemeItem?>(null); 28 final selectedThemeItem = Rx<WatchThemeItem?>(null);
  29 + int _headerTapCount = 0;
  30 + DateTime? _firstHeaderTapAt;
26 31
27 @override 32 @override
28 void onInit() { 33 void onInit() {
@@ -63,6 +68,26 @@ class WatchThemeController extends GetxController { @@ -63,6 +68,26 @@ class WatchThemeController extends GetxController {
63 Get.back(); 68 Get.back();
64 } 69 }
65 70
  71 + void onHeaderTap() {
  72 + final now = DateTime.now();
  73 + final firstTapAt = _firstHeaderTapAt;
  74 + if (firstTapAt == null ||
  75 + now.difference(firstTapAt) > _developerOptionsTapWindow) {
  76 + _firstHeaderTapAt = now;
  77 + _headerTapCount = 1;
  78 + return;
  79 + }
  80 +
  81 + _headerTapCount += 1;
  82 + if (_headerTapCount < _developerOptionsTapCount) {
  83 + return;
  84 + }
  85 +
  86 + _firstHeaderTapAt = null;
  87 + _headerTapCount = 0;
  88 + Get.toNamed(Routes.DEVELOPER_OPTIONS);
  89 + }
  90 +
66 Future<void> selectOfficialTheme(int index) async { 91 Future<void> selectOfficialTheme(int index) async {
67 await Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: { 92 await Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: {
68 'theme': officialThemeItems[index], 93 'theme': officialThemeItems[index],
@@ -23,9 +23,13 @@ class WatchThemeView extends GetView<WatchThemeController> { @@ -23,9 +23,13 @@ class WatchThemeView extends GetView<WatchThemeController> {
23 padding: EdgeInsets.only(top: topPadding, bottom: 24), 23 padding: EdgeInsets.only(top: topPadding, bottom: 24),
24 child: Column( 24 child: Column(
25 children: [ 25 children: [
26 - WatchThemeHeader(  
27 - themeImageUrl:  
28 - controller.selectedThemeItem.value?.energeticImage, 26 + GestureDetector(
  27 + behavior: HitTestBehavior.opaque,
  28 + onTap: controller.onHeaderTap,
  29 + child: WatchThemeHeader(
  30 + themeImageUrl:
  31 + controller.selectedThemeItem.value?.energeticImage,
  32 + ),
29 ), 33 ),
30 const SizedBox(height: 26), 34 const SizedBox(height: 26),
31 OfficialThemeGrid( 35 OfficialThemeGrid(
@@ -18,7 +18,7 @@ typedef AppPigeonPlatformProvider = AppPigeonPlatform Function(); @@ -18,7 +18,7 @@ typedef AppPigeonPlatformProvider = AppPigeonPlatform Function();
18 18
19 AppPigeonPlatform resolveAppPigeonPlatform({TargetPlatform? targetPlatform}) { 19 AppPigeonPlatform resolveAppPigeonPlatform({TargetPlatform? targetPlatform}) {
20 final platform = targetPlatform ?? defaultTargetPlatform; 20 final platform = targetPlatform ?? defaultTargetPlatform;
21 - if (platform == TargetPlatform.ohos) { 21 + if (platform.name == 'ohos') {
22 return AppPigeonPlatform.ohos; 22 return AppPigeonPlatform.ohos;
23 } 23 }
24 if (platform == TargetPlatform.iOS) { 24 if (platform == TargetPlatform.iOS) {
@@ -192,26 +192,22 @@ class AppleHealthRawDataCoreService { @@ -192,26 +192,22 @@ class AppleHealthRawDataCoreService {
192 } 192 }
193 193
194 Future<bool> shareNativeAppleHealthObserverRecord() async { 194 Future<bool> shareNativeAppleHealthObserverRecord() async {
195 - if (!_isDebug) return false;  
196 return _rawDataApi.shareNativeAppleHealthObserverRecord(); 195 return _rawDataApi.shareNativeAppleHealthObserverRecord();
197 } 196 }
198 197
199 Future<bool> shareFlutterObserverRecord() async { 198 Future<bool> shareFlutterObserverRecord() async {
200 - if (!_isDebug) return false;  
201 return _rawDataApi.shareFlutterObserverRecord(); 199 return _rawDataApi.shareFlutterObserverRecord();
202 } 200 }
203 201
204 Future<bool> shareUploadTaskRecord() async { 202 Future<bool> shareUploadTaskRecord() async {
205 - if (!_isDebug) return false;  
206 return _rawDataApi.shareUploadTaskRecord(); 203 return _rawDataApi.shareUploadTaskRecord();
207 } 204 }
208 205
209 Future<String> readLocalNotificationDebugLogText() { 206 Future<String> readLocalNotificationDebugLogText() {
210 - return _localNotificationDebugStore.readText(isDebug: _isDebug); 207 + return _localNotificationDebugStore.readText();
211 } 208 }
212 209
213 Future<bool> shareLocalNotificationDebugRecord() async { 210 Future<bool> shareLocalNotificationDebugRecord() async {
214 - if (!_isDebug) return false;  
215 return _shareNativeLocalNotificationDebugRecord(); 211 return _shareNativeLocalNotificationDebugRecord();
216 } 212 }
217 213
@@ -1350,7 +1346,6 @@ class AppleHealthRawDataCoreService { @@ -1350,7 +1346,6 @@ class AppleHealthRawDataCoreService {
1350 required String apiName, 1346 required String apiName,
1351 required Object? result, 1347 required Object? result,
1352 }) async { 1348 }) async {
1353 - if (!_isDebug) return;  
1354 final file = await _uploadApiLogFile(); 1349 final file = await _uploadApiLogFile();
1355 await file.parent.create(recursive: true); 1350 await file.parent.create(recursive: true);
1356 final line = 1351 final line =
@@ -1359,7 +1354,6 @@ class AppleHealthRawDataCoreService { @@ -1359,7 +1354,6 @@ class AppleHealthRawDataCoreService {
1359 } 1354 }
1360 1355
1361 Future<void> _saveNativeCallFlutterChange(List<int>? dataTypes) async { 1356 Future<void> _saveNativeCallFlutterChange(List<int>? dataTypes) async {
1362 - if (!_isDebug) return;  
1363 try { 1357 try {
1364 await _rawDataApi.saveNativeCallFlutterChange( 1358 await _rawDataApi.saveNativeCallFlutterChange(
1365 relativeDataTypes: dataTypes ?? const <int>[], 1359 relativeDataTypes: dataTypes ?? const <int>[],
@@ -1370,7 +1364,6 @@ class AppleHealthRawDataCoreService { @@ -1370,7 +1364,6 @@ class AppleHealthRawDataCoreService {
1370 } 1364 }
1371 1365
1372 Future<void> _saveFlutterCaculateFinished(List<int> dataTypes) async { 1366 Future<void> _saveFlutterCaculateFinished(List<int> dataTypes) async {
1373 - if (!_isDebug) return;  
1374 try { 1367 try {
1375 await _rawDataApi.saveFlutterCaculateFinished( 1368 await _rawDataApi.saveFlutterCaculateFinished(
1376 relativeDataTypes: dataTypes, 1369 relativeDataTypes: dataTypes,
@@ -1383,7 +1376,6 @@ class AppleHealthRawDataCoreService { @@ -1383,7 +1376,6 @@ class AppleHealthRawDataCoreService {
1383 Future<void> _saveLocalNotificationDebugEvent( 1376 Future<void> _saveLocalNotificationDebugEvent(
1384 Map<String, Object?> event, 1377 Map<String, Object?> event,
1385 ) async { 1378 ) async {
1386 - if (!_isDebug) return;  
1387 try { 1379 try {
1388 await _localNotificationDebugStore.append(event); 1380 await _localNotificationDebugStore.append(event);
1389 await _saveNativeLocalNotificationDebugEvent(event); 1381 await _saveNativeLocalNotificationDebugEvent(event);
@@ -2058,7 +2050,7 @@ class HealthRawStressLocalStore { @@ -2058,7 +2050,7 @@ class HealthRawStressLocalStore {
2058 final db = await factory.openDatabase( 2050 final db = await factory.openDatabase(
2059 path, 2051 path,
2060 options: OpenDatabaseOptions( 2052 options: OpenDatabaseOptions(
2061 - version: 7, 2053 + version: 8,
2062 onCreate: (db, version) async { 2054 onCreate: (db, version) async {
2063 await _createTables(db); 2055 await _createTables(db);
2064 }, 2056 },
@@ -2082,6 +2074,9 @@ class HealthRawStressLocalStore { @@ -2082,6 +2074,9 @@ class HealthRawStressLocalStore {
2082 if (oldVersion < 7) { 2074 if (oldVersion < 7) {
2083 await _createSleepResultsTable(db); 2075 await _createSleepResultsTable(db);
2084 } 2076 }
  2077 + if (oldVersion < 8) {
  2078 + await _addUpdateTimeColumns(db);
  2079 + }
2085 }, 2080 },
2086 ), 2081 ),
2087 ); 2082 );
@@ -2107,7 +2102,8 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable ( @@ -2107,7 +2102,8 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
2107 is_workout_recovery INTEGER NOT NULL DEFAULT 0, 2102 is_workout_recovery INTEGER NOT NULL DEFAULT 0,
2108 is_sleep_likely INTEGER NOT NULL DEFAULT 0, 2103 is_sleep_likely INTEGER NOT NULL DEFAULT 0,
2109 is_suspected_activity INTEGER NOT NULL DEFAULT 0, 2104 is_suspected_activity INTEGER NOT NULL DEFAULT 0,
2110 - uploaded INTEGER NOT NULL DEFAULT 0 2105 + uploaded INTEGER NOT NULL DEFAULT 0,
  2106 + update_time INTEGER NOT NULL DEFAULT 0
2111 ) 2107 )
2112 '''); 2108 ''');
2113 await db.execute(''' 2109 await db.execute('''
@@ -2122,7 +2118,8 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( @@ -2122,7 +2118,8 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
2122 is_workout_recovery INTEGER NOT NULL DEFAULT 0, 2118 is_workout_recovery INTEGER NOT NULL DEFAULT 0,
2123 is_sleep_likely INTEGER NOT NULL DEFAULT 0, 2119 is_sleep_likely INTEGER NOT NULL DEFAULT 0,
2124 is_suspected_activity INTEGER NOT NULL DEFAULT 0, 2120 is_suspected_activity INTEGER NOT NULL DEFAULT 0,
2125 - uploaded INTEGER NOT NULL DEFAULT 0 2121 + uploaded INTEGER NOT NULL DEFAULT 0,
  2122 + update_time INTEGER NOT NULL DEFAULT 0
2126 ) 2123 )
2127 '''); 2124 ''');
2128 await _createDailyStressTable(db); 2125 await _createDailyStressTable(db);
@@ -2138,7 +2135,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( @@ -2138,7 +2135,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
2138 stress_score INTEGER NOT NULL, 2135 stress_score INTEGER NOT NULL,
2139 state INTEGER NOT NULL, 2136 state INTEGER NOT NULL,
2140 data_time INTEGER NOT NULL, 2137 data_time INTEGER NOT NULL,
2141 - uploaded INTEGER NOT NULL DEFAULT 0 2138 + uploaded INTEGER NOT NULL DEFAULT 0,
  2139 + update_time INTEGER NOT NULL DEFAULT 0
2142 ) 2140 )
2143 '''); 2141 ''');
2144 } 2142 }
@@ -2154,11 +2152,29 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2154,11 +2152,29 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2154 in_bed_minutes INTEGER NOT NULL, 2152 in_bed_minutes INTEGER NOT NULL,
2155 awak_minutes INTEGER NOT NULL, 2153 awak_minutes INTEGER NOT NULL,
2156 sleep_minutes INTEGER NOT NULL, 2154 sleep_minutes INTEGER NOT NULL,
2157 - uploaded INTEGER NOT NULL DEFAULT 0 2155 + uploaded INTEGER NOT NULL DEFAULT 0,
  2156 + update_time INTEGER NOT NULL DEFAULT 0
2158 ) 2157 )
2159 '''); 2158 ''');
2160 } 2159 }
2161 2160
  2161 + Future<void> _addUpdateTimeColumns(DatabaseExecutor db) async {
  2162 + for (final table in const [
  2163 + hrvResultsTable,
  2164 + realtimeStressResultsTable,
  2165 + dailyStressResultsTable,
  2166 + sleepResultsTable,
  2167 + ]) {
  2168 + try {
  2169 + await db.execute(
  2170 + 'ALTER TABLE $table ADD COLUMN update_time INTEGER NOT NULL DEFAULT 0',
  2171 + );
  2172 + } on DatabaseException catch (error) {
  2173 + if (!error.isDuplicateColumnError()) rethrow;
  2174 + }
  2175 + }
  2176 + }
  2177 +
2162 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async { 2178 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
2163 for (final column in const [ 2179 for (final column in const [
2164 'is_workout', 2180 'is_workout',
@@ -2320,6 +2336,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2320,6 +2336,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2320 'baseline_resting_hr': point.baselineRestingHr, 2336 'baseline_resting_hr': point.baselineRestingHr,
2321 ..._flagsRow(point.flags), 2337 ..._flagsRow(point.flags),
2322 'uploaded': point.uploaded ? 1 : 0, 2338 'uploaded': point.uploaded ? 1 : 0,
  2339 + 'update_time': _currentUnixSeconds(),
2323 }; 2340 };
2324 } 2341 }
2325 2342
@@ -2333,6 +2350,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2333,6 +2350,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2333 'source_end_time': point.sourceEndTime, 2350 'source_end_time': point.sourceEndTime,
2334 ..._flagsRow(point.flags), 2351 ..._flagsRow(point.flags),
2335 'uploaded': point.uploaded ? 1 : 0, 2352 'uploaded': point.uploaded ? 1 : 0,
  2353 + 'update_time': _currentUnixSeconds(),
2336 }; 2354 };
2337 } 2355 }
2338 2356
@@ -2345,6 +2363,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2345,6 +2363,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2345 'state': point.state.value, 2363 'state': point.state.value,
2346 'data_time': point.dataTime, 2364 'data_time': point.dataTime,
2347 'uploaded': point.uploaded ? 1 : 0, 2365 'uploaded': point.uploaded ? 1 : 0,
  2366 + 'update_time': _currentUnixSeconds(),
2348 }; 2367 };
2349 } 2368 }
2350 2369
@@ -2359,6 +2378,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2359,6 +2378,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2359 'awak_minutes': result.awakMinutes, 2378 'awak_minutes': result.awakMinutes,
2360 'sleep_minutes': result.sleepMinutes, 2379 'sleep_minutes': result.sleepMinutes,
2361 'uploaded': result.uploaded ? 1 : 0, 2380 'uploaded': result.uploaded ? 1 : 0,
  2381 + 'update_time': _currentUnixSeconds(),
2362 }; 2382 };
2363 } 2383 }
2364 2384
@@ -2389,6 +2409,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2389,6 +2409,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2389 final existingRow = existing.first; 2409 final existingRow = existing.first;
2390 if (_matchesStoredValues( 2410 if (_matchesStoredValues(
2391 existingRow, row, _rawResultStoredValueKeys(row))) { 2411 existingRow, row, _rawResultStoredValueKeys(row))) {
  2412 + await db.update(
  2413 + table,
  2414 + <String, Object?>{'update_time': row['update_time']},
  2415 + where: 'raw_end_time = ?',
  2416 + whereArgs: [row['raw_end_time']],
  2417 + );
2392 return; 2418 return;
2393 } 2419 }
2394 final uploadPayloadChanged = !_matchesStoredValues( 2420 final uploadPayloadChanged = !_matchesStoredValues(
@@ -2419,6 +2445,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2419,6 +2445,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2419 'is_sleep_likely': row['is_sleep_likely'], 2445 'is_sleep_likely': row['is_sleep_likely'],
2420 'is_suspected_activity': row['is_suspected_activity'], 2446 'is_suspected_activity': row['is_suspected_activity'],
2421 'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'], 2447 'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'],
  2448 + 'update_time': row['update_time'],
2422 }, 2449 },
2423 where: 'raw_end_time = ?', 2450 where: 'raw_end_time = ?',
2424 whereArgs: [row['raw_end_time']], 2451 whereArgs: [row['raw_end_time']],
@@ -2458,6 +2485,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2458,6 +2485,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2458 'state': row['state'], 2485 'state': row['state'],
2459 'data_time': row['data_time'], 2486 'data_time': row['data_time'],
2460 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']), 2487 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']),
  2488 + 'update_time': row['update_time'],
2461 }, 2489 },
2462 where: 'date = ?', 2490 where: 'date = ?',
2463 whereArgs: [row['date']], 2491 whereArgs: [row['date']],
@@ -2503,12 +2531,17 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2503,12 +2531,17 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2503 'awak_minutes': row['awak_minutes'], 2531 'awak_minutes': row['awak_minutes'],
2504 'sleep_minutes': row['sleep_minutes'], 2532 'sleep_minutes': row['sleep_minutes'],
2505 'uploaded': valueChanged ? 0 : existingRow['uploaded'], 2533 'uploaded': valueChanged ? 0 : existingRow['uploaded'],
  2534 + 'update_time': row['update_time'],
2506 }, 2535 },
2507 where: 'date = ?', 2536 where: 'date = ?',
2508 whereArgs: [row['date']], 2537 whereArgs: [row['date']],
2509 ); 2538 );
2510 } 2539 }
2511 2540
  2541 + int _currentUnixSeconds() {
  2542 + return DateTime.now().millisecondsSinceEpoch ~/ 1000;
  2543 + }
  2544 +
2512 List<String> _rawResultStoredValueKeys(Map<String, Object?> row) { 2545 List<String> _rawResultStoredValueKeys(Map<String, Object?> row) {
2513 return <String>[ 2546 return <String>[
2514 'user_id', 2547 'user_id',
@@ -35,10 +35,7 @@ class HealthRawLocalNotificationDebugStore { @@ -35,10 +35,7 @@ class HealthRawLocalNotificationDebugStore {
35 ); 35 );
36 } 36 }
37 37
38 - Future<String> readText({required bool isDebug}) async {  
39 - if (!isDebug) {  
40 - return '当前非 Debug 模式,未记录本地推送诊断日志';  
41 - } 38 + Future<String> readText() async {
42 final file = await _file(); 39 final file = await _file();
43 if (!await file.exists()) { 40 if (!await file.exists()) {
44 return '暂无本地推送诊断日志'; 41 return '暂无本地推送诊断日志';
@@ -181,10 +181,10 @@ packages: @@ -181,10 +181,10 @@ packages:
181 dependency: transitive 181 dependency: transitive
182 description: 182 description:
183 name: cross_file 183 name: cross_file
184 - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" 184 + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
185 url: "https://pub.dev" 185 url: "https://pub.dev"
186 source: hosted 186 source: hosted
187 - version: "0.3.5+4" 187 + version: "0.3.5+5"
188 crypto: 188 crypto:
189 dependency: "direct main" 189 dependency: "direct main"
190 description: 190 description:
@@ -391,6 +391,71 @@ void main() { @@ -391,6 +391,71 @@ void main() {
391 expect(daily.single.uploaded, isTrue); 391 expect(daily.single.uploaded, isTrue);
392 }); 392 });
393 393
  394 + test('local result rows include update time and refresh on upsert', () async {
  395 + const base = 1800000000;
  396 + final store = _MemoryHealthRawStressLocalStore();
  397 + await store.upsertResult(
  398 + HealthRawStressCalculationResult(
  399 + userId: 42,
  400 + hrvStressPoints: [_hrvStressPoint(base + 10)],
  401 + realtimeStressPoints: [_realtimeStressPoint(base + 20)],
  402 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  403 + ),
  404 + );
  405 + await store.upsertDailyStressPoints(
  406 + userId: 42,
  407 + points: [
  408 + HealthRawDailyStressPoint(
  409 + userId: 42,
  410 + date: 20260101,
  411 + stressValue: 40,
  412 + stressScore: 40,
  413 + state: HealthRawStressState.normal,
  414 + dataTime: base + 20,
  415 + ),
  416 + ],
  417 + );
  418 + await store.upsertSleepResults(
  419 + userId: 42,
  420 + results: const [
  421 + HealthRawSleepResult(
  422 + userId: 42,
  423 + date: base + 30,
  424 + startDate: base,
  425 + sleepScore: 80,
  426 + sleepState: 2,
  427 + inBedMinutes: 480,
  428 + awakMinutes: 10,
  429 + sleepMinutes: 470,
  430 + ),
  431 + ],
  432 + );
  433 + final firstRows = await store.debugQueryAllRows(42);
  434 + final firstHrvUpdateTime = firstRows.hrvRows.single['update_time'] as int;
  435 +
  436 + expect(firstHrvUpdateTime, greaterThan(0));
  437 + expect(firstRows.realtimeRows.single['update_time'], greaterThan(0));
  438 + expect(firstRows.dailyStressRows.single['update_time'], greaterThan(0));
  439 + expect(firstRows.sleepRows.single['update_time'], greaterThan(0));
  440 +
  441 + await store.markHrvStressUploaded(rawEndTimes: [base + 10], userId: 42);
  442 + await store.upsertResult(
  443 + HealthRawStressCalculationResult(
  444 + userId: 42,
  445 + hrvStressPoints: [_hrvStressPoint(base + 10)],
  446 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  447 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  448 + ),
  449 + );
  450 + final secondRows = await store.debugQueryAllRows(42);
  451 +
  452 + expect(
  453 + secondRows.hrvRows.single['update_time'],
  454 + greaterThan(firstHrvUpdateTime),
  455 + );
  456 + expect(secondRows.hrvRows.single['uploaded'], 1);
  457 + });
  458 +
394 test( 459 test(
395 'startCoreCaculate skips raw reads when health auth and local data are missing', 460 'startCoreCaculate skips raw reads when health auth and local data are missing',
396 () async { 461 () async {
@@ -1185,7 +1250,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1185,7 +1250,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1185 final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {}; 1250 final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {};
1186 final Map<int, List<HealthRawDailyStressPoint>> _daily = {}; 1251 final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
1187 final Map<int, List<HealthRawSleepResult>> _sleep = {}; 1252 final Map<int, List<HealthRawSleepResult>> _sleep = {};
  1253 + final Map<int, Map<int, int>> _hrvUpdateTimes = {};
  1254 + final Map<int, Map<int, int>> _realtimeUpdateTimes = {};
  1255 + final Map<int, Map<int, int>> _dailyUpdateTimes = {};
  1256 + final Map<int, Map<int, int>> _sleepUpdateTimes = {};
1188 final List<String> operationLog; 1257 final List<String> operationLog;
  1258 + var _updateTimeClock = 1;
1189 var realtimeNotificationWindowQueryCount = 0; 1259 var realtimeNotificationWindowQueryCount = 0;
1190 1260
1191 void insertDailyStress(HealthRawDailyStressPoint point) { 1261 void insertDailyStress(HealthRawDailyStressPoint point) {
@@ -1196,6 +1266,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1196,6 +1266,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1196 byDate[point.date] = point; 1266 byDate[point.date] = point;
1197 _daily[point.userId] = byDate.values.toList() 1267 _daily[point.userId] = byDate.values.toList()
1198 ..sort((a, b) => a.date.compareTo(b.date)); 1268 ..sort((a, b) => a.date.compareTo(b.date));
  1269 + _dailyUpdateTimes.putIfAbsent(
  1270 + point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
1199 } 1271 }
1200 1272
1201 @override 1273 @override
@@ -1232,6 +1304,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1232,6 +1304,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1232 flags: point.flags, 1304 flags: point.flags,
1233 uploaded: same ? existing.uploaded : false, 1305 uploaded: same ? existing.uploaded : false,
1234 ); 1306 );
  1307 + _hrvUpdateTimes.putIfAbsent(
  1308 + point.userId, () => <int, int>{})[point.rawEndTime] =
  1309 + _nextUpdateTime();
1235 } 1310 }
1236 _hrv[result.userId] = hrvByTime.values.toList() 1311 _hrv[result.userId] = hrvByTime.values.toList()
1237 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime)); 1312 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
@@ -1255,6 +1330,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1255,6 +1330,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1255 flags: point.flags, 1330 flags: point.flags,
1256 uploaded: same ? existing.uploaded : false, 1331 uploaded: same ? existing.uploaded : false,
1257 ); 1332 );
  1333 + _realtimeUpdateTimes.putIfAbsent(
  1334 + point.userId, () => <int, int>{})[point.rawEndTime] =
  1335 + _nextUpdateTime();
1258 } 1336 }
1259 _realtime[result.userId] = realtimeByTime.values.toList() 1337 _realtime[result.userId] = realtimeByTime.values.toList()
1260 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime)); 1338 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
@@ -1287,6 +1365,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1287,6 +1365,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1287 dataTime: point.dataTime, 1365 dataTime: point.dataTime,
1288 uploaded: isToday ? false : (same ? existing.uploaded : false), 1366 uploaded: isToday ? false : (same ? existing.uploaded : false),
1289 ); 1367 );
  1368 + _dailyUpdateTimes.putIfAbsent(
  1369 + point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
1290 } 1370 }
1291 _daily[userId] = byDate.values.toList() 1371 _daily[userId] = byDate.values.toList()
1292 ..sort((a, b) => a.date.compareTo(b.date)); 1372 ..sort((a, b) => a.date.compareTo(b.date));
@@ -1303,6 +1383,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1303,6 +1383,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1303 }; 1383 };
1304 for (final result in results) { 1384 for (final result in results) {
1305 byDate[result.date] = result; 1385 byDate[result.date] = result;
  1386 + _sleepUpdateTimes.putIfAbsent(
  1387 + result.userId, () => <int, int>{})[result.date] = _nextUpdateTime();
1306 } 1388 }
1307 _sleep[userId] = byDate.values.toList() 1389 _sleep[userId] = byDate.values.toList()
1308 ..sort((a, b) => a.date.compareTo(b.date)); 1390 ..sort((a, b) => a.date.compareTo(b.date));
@@ -1630,6 +1712,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1630,6 +1712,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1630 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0, 1712 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
1631 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0, 1713 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
1632 'uploaded': point.uploaded ? 1 : 0, 1714 'uploaded': point.uploaded ? 1 : 0,
  1715 + 'update_time': _hrvUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
1633 }; 1716 };
1634 } 1717 }
1635 1718
@@ -1646,6 +1729,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1646,6 +1729,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1646 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0, 1729 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
1647 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0, 1730 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
1648 'uploaded': point.uploaded ? 1 : 0, 1731 'uploaded': point.uploaded ? 1 : 0,
  1732 + 'update_time': _realtimeUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
1649 }; 1733 };
1650 } 1734 }
1651 1735
@@ -1658,6 +1742,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1658,6 +1742,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1658 'state': point.state.value, 1742 'state': point.state.value,
1659 'data_time': point.dataTime, 1743 'data_time': point.dataTime,
1660 'uploaded': point.uploaded ? 1 : 0, 1744 'uploaded': point.uploaded ? 1 : 0,
  1745 + 'update_time': _dailyUpdateTimes[point.userId]?[point.date] ?? 0,
1661 }; 1746 };
1662 } 1747 }
1663 1748
@@ -1672,9 +1757,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1672,9 +1757,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1672 'awak_minutes': result.awakMinutes, 1757 'awak_minutes': result.awakMinutes,
1673 'sleep_minutes': result.sleepMinutes, 1758 'sleep_minutes': result.sleepMinutes,
1674 'uploaded': result.uploaded ? 1 : 0, 1759 'uploaded': result.uploaded ? 1 : 0,
  1760 + 'update_time': _sleepUpdateTimes[result.userId]?[result.date] ?? 0,
1675 }; 1761 };
1676 } 1762 }
1677 1763
  1764 + int _nextUpdateTime() => _updateTimeClock++;
  1765 +
1678 int _todayDateKey() { 1766 int _todayDateKey() {
1679 final now = DateTime.now(); 1767 final now = DateTime.now();
1680 return now.year * 10000 + now.month * 100 + now.day; 1768 return now.year * 10000 + now.month * 100 + now.day;