Commit 37deec526d7c61d6813a1bae61d1f0ca6c284d74

Authored by 权海
1 parent db377e19

feat(ui):ohos同一数据类型批量请求时修改为并发

@@ -22,6 +22,7 @@ import '../../core/services/push_service.dart'; @@ -22,6 +22,7 @@ import '../../core/services/push_service.dart';
22 import '../../core/services/thinking_data_service.dart'; 22 import '../../core/services/thinking_data_service.dart';
23 import '../../core/services/user_state_service.dart'; 23 import '../../core/services/user_state_service.dart';
24 import '../../core/services/web_socket_service.dart'; 24 import '../../core/services/web_socket_service.dart';
  25 +import '../../core/util/app_toast.dart';
25 import '../../data/local/local_storage.dart'; 26 import '../../data/local/local_storage.dart';
26 import '../../data/local/user_account_storage.dart'; 27 import '../../data/local/user_account_storage.dart';
27 import '../../data/local/user_preferences_storage.dart'; 28 import '../../data/local/user_preferences_storage.dart';
@@ -74,6 +75,7 @@ void registerUserSessionDeps() { @@ -74,6 +75,7 @@ void registerUserSessionDeps() {
74 environmentConfig: environmentConfig, 75 environmentConfig: environmentConfig,
75 userIdProvider: () => userPrefs.preferences.value.meUserInfo?.id ?? 0, 76 userIdProvider: () => userPrefs.preferences.value.meUserInfo?.id ?? 0,
76 serverHealthApi: HealthApi(dioClient), 77 serverHealthApi: HealthApi(dioClient),
  78 + ohosSyncFailureToastSink: AppToast.show,
77 ), 79 ),
78 permanent: true, 80 permanent: true,
79 ); 81 );
@@ -24,6 +24,7 @@ class HealthRawDataCoreService { @@ -24,6 +24,7 @@ class HealthRawDataCoreService {
24 int Function()? userIdProvider, 24 int Function()? userIdProvider,
25 bool uploadResultsAfterCalculation = true, 25 bool uploadResultsAfterCalculation = true,
26 HealthApi? serverHealthApi, 26 HealthApi? serverHealthApi,
  27 + void Function(String message)? ohosSyncFailureToastSink,
27 }) : _appleService = appleService ?? 28 }) : _appleService = appleService ??
28 AppleHealthRawDataCoreService( 29 AppleHealthRawDataCoreService(
29 environmentConfig: environmentConfig, 30 environmentConfig: environmentConfig,
@@ -37,6 +38,7 @@ class HealthRawDataCoreService { @@ -37,6 +38,7 @@ class HealthRawDataCoreService {
37 userIdProvider: userIdProvider, 38 userIdProvider: userIdProvider,
38 uploadResultsAfterCalculation: uploadResultsAfterCalculation, 39 uploadResultsAfterCalculation: uploadResultsAfterCalculation,
39 serverHealthApi: serverHealthApi, 40 serverHealthApi: serverHealthApi,
  41 + toastSink: ohosSyncFailureToastSink,
40 ), 42 ),
41 _targetPlatform = targetPlatform; 43 _targetPlatform = targetPlatform;
42 44
@@ -1484,9 +1484,8 @@ class AppleHealthRawDataCoreService { @@ -1484,9 +1484,8 @@ class AppleHealthRawDataCoreService {
1484 }) async { 1484 }) async {
1485 if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[]; 1485 if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[];
1486 final days = { 1486 final days = {
1487 - for (final interval in sleepIntervals)  
1488 - DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000)  
1489 - }.map((date) => DateTime(date.year, date.month, date.day)).toList() 1487 + for (final interval in sleepIntervals) _localDay(interval.endTime),
  1488 + }.toList()
1490 ..sort((a, b) => a.compareTo(b)); 1489 ..sort((a, b) => a.compareTo(b));
1491 final results = <HealthRawSleepResult>[]; 1490 final results = <HealthRawSleepResult>[];
1492 for (final day in days) { 1491 for (final day in days) {
@@ -1608,6 +1607,11 @@ class AppleHealthRawDataCoreService { @@ -1608,6 +1607,11 @@ class AppleHealthRawDataCoreService {
1608 return date.year * 10000 + date.month * 100 + date.day; 1607 return date.year * 10000 + date.month * 100 + date.day;
1609 } 1608 }
1610 1609
  1610 + static DateTime _localDay(int seconds) {
  1611 + final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
  1612 + return DateTime(date.year, date.month, date.day);
  1613 + }
  1614 +
1611 static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) { 1615 static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) {
1612 final year = dateKey ~/ 10000; 1616 final year = dateKey ~/ 10000;
1613 final month = (dateKey ~/ 100) % 100; 1617 final month = (dateKey ~/ 100) % 100;
@@ -43,6 +43,8 @@ class OHOSHealthRawDataCoreService { @@ -43,6 +43,8 @@ class OHOSHealthRawDataCoreService {
43 Future<bool> Function()? healthReadAuthorizationChecker, 43 Future<bool> Function()? healthReadAuthorizationChecker,
44 OhosHealthRawResultUploadService? resultUploadService, 44 OhosHealthRawResultUploadService? resultUploadService,
45 HealthRawLocalNotificationDispatcher? localNotificationDispatcher, 45 HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
  46 + bool Function()? debugModeProvider,
  47 + void Function(String message)? toastSink,
46 }) : _rawDataSource = rawDataSource ?? 48 }) : _rawDataSource = rawDataSource ??
47 OhosHealthRawDataSource( 49 OhosHealthRawDataSource(
48 userIdProvider: userIdProvider, 50 userIdProvider: userIdProvider,
@@ -58,6 +60,8 @@ class OHOSHealthRawDataCoreService { @@ -58,6 +60,8 @@ class OHOSHealthRawDataCoreService {
58 (Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null), 60 (Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null),
59 _healthReadAuthorizationChecker = healthReadAuthorizationChecker, 61 _healthReadAuthorizationChecker = healthReadAuthorizationChecker,
60 _resultUploadService = resultUploadService, 62 _resultUploadService = resultUploadService,
  63 + _debugModeProvider = debugModeProvider,
  64 + _toastSink = toastSink,
61 _localNotificationDispatcher = localNotificationDispatcher ?? 65 _localNotificationDispatcher = localNotificationDispatcher ??
62 HealthRawLocalNotificationDispatcher(); 66 HealthRawLocalNotificationDispatcher();
63 67
@@ -70,13 +74,16 @@ class OHOSHealthRawDataCoreService { @@ -70,13 +74,16 @@ class OHOSHealthRawDataCoreService {
70 final HarmonyApi? _harmonyApi; 74 final HarmonyApi? _harmonyApi;
71 final Future<bool> Function()? _healthReadAuthorizationChecker; 75 final Future<bool> Function()? _healthReadAuthorizationChecker;
72 OhosHealthRawResultUploadService? _resultUploadService; 76 OhosHealthRawResultUploadService? _resultUploadService;
  77 + final bool Function()? _debugModeProvider;
  78 + final void Function(String message)? _toastSink;
73 final HealthRawLocalNotificationDispatcher _localNotificationDispatcher; 79 final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
74 final StreamController<HealthRawDataUpdatedEvent> 80 final StreamController<HealthRawDataUpdatedEvent>
75 _healthDataUpdatedController = 81 _healthDataUpdatedController =
76 StreamController<HealthRawDataUpdatedEvent>.broadcast(); 82 StreamController<HealthRawDataUpdatedEvent>.broadcast();
77 Future<HealthRawStressCalculationResult>? _coreCalculation; 83 Future<HealthRawStressCalculationResult>? _coreCalculation;
78 84
79 - bool get _isDebug => _environmentConfig?.isDebug ?? false; 85 + bool get _isDebug =>
  86 + _debugModeProvider?.call() ?? _environmentConfig?.isDebug ?? false;
80 87
81 int get _userId { 88 int get _userId {
82 final userId = _userIdProvider?.call() ?? 0; 89 final userId = _userIdProvider?.call() ?? 0;
@@ -267,13 +274,6 @@ class OHOSHealthRawDataCoreService { @@ -267,13 +274,6 @@ class OHOSHealthRawDataCoreService {
267 }) async { 274 }) async {
268 var hasExistingHrv = false; 275 var hasExistingHrv = false;
269 var hasExistingSleep = false; 276 var hasExistingSleep = false;
270 - final emptyResult = HealthRawStressCalculationResult(  
271 - userId: userId,  
272 - hrvStressPoints: const <HealthRawHrvStressPoint>[],  
273 - realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],  
274 - dailyStressPoints: const <HealthRawDailyStressPoint>[],  
275 - sleepResults: const <HealthRawSleepResult>[],  
276 - );  
277 try { 277 try {
278 final hrvContextStart = 278 final hrvContextStart =
279 await _localStore.latestHrvSourceStartTime(userId); 279 await _localStore.latestHrvSourceStartTime(userId);
@@ -433,11 +433,7 @@ class OHOSHealthRawDataCoreService { @@ -433,11 +433,7 @@ class OHOSHealthRawDataCoreService {
433 error, 433 error,
434 stackTrace, 434 stackTrace,
435 ); 435 );
436 - return _OhosStoredCalculationResult(  
437 - result: emptyResult,  
438 - hasExistingHrv: hasExistingHrv,  
439 - hasExistingSleep: hasExistingSleep,  
440 - ); 436 + rethrow;
441 } 437 }
442 } 438 }
443 439
@@ -895,7 +891,6 @@ class OHOSHealthRawDataCoreService { @@ -895,7 +891,6 @@ class OHOSHealthRawDataCoreService {
895 if (rawDataSource is! OhosHealthRawDataSource) { 891 if (rawDataSource is! OhosHealthRawDataSource) {
896 return const <OhosHealthRawDataSyncResult>[]; 892 return const <OhosHealthRawDataSyncResult>[];
897 } 893 }
898 - await rawDataSource.getActivityGoal(refresh: true);  
899 return rawDataSource.syncCalculationRawData( 894 return rawDataSource.syncCalculationRawData(
900 startTime: startTime, 895 startTime: startTime,
901 endTime: endTime, 896 endTime: endTime,
@@ -921,12 +916,15 @@ class OHOSHealthRawDataCoreService { @@ -921,12 +916,15 @@ class OHOSHealthRawDataCoreService {
921 ); 916 );
922 } catch (error, stackTrace) { 917 } catch (error, stackTrace) {
923 _logError( 918 _logError(
924 - '$_calculateLogMarker sync_failed_continue ' 919 + '$_calculateLogMarker sync_failed_abort '
925 'startTime=$startTime endTime=$endTime', 920 'startTime=$startTime endTime=$endTime',
926 error, 921 error,
927 stackTrace, 922 stackTrace,
928 ); 923 );
929 - return const <OhosHealthRawDataSyncResult>[]; 924 + if (_isDebug) {
  925 + _toastSink?.call('数据同步失败,请刷新首页重试');
  926 + }
  927 + rethrow;
930 } 928 }
931 } 929 }
932 930
@@ -1070,9 +1068,8 @@ class OHOSHealthRawDataCoreService { @@ -1070,9 +1068,8 @@ class OHOSHealthRawDataCoreService {
1070 return const <HealthRawSleepResult>[]; 1068 return const <HealthRawSleepResult>[];
1071 } 1069 }
1072 final days = { 1070 final days = {
1073 - for (final interval in sleepIntervals)  
1074 - DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000)  
1075 - }.map((date) => DateTime(date.year, date.month, date.day)).toList() 1071 + for (final interval in sleepIntervals) _localDay(interval.endTime),
  1072 + }.toList()
1076 ..sort((a, b) => a.compareTo(b)); 1073 ..sort((a, b) => a.compareTo(b));
1077 _logInfo( 1074 _logInfo(
1078 '$_sleepCalcLogMarker days userId=$userId ' 1075 '$_sleepCalcLogMarker days userId=$userId '
@@ -1224,6 +1221,11 @@ class OHOSHealthRawDataCoreService { @@ -1224,6 +1221,11 @@ class OHOSHealthRawDataCoreService {
1224 return _dateKeyFromDateTime(date); 1221 return _dateKeyFromDateTime(date);
1225 } 1222 }
1226 1223
  1224 + static DateTime _localDay(int seconds) {
  1225 + final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
  1226 + return DateTime(date.year, date.month, date.day);
  1227 + }
  1228 +
1227 static int _dateKeyFromDateTime(DateTime date) { 1229 static int _dateKeyFromDateTime(DateTime date) {
1228 return date.year * 10000 + date.month * 100 + date.day; 1230 return date.year * 10000 + date.month * 100 + date.day;
1229 } 1231 }
@@ -109,9 +109,10 @@ class OhosHealthRawDataSyncService { @@ -109,9 +109,10 @@ class OhosHealthRawDataSyncService {
109 ); 109 );
110 final List<OhosHealthRawDataSyncResult> results; 110 final List<OhosHealthRawDataSyncResult> results;
111 try { 111 try {
112 - results = await Future.wait( 112 + final activityGoalFuture = _remoteDataSource.fetchActivityGoal();
  113 + final rawFetchesFuture = Future.wait(
113 resolvedDataTypes.map( 114 resolvedDataTypes.map(
114 - (dataType) => syncRawData( 115 + (dataType) => _fetchRawDataForSync(
115 dataType: dataType, 116 dataType: dataType,
116 startTime: startTime, 117 startTime: startTime,
117 endTime: resolvedEndTime, 118 endTime: resolvedEndTime,
@@ -119,6 +120,27 @@ class OhosHealthRawDataSyncService { @@ -119,6 +120,27 @@ class OhosHealthRawDataSyncService {
119 ), 120 ),
120 eagerError: true, 121 eagerError: true,
121 ); 122 );
  123 + final fetched = await Future.wait<Object?>(
  124 + <Future<Object?>>[
  125 + activityGoalFuture,
  126 + rawFetchesFuture,
  127 + ],
  128 + eagerError: true,
  129 + );
  130 + final activityGoal = fetched[0] as V2ActivityTarget?;
  131 + final rawFetches = fetched[1] as List<_FetchedRawDataSync>;
  132 + if (activityGoal != null) {
  133 + await _localStore.upsertActivityGoal(activityGoal);
  134 + _log(
  135 + 'calculation_sync_activity_goal_stored '
  136 + 'move=${activityGoal.move} step=${activityGoal.step} '
  137 + 'exercise=${activityGoal.exercise} stand=${activityGoal.stand}',
  138 + );
  139 + }
  140 + results = <OhosHealthRawDataSyncResult>[];
  141 + for (final fetch in rawFetches) {
  142 + results.add(await _storeFetchedRawData(fetch));
  143 + }
122 } catch (error, stackTrace) { 144 } catch (error, stackTrace) {
123 _log( 145 _log(
124 'calculation_sync_failed dataTypes=${resolvedDataTypes.join(',')} ' 146 'calculation_sync_failed dataTypes=${resolvedDataTypes.join(',')} '
@@ -142,6 +164,26 @@ class OhosHealthRawDataSyncService { @@ -142,6 +164,26 @@ class OhosHealthRawDataSyncService {
142 return results; 164 return results;
143 } 165 }
144 166
  167 + Future<_FetchedRawDataSync> _fetchRawDataForSync({
  168 + required int dataType,
  169 + required int? startTime,
  170 + required int endTime,
  171 + }) async {
  172 + final resolvedStartTime = await _resolveStartTime(
  173 + dataType: dataType,
  174 + requestedStartTime: startTime,
  175 + endTime: endTime,
  176 + );
  177 + if (endTime < resolvedStartTime) {
  178 + throw ArgumentError.value(endTime, 'endTime');
  179 + }
  180 + return _fetchResolvedRawData(
  181 + dataType: dataType,
  182 + startTime: resolvedStartTime,
  183 + endTime: endTime,
  184 + );
  185 + }
  186 +
145 Future<List<OhosHealthRawDataItem>> queryRawData({ 187 Future<List<OhosHealthRawDataItem>> queryRawData({
146 required int dataType, 188 required int dataType,
147 required int startTime, 189 required int startTime,
@@ -187,6 +229,19 @@ class OhosHealthRawDataSyncService { @@ -187,6 +229,19 @@ class OhosHealthRawDataSyncService {
187 required int startTime, 229 required int startTime,
188 required int endTime, 230 required int endTime,
189 }) async { 231 }) async {
  232 + final fetched = await _fetchResolvedRawData(
  233 + dataType: dataType,
  234 + startTime: startTime,
  235 + endTime: endTime,
  236 + );
  237 + return _storeFetchedRawData(fetched);
  238 + }
  239 +
  240 + Future<_FetchedRawDataSync> _fetchResolvedRawData({
  241 + required int dataType,
  242 + required int startTime,
  243 + required int endTime,
  244 + }) async {
190 final fetchRanges = _splitIntoFetchRanges( 245 final fetchRanges = _splitIntoFetchRanges(
191 dataType: dataType, 246 dataType: dataType,
192 startTime: startTime, 247 startTime: startTime,
@@ -199,72 +254,105 @@ class OhosHealthRawDataSyncService { @@ -199,72 +254,105 @@ class OhosHealthRawDataSyncService {
199 'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length}', 254 'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length}',
200 ); 255 );
201 256
  257 + final segments = await Future.wait(
  258 + <Future<_FetchedRawDataSegment>>[
  259 + for (var index = 0; index < fetchRanges.length; index++)
  260 + _fetchRawDataSegment(
  261 + dataType: dataType,
  262 + range: fetchRanges[index],
  263 + segmentIndex: index,
  264 + segmentCount: fetchRanges.length,
  265 + fetchChunkDays: fetchChunkDays,
  266 + ),
  267 + ],
  268 + eagerError: true,
  269 + );
  270 + segments.sort((a, b) => a.segmentIndex.compareTo(b.segmentIndex));
  271 + return _FetchedRawDataSync(
  272 + dataType: dataType,
  273 + startTime: startTime,
  274 + endTime: endTime,
  275 + fetchChunkDays: fetchChunkDays,
  276 + segments: segments,
  277 + );
  278 + }
  279 +
  280 + Future<_FetchedRawDataSegment> _fetchRawDataSegment({
  281 + required int dataType,
  282 + required OhosHealthRawDataFetchRange range,
  283 + required int segmentIndex,
  284 + required int segmentCount,
  285 + required int fetchChunkDays,
  286 + }) async {
  287 + _log(
  288 + 'segment_start dataType=$dataType '
  289 + 'segment=${segmentIndex + 1}/$segmentCount '
  290 + 'chunkDays=$fetchChunkDays '
  291 + 'startTime=${range.startTime} endTime=${range.endTime} '
  292 + 'startDate=${_dateKeyFromUnixSeconds(range.startTime)} '
  293 + 'endDate=${_dateKeyFromUnixSeconds(range.endTime)}',
  294 + );
  295 +
  296 + final page = await _remoteDataSource.fetchRawDataPage(
  297 + dataType: dataType,
  298 + startTime: range.startTime,
  299 + endTime: range.endTime,
  300 + );
  301 + _log(
  302 + 'page_fetched dataType=$dataType '
  303 + 'segment=${segmentIndex + 1}/$segmentCount '
  304 + 'items=${page.items.length}',
  305 + );
  306 +
  307 + return _FetchedRawDataSegment(
  308 + segmentIndex: segmentIndex,
  309 + range: range,
  310 + page: page,
  311 + );
  312 + }
  313 +
  314 + Future<OhosHealthRawDataSyncResult> _storeFetchedRawData(
  315 + _FetchedRawDataSync fetched,
  316 + ) async {
202 var pageCount = 0; 317 var pageCount = 0;
203 var storedCount = 0; 318 var storedCount = 0;
204 319
205 - for (var rangeIndex = 0; rangeIndex < fetchRanges.length; rangeIndex++) {  
206 - final range = fetchRanges[rangeIndex];  
207 - _log(  
208 - 'segment_start dataType=$dataType '  
209 - 'segment=${rangeIndex + 1}/${fetchRanges.length} '  
210 - 'chunkDays=$fetchChunkDays '  
211 - 'startTime=${range.startTime} endTime=${range.endTime} '  
212 - 'startDate=${_dateKeyFromUnixSeconds(range.startTime)} '  
213 - 'endDate=${_dateKeyFromUnixSeconds(range.endTime)}',  
214 - );  
215 -  
216 - String? pageToken;  
217 - do {  
218 - final page = await _remoteDataSource.fetchRawDataPage(  
219 - dataType: dataType,  
220 - startTime: range.startTime,  
221 - endTime: range.endTime,  
222 - pageToken: pageToken, 320 + for (final segment in fetched.segments) {
  321 + final page = segment.page;
  322 + pageCount += 1;
  323 + if (page.items.isNotEmpty) {
  324 + final pageStoredCount = await _localStore.upsertRawDataBatch(
  325 + dataType: fetched.dataType,
  326 + items: page.items,
223 ); 327 );
224 - pageCount += 1; 328 + storedCount += pageStoredCount;
225 _log( 329 _log(
226 - 'page_fetched dataType=$dataType '  
227 - 'segment=${rangeIndex + 1}/${fetchRanges.length} '  
228 - 'page=$pageCount items=${page.items.length} '  
229 - 'nextPageToken=${page.nextPageToken ?? ''}', 330 + 'page_stored dataType=${fetched.dataType} '
  331 + 'segment=${segment.segmentIndex + 1}/${fetched.segmentCount} '
  332 + 'fetchedItems=${page.items.length} '
  333 + 'storedItems=$pageStoredCount totalStored=$storedCount',
230 ); 334 );
231 -  
232 - if (page.items.isNotEmpty) {  
233 - final pageStoredCount = await _localStore.upsertRawDataBatch(  
234 - dataType: dataType,  
235 - items: page.items,  
236 - );  
237 - storedCount += pageStoredCount;  
238 - _log(  
239 - 'page_stored dataType=$dataType '  
240 - 'segment=${rangeIndex + 1}/${fetchRanges.length} '  
241 - 'fetchedItems=${page.items.length} '  
242 - 'storedItems=$pageStoredCount totalStored=$storedCount',  
243 - );  
244 - }  
245 -  
246 - pageToken = page.nextPageToken;  
247 - } while (pageToken != null && pageToken.isNotEmpty); 335 + }
248 336
249 _log( 337 _log(
250 - 'segment_finish dataType=$dataType '  
251 - 'segment=${rangeIndex + 1}/${fetchRanges.length}', 338 + 'segment_finish dataType=${fetched.dataType} '
  339 + 'segment=${segment.segmentIndex + 1}/${fetched.segmentCount}',
252 ); 340 );
253 } 341 }
254 342
255 _log( 343 _log(
256 - 'sync_finish dataType=$dataType '  
257 - 'startTime=$startTime endTime=$endTime '  
258 - 'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length} ' 344 + 'sync_finish dataType=${fetched.dataType} '
  345 + 'startTime=${fetched.startTime} endTime=${fetched.endTime} '
  346 + 'chunkDays=${fetched.fetchChunkDays} rangeCount=${fetched.segmentCount} '
259 'pageCount=$pageCount ' 347 'pageCount=$pageCount '
260 'storedCount=$storedCount', 348 'storedCount=$storedCount',
261 ); 349 );
262 350
263 return OhosHealthRawDataSyncResult( 351 return OhosHealthRawDataSyncResult(
264 - dataType: dataType,  
265 - startTime: startTime,  
266 - endTime: endTime,  
267 - segmentCount: fetchRanges.length, 352 + dataType: fetched.dataType,
  353 + startTime: fetched.startTime,
  354 + endTime: fetched.endTime,
  355 + segmentCount: fetched.segmentCount,
268 pageCount: pageCount, 356 pageCount: pageCount,
269 storedCount: storedCount, 357 storedCount: storedCount,
270 ); 358 );
@@ -350,7 +438,6 @@ abstract class OhosHealthRawDataRemoteDataSource { @@ -350,7 +438,6 @@ abstract class OhosHealthRawDataRemoteDataSource {
350 required int dataType, 438 required int dataType,
351 required int startTime, 439 required int startTime,
352 required int endTime, 440 required int endTime,
353 - String? pageToken,  
354 }); 441 });
355 442
356 Future<V2ActivityTarget?> fetchActivityGoal(); 443 Future<V2ActivityTarget?> fetchActivityGoal();
@@ -410,7 +497,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource @@ -410,7 +497,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource
410 required int dataType, 497 required int dataType,
411 required int startTime, 498 required int startTime,
412 required int endTime, 499 required int endTime,
413 - String? pageToken,  
414 }) async { 500 }) async {
415 final startDate = _dateKeyFromUnixSeconds(startTime); 501 final startDate = _dateKeyFromUnixSeconds(startTime);
416 final endDate = _endDateKeyFromUnixSeconds(endTime); 502 final endDate = _endDateKeyFromUnixSeconds(endTime);
@@ -552,7 +638,6 @@ class TodoOhosHealthRawDataRemoteDataSource @@ -552,7 +638,6 @@ class TodoOhosHealthRawDataRemoteDataSource
552 required int dataType, 638 required int dataType,
553 required int startTime, 639 required int startTime,
554 required int endTime, 640 required int endTime,
555 - String? pageToken,  
556 }) async { 641 }) async {
557 // TODO: Call the OHOS raw-data API after the endpoint and response schema 642 // TODO: Call the OHOS raw-data API after the endpoint and response schema
558 // are available. 643 // are available.
@@ -607,11 +692,9 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -607,11 +692,9 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
607 class OhosHealthRawDataPage { 692 class OhosHealthRawDataPage {
608 const OhosHealthRawDataPage({ 693 const OhosHealthRawDataPage({
609 required this.items, 694 required this.items,
610 - this.nextPageToken,  
611 }); 695 });
612 696
613 final List<OhosHealthRawDataItem> items; 697 final List<OhosHealthRawDataItem> items;
614 - final String? nextPageToken;  
615 } 698 }
616 699
617 class OhosHealthRawDataItem { 700 class OhosHealthRawDataItem {
@@ -636,6 +719,36 @@ class OhosHealthRawDataFetchRange { @@ -636,6 +719,36 @@ class OhosHealthRawDataFetchRange {
636 final int endTime; 719 final int endTime;
637 } 720 }
638 721
  722 +class _FetchedRawDataSync {
  723 + const _FetchedRawDataSync({
  724 + required this.dataType,
  725 + required this.startTime,
  726 + required this.endTime,
  727 + required this.fetchChunkDays,
  728 + required this.segments,
  729 + });
  730 +
  731 + final int dataType;
  732 + final int startTime;
  733 + final int endTime;
  734 + final int fetchChunkDays;
  735 + final List<_FetchedRawDataSegment> segments;
  736 +
  737 + int get segmentCount => segments.length;
  738 +}
  739 +
  740 +class _FetchedRawDataSegment {
  741 + const _FetchedRawDataSegment({
  742 + required this.segmentIndex,
  743 + required this.range,
  744 + required this.page,
  745 + });
  746 +
  747 + final int segmentIndex;
  748 + final OhosHealthRawDataFetchRange range;
  749 + final OhosHealthRawDataPage page;
  750 +}
  751 +
639 class OhosHealthRawDataSyncResult { 752 class OhosHealthRawDataSyncResult {
640 const OhosHealthRawDataSyncResult({ 753 const OhosHealthRawDataSyncResult({
641 required this.dataType, 754 required this.dataType,
@@ -109,10 +109,10 @@ packages: @@ -109,10 +109,10 @@ packages:
109 dependency: transitive 109 dependency: transitive
110 description: 110 description:
111 name: built_value 111 name: built_value
112 - sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" 112 + sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7
113 url: "https://pub.flutter-io.cn" 113 url: "https://pub.flutter-io.cn"
114 source: hosted 114 source: hosted
115 - version: "8.12.7" 115 + version: "8.13.0"
116 cached_network_image: 116 cached_network_image:
117 dependency: "direct main" 117 dependency: "direct main"
118 description: 118 description:
@@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_dat @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_dat
2 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; 2 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
3 import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart'; 3 import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart';
4 import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_core_service.dart'; 4 import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_core_service.dart';
  5 +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart';
5 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; 6 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
6 import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; 7 import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
7 import 'package:flutter_test/flutter_test.dart'; 8 import 'package:flutter_test/flutter_test.dart';
@@ -54,6 +55,93 @@ void main() { @@ -54,6 +55,93 @@ void main() {
54 expect(store.dailyStressPoints, hasLength(1)); 55 expect(store.dailyStressPoints, hasLength(1));
55 expect(store.sleepResults, hasLength(1)); 56 expect(store.sleepResults, hasLength(1));
56 }); 57 });
  58 +
  59 + test('OHOS core aborts calculation and shows debug toast when sync fails',
  60 + () async {
  61 + final store = _FakeHealthRawStressLocalStore();
  62 + final toasts = <String>[];
  63 + final service = OHOSHealthRawDataCoreService(
  64 + rawDataSource: OhosHealthRawDataSource(
  65 + syncService: OhosHealthRawDataSyncService(
  66 + remoteDataSource: _FailingOhosHealthRawDataRemoteDataSource(),
  67 + localStore: _FakeOhosHealthRawDataLocalStore(),
  68 + ),
  69 + ),
  70 + localStore: store,
  71 + userIdProvider: () => 42,
  72 + uploadResultsAfterCalculation: false,
  73 + healthReadAuthorizationChecker: () async => true,
  74 + debugModeProvider: () => true,
  75 + toastSink: toasts.add,
  76 + );
  77 +
  78 + await expectLater(
  79 + service.syncAndStore(
  80 + startTime: DateTime.now()
  81 + .subtract(const Duration(days: 1))
  82 + .millisecondsSinceEpoch ~/
  83 + 1000,
  84 + endTime: DateTime.now().millisecondsSinceEpoch ~/ 1000,
  85 + readChunkDays: 1,
  86 + ),
  87 + throwsA(isA<StateError>()),
  88 + );
  89 +
  90 + expect(toasts, ['数据同步失败,请刷新首页重试']);
  91 + expect(store.hrvStressPoints, isEmpty);
  92 + expect(store.realtimeStressPoints, isEmpty);
  93 + expect(store.dailyStressPoints, isEmpty);
  94 + expect(store.sleepResults, isEmpty);
  95 + });
  96 +}
  97 +
  98 +class _FailingOhosHealthRawDataRemoteDataSource
  99 + implements OhosHealthRawDataRemoteDataSource {
  100 + @override
  101 + Future<OhosHealthRawDataPage> fetchRawDataPage({
  102 + required int dataType,
  103 + required int startTime,
  104 + required int endTime,
  105 + }) async {
  106 + throw StateError('failed dataType=$dataType');
  107 + }
  108 +
  109 + @override
  110 + Future<V2ActivityTarget?> fetchActivityGoal() async {
  111 + return null;
  112 + }
  113 +}
  114 +
  115 +class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
  116 + @override
  117 + Future<int?> latestDataTime({required int dataType}) async {
  118 + return null;
  119 + }
  120 +
  121 + @override
  122 + Future<List<OhosHealthRawDataItem>> queryRawData({
  123 + required int dataType,
  124 + required int startTime,
  125 + required int endTime,
  126 + }) async {
  127 + return const <OhosHealthRawDataItem>[];
  128 + }
  129 +
  130 + @override
  131 + Future<V2ActivityTarget?> queryActivityGoal() async {
  132 + return null;
  133 + }
  134 +
  135 + @override
  136 + Future<void> upsertActivityGoal(V2ActivityTarget goal) async {}
  137 +
  138 + @override
  139 + Future<int> upsertRawDataBatch({
  140 + required int dataType,
  141 + required List<OhosHealthRawDataItem> items,
  142 + }) async {
  143 + return items.length;
  144 + }
57 } 145 }
58 146
59 class _FakeHealthRawDataSource implements HealthRawDataSource { 147 class _FakeHealthRawDataSource implements HealthRawDataSource {
@@ -10,7 +10,7 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; @@ -10,7 +10,7 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
10 import 'package:flutter_test/flutter_test.dart'; 10 import 'package:flutter_test/flutter_test.dart';
11 11
12 void main() { 12 void main() {
13 - test('syncRawData starts from latest local data time and stores each page', 13 + test('syncRawData starts from latest local data time and stores fetched data',
14 () async { 14 () async {
15 final remote = _FakeOhosHealthRawDataRemoteDataSource([ 15 final remote = _FakeOhosHealthRawDataRemoteDataSource([
16 const OhosHealthRawDataPage( 16 const OhosHealthRawDataPage(
@@ -20,11 +20,6 @@ void main() { @@ -20,11 +20,6 @@ void main() {
20 dataTime: 1001, 20 dataTime: 1001,
21 payload: {'value': 70}, 21 payload: {'value': 70},
22 ), 22 ),
23 - ],  
24 - nextPageToken: 'next',  
25 - ),  
26 - const OhosHealthRawDataPage(  
27 - items: [  
28 OhosHealthRawDataItem( 23 OhosHealthRawDataItem(
29 dataType: 2, 24 dataType: 2,
30 dataTime: 1002, 25 dataTime: 1002,
@@ -46,11 +41,10 @@ void main() { @@ -46,11 +41,10 @@ void main() {
46 expect(result.startTime, expectedStart); 41 expect(result.startTime, expectedStart);
47 expect(result.endTime, 2000); 42 expect(result.endTime, 2000);
48 expect(result.segmentCount, 1); 43 expect(result.segmentCount, 1);
49 - expect(result.pageCount, 2); 44 + expect(result.pageCount, 1);
50 expect(result.storedCount, 2); 45 expect(result.storedCount, 2);
51 - expect(remote.calls.map((e) => e.pageToken), [null, 'next']);  
52 expect(remote.calls.every((e) => e.startTime == expectedStart), isTrue); 46 expect(remote.calls.every((e) => e.startTime == expectedStart), isTrue);
53 - expect(local.storedBatches.map((e) => e.length), [1, 1]); 47 + expect(local.storedBatches.map((e) => e.length), [2]);
54 }); 48 });
55 49
56 test('syncRawData falls back to half-year lookback when local data is empty', 50 test('syncRawData falls back to half-year lookback when local data is empty',
@@ -212,6 +206,38 @@ void main() { @@ -212,6 +206,38 @@ void main() {
212 ); 206 );
213 }); 207 });
214 208
  209 + test('syncRawData fetches split ranges concurrently before storing',
  210 + () async {
  211 + final start = _unixSeconds(DateTime(2026, 1, 30, 10));
  212 + final end = _unixSeconds(DateTime(2026, 2, 22, 9));
  213 + final gate = Completer<void>();
  214 + final remote = _FakeOhosHealthRawDataRemoteDataSource(
  215 + const <OhosHealthRawDataPage>[],
  216 + gate: gate,
  217 + );
  218 + final local = _FakeOhosHealthRawDataLocalStore();
  219 + final service = OhosHealthRawDataSyncService(
  220 + remoteDataSource: remote,
  221 + localStore: local,
  222 + );
  223 +
  224 + final sync = service.syncRawData(
  225 + dataType: HuaweiHealthDataType.heartRate.dataType,
  226 + startTime: start,
  227 + endTime: end,
  228 + );
  229 + await Future<void>.delayed(Duration.zero);
  230 +
  231 + expect(remote.calls, hasLength(3));
  232 + expect(local.storedBatches, isEmpty);
  233 +
  234 + gate.complete();
  235 + final result = await sync;
  236 +
  237 + expect(result.segmentCount, 3);
  238 + expect(result.pageCount, 3);
  239 + });
  240 +
215 test('syncRawData joins duplicate in-flight syncs', () async { 241 test('syncRawData joins duplicate in-flight syncs', () async {
216 final gate = Completer<void>(); 242 final gate = Completer<void>();
217 final remote = _FakeOhosHealthRawDataRemoteDataSource( 243 final remote = _FakeOhosHealthRawDataRemoteDataSource(
@@ -318,6 +344,81 @@ void main() { @@ -318,6 +344,81 @@ void main() {
318 expect(logs, contains(contains('calculation_sync_finish'))); 344 expect(logs, contains(contains('calculation_sync_finish')));
319 }); 345 });
320 346
  347 + test('syncCalculationRawData fetches data types concurrently', () async {
  348 + final gate = Completer<void>();
  349 + final remote = _FakeOhosHealthRawDataRemoteDataSource(
  350 + const <OhosHealthRawDataPage>[],
  351 + gate: gate,
  352 + );
  353 + final local = _FakeOhosHealthRawDataLocalStore();
  354 + final service = OhosHealthRawDataSyncService(
  355 + remoteDataSource: remote,
  356 + localStore: local,
  357 + );
  358 +
  359 + final sync = service.syncCalculationRawData(
  360 + startTime: 1000,
  361 + endTime: 2000,
  362 + dataTypes: const <int>[1, 2, OhosHealthRawDataType.sleepAnalysis],
  363 + );
  364 + await Future<void>.delayed(Duration.zero);
  365 +
  366 + expect(remote.calls.map((call) => call.dataType).toSet(), {
  367 + 1,
  368 + 2,
  369 + OhosHealthRawDataType.sleepAnalysis,
  370 + });
  371 + expect(local.storedBatches, isEmpty);
  372 +
  373 + gate.complete();
  374 + final results = await sync;
  375 +
  376 + expect(results.map((result) => result.dataType), [
  377 + 1,
  378 + 2,
  379 + OhosHealthRawDataType.sleepAnalysis,
  380 + ]);
  381 + });
  382 +
  383 + test(
  384 + 'syncCalculationRawData does not store raw data when activity goal fails',
  385 + () async {
  386 + final remote = _FakeOhosHealthRawDataRemoteDataSource(
  387 + const <OhosHealthRawDataPage>[
  388 + OhosHealthRawDataPage(
  389 + items: [
  390 + OhosHealthRawDataItem(
  391 + dataType: 1,
  392 + dataTime: 1001,
  393 + payload: {'value': 45},
  394 + ),
  395 + ],
  396 + ),
  397 + ],
  398 + failingActivityGoal: true,
  399 + );
  400 + final local = _FakeOhosHealthRawDataLocalStore();
  401 + final logs = <String>[];
  402 + final service = OhosHealthRawDataSyncService(
  403 + remoteDataSource: remote,
  404 + localStore: local,
  405 + logSink: logs.add,
  406 + );
  407 +
  408 + await expectLater(
  409 + service.syncCalculationRawData(
  410 + startTime: 1000,
  411 + endTime: 2000,
  412 + dataTypes: const <int>[1],
  413 + ),
  414 + throwsA(isA<StateError>()),
  415 + );
  416 +
  417 + expect(local.storedBatches, isEmpty);
  418 + expect(local.activityGoal, isNull);
  419 + expect(logs, contains(contains('calculation_sync_failed')));
  420 + });
  421 +
321 test('Harmony remote data source calls getHealthData and converts items', 422 test('Harmony remote data source calls getHealthData and converts items',
322 () async { 423 () async {
323 final client = _FakeOhosHarmonyRawDataClient( 424 final client = _FakeOhosHarmonyRawDataClient(
@@ -350,7 +451,6 @@ void main() { @@ -350,7 +451,6 @@ void main() {
350 expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv); 451 expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv);
351 expect(client.healthCalls.single.startDate, 20260801); 452 expect(client.healthCalls.single.startDate, 20260801);
352 expect(client.healthCalls.single.endDate, 20260831); 453 expect(client.healthCalls.single.endDate, 20260831);
353 - expect(page.nextPageToken, isNull);  
354 expect(page.items, hasLength(1)); 454 expect(page.items, hasLength(1));
355 expect(page.items.single.dataType, 1); 455 expect(page.items.single.dataType, 1);
356 expect(page.items.single.dataTime, 1787900000); 456 expect(page.items.single.dataTime, 1787900000);
@@ -470,14 +570,17 @@ class _FakeOhosHealthRawDataRemoteDataSource @@ -470,14 +570,17 @@ class _FakeOhosHealthRawDataRemoteDataSource
470 Completer<void>? gate, 570 Completer<void>? gate,
471 Set<int> failingDataTypes = const <int>{}, 571 Set<int> failingDataTypes = const <int>{},
472 V2ActivityTarget? activityGoal, 572 V2ActivityTarget? activityGoal,
  573 + bool failingActivityGoal = false,
473 }) : _gate = gate, 574 }) : _gate = gate,
474 _failingDataTypes = failingDataTypes, 575 _failingDataTypes = failingDataTypes,
475 - _activityGoal = activityGoal; 576 + _activityGoal = activityGoal,
  577 + _failingActivityGoal = failingActivityGoal;
476 578
477 final List<OhosHealthRawDataPage> _pages; 579 final List<OhosHealthRawDataPage> _pages;
478 final Completer<void>? _gate; 580 final Completer<void>? _gate;
479 final Set<int> _failingDataTypes; 581 final Set<int> _failingDataTypes;
480 final V2ActivityTarget? _activityGoal; 582 final V2ActivityTarget? _activityGoal;
  583 + final bool _failingActivityGoal;
481 final List<_FetchCall> calls = <_FetchCall>[]; 584 final List<_FetchCall> calls = <_FetchCall>[];
482 585
483 @override 586 @override
@@ -485,14 +588,12 @@ class _FakeOhosHealthRawDataRemoteDataSource @@ -485,14 +588,12 @@ class _FakeOhosHealthRawDataRemoteDataSource
485 required int dataType, 588 required int dataType,
486 required int startTime, 589 required int startTime,
487 required int endTime, 590 required int endTime,
488 - String? pageToken,  
489 }) async { 591 }) async {
490 calls.add( 592 calls.add(
491 _FetchCall( 593 _FetchCall(
492 dataType: dataType, 594 dataType: dataType,
493 startTime: startTime, 595 startTime: startTime,
494 endTime: endTime, 596 endTime: endTime,
495 - pageToken: pageToken,  
496 ), 597 ),
497 ); 598 );
498 if (_gate != null) { 599 if (_gate != null) {
@@ -510,6 +611,9 @@ class _FakeOhosHealthRawDataRemoteDataSource @@ -510,6 +611,9 @@ class _FakeOhosHealthRawDataRemoteDataSource
510 611
511 @override 612 @override
512 Future<V2ActivityTarget?> fetchActivityGoal() async { 613 Future<V2ActivityTarget?> fetchActivityGoal() async {
  614 + if (_failingActivityGoal) {
  615 + throw StateError('failed activity goal');
  616 + }
513 return _activityGoal; 617 return _activityGoal;
514 } 618 }
515 } 619 }
@@ -570,13 +674,11 @@ class _FetchCall { @@ -570,13 +674,11 @@ class _FetchCall {
570 required this.dataType, 674 required this.dataType,
571 required this.startTime, 675 required this.startTime,
572 required this.endTime, 676 required this.endTime,
573 - required this.pageToken,  
574 }); 677 });
575 678
576 final int dataType; 679 final int dataType;
577 final int startTime; 680 final int startTime;
578 final int endTime; 681 final int endTime;
579 - final String? pageToken;  
580 } 682 }
581 683
582 class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient { 684 class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient {