Commit 4c43a3d4b23566b79627ba30998ec9459b804188

Authored by 权海
1 parent ac583fa7

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

(cherry picked from commit da29459c)

# Conflicts:
#	lib/core/platform/pigeon_api_facade.dart
#	pubspec.lock
@@ -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 PlatformHostApi _platformHostApi = PlatformHostApi(); 50 final PlatformHostApi _platformHostApi = PlatformHostApi();
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(
@@ -193,26 +193,22 @@ class AppleHealthRawDataCoreService { @@ -193,26 +193,22 @@ class AppleHealthRawDataCoreService {
193 } 193 }
194 194
195 Future<bool> shareNativeAppleHealthObserverRecord() async { 195 Future<bool> shareNativeAppleHealthObserverRecord() async {
196 - if (!_isDebug) return false;  
197 return _rawDataApi.shareNativeAppleHealthObserverRecord(); 196 return _rawDataApi.shareNativeAppleHealthObserverRecord();
198 } 197 }
199 198
200 Future<bool> shareFlutterObserverRecord() async { 199 Future<bool> shareFlutterObserverRecord() async {
201 - if (!_isDebug) return false;  
202 return _rawDataApi.shareFlutterObserverRecord(); 200 return _rawDataApi.shareFlutterObserverRecord();
203 } 201 }
204 202
205 Future<bool> shareUploadTaskRecord() async { 203 Future<bool> shareUploadTaskRecord() async {
206 - if (!_isDebug) return false;  
207 return _rawDataApi.shareUploadTaskRecord(); 204 return _rawDataApi.shareUploadTaskRecord();
208 } 205 }
209 206
210 Future<String> readLocalNotificationDebugLogText() { 207 Future<String> readLocalNotificationDebugLogText() {
211 - return _localNotificationDebugStore.readText(isDebug: _isDebug); 208 + return _localNotificationDebugStore.readText();
212 } 209 }
213 210
214 Future<bool> shareLocalNotificationDebugRecord() async { 211 Future<bool> shareLocalNotificationDebugRecord() async {
215 - if (!_isDebug) return false;  
216 return _shareNativeLocalNotificationDebugRecord(); 212 return _shareNativeLocalNotificationDebugRecord();
217 } 213 }
218 214
@@ -1477,7 +1473,6 @@ class AppleHealthRawDataCoreService { @@ -1477,7 +1473,6 @@ class AppleHealthRawDataCoreService {
1477 required String apiName, 1473 required String apiName,
1478 required Object? result, 1474 required Object? result,
1479 }) async { 1475 }) async {
1480 - if (!_isDebug) return;  
1481 final file = await _uploadApiLogFile(); 1476 final file = await _uploadApiLogFile();
1482 await file.parent.create(recursive: true); 1477 await file.parent.create(recursive: true);
1483 final line = 1478 final line =
@@ -1486,7 +1481,6 @@ class AppleHealthRawDataCoreService { @@ -1486,7 +1481,6 @@ class AppleHealthRawDataCoreService {
1486 } 1481 }
1487 1482
1488 Future<void> _saveNativeCallFlutterChange(List<int>? dataTypes) async { 1483 Future<void> _saveNativeCallFlutterChange(List<int>? dataTypes) async {
1489 - if (!_isDebug) return;  
1490 try { 1484 try {
1491 await _rawDataApi.saveNativeCallFlutterChange( 1485 await _rawDataApi.saveNativeCallFlutterChange(
1492 relativeDataTypes: dataTypes ?? const <int>[], 1486 relativeDataTypes: dataTypes ?? const <int>[],
@@ -1497,7 +1491,6 @@ class AppleHealthRawDataCoreService { @@ -1497,7 +1491,6 @@ class AppleHealthRawDataCoreService {
1497 } 1491 }
1498 1492
1499 Future<void> _saveFlutterCaculateFinished(List<int> dataTypes) async { 1493 Future<void> _saveFlutterCaculateFinished(List<int> dataTypes) async {
1500 - if (!_isDebug) return;  
1501 try { 1494 try {
1502 await _rawDataApi.saveFlutterCaculateFinished( 1495 await _rawDataApi.saveFlutterCaculateFinished(
1503 relativeDataTypes: dataTypes, 1496 relativeDataTypes: dataTypes,
@@ -1510,7 +1503,6 @@ class AppleHealthRawDataCoreService { @@ -1510,7 +1503,6 @@ class AppleHealthRawDataCoreService {
1510 Future<void> _saveLocalNotificationDebugEvent( 1503 Future<void> _saveLocalNotificationDebugEvent(
1511 Map<String, Object?> event, 1504 Map<String, Object?> event,
1512 ) async { 1505 ) async {
1513 - if (!_isDebug) return;  
1514 try { 1506 try {
1515 await _localNotificationDebugStore.append(event); 1507 await _localNotificationDebugStore.append(event);
1516 await _saveNativeLocalNotificationDebugEvent(event); 1508 await _saveNativeLocalNotificationDebugEvent(event);
@@ -2229,7 +2221,7 @@ class HealthRawStressLocalStore { @@ -2229,7 +2221,7 @@ class HealthRawStressLocalStore {
2229 final db = await factory.openDatabase( 2221 final db = await factory.openDatabase(
2230 path, 2222 path,
2231 options: OpenDatabaseOptions( 2223 options: OpenDatabaseOptions(
2232 - version: 7, 2224 + version: 8,
2233 onCreate: (db, version) async { 2225 onCreate: (db, version) async {
2234 await _createTables(db); 2226 await _createTables(db);
2235 }, 2227 },
@@ -2253,6 +2245,9 @@ class HealthRawStressLocalStore { @@ -2253,6 +2245,9 @@ class HealthRawStressLocalStore {
2253 if (oldVersion < 7) { 2245 if (oldVersion < 7) {
2254 await _createSleepResultsTable(db); 2246 await _createSleepResultsTable(db);
2255 } 2247 }
  2248 + if (oldVersion < 8) {
  2249 + await _addUpdateTimeColumns(db);
  2250 + }
2256 }, 2251 },
2257 ), 2252 ),
2258 ); 2253 );
@@ -2278,7 +2273,8 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable ( @@ -2278,7 +2273,8 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
2278 is_workout_recovery INTEGER NOT NULL DEFAULT 0, 2273 is_workout_recovery INTEGER NOT NULL DEFAULT 0,
2279 is_sleep_likely INTEGER NOT NULL DEFAULT 0, 2274 is_sleep_likely INTEGER NOT NULL DEFAULT 0,
2280 is_suspected_activity INTEGER NOT NULL DEFAULT 0, 2275 is_suspected_activity INTEGER NOT NULL DEFAULT 0,
2281 - uploaded INTEGER NOT NULL DEFAULT 0 2276 + uploaded INTEGER NOT NULL DEFAULT 0,
  2277 + update_time INTEGER NOT NULL DEFAULT 0
2282 ) 2278 )
2283 '''); 2279 ''');
2284 await db.execute(''' 2280 await db.execute('''
@@ -2293,7 +2289,8 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( @@ -2293,7 +2289,8 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
2293 is_workout_recovery INTEGER NOT NULL DEFAULT 0, 2289 is_workout_recovery INTEGER NOT NULL DEFAULT 0,
2294 is_sleep_likely INTEGER NOT NULL DEFAULT 0, 2290 is_sleep_likely INTEGER NOT NULL DEFAULT 0,
2295 is_suspected_activity INTEGER NOT NULL DEFAULT 0, 2291 is_suspected_activity INTEGER NOT NULL DEFAULT 0,
2296 - uploaded INTEGER NOT NULL DEFAULT 0 2292 + uploaded INTEGER NOT NULL DEFAULT 0,
  2293 + update_time INTEGER NOT NULL DEFAULT 0
2297 ) 2294 )
2298 '''); 2295 ''');
2299 await _createDailyStressTable(db); 2296 await _createDailyStressTable(db);
@@ -2309,7 +2306,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( @@ -2309,7 +2306,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
2309 stress_score INTEGER NOT NULL, 2306 stress_score INTEGER NOT NULL,
2310 state INTEGER NOT NULL, 2307 state INTEGER NOT NULL,
2311 data_time INTEGER NOT NULL, 2308 data_time INTEGER NOT NULL,
2312 - uploaded INTEGER NOT NULL DEFAULT 0 2309 + uploaded INTEGER NOT NULL DEFAULT 0,
  2310 + update_time INTEGER NOT NULL DEFAULT 0
2313 ) 2311 )
2314 '''); 2312 ''');
2315 } 2313 }
@@ -2325,11 +2323,29 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2325,11 +2323,29 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2325 in_bed_minutes INTEGER NOT NULL, 2323 in_bed_minutes INTEGER NOT NULL,
2326 awak_minutes INTEGER NOT NULL, 2324 awak_minutes INTEGER NOT NULL,
2327 sleep_minutes INTEGER NOT NULL, 2325 sleep_minutes INTEGER NOT NULL,
2328 - uploaded INTEGER NOT NULL DEFAULT 0 2326 + uploaded INTEGER NOT NULL DEFAULT 0,
  2327 + update_time INTEGER NOT NULL DEFAULT 0
2329 ) 2328 )
2330 '''); 2329 ''');
2331 } 2330 }
2332 2331
  2332 + Future<void> _addUpdateTimeColumns(DatabaseExecutor db) async {
  2333 + for (final table in const [
  2334 + hrvResultsTable,
  2335 + realtimeStressResultsTable,
  2336 + dailyStressResultsTable,
  2337 + sleepResultsTable,
  2338 + ]) {
  2339 + try {
  2340 + await db.execute(
  2341 + 'ALTER TABLE $table ADD COLUMN update_time INTEGER NOT NULL DEFAULT 0',
  2342 + );
  2343 + } on DatabaseException catch (error) {
  2344 + if (!error.isDuplicateColumnError()) rethrow;
  2345 + }
  2346 + }
  2347 + }
  2348 +
2333 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async { 2349 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
2334 for (final column in const [ 2350 for (final column in const [
2335 'is_workout', 2351 'is_workout',
@@ -2491,6 +2507,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2491,6 +2507,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2491 'baseline_resting_hr': point.baselineRestingHr, 2507 'baseline_resting_hr': point.baselineRestingHr,
2492 ..._flagsRow(point.flags), 2508 ..._flagsRow(point.flags),
2493 'uploaded': point.uploaded ? 1 : 0, 2509 'uploaded': point.uploaded ? 1 : 0,
  2510 + 'update_time': _currentUnixSeconds(),
2494 }; 2511 };
2495 } 2512 }
2496 2513
@@ -2504,6 +2521,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2504,6 +2521,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2504 'source_end_time': point.sourceEndTime, 2521 'source_end_time': point.sourceEndTime,
2505 ..._flagsRow(point.flags), 2522 ..._flagsRow(point.flags),
2506 'uploaded': point.uploaded ? 1 : 0, 2523 'uploaded': point.uploaded ? 1 : 0,
  2524 + 'update_time': _currentUnixSeconds(),
2507 }; 2525 };
2508 } 2526 }
2509 2527
@@ -2516,6 +2534,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2516,6 +2534,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2516 'state': point.state.value, 2534 'state': point.state.value,
2517 'data_time': point.dataTime, 2535 'data_time': point.dataTime,
2518 'uploaded': point.uploaded ? 1 : 0, 2536 'uploaded': point.uploaded ? 1 : 0,
  2537 + 'update_time': _currentUnixSeconds(),
2519 }; 2538 };
2520 } 2539 }
2521 2540
@@ -2530,6 +2549,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2530,6 +2549,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2530 'awak_minutes': result.awakMinutes, 2549 'awak_minutes': result.awakMinutes,
2531 'sleep_minutes': result.sleepMinutes, 2550 'sleep_minutes': result.sleepMinutes,
2532 'uploaded': result.uploaded ? 1 : 0, 2551 'uploaded': result.uploaded ? 1 : 0,
  2552 + 'update_time': _currentUnixSeconds(),
2533 }; 2553 };
2534 } 2554 }
2535 2555
@@ -2560,6 +2580,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2560,6 +2580,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2560 final existingRow = existing.first; 2580 final existingRow = existing.first;
2561 if (_matchesStoredValues( 2581 if (_matchesStoredValues(
2562 existingRow, row, _rawResultStoredValueKeys(row))) { 2582 existingRow, row, _rawResultStoredValueKeys(row))) {
  2583 + await db.update(
  2584 + table,
  2585 + <String, Object?>{'update_time': row['update_time']},
  2586 + where: 'raw_end_time = ?',
  2587 + whereArgs: [row['raw_end_time']],
  2588 + );
2563 return; 2589 return;
2564 } 2590 }
2565 final uploadPayloadChanged = !_matchesStoredValues( 2591 final uploadPayloadChanged = !_matchesStoredValues(
@@ -2590,6 +2616,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2590,6 +2616,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2590 'is_sleep_likely': row['is_sleep_likely'], 2616 'is_sleep_likely': row['is_sleep_likely'],
2591 'is_suspected_activity': row['is_suspected_activity'], 2617 'is_suspected_activity': row['is_suspected_activity'],
2592 'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'], 2618 'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'],
  2619 + 'update_time': row['update_time'],
