Commit 8250ab7087e609cb7ca45588cdfc74e9420f6514

Authored by 权海
1 parent c159902a

feat(ui):只要能拿到数据也开始计算趋势

@@ -996,7 +996,15 @@ class AppleHealthRawDataCoreService { @@ -996,7 +996,15 @@ class AppleHealthRawDataCoreService {
996 996
997 Future<bool> _hasHealthReadAuthorization() async { 997 Future<bool> _hasHealthReadAuthorization() async {
998 final authorization = await _healthApi.checkHealthAppAuthorization(); 998 final authorization = await _healthApi.checkHealthAppAuthorization();
999 - return authorization.status == 1; 999 + if (authorization.status == 1) {
  1000 + return true;
  1001 + }
  1002 + try {
  1003 + return await _rawDataApi.hasHealthData();
  1004 + } catch (error, stackTrace) {
  1005 + _logError('check health data availability failed', error, stackTrace);
  1006 + return false;
  1007 + }
1000 } 1008 }
1001 1009
1002 Stream<HealthKitRawDataPoint> streamRawData({ 1010 Stream<HealthKitRawDataPoint> streamRawData({
@@ -391,9 +391,10 @@ void main() { @@ -391,9 +391,10 @@ void main() {
391 expect(daily.single.uploaded, isTrue); 391 expect(daily.single.uploaded, isTrue);
392 }); 392 });
393 393
394 - test('startCoreCaculate skips raw reads when health auth is missing', 394 + test(
  395 + 'startCoreCaculate skips raw reads when health auth and local data are missing',
395 () async { 396 () async {
396 - final api = _FakeHealthKitRawDataHostApi(); 397 + final api = _FakeHealthKitRawDataHostApi()..hasData = false;
397 final service = AppleHealthRawDataCoreService( 398 final service = AppleHealthRawDataCoreService(
398 healthApi: _FakeHealthKitHostApi(status: 2), 399 healthApi: _FakeHealthKitHostApi(status: 2),
399 rawDataApi: api, 400 rawDataApi: api,
@@ -411,6 +412,79 @@ void main() { @@ -411,6 +412,79 @@ void main() {
411 expect(api.workoutCallCount, 0); 412 expect(api.workoutCallCount, 0);
412 }); 413 });
413 414
  415 + test(
  416 + 'startCoreCaculate reads raw data when health auth is missing but data exists',
  417 + () async {
  418 + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  419 + final api = _FakeHealthKitRawDataHostApi()..hasData = true;
  420 + api.setPoints(HealthDataUploadType.hrv.type, [
  421 + _point(now - 120, 35),
  422 + ]);
  423 + final service = AppleHealthRawDataCoreService(
  424 + healthApi: _FakeHealthKitHostApi(status: 2),
  425 + rawDataApi: api,
  426 + localStore: _MemoryHealthRawStressLocalStore(),
  427 + userIdProvider: () => 42,
  428 + uploadResultsAfterCalculation: false,
  429 + );
  430 +
  431 + final result = await service.startCoreCaculate(
  432 + endTime: now,
  433 + readChunkDays: 1,
  434 + );
  435 +
  436 + expect(result.hrvStressPoints.map((e) => e.rawEndTime), [now - 120]);
  437 + expect(api.calls, isNotEmpty);
  438 + expect(api.hasHealthDataCallCount, 1);
  439 + });
  440 +
  441 + test('startCoreCaculate skips raw reads when local data check fails',
  442 + () async {
  443 + final api = _FakeHealthKitRawDataHostApi()
  444 + ..hasDataError = StateError('health data unavailable');
  445 + final service = AppleHealthRawDataCoreService(
  446 + healthApi: _FakeHealthKitHostApi(status: 2),
  447 + rawDataApi: api,
  448 + localStore: _MemoryHealthRawStressLocalStore(),
  449 + userIdProvider: () => 42,
  450 + uploadResultsAfterCalculation: false,
  451 + );
  452 +
  453 + final result = await service.startCoreCaculate(readChunkDays: 1);
  454 +
  455 + expect(result.hrvStressPoints, isEmpty);
  456 + expect(result.realtimeStressPoints, isEmpty);
  457 + expect(api.calls, isEmpty);
  458 + expect(api.hasHealthDataCallCount, 1);
  459 + });
  460 +
  461 + test(
  462 + 'startCoreCaculate does not call local data fallback when auth is granted',
  463 + () async {
  464 + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  465 + final api = _FakeHealthKitRawDataHostApi()
  466 + ..hasData = false
  467 + ..hasDataError = StateError('should not call hasHealthData');
  468 + api.setPoints(HealthDataUploadType.hrv.type, [
  469 + _point(now - 120, 35),
  470 + ]);
  471 + final service = AppleHealthRawDataCoreService(
  472 + healthApi: _FakeHealthKitHostApi(status: 1),
  473 + rawDataApi: api,
  474 + localStore: _MemoryHealthRawStressLocalStore(),
  475 + userIdProvider: () => 42,
  476 + uploadResultsAfterCalculation: false,
  477 + );
  478 +
  479 + final result = await service.startCoreCaculate(
  480 + endTime: now,
  481 + readChunkDays: 1,
  482 + );
  483 +
  484 + expect(result.hrvStressPoints.map((e) => e.rawEndTime), [now - 120]);
  485 + expect(api.hasHealthDataCallCount, 0);
  486 + });
  487 +
414 test('onHealthDataUpdated emits event with data types after calculation', 488 test('onHealthDataUpdated emits event with data types after calculation',
415 () async { 489 () async {
416 final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; 490 final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
@@ -943,6 +1017,8 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi { @@ -943,6 +1017,8 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
943 Completer<int>? hrUploadCompleter; 1017 Completer<int>? hrUploadCompleter;
944 Completer<int>? hrvUploadCompleter; 1018 Completer<int>? hrvUploadCompleter;
945 var hasData = true; 1019 var hasData = true;
  1020 + Object? hasDataError;
  1021 + var hasHealthDataCallCount = 0;
946 1022
947 void setPoints(int dataType, List<HealthKitRawDataPoint> points) { 1023 void setPoints(int dataType, List<HealthKitRawDataPoint> points) {
948 _pointsByType[dataType] = points; 1024 _pointsByType[dataType] = points;
@@ -965,7 +1041,14 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi { @@ -965,7 +1041,14 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
965 } 1041 }
966 1042
967 @override 1043 @override
968 - Future<bool> hasHealthData() async => hasData; 1044 + Future<bool> hasHealthData() async {
  1045 + hasHealthDataCallCount += 1;
  1046 + final error = hasDataError;
  1047 + if (error != null) {
  1048 + throw error;
  1049 + }
  1050 + return hasData;
  1051 + }
969 1052
970 @override 1053 @override
971 Future<List<HealthKitRawDataPoint>> getHealthKitRawData( 1054 Future<List<HealthKitRawDataPoint>> getHealthKitRawData(
  1 +import 'dart:convert';
  2 +import 'dart:io';
  3 +
  4 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
  5 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_stress_calculator.dart';
  6 +import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
  7 +import 'package:flutter/foundation.dart';
  8 +import 'package:flutter_test/flutter_test.dart';
  9 +
  10 +void main() {
  11 + test('generate hrv trend results from exported raw health rows', () async {
  12 + final jsonPath = Platform.environment['HEALTH_RAW_ROWS_JSON'];
  13 + if (jsonPath == null || jsonPath.isEmpty) {
  14 + markTestSkipped(
  15 + 'Set HEALTH_RAW_ROWS_JSON to a JSON export of the raw health rows.',
  16 + );
  17 + return;
  18 + }
  19 +
  20 + final hrvValueDivisor = double.tryParse(
  21 + Platform.environment['HRV_VALUE_DIVISOR'] ?? '',
  22 + ) ??
  23 + 1;
  24 + final rows = (jsonDecode(await File(jsonPath).readAsString()) as List)
  25 + .cast<Map<String, Object?>>();
  26 + final userId = (rows.firstOrNull?['user_id'] as num?)?.toInt() ?? 0;
  27 + final hrvPoints = <HealthKitRawDataPoint>[];
  28 + final heartRatePoints = <HealthKitRawDataPoint>[];
  29 + final restingHeartRatePoints = <HealthKitRawDataPoint>[];
  30 +
  31 + for (final row in rows) {
  32 + final dataType = (row['data_type'] as num).toInt();
  33 + final dataTime = (row['data_time'] as num).toInt();
  34 + final rawValue = (row['value'] as num).toDouble();
  35 + final value = dataType == 1 ? rawValue / hrvValueDivisor : rawValue;
  36 + final point = HealthKitRawDataPoint(
  37 + dataType: dataType,
  38 + startTime: dataTime,
  39 + endTime: dataTime,
  40 + value: value,
  41 + isMotionLike: false,
  42 + );
  43 + switch (dataType) {
  44 + case 1:
  45 + hrvPoints.add(point);
  46 + break;
  47 + case 2:
  48 + heartRatePoints.add(point);
  49 + break;
  50 + case 9:
  51 + restingHeartRatePoints.add(point);
  52 + break;
  53 + }
  54 + }
  55 +
  56 + final times = rows.map((row) => (row['data_time'] as num).toInt()).toList()
  57 + ..sort();
  58 + final result = HealthRawStressCalculator(userId: userId).calculate(
  59 + hrvPoints: hrvPoints,
  60 + heartRatePoints: heartRatePoints,
  61 + restingHeartRatePoints: restingHeartRatePoints,
  62 + startTime: times.first,
  63 + endTime: times.last,
  64 + );
  65 +
  66 + debugPrint('raw rows: ${rows.length}');
  67 + debugPrint('user_id: $userId');
  68 + debugPrint('hrv raw points: ${hrvPoints.length}');
  69 + debugPrint('heart rate raw points: ${heartRatePoints.length}');
  70 + debugPrint(
  71 + 'resting heart rate raw points: ${restingHeartRatePoints.length}');
  72 + debugPrint('hrv value divisor: $hrvValueDivisor');
  73 + debugPrint('hrv trend result count: ${result.hrvStressPoints.length}');
  74 + debugPrint(jsonEncode(result.hrvStressPoints.map(_hrvResultJson).toList()));
  75 +
  76 + expect(result.hrvStressPoints, isNotNull);
  77 + });
  78 +}
  79 +
  80 +Map<String, Object?> _hrvResultJson(HealthRawHrvStressPoint point) {
  81 + return <String, Object?>{
  82 + 'raw_end_time': point.rawEndTime,
  83 + 'raw_end_time_text': DateTime.fromMillisecondsSinceEpoch(
  84 + point.rawEndTime * 1000,
  85 + ).toIso8601String(),
  86 + 'raw_hrv': point.rawHrv,
  87 + 'result': point.result,
  88 + 'state': point.state.name,
  89 + 'state_value': point.state.value,
  90 + 'baseline_hrv': point.baselineHrv,
  91 + 'baseline_awake_hrv': point.baselineAwakeHrv,
  92 + 'baseline_sleep_hrv': point.baselineSleepHrv,
  93 + 'baseline_resting_hr': point.baselineRestingHr,
  94 + 'source_start_time': point.sourceStartTime,
  95 + 'source_end_time': point.sourceEndTime,
  96 + };
  97 +}