Commit 2166905b7a2dea583159da9cba3fca8aaadb16a0

Authored by 常守达
1 parent ffbc6b92

feat(today): 首页接口修改

... ... @@ -81,4 +81,7 @@ class PlatformHostApiImpl(private val application: android.app.Application) : Pl
callback: (Result<AppleProductPaymentResult?>) -> Unit
) {
}
override fun performRestore(callback: (Result<Boolean>) -> Unit) {
}
}
... ...
... ... @@ -6,15 +6,16 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/data/models/health/health_models.dart';
import 'package:doublefeel_flutter/data/models/health/health_upload_models.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
/// HRV 趋势数据点
class HrvDataPoint {
... ... @@ -115,6 +116,12 @@ class TodayController extends GetxController {
final hrvAnnotations = <HrvAnnotation>[].obs;
final v2HealthData = Rxn<V2HealthData>();
final v2StressScore = Rxn<V2StressScore>();
final v2LatestHrv = Rxn<V2LatestHrvData>();
final v2HrvTrend = Rxn<V2HrvTrendData>();
final v2RealtimeStress = Rxn<V2RealtimeStressData>();
Future<void> requestHealthAuthorization() async {
if (GetPlatform.isIOS) {
try {
... ... @@ -211,89 +218,44 @@ class TodayController extends GetxController {
}
Future<void> _refreshHealthDataForDate(DateTime date) async {
final startYear = date.year;
final startMonth = date.month;
final startDate = startYear * 10000 + startMonth * 100 + 1; // e.g. 20260601
const dateRangeType = 1;
_clearRealTimeData();
final intDate = int.parse(DateFormat('yyyyMM01').format(date));
final isToday = DateUtils.isSameDay(date, lastSelectableDay);
stressSubtitle.value = isToday ? 'Hi, 你今日的综合压力状态' : '该日压力状态';
final todayResultFuture = isToday
? _healthApi.getTodayData(
isOther: isFriend,
errorHandlingPolicy: null,
)
: Future.value(
AppFailure<TodayStatusData>(AppUnknownError('Not today')));
final latestHrvResultFuture = isToday && !isFriend
? _healthApi.getLatestHrvData(
errorHandlingPolicy: null,
)
: Future.value(AppFailure<LatestHrvData>(AppUnknownError('Not today')));
final hrvStatisticsResultFuture = _healthApi.getHrvStatistics(
isOther: isFriend,
dateRangeType: dateRangeType,
startDate: startDate,
targetUserId: friendUserId,
errorHandlingPolicy: null,
);
final sleepStatisticsResultFuture = _healthApi.getSleepStateStatistics(
isOther: isFriend,
dateRangeType: dateRangeType,
startDate: startDate,
targetUserId: friendUserId,
errorHandlingPolicy: null,
);
final activityStatisticsResultFuture = _healthApi.getActivityBurnStatistics(
isOther: isFriend,
dateRangeType: dateRangeType,
startDate: startDate,
targetUserId: friendUserId,
errorHandlingPolicy: null,
);
final todayResult = await todayResultFuture;
final latestHrvResult = await latestHrvResultFuture;
final hrvStatisticsResult = await hrvStatisticsResultFuture;
final sleepStatisticsResult = await sleepStatisticsResultFuture;
final activityStatisticsResult = await activityStatisticsResultFuture;
if (selectedDate.value != date) return;
if (!isToday) {
_clearRealTimeData();
}
if (todayResult case AppSuccess<TodayStatusData>(data: final today)) {
_applyTodayData(today);
} else if (todayResult case AppFailure(error: final error)) {
if (isToday) {
AppLogger.w('HealthApi.getTodayData failed: $error');
}
switch (await _healthApi.getV2HealthData(friendUserId, intDate)) {
case AppSuccess(:final data):
v2HealthData.value = data;
case AppFailure():
v2HealthData.value = null;
}
if (latestHrvResult case AppSuccess<LatestHrvData>(data: final latestHrv)) {
_applyLatestHrvData(latestHrv);
switch (await _healthApi.getV2StressScore(friendUserId, intDate)) {
case AppSuccess(:final data):
v2StressScore.value = data;
case AppFailure():
v2StressScore.value = null;
}
if (hrvStatisticsResult
case AppSuccess<HrvStatisticsData>(data: final hrvStatistics)) {
_applyHrvStatistics(hrvStatistics, date);
switch (await _healthApi.getV2LatestHrv(friendUserId, intDate)) {
case AppSuccess(:final data):
v2LatestHrv.value = data;
case AppFailure():
v2LatestHrv.value = null;
}
if (sleepStatisticsResult
case AppSuccess<SleepStatisticsData>(data: final sleepStatistics)) {
_applySleepStatistics(sleepStatistics, date);
switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) {
case AppSuccess(:final data):
v2HrvTrend.value = data;
case AppFailure():
v2HrvTrend.value = null;
}
if (activityStatisticsResult
case AppSuccess<ActivityBurnStatisticsData>(
data: final activityStatistics
)) {
_applyActivityStatistics(activityStatistics, date);
switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) {
case AppSuccess(:final data):
v2RealtimeStress.value = data;
case AppFailure():
v2RealtimeStress.value = null;
}
}
... ...
... ... @@ -477,7 +477,22 @@ class _LatestHrvCard extends StatelessWidget {
final colors = context.colors;
return Obx(() {
controller.avgHrv.value;
final latestHrv = controller.v2LatestHrv.value?.latestHrv;
final latestDataTime = controller.v2LatestHrv.value?.latestDataTime;
final state = controller.v2LatestHrv.value?.state;
final tip = controller.v2LatestHrv.value?.tip;
if (latestHrv == null ||
latestDataTime == null ||
state == null ||
tip == null) {
return const SizedBox.shrink();
}
final latestHrvText = '$latestHrv ms';
final latestDataTimeText =
'${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(latestDataTime * 1000))}';
final stateText =
'${state == 1 ? '状态优秀' : state == 2 ? '状态' : state == 3 ? '状态一般' : state == 4 ? '状态较差' : '状态极差'}';
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
decoration: BoxDecoration(
... ... @@ -488,7 +503,7 @@ class _LatestHrvCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最近一次(17:20)',
'最近一次($latestDataTimeText)',
style: TextStyle(
color: colors.textSecondary,
fontSize: 12,
... ... @@ -508,7 +523,7 @@ class _LatestHrvCard extends StatelessWidget {
),
const SizedBox(width: 8),
Text(
'66ms·状态优秀',
'${latestHrvText}ms·$stateText',
style: TextStyle(
color: colors.textPrimary,
fontSize: 18,
... ... @@ -519,7 +534,7 @@ class _LatestHrvCard extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'你的 HRV 高于平时水平。你目前状态放松,压力控制良好——继续保持这样的平衡状态。',
tip,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
... ...
... ... @@ -25,7 +25,8 @@ class TodayHrvNumberCard extends StatelessWidget {
Expanded(
child: _NumberItem(
label: context.l10n.averageHrvForTheDay,
value: controller.avgHrv.value,
value:
controller.v2HealthData.value?.hrvAvg?.toString() ?? '--',
unit: 'ms',
),
),
... ... @@ -33,7 +34,9 @@ class TodayHrvNumberCard extends StatelessWidget {
Expanded(
child: _NumberItem(
label: context.l10n.restingHeartRate,
value: controller.restingHeartRate.value,
value: controller.v2HealthData.value?.lastRestingHrValue
?.toString() ??
'--',
unit: 'bpm',
),
),
... ...
import 'dart:async';
import 'dart:convert';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/constants/app_const.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:flutter/widgets.dart';
... ... @@ -177,11 +181,7 @@ class LoginController extends GetxController {
Future<void> _loginWithApple(AppleSignInModel appleModel) async {
isLoggingIn.value = true;
final loginRes = await _userApi.loginWithApple(
identityToken: appleModel.identityToken!,
email: appleModel.email,
nickname: appleModel.nickname,
);
final loginRes = await _userApi.loginWithApple(appleModel);
if (loginRes is AppSuccess<LoginResponse>) {
await _handleLoginSuccess(loginRes.data, isApple: true);
... ... @@ -246,6 +246,24 @@ class LoginController extends GetxController {
vip: vip,
);
try {
if (Get.isRegistered<AppEnvironmentConfig>()) {
var env = Get.find<AppEnvironmentConfig>();
await PlatformHostApi().updateLoginInfo(
jsonEncode(UserPreferences(
accessToken: accessToken,
meUserInfo: me,
partnerUserInfo: partner,
vipInfo: vip != null
? UserPreferencesVipInfo.fromVipInfo(vip)
: null,
)),
env.serverBaseUrl);
}
} on Exception catch (e) {
AppLogger.e(e);
}
await _userStateService.onLogin();
// 用户登录成功即代表同意了协议,持久化到本地供冷启动时 SDK 初始化判断使用
... ...
... ... @@ -4,6 +4,7 @@ import '../../result/safe_call.dart';
import '../../../data/models/enums/app_enums.dart';
import '../../../data/models/health/health_models.dart';
import '../../../data/models/health/health_upload_models.dart';
import '../../../data/models/health/health_v2_models.dart';
import '../api_paths.dart';
import '../dio_client.dart';
... ... @@ -248,4 +249,92 @@ class HealthApi {
errorHandlingPolicy: errorHandlingPolicy,
);
}
/// V2
Future<AppResult<V2LatestHrvData>> getV2LatestHrv(
int? friendUserId,
int intDate,
) {
return safeCall(
call: () async {
final response =
await _dioClient.dio.get(ApiPaths.v2LastestHrv, queryParameters: {
if (friendUserId != null) 'query_user_id': friendUserId,
'date': intDate,
});
return V2LatestHrvData.fromJson(response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
Future<AppResult<V2HealthData>> getV2HealthData(
int? friendUserId,
int intDate,
) {
return safeCall(
call: () async {
final response =
await _dioClient.dio.get(ApiPaths.v2LastestData, queryParameters: {
if (friendUserId != null) 'query_user_id': friendUserId,
'date': intDate,
});
return V2HealthData.fromJson(response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
Future<AppResult<V2StressScore>> getV2StressScore(
int? friendUserId,
int intDate,
) {
return safeCall(
call: () async {
final response =
await _dioClient.dio.get(ApiPaths.v2StressScore, queryParameters: {
if (friendUserId != null) 'query_user_id': friendUserId,
'date': intDate,
});
return V2StressScore.fromJson(response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
Future<AppResult<V2HrvTrendData>> getV2HrvTrend(
int? friendUserId,
int intDate,
) {
return safeCall(
call: () async {
final response =
await _dioClient.dio.get(ApiPaths.v2HrvTrend, queryParameters: {
if (friendUserId != null) 'query_user_id': friendUserId,
'date': intDate,
});
return V2HrvTrendData.fromJson(response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
Future<AppResult<V2RealtimeStressData>> getV2RealtimeStress(
int? friendUserId,
int intDate,
) {
return safeCall(
call: () async {
final response = await _dioClient.dio
.get(ApiPaths.v2RealtimeStress, queryParameters: {
if (friendUserId != null) 'query_user_id': friendUserId,
'date': intDate,
});
return V2RealtimeStressData.fromJson(
response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
}
... ...
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import '../../constants/app_const.dart';
import '../../error/http_error_handling_policy.dart';
import '../../result/app_result.dart';
... ... @@ -42,20 +44,17 @@ class UserApi {
);
}
Future<AppResult<LoginResponse>> loginWithApple({
required String identityToken,
String? email,
String? nickname,
}) {
Future<AppResult<LoginResponse>> loginWithApple(AppleSignInModel appleModel) {
return safeCall(
call: () async {
final response = await _dioClient.dio.post(
ApiPaths.userLogin,
data: AppleLoginRequest(
identityToken: identityToken,
email: email,
nickname: nickname,
).toJson(),
identityToken: appleModel.identityToken ?? '',
email: appleModel.email ?? '',
nickname: appleModel.nickname ?? '',
userId: appleModel.userId)
.toJson(),
options: _dioClient.noTokenOptions(),
);
return LoginResponse.fromJson(response.data as Map<String, dynamic>);
... ...
... ... @@ -29,6 +29,13 @@ abstract final class ApiPaths {
static const healthStatsHrv = '/client/doublefeel/health/statistics/hrv/';
static const healthPulse = '/client/doublefeel/health/pulse/';
static const healthPulseLatest = '/client/doublefeel/health/pulse/latest/';
//Health v2
static const v2LastestHrv = '/client/doublefeel/health/v2/lastest_hrv/';
static const v2LastestData = '/client/doublefeel/health/v2/health_data/';
static const v2StressScore = '/client/doublefeel/health/v2/stress_score/';
static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/';
static const v2RealtimeStress =
'/client/doublefeel/health/v2/realtime_stress/';
// Pay
static const paymentProducts = '/client/doublefeel/payment/products/';
... ...
int? _parseInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) return int.tryParse(value);
return null;
}
double? _parseDouble(dynamic value) {
if (value == null) return null;
if (value is num) return value.toDouble();
if (value is String) {
if (value == 'NaN') return double.nan;
if (value == 'Infinity') return double.infinity;
if (value == '-Infinity') return double.negativeInfinity;
return double.tryParse(value);
}
return null;
}
/// v2/lastest_hrv/ response
class V2LatestHrvData {
const V2LatestHrvData(
{this.latestHrv, this.latestDataTime, this.state, this.tip});
final double? latestHrv;
final int? latestDataTime;
final int? state;
final String? tip;
factory V2LatestHrvData.fromJson(Map<String, dynamic> json) {
return V2LatestHrvData(
latestHrv: _parseDouble(json['latest_hrv']),
latestDataTime: _parseInt(json['latest_data_time']),
state: _parseInt(json['state']),
tip: json['tip_text'],
);
}
}
/// v2/health_data/ response
class V2HealthData {
const V2HealthData({
this.hrvAvg,
this.lastRestingHrValue,
this.hrAvg,
this.move,
this.exercise,
this.stand,
this.steps,
this.sleepDuration,
this.sleepScore,
this.sleepState,
});
final int? hrvAvg;
final int? lastRestingHrValue;
final int? hrAvg;
final int? move;
final int? exercise;
final int? stand;
final int? steps;
final int? sleepDuration;
final double? sleepScore;
final int? sleepState;
factory V2HealthData.fromJson(Map<String, dynamic> json) {
return V2HealthData(
hrvAvg: _parseInt(json['hrv_avg']),
lastRestingHrValue: _parseInt(json['last_resting_hr_value']),
hrAvg: _parseInt(json['hr_avg']),
move: _parseInt(json['move']),
exercise: _parseInt(json['exercise']),
stand: _parseInt(json['stand']),
steps: _parseInt(json['steps']),
sleepDuration: _parseInt(json['sleep_duration']),
sleepScore: _parseDouble(json['sleep_score']),
sleepState: _parseInt(json['sleep_state']),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (hrvAvg != null) val['hrv_avg'] = hrvAvg;
if (lastRestingHrValue != null) {
val['last_resting_hr_value'] = lastRestingHrValue;
}
if (hrAvg != null) val['hr_avg'] = hrAvg;
if (move != null) val['move'] = move;
if (exercise != null) val['exercise'] = exercise;
if (stand != null) val['stand'] = stand;
if (steps != null) val['steps'] = steps;
if (sleepDuration != null) val['sleep_duration'] = sleepDuration;
if (sleepScore != null) val['sleep_score'] = sleepScore;
if (sleepState != null) val['sleep_state'] = sleepState;
return val;
}
}
/// v2/stress_score/ response
class V2StressScore {
const V2StressScore({
this.state,
this.hrvScore,
this.hrScore,
this.latestHr,
this.comprehensiveScore,
});
final int? state;
final int? hrvScore;
final int? hrScore;
final int? latestHr;
final int? comprehensiveScore;
factory V2StressScore.fromJson(Map<String, dynamic> json) {
return V2StressScore(
state: _parseInt(json['state']),
hrvScore: _parseInt(json['hrv_score']),
hrScore: _parseInt(json['hr_score']),
latestHr: _parseInt(json['latest_hr']),
comprehensiveScore: _parseInt(json['comprehensive_score']),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (state != null) val['state'] = state;
if (hrvScore != null) val['hrv_score'] = hrvScore;
if (hrScore != null) val['hr_score'] = hrScore;
if (latestHr != null) val['latest_hr'] = latestHr;
if (comprehensiveScore != null) {
val['comprehensive_score'] = comprehensiveScore;
}
return val;
}
}
/// Single item in v2/hrv_trend/ list
class V2HrvTrendItem {
const V2HrvTrendItem({this.time, this.rawHrv, this.trendHrv, this.state});
final int? time;
final double? rawHrv;
final int? trendHrv;
final int? state;
factory V2HrvTrendItem.fromJson(Map<String, dynamic> json) {
return V2HrvTrendItem(
time: _parseInt(json['time']),
rawHrv: _parseDouble(json['raw_hrv']),
trendHrv: _parseInt(json['trend_hrv']),
state: _parseInt(json['state']),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (time != null) val['time'] = time;
if (rawHrv != null) val['raw_hrv'] = rawHrv;
if (trendHrv != null) val['trend_hrv'] = trendHrv;
if (state != null) val['state'] = state;
return val;
}
}
/// v2/hrv_trend/ response
class V2HrvTrendData {
const V2HrvTrendData({this.list});
final List<V2HrvTrendItem>? list;
factory V2HrvTrendData.fromJson(Map<String, dynamic> json) {
return V2HrvTrendData(
list: (json['list'] as List<dynamic>?)
?.map((e) => V2HrvTrendItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (list != null) {
val['list'] = list!.map((e) => e.toJson()).toList();
}
return val;
}
}
/// Single item in v2/realtime_stress/ list
class V2RealtimeStressItem {
const V2RealtimeStressItem({this.time, this.value, this.state});
final int? time;
final int? value;
final int? state;
factory V2RealtimeStressItem.fromJson(Map<String, dynamic> json) {
return V2RealtimeStressItem(
time: _parseInt(json['time']),
value: _parseInt(json['value']),
state: _parseInt(json['state']),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (time != null) val['time'] = time;
if (value != null) val['value'] = value;
if (state != null) val['state'] = state;
return val;
}
}
/// v2/realtime_stress/ response
class V2RealtimeStressData {
const V2RealtimeStressData({this.list});
final List<V2RealtimeStressItem>? list;
factory V2RealtimeStressData.fromJson(Map<String, dynamic> json) {
return V2RealtimeStressData(
list: (json['list'] as List<dynamic>?)
?.map((e) => V2RealtimeStressItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (list != null) {
val['list'] = list!.map((e) => e.toJson()).toList();
}
return val;
}
}
... ...
... ... @@ -22,17 +22,20 @@ class AppleLoginRequest {
const AppleLoginRequest({
this.loginType = 'apple',
required this.identityToken,
required this.userId,
this.email,
this.nickname,
});
final String loginType;
final String identityToken;
final String userId;
final String? email;
final String? nickname;
Map<String, dynamic> toJson() => {
'login_type': loginType,
'apple_user_id': userId,
'identity_token': identityToken,
if (email != null) 'email': email,
if (nickname != null) 'nickname': nickname,
... ...