Showing
6 changed files
with
85 additions
and
9 deletions
| @@ -62,6 +62,7 @@ void registerUserSessionDeps() { | @@ -62,6 +62,7 @@ void registerUserSessionDeps() { | ||
| 62 | HealthRawDataCoreService( | 62 | HealthRawDataCoreService( |
| 63 | environmentConfig: Get.find<AppEnvironmentConfig>(), | 63 | environmentConfig: Get.find<AppEnvironmentConfig>(), |
| 64 | userIdProvider: () => userPrefs.preferences.value.meUserInfo?.id ?? 0, | 64 | userIdProvider: () => userPrefs.preferences.value.meUserInfo?.id ?? 0, |
| 65 | + serverHealthApi: HealthApi(dioClient), | ||
| 65 | ), | 66 | ), |
| 66 | permanent: true, | 67 | permanent: true, |
| 67 | ); | 68 | ); |
| @@ -219,6 +219,25 @@ class HealthApi { | @@ -219,6 +219,25 @@ class HealthApi { | ||
| 219 | ); | 219 | ); |
| 220 | } | 220 | } |
| 221 | 221 | ||
| 222 | + Future<AppResult<void>> pushRealtimeStressNotification({ | ||
| 223 | + required int latestDataTime, | ||
| 224 | + required int currentState, | ||
| 225 | + int isHrvChange = 0, | ||
| 226 | + }) { | ||
| 227 | + return safeCall( | ||
| 228 | + call: () async { | ||
| 229 | + await _dioClient.dio.post( | ||
| 230 | + ApiPaths.v2RealtimeStressPush, | ||
| 231 | + data: { | ||
| 232 | + 'latest_data_time': latestDataTime, | ||
| 233 | + 'current_state': currentState, | ||
| 234 | + 'is_hrv_change': isHrvChange, | ||
| 235 | + }, | ||
| 236 | + ); | ||
| 237 | + }, | ||
| 238 | + ); | ||
| 239 | + } | ||
| 240 | + | ||
| 222 | Future<AppResult<void>> uploadPulseTypeResult(PulseType pulseType) { | 241 | Future<AppResult<void>> uploadPulseTypeResult(PulseType pulseType) { |
| 223 | return safeCall( | 242 | return safeCall( |
| 224 | call: () async { | 243 | call: () async { |
| @@ -37,6 +37,8 @@ abstract final class ApiPaths { | @@ -37,6 +37,8 @@ abstract final class ApiPaths { | ||
| 37 | static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/'; | 37 | static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/'; |
| 38 | static const v2RealtimeStress = | 38 | static const v2RealtimeStress = |
| 39 | '/client/doublefeel/health/v2/realtime_stress/'; | 39 | '/client/doublefeel/health/v2/realtime_stress/'; |
| 40 | + static const v2RealtimeStressPush = | ||
| 41 | + '/client/doublefeel/health/v2/realtime_stress/push/'; | ||
| 40 | static const v2ActivityTarget = | 42 | static const v2ActivityTarget = |
| 41 | '/client/doublefeel/health/v2/activity_target/'; | 43 | '/client/doublefeel/health/v2/activity_target/'; |
| 42 | static const weatherUploadedData = | 44 | static const weatherUploadedData = |
| @@ -14,6 +14,8 @@ import '../../pigeon/health_kit_api.g.dart'; | @@ -14,6 +14,8 @@ import '../../pigeon/health_kit_api.g.dart'; | ||
| 14 | import '../../pigeon/health_kit_raw_data_api.g.dart'; | 14 | import '../../pigeon/health_kit_raw_data_api.g.dart'; |
| 15 | import '../config/app_environment_config.dart'; | 15 | import '../config/app_environment_config.dart'; |
| 16 | import '../logging/app_logger.dart'; | 16 | import '../logging/app_logger.dart'; |
| 17 | +import '../network/api/health_api.dart'; | ||
| 18 | +import '../result/app_result.dart'; | ||
| 17 | import 'health_raw_local_notification.dart'; | 19 | import 'health_raw_local_notification.dart'; |
| 18 | import 'health_raw_stress_calculator.dart'; | 20 | import 'health_raw_stress_calculator.dart'; |
| 19 | import 'health_sleep_calculator.dart'; | 21 | import 'health_sleep_calculator.dart'; |
| @@ -30,6 +32,7 @@ class HealthRawDataCoreService { | @@ -30,6 +32,7 @@ class HealthRawDataCoreService { | ||
| 30 | int Function()? userIdProvider, | 32 | int Function()? userIdProvider, |
| 31 | bool uploadResultsAfterCalculation = true, | 33 | bool uploadResultsAfterCalculation = true, |
| 32 | HealthRawLocalNotificationDispatcher? localNotificationDispatcher, | 34 | HealthRawLocalNotificationDispatcher? localNotificationDispatcher, |
| 35 | + HealthApi? serverHealthApi, | ||
| 33 | }) : _healthApi = healthApi ?? HealthKitHostApi(), | 36 | }) : _healthApi = healthApi ?? HealthKitHostApi(), |
| 34 | _rawDataApi = rawDataApi ?? HealthKitRawDataHostApi(), | 37 | _rawDataApi = rawDataApi ?? HealthKitRawDataHostApi(), |
| 35 | _localStore = localStore ?? HealthRawStressLocalStore(), | 38 | _localStore = localStore ?? HealthRawStressLocalStore(), |
| @@ -37,7 +40,8 @@ class HealthRawDataCoreService { | @@ -37,7 +40,8 @@ class HealthRawDataCoreService { | ||
| 37 | _userIdProvider = userIdProvider, | 40 | _userIdProvider = userIdProvider, |
| 38 | _uploadResultsAfterCalculation = uploadResultsAfterCalculation, | 41 | _uploadResultsAfterCalculation = uploadResultsAfterCalculation, |
| 39 | _localNotificationDispatcher = localNotificationDispatcher ?? | 42 | _localNotificationDispatcher = localNotificationDispatcher ?? |
| 40 | - HealthRawLocalNotificationDispatcher(); | 43 | + HealthRawLocalNotificationDispatcher(), |
| 44 | + _serverHealthApi = serverHealthApi; | ||
| 41 | 45 | ||
| 42 | final HealthKitHostApi _healthApi; | 46 | final HealthKitHostApi _healthApi; |
| 43 | final HealthKitRawDataHostApi _rawDataApi; | 47 | final HealthKitRawDataHostApi _rawDataApi; |
| @@ -46,6 +50,7 @@ class HealthRawDataCoreService { | @@ -46,6 +50,7 @@ class HealthRawDataCoreService { | ||
| 46 | final int Function()? _userIdProvider; | 50 | final int Function()? _userIdProvider; |
| 47 | final bool _uploadResultsAfterCalculation; | 51 | final bool _uploadResultsAfterCalculation; |
| 48 | final HealthRawLocalNotificationDispatcher _localNotificationDispatcher; | 52 | final HealthRawLocalNotificationDispatcher _localNotificationDispatcher; |
| 53 | + final HealthApi? _serverHealthApi; | ||
| 49 | final StreamController<HealthRawDataUpdatedEvent> | 54 | final StreamController<HealthRawDataUpdatedEvent> |
| 50 | _healthDataUpdatedController = | 55 | _healthDataUpdatedController = |
| 51 | StreamController<HealthRawDataUpdatedEvent>.broadcast(); | 56 | StreamController<HealthRawDataUpdatedEvent>.broadcast(); |
| @@ -424,15 +429,60 @@ class HealthRawDataCoreService { | @@ -424,15 +429,60 @@ class HealthRawDataCoreService { | ||
| 424 | record: record, | 429 | record: record, |
| 425 | ); | 430 | ); |
| 426 | if (notifications.isEmpty) return; | 431 | if (notifications.isEmpty) return; |
| 427 | - await _localNotificationDispatcher.sendAll( | 432 | + final sentNotifications = await _localNotificationDispatcher.sendAll( |
| 428 | userId: result.userId, | 433 | userId: result.userId, |
| 429 | notifications: notifications, | 434 | notifications: notifications, |
| 430 | ); | 435 | ); |
| 436 | + _scheduleRealtimeStressServerPush(sentNotifications); | ||
| 431 | } catch (error, stackTrace) { | 437 | } catch (error, stackTrace) { |
| 432 | _logError('send local health notifications failed', error, stackTrace); | 438 | _logError('send local health notifications failed', error, stackTrace); |
| 433 | } | 439 | } |
| 434 | } | 440 | } |
| 435 | 441 | ||
| 442 | + void _scheduleRealtimeStressServerPush( | ||
| 443 | + Iterable<HealthRawLocalNotification> sentNotifications, | ||
| 444 | + ) { | ||
| 445 | + final realtimeNotifications = sentNotifications.where( | ||
| 446 | + (notification) => | ||
| 447 | + notification.recordType == | ||
| 448 | + HealthRawLocalNotificationRecordType.realtimeStress && | ||
| 449 | + notification.currentState != null, | ||
| 450 | + ); | ||
| 451 | + for (final notification in realtimeNotifications) { | ||
| 452 | + unawaited( | ||
| 453 | + _pushRealtimeStressServerNotification(notification), | ||
| 454 | + ); | ||
| 455 | + } | ||
| 456 | + } | ||
| 457 | + | ||
| 458 | + Future<void> _pushRealtimeStressServerNotification( | ||
| 459 | + HealthRawLocalNotification notification, | ||
| 460 | + ) async { | ||
| 461 | + final serverHealthApi = _serverHealthApi; | ||
| 462 | + if (serverHealthApi == null) { | ||
| 463 | + return; | ||
| 464 | + } | ||
| 465 | + try { | ||
| 466 | + final result = await serverHealthApi.pushRealtimeStressNotification( | ||
| 467 | + latestDataTime: notification.recordTime, | ||
| 468 | + currentState: notification.currentState!, | ||
| 469 | + ); | ||
| 470 | + if (result is AppFailure) { | ||
| 471 | + _logError( | ||
| 472 | + 'push realtime stress server notification failed', | ||
| 473 | + result.error, | ||
| 474 | + StackTrace.current, | ||
| 475 | + ); | ||
| 476 | + } | ||
| 477 | + } catch (error, stackTrace) { | ||
| 478 | + _logError( | ||
| 479 | + 'push realtime stress server notification failed', | ||
| 480 | + error, | ||
| 481 | + stackTrace, | ||
| 482 | + ); | ||
| 483 | + } | ||
| 484 | + } | ||
| 485 | + | ||
| 436 | Future<bool> _hasHealthReadAuthorization() async { | 486 | Future<bool> _hasHealthReadAuthorization() async { |
| 437 | final authorization = await _healthApi.checkHealthAppAuthorization(); | 487 | final authorization = await _healthApi.checkHealthAppAuthorization(); |
| 438 | return authorization.status == 1; | 488 | return authorization.status == 1; |
| 1 | import 'dart:convert'; | 1 | import 'dart:convert'; |
| 2 | import 'dart:io'; | 2 | import 'dart:io'; |
| 3 | -import 'dart:math' as math; | ||
| 4 | 3 | ||
| 5 | import 'package:path_provider/path_provider.dart'; | 4 | import 'package:path_provider/path_provider.dart'; |
| 6 | 5 | ||
| @@ -19,6 +18,7 @@ class HealthRawLocalNotification { | @@ -19,6 +18,7 @@ class HealthRawLocalNotification { | ||
| 19 | required this.link, | 18 | required this.link, |
| 20 | required this.recordType, | 19 | required this.recordType, |
| 21 | required this.recordTime, | 20 | required this.recordTime, |
| 21 | + this.currentState, | ||
| 22 | }); | 22 | }); |
| 23 | 23 | ||
| 24 | final String title; | 24 | final String title; |
| @@ -26,6 +26,7 @@ class HealthRawLocalNotification { | @@ -26,6 +26,7 @@ class HealthRawLocalNotification { | ||
| 26 | final String link; | 26 | final String link; |
| 27 | final HealthRawLocalNotificationRecordType recordType; | 27 | final HealthRawLocalNotificationRecordType recordType; |
| 28 | final int recordTime; | 28 | final int recordTime; |
| 29 | + final int? currentState; | ||
| 29 | } | 30 | } |
| 30 | 31 | ||
| 31 | enum HealthRawLocalNotificationRecordType { | 32 | enum HealthRawLocalNotificationRecordType { |
| @@ -121,10 +122,7 @@ class HealthRawLocalNotificationBuilder { | @@ -121,10 +122,7 @@ class HealthRawLocalNotificationBuilder { | ||
| 121 | } | 122 | } |
| 122 | } | 123 | } |
| 123 | 124 | ||
| 124 | - final windowStart = math.max( | ||
| 125 | - valid.first.rawEndTime, | ||
| 126 | - latest.rawEndTime - _realtimeWindowSeconds + 1, | ||
| 127 | - ); | 125 | + final windowStart = latest.rawEndTime - _realtimeWindowSeconds; |
| 128 | final average = | 126 | final average = |
| 129 | valid.map((e) => e.result).reduce((a, b) => a + b) / valid.length; | 127 | valid.map((e) => e.result).reduce((a, b) => a + b) / valid.length; |
| 130 | final state = healthRawRealtimeStressState(average); | 128 | final state = healthRawRealtimeStressState(average); |
| @@ -138,6 +136,7 @@ class HealthRawLocalNotificationBuilder { | @@ -138,6 +136,7 @@ class HealthRawLocalNotificationBuilder { | ||
| 138 | link: healthRawTodayLink, | 136 | link: healthRawTodayLink, |
| 139 | recordType: HealthRawLocalNotificationRecordType.realtimeStress, | 137 | recordType: HealthRawLocalNotificationRecordType.realtimeStress, |
| 140 | recordTime: latest.rawEndTime, | 138 | recordTime: latest.rawEndTime, |
| 139 | + currentState: state.value, | ||
| 141 | ); | 140 | ); |
| 142 | } | 141 | } |
| 143 | 142 | ||
| @@ -214,11 +213,12 @@ class HealthRawLocalNotificationDispatcher { | @@ -214,11 +213,12 @@ class HealthRawLocalNotificationDispatcher { | ||
| 214 | final PlatformHostApi _platformApi; | 213 | final PlatformHostApi _platformApi; |
| 215 | final HealthRawLocalNotificationRecordStore _recordStore; | 214 | final HealthRawLocalNotificationRecordStore _recordStore; |
| 216 | 215 | ||
| 217 | - Future<void> sendAll({ | 216 | + Future<List<HealthRawLocalNotification>> sendAll({ |
| 218 | required int userId, | 217 | required int userId, |
| 219 | required Iterable<HealthRawLocalNotification> notifications, | 218 | required Iterable<HealthRawLocalNotification> notifications, |
| 220 | }) async { | 219 | }) async { |
| 221 | var record = await _recordStore.read(userId); | 220 | var record = await _recordStore.read(userId); |
| 221 | + final sentNotifications = <HealthRawLocalNotification>[]; | ||
| 222 | for (final notification in notifications) { | 222 | for (final notification in notifications) { |
| 223 | final sent = await _platformApi.sendLocalNotification( | 223 | final sent = await _platformApi.sendLocalNotification( |
| 224 | notification.title, | 224 | notification.title, |
| @@ -226,9 +226,11 @@ class HealthRawLocalNotificationDispatcher { | @@ -226,9 +226,11 @@ class HealthRawLocalNotificationDispatcher { | ||
| 226 | notification.link, | 226 | notification.link, |
| 227 | ); | 227 | ); |
| 228 | if (!sent) continue; | 228 | if (!sent) continue; |
| 229 | + sentNotifications.add(notification); | ||
| 229 | record = record.withNotification(notification); | 230 | record = record.withNotification(notification); |
| 230 | await _recordStore.write(userId, record); | 231 | await _recordStore.write(userId, record); |
| 231 | } | 232 | } |
| 233 | + return sentNotifications; | ||
| 232 | } | 234 | } |
| 233 | 235 | ||
| 234 | Future<HealthRawLocalNotificationRecord> readRecord(int userId) { | 236 | Future<HealthRawLocalNotificationRecord> readRecord(int userId) { |
| @@ -95,9 +95,11 @@ void main() { | @@ -95,9 +95,11 @@ void main() { | ||
| 95 | ); | 95 | ); |
| 96 | 96 | ||
| 97 | expect(notifications, hasLength(1)); | 97 | expect(notifications, hasLength(1)); |
| 98 | - expect(notifications.single.title, '注意压力 · 09:00-09:45'); | 98 | + expect(notifications.single.title, '注意压力 · 08:45-09:45'); |
| 99 | expect(notifications.single.content, '你过去60分钟压力偏高,建议适当放松,并注意休息与恢复。'); | 99 | expect(notifications.single.content, '你过去60分钟压力偏高,建议适当放松,并注意休息与恢复。'); |
| 100 | expect(notifications.single.link, healthRawTodayLink); | 100 | expect(notifications.single.link, healthRawTodayLink); |
| 101 | + expect(notifications.single.currentState, | ||
| 102 | + HealthRawStressState.attention.value); | ||
| 101 | }); | 103 | }); |
| 102 | 104 | ||
| 103 | test('skips realtime stress notification during likely sleep', () { | 105 | test('skips realtime stress notification during likely sleep', () { |
-
Please register or login to post a comment