2593 }, 2620 },
2594 where: 'raw_end_time = ?', 2621 where: 'raw_end_time = ?',
2595 whereArgs: [row['raw_end_time']], 2622 whereArgs: [row['raw_end_time']],
@@ -2629,6 +2656,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2629,6 +2656,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2629 'state': row['state'], 2656 'state': row['state'],
2630 'data_time': row['data_time'], 2657 'data_time': row['data_time'],
2631 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']), 2658 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']),
  2659 + 'update_time': row['update_time'],
2632 }, 2660 },
2633 where: 'date = ?', 2661 where: 'date = ?',
2634 whereArgs: [row['date']], 2662 whereArgs: [row['date']],
@@ -2674,12 +2702,17 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2674,12 +2702,17 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2674 'awak_minutes': row['awak_minutes'], 2702 'awak_minutes': row['awak_minutes'],
2675 'sleep_minutes': row['sleep_minutes'], 2703 'sleep_minutes': row['sleep_minutes'],
2676 'uploaded': valueChanged ? 0 : existingRow['uploaded'], 2704 'uploaded': valueChanged ? 0 : existingRow['uploaded'],
  2705 + 'update_time': row['update_time'],
2677 }, 2706 },
2678 where: 'date = ?', 2707 where: 'date = ?',
2679 whereArgs: [row['date']], 2708 whereArgs: [row['date']],
2680 ); 2709 );
2681 } 2710 }
2682 2711
  2712 + int _currentUnixSeconds() {
  2713 + return DateTime.now().millisecondsSinceEpoch ~/ 1000;
  2714 + }
  2715 +
