Commit 2ad10b3d6a83300c66d512b566b8359e7d423fbd

Authored by 权海
1 parent 570248cd

feat(ui):重新调整代码

... ... @@ -404,41 +404,114 @@ class HealthRawDataCoreService {
result.sleepResults.isEmpty) {
return;
}
final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty
? null
: result.realtimeStressPoints.reduce(
(a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
);
var notificationResult = result;
var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
if (latestRealtimeStressPoint != null) {
final shouldResetRealtimeStressPushTime =
latestRealtimeStressPoint.isWorkout ||
latestRealtimeStressPoint.isWorkoutRecovery;
if (shouldResetRealtimeStressPushTime) {
await _recordRealtimeStressTimeSafely(
userId: result.userId,
recordTime: latestRealtimeStressPoint.rawEndTime,
);
notificationResult = result.copyWith(
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
);
} else {
realtimeWindow = await _queryRealtimeStressNotificationWindowSafely(
userId: result.userId,
latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
);
}
}
final record = await _readLocalNotificationRecordSafely(result.userId);
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
result: notificationResult,
hasExistingHrv: hasExistingHrv,
realtimeWindow: realtimeWindow,
record: record,
);
if (notifications.isEmpty) return;
final sentNotifications = <HealthRawLocalNotification>[];
for (final notification in notifications) {
final sent = await _sendLocalNotificationSafely(
userId: result.userId,
notification: notification,
);
if (sent) {
sentNotifications.add(notification);
}
}
_scheduleRealtimeStressServerPush(sentNotifications);
}
Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecordSafely(
int userId,
) async {
try {
return await _localNotificationDispatcher.readRecord(userId);
} catch (error, stackTrace) {
_logError('read local notification record failed', error, stackTrace);
return const HealthRawLocalNotificationRecord();
}
}
Future<void> _recordRealtimeStressTimeSafely({
required int userId,
required int recordTime,
}) async {
try {
final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty
? null
: result.realtimeStressPoints.reduce(
(a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
);
final shouldSkipRealtimeStressNotification =
latestRealtimeStressPoint?.isWorkout ?? false;
final realtimeWindow = latestRealtimeStressPoint == null ||
shouldSkipRealtimeStressNotification
? const <HealthRawRealtimeStressPoint>[]
: await _localStore.queryRealtimeStressPoints(
userId: result.userId,
startTime: latestRealtimeStressPoint.rawEndTime -
Duration.secondsPerHour +
1,
endTime: latestRealtimeStressPoint.rawEndTime,
);
final record = await _localNotificationDispatcher.readRecord(
result.userId,
await _localNotificationDispatcher.recordRealtimeStressTime(
userId: userId,
recordTime: recordTime,
);
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
result: result,
hasExistingHrv: hasExistingHrv,
realtimeWindow: realtimeWindow,
record: record,
} catch (error, stackTrace) {
_logError('record realtime stress push time failed', error, stackTrace);
}
}
Future<List<HealthRawRealtimeStressPoint>>
_queryRealtimeStressNotificationWindowSafely({
required int userId,
required int latestRawEndTime,
}) async {
try {
return await _localStore.queryRealtimeStressPoints(
userId: userId,
startTime: latestRawEndTime - Duration.secondsPerHour + 1,
endTime: latestRawEndTime,
);
if (notifications.isEmpty) return;
final sentNotifications = await _localNotificationDispatcher.sendAll(
userId: result.userId,
notifications: notifications,
} catch (error, stackTrace) {
_logError(
'query realtime stress notification window failed',
error,
stackTrace,
);
return const <HealthRawRealtimeStressPoint>[];
}
}
Future<bool> _sendLocalNotificationSafely({
required int userId,
required HealthRawLocalNotification notification,
}) async {
try {
return await _localNotificationDispatcher.sendOne(
userId: userId,
notification: notification,
);
_scheduleRealtimeStressServerPush(sentNotifications);
} catch (error, stackTrace) {
_logError('send local health notifications failed', error, stackTrace);
_logError('send local health notification failed', error, stackTrace);
return false;
}
}
... ...
... ... @@ -55,7 +55,13 @@ class HealthRawLocalNotificationBuilder {
if (_hrvNotification(result.hrvStressPoints, hasExistingHrv, record)
case final notification?)
notification,
if (_realtimeStressNotification(realtimeWindow, record)
if (_realtimeStressNotification(
realtimeWindow,
record,
latestRealtimeStressPoint: _latestRealtimeStressPoint(
result.realtimeStressPoints,
),
)
case final notification?)
notification,
];
... ... @@ -106,15 +112,22 @@ class HealthRawLocalNotificationBuilder {
HealthRawLocalNotification? _realtimeStressNotification(
List<HealthRawRealtimeStressPoint> realtimeWindow,
HealthRawLocalNotificationRecord? record,
) {
HealthRawLocalNotificationRecord? record, {
required HealthRawRealtimeStressPoint? latestRealtimeStressPoint,
}) {
if (latestRealtimeStressPoint == null) return null;
if (latestRealtimeStressPoint.isWorkout ||
latestRealtimeStressPoint.isWorkoutRecovery) {
return null;
}
final valid = realtimeWindow
.where((e) => e.result >= 1 && e.result <= 100)
.toList()
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
if (valid.length < _realtimeMinPointCount) return null;
final latest = valid.last;
final latest = latestRealtimeStressPoint;
if (latest.isSleepLikely) return null;
if (record?.lastRealtimeStressTime case final lastPushTime?) {
if (latest.rawEndTime - lastPushTime < _realtimeWindowSeconds) {
... ... @@ -140,6 +153,13 @@ class HealthRawLocalNotificationBuilder {
);
}
HealthRawRealtimeStressPoint? _latestRealtimeStressPoint(
List<HealthRawRealtimeStressPoint> points,
) {
if (points.isEmpty) return null;
return points.reduce((a, b) => a.rawEndTime >= b.rawEndTime ? a : b);
}
String _sleepDurationText(int minutes) {
final hours = minutes ~/ 60;
final remainingMinutes = minutes % 60;
... ... @@ -217,25 +237,46 @@ class HealthRawLocalNotificationDispatcher {
required int userId,
required Iterable<HealthRawLocalNotification> notifications,
}) async {
var record = await _recordStore.read(userId);
final sentNotifications = <HealthRawLocalNotification>[];
for (final notification in notifications) {
final sent = await _platformApi.sendLocalNotification(
notification.title,
notification.content,
notification.link,
);
if (!sent) continue;
final sent = await sendOne(userId: userId, notification: notification);
if (!sent) {
continue;
}
sentNotifications.add(notification);
record = record.withNotification(notification);
await _recordStore.write(userId, record);
}
return sentNotifications;
}
Future<bool> sendOne({
required int userId,
required HealthRawLocalNotification notification,
}) async {
final sent = await _platformApi.sendLocalNotification(
notification.title,
notification.content,
notification.link,
);
if (!sent) return false;
final record = await _recordStore.read(userId);
await _recordStore.write(userId, record.withNotification(notification));
return true;
}
Future<HealthRawLocalNotificationRecord> readRecord(int userId) {
return _recordStore.read(userId);
}
Future<void> recordRealtimeStressTime({
required int userId,
required int recordTime,
}) async {
final record = await _recordStore.read(userId);
await _recordStore.write(
userId,
record.copyWith(lastRealtimeStressTime: recordTime),
);
}
}
class HealthRawLocalNotificationRecord {
... ...
import 'dart:io';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart';
... ... @@ -123,6 +125,66 @@ void main() {
expect(notifications, isEmpty);
});
test('skips realtime stress notification when latest point is workout', () {
final base = _seconds(DateTime(2026, 1, 1, 9));
final notifications = builder.build(
result: HealthRawStressCalculationResult(
userId: 1,
hrvStressPoints: const [],
realtimeStressPoints: [
_realtimePoint(base + 9 * 300, 70, isWorkout: true),
],
dailyStressPoints: const [],
),
hasExistingHrv: false,
realtimeWindow: [
for (var i = 0; i < 9; i++) _realtimePoint(base + i * 300, 70),
_realtimePoint(base + 9 * 300, 70, isWorkout: true),
],
record: const HealthRawLocalNotificationRecord(),
);
expect(notifications, isEmpty);
});
test('skips realtime stress notification when latest point is recovery', () {
final base = _seconds(DateTime(2026, 1, 1, 9));
final notifications = builder.build(
result: HealthRawStressCalculationResult(
userId: 1,
hrvStressPoints: const [],
realtimeStressPoints: [
_realtimePoint(base + 9 * 300, 70, isWorkoutRecovery: true),
],
dailyStressPoints: const [],
),
hasExistingHrv: false,
realtimeWindow: [
for (var i = 0; i < 9; i++) _realtimePoint(base + i * 300, 70),
_realtimePoint(base + 9 * 300, 70, isWorkoutRecovery: true),
],
record: const HealthRawLocalNotificationRecord(),
);
expect(notifications, isEmpty);
});
test('records realtime stress push time without sending notification',
() async {
final dir = await Directory.systemTemp.createTemp(
'health_raw_notification_test_',
);
addTearDown(() => dir.delete(recursive: true));
final dispatcher = HealthRawLocalNotificationDispatcher(
recordStore: HealthRawLocalNotificationRecordStore(rootDirectory: dir),
);
await dispatcher.recordRealtimeStressTime(userId: 1, recordTime: 1234);
final record = await dispatcher.readRecord(1);
expect(record.lastRealtimeStressTime, 1234);
});
}
int _seconds(DateTime time) => time.millisecondsSinceEpoch ~/ 1000;
... ... @@ -147,6 +209,8 @@ HealthRawRealtimeStressPoint _realtimePoint(
int rawEndTime,
double result, {
bool isSleepLikely = false,
bool isWorkout = false,
bool isWorkoutRecovery = false,
}) {
return HealthRawRealtimeStressPoint(
userId: 1,
... ... @@ -157,8 +221,8 @@ HealthRawRealtimeStressPoint _realtimePoint(
sourceEndTime: rawEndTime,
flags: HealthRawPointFlags(
isSleepLikely: isSleepLikely,
isWorkout: false,
isWorkoutRecovery: false,
isWorkout: isWorkout,
isWorkoutRecovery: isWorkoutRecovery,
isSuspectedActivity: false,
),
);
... ...