Commit 3cbf45f89f7e34b54afdedd7a514546030f8c49a

Authored by 权海
1 parent 206eb9bf

feat(ui):实时压力通知由客户端通知服务端发送

@@ -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', () {