Commit 5e4f526fd52fb881630555f4c15d16ed053b99f1

Authored by 权海
1 parent 0df5140e

feat(ui):修复hr实时压力推送抛异常导致后续代码不执行

@@ -404,6 +404,87 @@ class HealthRawDataCoreService { @@ -404,6 +404,87 @@ class HealthRawDataCoreService {
404 result.sleepResults.isEmpty) { 404 result.sleepResults.isEmpty) {
405 return; 405 return;
406 } 406 }
  407 + final record = await _readLocalNotificationRecord(result.userId);
  408 + final notifications = <HealthRawLocalNotification>[
  409 + ..._buildSleepNotificationsSafely(result, record),
  410 + ..._buildHrvNotificationsSafely(
  411 + result: result,
  412 + hasExistingHrv: hasExistingHrv,
  413 + record: record,
  414 + ),
  415 + ...await _buildRealtimeStressNotificationsSafely(result, record),
  416 + ];
  417 + for (final notification in notifications) {
  418 + final sent = await _sendLocalNotificationSafely(
  419 + userId: result.userId,
  420 + notification: notification,
  421 + );
  422 + if (sent &&
  423 + notification.recordType ==
  424 + HealthRawLocalNotificationRecordType.realtimeStress) {
  425 + _scheduleRealtimeStressServerPush([notification]);
  426 + }
  427 + }
  428 + }
  429 +
  430 + Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecord(
  431 + int userId,
  432 + ) async {
  433 + try {
  434 + return await _localNotificationDispatcher.readRecord(userId);
  435 + } catch (error, stackTrace) {
  436 + _logError(
  437 + 'read local health notification record failed', error, stackTrace);
  438 + return const HealthRawLocalNotificationRecord();
  439 + }
  440 + }
  441 +
  442 + List<HealthRawLocalNotification> _buildSleepNotificationsSafely(
  443 + HealthRawStressCalculationResult result,
  444 + HealthRawLocalNotificationRecord record,
  445 + ) {
  446 + try {
  447 + return HealthRawLocalNotificationBuilder(l10n).build(
  448 + result: result.copyWith(
  449 + hrvStressPoints: const <HealthRawHrvStressPoint>[],
  450 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  451 + ),
  452 + hasExistingHrv: false,
  453 + realtimeWindow: const <HealthRawRealtimeStressPoint>[],
  454 + record: record,
  455 + );
  456 + } catch (error, stackTrace) {
  457 + _logError('build sleep local notification failed', error, stackTrace);
  458 + return const <HealthRawLocalNotification>[];
  459 + }
  460 + }
  461 +
  462 + List<HealthRawLocalNotification> _buildHrvNotificationsSafely({
  463 + required HealthRawStressCalculationResult result,
  464 + required bool hasExistingHrv,
  465 + required HealthRawLocalNotificationRecord record,
  466 + }) {
  467 + try {
  468 + return HealthRawLocalNotificationBuilder(l10n).build(
  469 + result: result.copyWith(
  470 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  471 + sleepResults: const <HealthRawSleepResult>[],
  472 + ),
  473 + hasExistingHrv: hasExistingHrv,
  474 + realtimeWindow: const <HealthRawRealtimeStressPoint>[],
  475 + record: record,
  476 + );
  477 + } catch (error, stackTrace) {
  478 + _logError('build hrv local notification failed', error, stackTrace);
  479 + return const <HealthRawLocalNotification>[];
  480 + }
  481 + }
  482 +
  483 + Future<List<HealthRawLocalNotification>>
  484 + _buildRealtimeStressNotificationsSafely(
  485 + HealthRawStressCalculationResult result,
  486 + HealthRawLocalNotificationRecord record,
  487 + ) async {
407 try { 488 try {
408 final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty 489 final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty
409 ? null 490 ? null
@@ -419,23 +500,41 @@ class HealthRawDataCoreService { @@ -419,23 +500,41 @@ class HealthRawDataCoreService {
419 1, 500 1,
420 endTime: latestRealtimeStressPoint.rawEndTime, 501 endTime: latestRealtimeStressPoint.rawEndTime,
421 ); 502 );
422 - final record = await _localNotificationDispatcher.readRecord(  
423 - result.userId,  
424 - );  
425 - final notifications = HealthRawLocalNotificationBuilder(l10n).build(  
426 - result: result,  
427 - hasExistingHrv: hasExistingHrv, 503 + return HealthRawLocalNotificationBuilder(l10n).build(
  504 + result: result.copyWith(
  505 + hrvStressPoints: const <HealthRawHrvStressPoint>[],
  506 + sleepResults: const <HealthRawSleepResult>[],
  507 + ),
  508 + hasExistingHrv: false,
428 realtimeWindow: realtimeWindow, 509 realtimeWindow: realtimeWindow,
429 record: record, 510 record: record,
430 ); 511 );
431 - if (notifications.isEmpty) return;  
432 - final sentNotifications = await _localNotificationDispatcher.sendAll(  
433 - userId: result.userId,  
434 - notifications: notifications, 512 + } catch (error, stackTrace) {
  513 + _logError(
  514 + 'build realtime stress local notification failed',
  515 + error,
  516 + stackTrace,
  517 + );
  518 + return const <HealthRawLocalNotification>[];
  519 + }
  520 + }
  521 +
  522 + Future<bool> _sendLocalNotificationSafely({
  523 + required int userId,
  524 + required HealthRawLocalNotification notification,
  525 + }) async {
  526 + try {
  527 + return await _localNotificationDispatcher.sendOne(
  528 + userId: userId,
  529 + notification: notification,
435 ); 530 );
436 - _scheduleRealtimeStressServerPush(sentNotifications);  
437 } catch (error, stackTrace) { 531 } catch (error, stackTrace) {
438 - _logError('send local health notifications failed', error, stackTrace); 532 + _logError(
  533 + 'send ${notification.recordType.name} local notification failed',
  534 + error,
  535 + stackTrace,
  536 + );
  537 + return false;
439 } 538 }
440 } 539 }
441 540
@@ -219,22 +219,30 @@ class HealthRawLocalNotificationDispatcher { @@ -219,22 +219,30 @@ class HealthRawLocalNotificationDispatcher {
219 final PlatformHostApi _platformApi; 219 final PlatformHostApi _platformApi;
220 final HealthRawLocalNotificationRecordStore _recordStore; 220 final HealthRawLocalNotificationRecordStore _recordStore;
221 221
  222 + Future<bool> sendOne({
  223 + required int userId,
  224 + required HealthRawLocalNotification notification,
  225 + }) async {
  226 + final sent = await _platformApi.sendLocalNotification(
  227 + notification.title,
  228 + notification.content,
  229 + notification.link,
  230 + );
  231 + if (!sent) return false;
  232 + final record = await _recordStore.read(userId);
  233 + await _recordStore.write(userId, record.withNotification(notification));
  234 + return true;
  235 + }
  236 +
222 Future<List<HealthRawLocalNotification>> sendAll({ 237 Future<List<HealthRawLocalNotification>> sendAll({
223 required int userId, 238 required int userId,
224 required Iterable<HealthRawLocalNotification> notifications, 239 required Iterable<HealthRawLocalNotification> notifications,
225 }) async { 240 }) async {
226 - var record = await _recordStore.read(userId);  
227 final sentNotifications = <HealthRawLocalNotification>[]; 241 final sentNotifications = <HealthRawLocalNotification>[];
228 for (final notification in notifications) { 242 for (final notification in notifications) {
229 - final sent = await _platformApi.sendLocalNotification(  
230 - notification.title,  
231 - notification.content,  
232 - notification.link,  
233 - );  
234 - if (!sent) continue;  
235 - sentNotifications.add(notification);  
236 - record = record.withNotification(notification);  
237 - await _recordStore.write(userId, record); 243 + if (await sendOne(userId: userId, notification: notification)) {
  244 + sentNotifications.add(notification);
  245 + }
238 } 246 }
239 return sentNotifications; 247 return sentNotifications;
240 } 248 }
@@ -2,6 +2,7 @@ import 'dart:async'; @@ -2,6 +2,7 @@ import 'dart:async';
2 2
3 import 'package:doublefeel_flutter/core/result/app_result.dart'; 3 import 'package:doublefeel_flutter/core/result/app_result.dart';
4 import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 4 import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
  5 +import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart';
5 import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart'; 6 import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
6 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; 7 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
7 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 8 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
@@ -642,6 +643,108 @@ void main() { @@ -642,6 +643,108 @@ void main() {
642 expect(rows.realtimeRows.single['uploaded'], 1); 643 expect(rows.realtimeRows.single['uploaded'], 1);
643 }); 644 });
644 645
  646 + test('hrv notification still sends when realtime window query fails',
  647 + () async {
  648 + final base =
  649 + DateTime.now().millisecondsSinceEpoch ~/ 1000 - Duration.secondsPerDay;
  650 + final api = _FakeHealthKitRawDataHostApi();
  651 + api.setPoints(HealthDataUploadType.hrv.type, [
  652 + _point(base + 60, 30),
  653 + _point(base + 360, 34),
  654 + ]);
  655 + api.setPoints(HealthDataUploadType.heartRate.type, [
  656 + for (var i = 0; i < 12; i++) _point(base + 60 + i * 60, 70),
  657 + ]);
  658 + api.setPoints(HealthDataUploadType.restingHeartRate.type, [
  659 + _point(base + 30, 60),
  660 + ]);
  661 + final store = _MemoryHealthRawStressLocalStore()
  662 + ..throwOnRealtimeNotificationWindowQuery = true;
  663 + await store.upsertResult(
  664 + HealthRawStressCalculationResult(
  665 + userId: 42,
  666 + hrvStressPoints: [_hrvStressPoint(base + 60)],
  667 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  668 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  669 + ),
  670 + );
  671 + final notificationDispatcher = _FakeLocalNotificationDispatcher();
  672 + final service = HealthRawDataCoreService(
  673 + healthApi: _FakeHealthKitHostApi(),
  674 + rawDataApi: api,
  675 + localStore: store,
  676 + userIdProvider: () => 42,
  677 + uploadResultsAfterCalculation: false,
  678 + localNotificationDispatcher: notificationDispatcher,
  679 + );
  680 +
  681 + await service.syncAndStore(
  682 + startTime: base,
  683 + endTime: base + 900,
  684 + readChunkDays: 1,
  685 + );
  686 +
  687 + expect(
  688 + notificationDispatcher.sentRecordTypes,
  689 + contains(HealthRawLocalNotificationRecordType.hrv),
  690 + );
  691 + });
  692 +
  693 + test('sleep notification still sends after hrv send fails', () async {
  694 + final base =
  695 + DateTime.now().millisecondsSinceEpoch ~/ 1000 - Duration.secondsPerDay;
  696 + final sleepStart = base + 60;
  697 + final sleepEnd = sleepStart + const Duration(hours: 7).inSeconds;
  698 + final api = _FakeHealthKitRawDataHostApi();
  699 + api.setPoints(HealthDataUploadType.hrv.type, [
  700 + _point(base + 60, 30),
  701 + _point(base + 360, 34),
  702 + ]);
  703 + api.setPoints(HealthDataUploadType.heartRate.type, [
  704 + for (var i = 0; i < 12; i++) _point(base + 60 + i * 60, 70),
  705 + ]);
  706 + api.setPoints(HealthDataUploadType.restingHeartRate.type, [
  707 + _point(base + 30, 60),
  708 + ]);
  709 + api.setSleepPoints([
  710 + HealthKitRawDataPoint(
  711 + dataType: 3,
  712 + startTime: sleepStart,
  713 + endTime: sleepEnd,
  714 + ),
  715 + ]);
  716 + final store = _MemoryHealthRawStressLocalStore();
  717 + await store.upsertResult(
  718 + HealthRawStressCalculationResult(
  719 + userId: 42,
  720 + hrvStressPoints: [_hrvStressPoint(base + 60)],
  721 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  722 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  723 + ),
  724 + );
  725 + final notificationDispatcher = _FakeLocalNotificationDispatcher()
  726 + ..throwOnRecordTypes.add(HealthRawLocalNotificationRecordType.hrv);
  727 + final service = HealthRawDataCoreService(
  728 + healthApi: _FakeHealthKitHostApi(),
  729 + rawDataApi: api,
  730 + localStore: store,
  731 + userIdProvider: () => 42,
  732 + uploadResultsAfterCalculation: false,
  733 + localNotificationDispatcher: notificationDispatcher,
  734 + );
  735 +
  736 + await service.syncAndStore(
  737 + startTime: base,
  738 + endTime: sleepEnd + 60,
  739 + readChunkDays: 1,
  740 + );
  741 +
  742 + expect(
  743 + notificationDispatcher.sentRecordTypes,
  744 + contains(HealthRawLocalNotificationRecordType.sleep),
  745 + );
  746 + });
  747 +
