Commit 85d12a8b6cf69944179e5ebde7eba9078b938372

Authored by 权海
1 parent 2a94da76

feat(ui):修复长时间不回调,触发计算时推送多条通知

@@ -64,7 +64,7 @@ class HealthRawLocalNotificationBuilder { @@ -64,7 +64,7 @@ class HealthRawLocalNotificationBuilder {
64 realtimeSleepIntervals.isEmpty && latestRealtimeStressPoint != null 64 realtimeSleepIntervals.isEmpty && latestRealtimeStressPoint != null
65 ? [_defaultSleepInterval(latestRealtimeStressPoint.rawEndTime)] 65 ? [_defaultSleepInterval(latestRealtimeStressPoint.rawEndTime)]
66 : realtimeSleepIntervals; 66 : realtimeSleepIntervals;
67 - return [ 67 + return _latestNotificationsByRecordType([
68 if (_sleepNotification(result.sleepResults, hasExistingSleep) 68 if (_sleepNotification(result.sleepResults, hasExistingSleep)
69 case final notification?) 69 case final notification?)
70 notification, 70 notification,
@@ -79,7 +79,22 @@ class HealthRawLocalNotificationBuilder { @@ -79,7 +79,22 @@ class HealthRawLocalNotificationBuilder {
79 ) 79 )
80 case final notification?) 80 case final notification?)
81 notification, 81 notification,
82 - ]; 82 + ]);
  83 + }
  84 +
  85 + List<HealthRawLocalNotification> _latestNotificationsByRecordType(
  86 + List<HealthRawLocalNotification> notifications,
  87 + ) {
  88 + if (notifications.length < 2) return notifications;
  89 + final latestByType =
  90 + <HealthRawLocalNotificationRecordType, HealthRawLocalNotification>{};
  91 + for (final notification in notifications) {
  92 + final existing = latestByType[notification.recordType];
  93 + if (existing == null || notification.recordTime > existing.recordTime) {
  94 + latestByType[notification.recordType] = notification;
  95 + }
  96 + }
  97 + return latestByType.values.toList(growable: false);
83 } 98 }
84 99
85 HealthRawLocalNotification? _sleepNotification( 100 HealthRawLocalNotification? _sleepNotification(
@@ -694,7 +694,7 @@ void main() { @@ -694,7 +694,7 @@ void main() {
694 }); 694 });
695 695
696 test( 696 test(
697 - 'startCoreCaculate sends hrv and sleep notifications for new non-first results', 697 + 'startCoreCaculate sends only latest hrv notification for new non-first results',
698 () async { 698 () async {
699 final now = DateTime.now(); 699 final now = DateTime.now();
700 final day = DateTime(now.year, now.month, now.day); 700 final day = DateTime(now.year, now.month, now.day);
@@ -770,11 +770,11 @@ void main() { @@ -770,11 +770,11 @@ void main() {
770 readChunkDays: 1, 770 readChunkDays: 1,
771 ); 771 );
772 772
773 - expect(  
774 - notificationDispatcher.sentNotifications.where(  
775 - (e) => e.recordType == HealthRawLocalNotificationRecordType.hrv),  
776 - hasLength(3),  
777 - ); 773 + final hrvNotifications = notificationDispatcher.sentNotifications
  774 + .where((e) => e.recordType == HealthRawLocalNotificationRecordType.hrv)
  775 + .toList();
  776 + expect(hrvNotifications, hasLength(1));
  777 + expect(hrvNotifications.single.recordTime, base + 720);
778 expect( 778 expect(
779 notificationDispatcher.sentNotifications.where( 779 notificationDispatcher.sentNotifications.where(
780 (e) => e.recordType == HealthRawLocalNotificationRecordType.sleep, 780 (e) => e.recordType == HealthRawLocalNotificationRecordType.sleep,
@@ -792,12 +792,17 @@ void main() { @@ -792,12 +792,17 @@ void main() {
792 ); 792 );
793 expect( 793 expect(
794 rows.hrvRows 794 rows.hrvRows
795 - .where((row) => [base + 120, base + 420, base + 720]  
796 - .contains(row['raw_end_time']))  
797 - .every((row) => row['push_send_time'] != null), 795 + .where(
  796 + (row) => [base + 120, base + 420].contains(row['raw_end_time']))
  797 + .every((row) => row['push_send_time'] == null),
798 isTrue, 798 isTrue,
799 ); 799 );
800 expect( 800 expect(
  801 + rows.hrvRows.singleWhere(
  802 + (row) => row['raw_end_time'] == base + 720)['push_send_time'],
  803 + isNotNull,
  804 + );
  805 + expect(
801 rows.sleepRows.singleWhere( 806 rows.sleepRows.singleWhere(
802 (row) => row['date'] == LocalHealthDataConvert.unixSeconds(sleepEnd), 807 (row) => row['date'] == LocalHealthDataConvert.unixSeconds(sleepEnd),
803 )['push_send_time'], 808 )['push_send_time'],
@@ -153,8 +153,7 @@ void main() { @@ -153,8 +153,7 @@ void main() {
153 expect(notifications.single.recordTime, latestTime); 153 expect(notifications.single.recordTime, latestTime);
154 }); 154 });
155 155
156 - test('builds hrv notifications for every candidate including sleep likely',  
157 - () { 156 + test('builds only the latest hrv notification for multiple candidates', () {
158 final firstTime = _seconds(DateTime(2026, 1, 1, 9)); 157 final firstTime = _seconds(DateTime(2026, 1, 1, 9));
159 final secondTime = _seconds(DateTime(2026, 1, 1, 10)); 158 final secondTime = _seconds(DateTime(2026, 1, 1, 10));
160 final notifications = builder.build( 159 final notifications = builder.build(
@@ -173,8 +172,55 @@ void main() { @@ -173,8 +172,55 @@ void main() {
173 record: const HealthRawLocalNotificationRecord(), 172 record: const HealthRawLocalNotificationRecord(),
174 ); 173 );
175 174
  175 + expect(notifications, hasLength(1));
  176 + expect(notifications.single.recordType,
  177 + HealthRawLocalNotificationRecordType.hrv);
  178 + expect(notifications.single.recordTime, secondTime);
  179 + });
  180 +
  181 + test('keeps the latest notification per type without dropping other types',
  182 + () {
  183 + final hrvFirstTime = _seconds(DateTime(2026, 1, 1, 9));
  184 + final hrvSecondTime = _seconds(DateTime(2026, 1, 1, 10));
  185 + final realtimeBase = _seconds(DateTime(2026, 1, 1, 11));
  186 + final notifications = builder.build(
  187 + result: HealthRawStressCalculationResult(
  188 + userId: 1,
  189 + hrvStressPoints: [
  190 + _hrvPoint(hrvFirstTime),
  191 + _hrvPoint(hrvSecondTime),
  192 + ],
  193 + realtimeStressPoints: [
  194 + _realtimePoint(realtimeBase + 9 * 300, 70),
  195 + ],
  196 + dailyStressPoints: const [],
  197 + ),
  198 + hasExistingHrv: true,
  199 + hasExistingSleep: false,
  200 + realtimeWindow: [
  201 + for (var i = 0; i < 10; i++) _realtimePoint(realtimeBase + i * 300, 70),
  202 + ],
  203 + record: const HealthRawLocalNotificationRecord(),
  204 + );
  205 +
176 expect(notifications, hasLength(2)); 206 expect(notifications, hasLength(2));
177 - expect(notifications.map((e) => e.recordTime), [firstTime, secondTime]); 207 + expect(
  208 + notifications
  209 + .where(
  210 + (e) => e.recordType == HealthRawLocalNotificationRecordType.hrv)
  211 + .single
  212 + .recordTime,
  213 + hrvSecondTime,
  214 + );
  215 + expect(
  216 + notifications
  217 + .where((e) =>
  218 + e.recordType ==
  219 + HealthRawLocalNotificationRecordType.realtimeStress)
  220 + .single
  221 + .recordTime,
  222 + realtimeBase + 9 * 300,
  223 + );
178 }); 224 });
179 225
180 test('builds realtime stress notification from latest 60 minute window', () { 226 test('builds realtime stress notification from latest 60 minute window', () {