2683 List<String> _rawResultStoredValueKeys(Map<String, Object?> row) { 2716 List<String> _rawResultStoredValueKeys(Map<String, Object?> row) {
2684 return <String>[ 2717 return <String>[
2685 'user_id', 2718 '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 '暂无本地推送诊断日志';
@@ -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 {
@@ -1266,7 +1331,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1266,7 +1331,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1266 final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {}; 1331 final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {};
1267 final Map<int, List<HealthRawDailyStressPoint>> _daily = {}; 1332 final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
1268 final Map<int, List<HealthRawSleepResult>> _sleep = {}; 1333 final Map<int, List<HealthRawSleepResult>> _sleep = {};
  1334 + final Map<int, Map<int, int>> _hrvUpdateTimes = {};
  1335 + final Map<int, Map<int, int>> _realtimeUpdateTimes = {};
  1336 + final Map<int, Map<int, int>> _dailyUpdateTimes = {};
  1337 + final Map<int, Map<int, int>> _sleepUpdateTimes = {};
1269 final List<String> operationLog; 1338 final List<String> operationLog;
  1339 + var _updateTimeClock = 1;
1270 var realtimeNotificationWindowQueryCount = 0; 1340 var realtimeNotificationWindowQueryCount = 0;
1271 1341
1272 void insertDailyStress(HealthRawDailyStressPoint point) { 1342 void insertDailyStress(HealthRawDailyStressPoint point) {
@@ -1277,6 +1347,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1277,6 +1347,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1277 byDate[point.date] = point; 1347 byDate[point.date] = point;
1278 _daily[point.userId] = byDate.values.toList() 1348 _daily[point.userId] = byDate.values.toList()
1279 ..sort((a, b) => a.date.compareTo(b.date)); 1349 ..sort((a, b) => a.date.compareTo(b.date));
  1350 + _dailyUpdateTimes.putIfAbsent(
  1351 + point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
1280 } 1352 }
1281 1353
1282 @override 1354 @override
@@ -1313,6 +1385,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1313,6 +1385,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1313 flags: point.flags, 1385 flags: point.flags,
1314 uploaded: same ? existing.uploaded : false, 1386 uploaded: same ? existing.uploaded : false,
1315 ); 1387 );
  1388 + _hrvUpdateTimes.putIfAbsent(
  1389 + point.userId, () => <int, int>{})[point.rawEndTime] =
  1390 + _nextUpdateTime();
1316 } 1391 }
1317 _hrv[result.userId] = hrvByTime.values.toList() 1392 _hrv[result.userId] = hrvByTime.values.toList()
1318 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime)); 1393 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
@@ -1336,6 +1411,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1336,6 +1411,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1336 flags: point.flags, 1411 flags: point.flags,
1337 uploaded: same ? existing.uploaded : false, 1412 uploaded: same ? existing.uploaded : false,
1338 ); 1413 );
  1414 + _realtimeUpdateTimes.putIfAbsent(
  1415 + point.userId, () => <int, int>{})[point.rawEndTime] =
  1416 + _nextUpdateTime();