645 test('local data source returns earliest local hr start time', () async { 748 test('local data source returns earliest local hr start time', () async {
646 const base = 1800000000; 749 const base = 1800000000;
647 final api = _FakeHealthKitRawDataHostApi(); 750 final api = _FakeHealthKitRawDataHostApi();
@@ -852,6 +955,31 @@ class _FakeHealthKitHostApi extends HealthKitHostApi { @@ -852,6 +955,31 @@ class _FakeHealthKitHostApi extends HealthKitHostApi {
852 } 955 }
853 } 956 }
854 957
  958 +class _FakeLocalNotificationDispatcher
  959 + extends HealthRawLocalNotificationDispatcher {
  960 + final sentRecordTypes = <HealthRawLocalNotificationRecordType>[];
  961 + final throwOnRecordTypes = <HealthRawLocalNotificationRecordType>{};
  962 + var record = const HealthRawLocalNotificationRecord();
  963 +
  964 + @override
  965 + Future<HealthRawLocalNotificationRecord> readRecord(int userId) async {
  966 + return record;
  967 + }
  968 +
  969 + @override
  970 + Future<bool> sendOne({
  971 + required int userId,
  972 + required HealthRawLocalNotification notification,
  973 + }) async {
  974 + if (throwOnRecordTypes.contains(notification.recordType)) {
  975 + throw StateError('send ${notification.recordType.name} failed');
  976 + }
  977 + sentRecordTypes.add(notification.recordType);
  978 + record = record.withNotification(notification);
  979 + return true;
  980 + }
  981 +}
  982 +
855 class _ReadCall { 983 class _ReadCall {
856 const _ReadCall(this.dataType, this.startTime, this.endTime); 984 const _ReadCall(this.dataType, this.startTime, this.endTime);
857 985
@@ -869,6 +997,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -869,6 +997,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
869 final Map<int, List<HealthRawDailyStressPoint>> _daily = {}; 997 final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
870 final Map<int, List<HealthRawSleepResult>> _sleep = {}; 998 final Map<int, List<HealthRawSleepResult>> _sleep = {};
871 final List<String> operationLog; 999 final List<String> operationLog;
  1000 + var throwOnRealtimeNotificationWindowQuery = false;
872 1001
873 void insertDailyStress(HealthRawDailyStressPoint point) { 1002 void insertDailyStress(HealthRawDailyStressPoint point) {
874 final byDate = <int, HealthRawDailyStressPoint>{ 1003 final byDate = <int, HealthRawDailyStressPoint>{
@@ -1039,6 +1168,10 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1039,6 +1168,10 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1039 required int endTime, 1168 required int endTime,
1040 }) async { 1169 }) async {
1041 operationLog.add('queryRealtimeStressPoints'); 1170 operationLog.add('queryRealtimeStressPoints');
  1171 + if (throwOnRealtimeNotificationWindowQuery &&
  1172 + endTime - startTime <= Duration.secondsPerHour) {
  1173 + throw StateError('realtime notification window query failed');
  1174 + }
1042 return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[]) 1175 return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
1043 .where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime) 1176 .where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime)
1044 .toList(); 1177 .toList();