Commit 2ad10b3d6a83300c66d512b566b8359e7d423fbd

Authored by 权海
1 parent 570248cd

feat(ui):重新调整代码

@@ -404,41 +404,114 @@ class HealthRawDataCoreService { @@ -404,41 +404,114 @@ class HealthRawDataCoreService {
404 result.sleepResults.isEmpty) { 404 result.sleepResults.isEmpty) {
405 return; 405 return;
406 } 406 }
  407 +
  408 + final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty
  409 + ? null
  410 + : result.realtimeStressPoints.reduce(
  411 + (a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
  412 + );
  413 + var notificationResult = result;
  414 + var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
  415 + if (latestRealtimeStressPoint != null) {
  416 + final shouldResetRealtimeStressPushTime =
  417 + latestRealtimeStressPoint.isWorkout ||
  418 + latestRealtimeStressPoint.isWorkoutRecovery;
  419 + if (shouldResetRealtimeStressPushTime) {
  420 + await _recordRealtimeStressTimeSafely(
  421 + userId: result.userId,
  422 + recordTime: latestRealtimeStressPoint.rawEndTime,
  423 + );
  424 + notificationResult = result.copyWith(
  425 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  426 + );
  427 + } else {
  428 + realtimeWindow = await _queryRealtimeStressNotificationWindowSafely(
  429 + userId: result.userId,
  430 + latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
  431 + );
  432 + }
  433 + }
  434 +
  435 + final record = await _readLocalNotificationRecordSafely(result.userId);
  436 + final notifications = HealthRawLocalNotificationBuilder(l10n).build(
  437 + result: notificationResult,
  438 + hasExistingHrv: hasExistingHrv,
  439 + realtimeWindow: realtimeWindow,
  440 + record: record,
  441 + );
  442 + if (notifications.isEmpty) return;
  443 +
  444 + final sentNotifications = <HealthRawLocalNotification>[];
  445 + for (final notification in notifications) {
  446 + final sent = await _sendLocalNotificationSafely(
  447 + userId: result.userId,
  448 + notification: notification,
  449 + );
  450 + if (sent) {
  451 + sentNotifications.add(notification);
  452 + }
  453 + }
  454 + _scheduleRealtimeStressServerPush(sentNotifications);
  455 + }
  456 +
  457 + Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecordSafely(
  458 + int userId,
  459 + ) async {
  460 + try {
  461 + return await _localNotificationDispatcher.readRecord(userId);
  462 + } catch (error, stackTrace) {
  463 + _logError('read local notification record failed', error, stackTrace);
  464 + return const HealthRawLocalNotificationRecord();
  465 + }
  466 + }
  467 +
  468 + Future<void> _recordRealtimeStressTimeSafely({
  469 + required int userId,
  470 + required int recordTime,
  471 + }) async {
407 try { 472 try {
408 - final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty  
409 - ? null  
410 - : result.realtimeStressPoints.reduce(  
411 - (a, b) => a.rawEndTime >= b.rawEndTime ? a : b,  
412 - );  
413 - final shouldSkipRealtimeStressNotification =  
414 - latestRealtimeStressPoint?.isWorkout ?? false;  
415 - final realtimeWindow = latestRealtimeStressPoint == null ||  
416 - shouldSkipRealtimeStressNotification  
417 - ? const <HealthRawRealtimeStressPoint>[]  
418 - : await _localStore.queryRealtimeStressPoints(  
419 - userId: result.userId,  
420 - startTime: latestRealtimeStressPoint.rawEndTime -  
421 - Duration.secondsPerHour +  
422 - 1,  
423 - endTime: latestRealtimeStressPoint.rawEndTime,  
424 - );  
425 - final record = await _localNotificationDispatcher.readRecord(  
426 - result.userId, 473 + await _localNotificationDispatcher.recordRealtimeStressTime(
  474 + userId: userId,
  475 + recordTime: recordTime,
427 ); 476 );
428 - final notifications = HealthRawLocalNotificationBuilder(l10n).build(  
429 - result: result,  
430 - hasExistingHrv: hasExistingHrv,  
431 - realtimeWindow: realtimeWindow,  
432 - record: record, 477 + } catch (error, stackTrace) {
  478 + _logError('record realtime stress push time failed', error, stackTrace);
  479 + }
  480 + }
  481 +
  482 + Future<List<HealthRawRealtimeStressPoint>>
  483 + _queryRealtimeStressNotificationWindowSafely({
  484 + required int userId,
  485 + required int latestRawEndTime,
  486 + }) async {
  487 + try {
  488 + return await _localStore.queryRealtimeStressPoints(
  489 + userId: userId,
  490 + startTime: latestRawEndTime - Duration.secondsPerHour + 1,
  491 + endTime: latestRawEndTime,
433 ); 492 );
434 - if (notifications.isEmpty) return;  
435 - final sentNotifications = await _localNotificationDispatcher.sendAll(  
436 - userId: result.userId,  
437 - notifications: notifications, 493 + } catch (error, stackTrace) {
  494 + _logError(
  495 + 'query realtime stress notification window failed',
  496 + error,
  497 + stackTrace,
  498 + );
  499 + return const <HealthRawRealtimeStressPoint>[];
  500 + }
  501 + }
  502 +
  503 + Future<bool> _sendLocalNotificationSafely({
  504 + required int userId,
  505 + required HealthRawLocalNotification notification,
  506 + }) async {
  507 + try {
  508 + return await _localNotificationDispatcher.sendOne(
  509 + userId: userId,
  510 + notification: notification,
438 ); 511 );
439 - _scheduleRealtimeStressServerPush(sentNotifications);  
440 } catch (error, stackTrace) { 512 } catch (error, stackTrace) {
441 - _logError('send local health notifications failed', error, stackTrace); 513 + _logError('send local health notification failed', error, stackTrace);
  514 + return false;
442 } 515 }
443 } 516 }
444 517
@@ -55,7 +55,13 @@ class HealthRawLocalNotificationBuilder { @@ -55,7 +55,13 @@ class HealthRawLocalNotificationBuilder {
55 if (_hrvNotification(result.hrvStressPoints, hasExistingHrv, record) 55 if (_hrvNotification(result.hrvStressPoints, hasExistingHrv, record)
56 case final notification?) 56 case final notification?)
57 notification, 57 notification,
58 - if (_realtimeStressNotification(realtimeWindow, record) 58 + if (_realtimeStressNotification(
  59 + realtimeWindow,
  60 + record,
  61 + latestRealtimeStressPoint: _latestRealtimeStressPoint(
  62 + result.realtimeStressPoints,
  63 + ),
  64 + )
59 case final notification?) 65 case final notification?)
60 notification, 66 notification,
61 ]; 67 ];
@@ -106,15 +112,22 @@ class HealthRawLocalNotificationBuilder { @@ -106,15 +112,22 @@ class HealthRawLocalNotificationBuilder {
106 112
107 HealthRawLocalNotification? _realtimeStressNotification( 113 HealthRawLocalNotification? _realtimeStressNotification(
108 List<HealthRawRealtimeStressPoint> realtimeWindow, 114 List<HealthRawRealtimeStressPoint> realtimeWindow,
109 - HealthRawLocalNotificationRecord? record,  
110 - ) { 115 + HealthRawLocalNotificationRecord? record, {
  116 + required HealthRawRealtimeStressPoint? latestRealtimeStressPoint,
  117 + }) {
  118 + if (latestRealtimeStressPoint == null) return null;
  119 + if (latestRealtimeStressPoint.isWorkout ||
  120 + latestRealtimeStressPoint.isWorkoutRecovery) {
  121 + return null;
  122 + }
  123 +
111 final valid = realtimeWindow 124 final valid = realtimeWindow
112 .where((e) => e.result >= 1 && e.result <= 100) 125 .where((e) => e.result >= 1 && e.result <= 100)
113 .toList() 126 .toList()
114 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime)); 127 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
115 if (valid.length < _realtimeMinPointCount) return null; 128 if (valid.length < _realtimeMinPointCount) return null;
116 129
117 - final latest = valid.last; 130 + final latest = latestRealtimeStressPoint;
118 if (latest.isSleepLikely) return null; 131 if (latest.isSleepLikely) return null;
119 if (record?.lastRealtimeStressTime case final lastPushTime?) { 132 if (record?.lastRealtimeStressTime case final lastPushTime?) {
120 if (latest.rawEndTime - lastPushTime < _realtimeWindowSeconds) { 133 if (latest.rawEndTime - lastPushTime < _realtimeWindowSeconds) {
@@ -140,6 +153,13 @@ class HealthRawLocalNotificationBuilder { @@ -140,6 +153,13 @@ class HealthRawLocalNotificationBuilder {
140 ); 153 );
141 } 154 }
142 155
  156 + HealthRawRealtimeStressPoint? _latestRealtimeStressPoint(
  157 + List<HealthRawRealtimeStressPoint> points,
  158 + ) {
  159 + if (points.isEmpty) return null;
  160 + return points.reduce((a, b) => a.rawEndTime >= b.rawEndTime ? a : b);
  161 + }
  162 +
143 String _sleepDurationText(int minutes) { 163 String _sleepDurationText(int minutes) {
144 final hours = minutes ~/ 60; 164 final hours = minutes ~/ 60;
145 final remainingMinutes = minutes % 60; 165 final remainingMinutes = minutes % 60;
@@ -217,25 +237,46 @@ class HealthRawLocalNotificationDispatcher { @@ -217,25 +237,46 @@ class HealthRawLocalNotificationDispatcher {
217 required int userId, 237 required int userId,
218 required Iterable<HealthRawLocalNotification> notifications, 238 required Iterable<HealthRawLocalNotification> notifications,
219 }) async { 239 }) async {
220 - var record = await _recordStore.read(userId);  
221 final sentNotifications = <HealthRawLocalNotification>[]; 240 final sentNotifications = <HealthRawLocalNotification>[];
222 for (final notification in notifications) { 241 for (final notification in notifications) {
223 - final sent = await _platformApi.sendLocalNotification(  
224 - notification.title,  
225 - notification.content,  
226 - notification.link,  
227 - );  
228 - if (!sent) continue; 242 + final sent = await sendOne(userId: userId, notification: notification);
  243 + if (!sent) {
  244 + continue;
  245 + }
229 sentNotifications.add(notification); 246 sentNotifications.add(notification);
230 - record = record.withNotification(notification);  
231 - await _recordStore.write(userId, record);  
232 } 247 }
233 return sentNotifications; 248 return sentNotifications;
234 } 249 }
235 250
  251 + Future<bool> sendOne({
  252 + required int userId,
  253 + required HealthRawLocalNotification notification,
  254 + }) async {
  255 + final sent = await _platformApi.sendLocalNotification(
  256 + notification.title,
  257 + notification.content,
  258 + notification.link,
  259 + );
  260 + if (!sent) return false;
  261 + final record = await _recordStore.read(userId);
  262 + await _recordStore.write(userId, record.withNotification(notification));
  263 + return true;
  264 + }
  265 +
236 Future<HealthRawLocalNotificationRecord> readRecord(int userId) { 266 Future<HealthRawLocalNotificationRecord> readRecord(int userId) {
237 return _recordStore.read(userId); 267 return _recordStore.read(userId);
238 } 268 }
  269 +
  270 + Future<void> recordRealtimeStressTime({
  271 + required int userId,
  272 + required int recordTime,
  273 + }) async {
  274 + final record = await _recordStore.read(userId);
  275 + await _recordStore.write(
  276 + userId,
  277 + record.copyWith(lastRealtimeStressTime: recordTime),
  278 + );
  279 + }
239 } 280 }
240 281
241 class HealthRawLocalNotificationRecord { 282 class HealthRawLocalNotificationRecord {
  1 +import 'dart:io';
  2 +
1 import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 3 import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
2 import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart'; 4 import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart';
3 import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart'; 5 import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart';
@@ -123,6 +125,66 @@ void main() { @@ -123,6 +125,66 @@ void main() {
123 125
124 expect(notifications, isEmpty); 126 expect(notifications, isEmpty);
125 }); 127 });
  128 +
  129 + test('skips realtime stress notification when latest point is workout', () {
  130 + final base = _seconds(DateTime(2026, 1, 1, 9));
  131 + final notifications = builder.build(
  132 + result: HealthRawStressCalculationResult(
  133 + userId: 1,
  134 + hrvStressPoints: const [],
  135 + realtimeStressPoints: [
  136 + _realtimePoint(base + 9 * 300, 70, isWorkout: true),
  137 + ],
  138 + dailyStressPoints: const [],
  139 + ),
  140 + hasExistingHrv: false,
  141 + realtimeWindow: [
  142 + for (var i = 0; i < 9; i++) _realtimePoint(base + i * 300, 70),
  143 + _realtimePoint(base + 9 * 300, 70, isWorkout: true),
  144 + ],
  145 + record: const HealthRawLocalNotificationRecord(),
  146 + );
  147 +
  148 + expect(notifications, isEmpty);
  149 + });
  150 +
  151 + test('skips realtime stress notification when latest point is recovery', () {
  152 + final base = _seconds(DateTime(2026, 1, 1, 9));
  153 + final notifications = builder.build(
  154 + result: HealthRawStressCalculationResult(
  155 + userId: 1,
  156 + hrvStressPoints: const [],
  157 + realtimeStressPoints: [
  158 + _realtimePoint(base + 9 * 300, 70, isWorkoutRecovery: true),
  159 + ],
  160 + dailyStressPoints: const [],
  161 + ),
  162 + hasExistingHrv: false,
  163 + realtimeWindow: [
  164 + for (var i = 0; i < 9; i++) _realtimePoint(base + i * 300, 70),
  165 + _realtimePoint(base + 9 * 300, 70, isWorkoutRecovery: true),
  166 + ],
  167 + record: const HealthRawLocalNotificationRecord(),
  168 + );
  169 +
  170 + expect(notifications, isEmpty);
  171 + });
  172 +
  173 + test('records realtime stress push time without sending notification',
  174 + () async {
  175 + final dir = await Directory.systemTemp.createTemp(
  176 + 'health_raw_notification_test_',
  177 + );
  178 + addTearDown(() => dir.delete(recursive: true));
  179 + final dispatcher = HealthRawLocalNotificationDispatcher(
  180 + recordStore: HealthRawLocalNotificationRecordStore(rootDirectory: dir),
  181 + );
  182 +
  183 + await dispatcher.recordRealtimeStressTime(userId: 1, recordTime: 1234);
  184 +
  185 + final record = await dispatcher.readRecord(1);
  186 + expect(record.lastRealtimeStressTime, 1234);
  187 + });
126 } 188 }
127 189
128 int _seconds(DateTime time) => time.millisecondsSinceEpoch ~/ 1000; 190 int _seconds(DateTime time) => time.millisecondsSinceEpoch ~/ 1000;
@@ -147,6 +209,8 @@ HealthRawRealtimeStressPoint _realtimePoint( @@ -147,6 +209,8 @@ HealthRawRealtimeStressPoint _realtimePoint(
147 int rawEndTime, 209 int rawEndTime,
148 double result, { 210 double result, {
149 bool isSleepLikely = false, 211 bool isSleepLikely = false,
  212 + bool isWorkout = false,
  213 + bool isWorkoutRecovery = false,
150 }) { 214 }) {
151 return HealthRawRealtimeStressPoint( 215 return HealthRawRealtimeStressPoint(
152 userId: 1, 216 userId: 1,
@@ -157,8 +221,8 @@ HealthRawRealtimeStressPoint _realtimePoint( @@ -157,8 +221,8 @@ HealthRawRealtimeStressPoint _realtimePoint(
157 sourceEndTime: rawEndTime, 221 sourceEndTime: rawEndTime,
158 flags: HealthRawPointFlags( 222 flags: HealthRawPointFlags(
159 isSleepLikely: isSleepLikely, 223 isSleepLikely: isSleepLikely,
160 - isWorkout: false,  
161 - isWorkoutRecovery: false, 224 + isWorkout: isWorkout,
  225 + isWorkoutRecovery: isWorkoutRecovery,
162 isSuspectedActivity: false, 226 isSuspectedActivity: false,
163 ), 227 ),
164 ); 228 );