1339 } 1417 }
1340 _realtime[result.userId] = realtimeByTime.values.toList() 1418 _realtime[result.userId] = realtimeByTime.values.toList()
1341 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime)); 1419 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
@@ -1368,6 +1446,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1368,6 +1446,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1368 dataTime: point.dataTime, 1446 dataTime: point.dataTime,
1369 uploaded: isToday ? false : (same ? existing.uploaded : false), 1447 uploaded: isToday ? false : (same ? existing.uploaded : false),
1370 ); 1448 );
  1449 + _dailyUpdateTimes.putIfAbsent(
  1450 + point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
1371 } 1451 }
1372 _daily[userId] = byDate.values.toList() 1452 _daily[userId] = byDate.values.toList()
1373 ..sort((a, b) => a.date.compareTo(b.date)); 1453 ..sort((a, b) => a.date.compareTo(b.date));
@@ -1384,6 +1464,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1384,6 +1464,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1384 }; 1464 };
1385 for (final result in results) { 1465 for (final result in results) {
1386 byDate[result.date] = result; 1466 byDate[result.date] = result;
  1467 + _sleepUpdateTimes.putIfAbsent(
  1468 + result.userId, () => <int, int>{})[result.date] = _nextUpdateTime();
1387 } 1469 }
1388 _sleep[userId] = byDate.values.toList() 1470 _sleep[userId] = byDate.values.toList()
1389 ..sort((a, b) => a.date.compareTo(b.date)); 1471 ..sort((a, b) => a.date.compareTo(b.date));
@@ -1711,6 +1793,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1711,6 +1793,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1711 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0, 1793 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
1712 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0, 1794 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
1713 'uploaded': point.uploaded ? 1 : 0, 1795 'uploaded': point.uploaded ? 1 : 0,
  1796 + 'update_time': _hrvUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
1714 }; 1797 };
1715 } 1798 }
1716 1799
@@ -1727,6 +1810,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1727,6 +1810,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1727 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0, 1810 'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
1728 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0, 1811 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
1729 'uploaded': point.uploaded ? 1 : 0, 1812 'uploaded': point.uploaded ? 1 : 0,
  1813 + 'update_time': _realtimeUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
1730 }; 1814 };
1731 } 1815 }
1732 1816
@@ -1739,6 +1823,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1739,6 +1823,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1739 'state': point.state.value, 1823 'state': point.state.value,
1740 'data_time': point.dataTime, 1824 'data_time': point.dataTime,
1741 'uploaded': point.uploaded ? 1 : 0, 1825 'uploaded': point.uploaded ? 1 : 0,
  1826 + 'update_time': _dailyUpdateTimes[point.userId]?[point.date] ?? 0,
1742 }; 1827 };
1743 } 1828 }
1744 1829
@@ -1753,9 +1838,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1753,9 +1838,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1753 'awak_minutes': result.awakMinutes, 1838 'awak_minutes': result.awakMinutes,
1754 'sleep_minutes': result.sleepMinutes, 1839 'sleep_minutes': result.sleepMinutes,
1755 'uploaded': result.uploaded ? 1 : 0, 1840 'uploaded': result.uploaded ? 1 : 0,
  1841 + 'update_time': _sleepUpdateTimes[result.userId]?[result.date] ?? 0,
1756 }; 1842 };
1757 } 1843 }
1758 1844
  1845 + int _nextUpdateTime() => _updateTimeClock++;
  1846 +
1759 int _todayDateKey() { 1847 int _todayDateKey() {
1760 final now = DateTime.now(); 1848 final now = DateTime.now();
1761 return now.year * 10000 + now.month * 100 + now.day; 1849 return now.year * 10000 + now.month * 100 + now.day;