diff --git a/frontend/mining-app/lib/core/network/api_endpoints.dart b/frontend/mining-app/lib/core/network/api_endpoints.dart index 87b9be0d..2b0f73bd 100644 --- a/frontend/mining-app/lib/core/network/api_endpoints.dart +++ b/frontend/mining-app/lib/core/network/api_endpoints.dart @@ -50,6 +50,7 @@ class ApiEndpoints { static const String transferHistory = '/api/v2/trading/transfers/history'; // Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions) + static const String contributionStats = '/api/v2/contribution/stats'; static String contribution(String accountSequence) => '/api/v2/contribution/accounts/$accountSequence'; static String contributionRecords(String accountSequence) => diff --git a/frontend/mining-app/lib/data/datasources/remote/contribution_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/contribution_remote_datasource.dart index 5853d2b1..c9b8a4cb 100644 --- a/frontend/mining-app/lib/data/datasources/remote/contribution_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/contribution_remote_datasource.dart @@ -1,5 +1,6 @@ import '../../models/contribution_model.dart'; import '../../models/contribution_record_model.dart'; +import '../../models/contribution_stats_model.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/api_endpoints.dart'; import '../../../core/error/exceptions.dart'; @@ -14,6 +15,7 @@ abstract class ContributionRemoteDataSource { int page = 1, int pageSize = 50, }); + Future getContributionStats(); } class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource { @@ -60,6 +62,16 @@ class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource { } } + @override + Future getContributionStats() async { + try { + final response = await client.get(ApiEndpoints.contributionStats); + return ContributionStatsModel.fromJson(response.data); + } catch (e) { + throw ServerException(e.toString()); + } + } + String _sourceTypeToString(ContributionSourceType type) { switch (type) { case ContributionSourceType.personal: diff --git a/frontend/mining-app/lib/data/models/contribution_stats_model.dart b/frontend/mining-app/lib/data/models/contribution_stats_model.dart new file mode 100644 index 00000000..874e0744 --- /dev/null +++ b/frontend/mining-app/lib/data/models/contribution_stats_model.dart @@ -0,0 +1,29 @@ +import '../../domain/entities/contribution_stats.dart'; + +class ContributionStatsModel extends ContributionStats { + const ContributionStatsModel({ + required super.totalUsers, + required super.totalAccounts, + required super.accountsWithContribution, + required super.totalAdoptions, + required super.totalContribution, + required super.personalContribution, + required super.teamLevelContribution, + required super.teamBonusContribution, + }); + + factory ContributionStatsModel.fromJson(Map json) { + final contributionByType = json['contributionByType'] as Map? ?? {}; + + return ContributionStatsModel( + totalUsers: json['totalUsers'] ?? 0, + totalAccounts: json['totalAccounts'] ?? 0, + accountsWithContribution: json['accountsWithContribution'] ?? 0, + totalAdoptions: json['totalAdoptions'] ?? 0, + totalContribution: json['totalContribution']?.toString() ?? '0', + personalContribution: contributionByType['personal']?.toString() ?? '0', + teamLevelContribution: contributionByType['teamLevel']?.toString() ?? '0', + teamBonusContribution: contributionByType['teamBonus']?.toString() ?? '0', + ); + } +} diff --git a/frontend/mining-app/lib/data/repositories/contribution_repository_impl.dart b/frontend/mining-app/lib/data/repositories/contribution_repository_impl.dart index 7d9d3ab7..60a2b41c 100644 --- a/frontend/mining-app/lib/data/repositories/contribution_repository_impl.dart +++ b/frontend/mining-app/lib/data/repositories/contribution_repository_impl.dart @@ -1,6 +1,7 @@ import 'package:dartz/dartz.dart'; import '../../domain/entities/contribution.dart'; import '../../domain/entities/contribution_record.dart'; +import '../../domain/entities/contribution_stats.dart'; import '../../domain/repositories/contribution_repository.dart'; import '../../core/error/exceptions.dart'; import '../../core/error/failures.dart'; @@ -46,4 +47,16 @@ class ContributionRepositoryImpl implements ContributionRepository { return Left(const NetworkFailure()); } } + + @override + Future> getContributionStats() async { + try { + final result = await remoteDataSource.getContributionStats(); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } } diff --git a/frontend/mining-app/lib/domain/entities/contribution_stats.dart b/frontend/mining-app/lib/domain/entities/contribution_stats.dart new file mode 100644 index 00000000..f27b993b --- /dev/null +++ b/frontend/mining-app/lib/domain/entities/contribution_stats.dart @@ -0,0 +1,51 @@ +import 'package:equatable/equatable.dart'; + +/// 贡献值统计数据 +class ContributionStats extends Equatable { + /// 总用户数 + final int totalUsers; + + /// 总账户数 + final int totalAccounts; + + /// 有算力的账户数 + final int accountsWithContribution; + + /// 总认种数 + final int totalAdoptions; + + /// 全网总算力 + final String totalContribution; + + /// 个人算力总量 + final String personalContribution; + + /// 团队层级算力总量 + final String teamLevelContribution; + + /// 团队奖励算力总量 + final String teamBonusContribution; + + const ContributionStats({ + required this.totalUsers, + required this.totalAccounts, + required this.accountsWithContribution, + required this.totalAdoptions, + required this.totalContribution, + required this.personalContribution, + required this.teamLevelContribution, + required this.teamBonusContribution, + }); + + @override + List get props => [ + totalUsers, + totalAccounts, + accountsWithContribution, + totalAdoptions, + totalContribution, + personalContribution, + teamLevelContribution, + teamBonusContribution, + ]; +} diff --git a/frontend/mining-app/lib/domain/repositories/contribution_repository.dart b/frontend/mining-app/lib/domain/repositories/contribution_repository.dart index eaa2444f..4ad92506 100644 --- a/frontend/mining-app/lib/domain/repositories/contribution_repository.dart +++ b/frontend/mining-app/lib/domain/repositories/contribution_repository.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import '../../core/error/failures.dart'; import '../entities/contribution.dart'; import '../entities/contribution_record.dart'; +import '../entities/contribution_stats.dart'; abstract class ContributionRepository { Future> getUserContribution(String accountSequence); @@ -12,4 +13,5 @@ abstract class ContributionRepository { int page = 1, int pageSize = 50, }); + Future> getContributionStats(); } diff --git a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart index e2a31d53..b981120f 100644 --- a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart +++ b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart @@ -32,11 +32,16 @@ class ContributionPage extends ConsumerWidget { ); final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams)); + // 获取预估收益 + final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence)); + final statsAsync = ref.watch(contributionStatsProvider); + // Extract loading state and data from AsyncValue final isLoading = contributionAsync.isLoading; final contribution = contributionAsync.valueOrNull; final hasError = contributionAsync.hasError; final error = contributionAsync.error; + final isStatsLoading = statsAsync.isLoading; return Scaffold( backgroundColor: const Color(0xFFF5F5F5), @@ -46,6 +51,7 @@ class ContributionPage extends ConsumerWidget { onRefresh: () async { ref.invalidate(contributionProvider(accountSequence)); ref.invalidate(contributionRecordsProvider(recordsParams)); + ref.invalidate(contributionStatsProvider); }, child: hasError && contribution == null ? Center( @@ -79,7 +85,7 @@ class ContributionPage extends ConsumerWidget { _buildThreeColumnStats(contribution, isLoading), const SizedBox(height: 16), // 今日预估收益 - _buildTodayEstimateCard(contribution, isLoading), + _buildTodayEstimateCard(estimatedEarnings, isLoading || isStatsLoading), const SizedBox(height: 16), // 贡献值明细 _buildContributionDetailCard(context, ref, recordsAsync), @@ -259,13 +265,7 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) { - // 基于贡献值计算预估收益(暂时显示占位符,后续可接入实际计算API) - final totalContribution = double.tryParse(contribution?.totalContribution ?? '0') ?? 0; - // 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配 - // 这里先显示"--"表示暂无数据,后续可接入实际计算 - final hasContribution = totalContribution > 0; - + Widget _buildTodayEstimateCard(EstimatedEarnings earnings, bool isLoading) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -314,9 +314,9 @@ class ContributionPage extends ConsumerWidget { TextSpan( children: [ TextSpan( - text: hasContribution ? '计算中' : '--', - style: TextStyle( - fontSize: hasContribution ? 14 : 18, + text: earnings.isValid ? formatAmount(earnings.dailyShares) : '--', + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold, color: _green, ), diff --git a/frontend/mining-app/lib/presentation/providers/contribution_providers.dart b/frontend/mining-app/lib/presentation/providers/contribution_providers.dart index ab17eaa6..f2de9a15 100644 --- a/frontend/mining-app/lib/presentation/providers/contribution_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/contribution_providers.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/contribution.dart'; import '../../domain/entities/contribution_record.dart'; +import '../../domain/entities/contribution_stats.dart'; import '../../domain/usecases/contribution/get_user_contribution.dart'; import '../../domain/repositories/contribution_repository.dart'; import '../../core/di/injection.dart'; @@ -67,7 +68,12 @@ class ContributionRecordsParams { final contributionRecordsProvider = FutureProvider.family( (ref, params) async { // 空字符串不请求 - if (params.accountSequence.isEmpty) return null; + if (params.accountSequence.isEmpty) { + print('[ContributionRecordsProvider] accountSequence is empty, skipping request'); + return null; + } + + print('[ContributionRecordsProvider] Fetching records for ${params.accountSequence}, page=${params.page}, pageSize=${params.pageSize}'); final repository = ref.watch(contributionRepositoryProvider); final result = await repository.getContributionRecords( @@ -76,6 +82,12 @@ final contributionRecordsProvider = FutureProvider.family print('[ContributionRecordsProvider] Error: ${failure.message}'), + (records) => print('[ContributionRecordsProvider] Records count: ${records.data.length}, total: ${records.total}'), + ); + // 保持 provider 活跃 ref.keepAlive(); @@ -91,3 +103,88 @@ final contributionRecordsProvider = FutureProvider.family((ref) async { + final repository = ref.watch(contributionRepositoryProvider); + final result = await repository.getContributionStats(); + + // 保持 provider 活跃 + ref.keepAlive(); + + // 5 分钟后自动失效 + final timer = Timer(const Duration(minutes: 5), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) { + print('[ContributionStatsProvider] Error: ${failure.message}'); + return null; + }, + (stats) { + print('[ContributionStatsProvider] Total contribution: ${stats.totalContribution}'); + return stats; + }, + ); +}); + +/// 计算用户今日预估收益 +/// 公式: (用户贡献值 / 全网总贡献值) × 每日发放量 +/// 每日发放量 = 每秒发放量 × 86400 +class EstimatedEarnings { + final String dailyShares; + final String perSecondShares; + final bool isValid; + + const EstimatedEarnings({ + required this.dailyShares, + required this.perSecondShares, + required this.isValid, + }); + + static const zero = EstimatedEarnings( + dailyShares: '0', + perSecondShares: '0', + isValid: false, + ); +} + +/// 每日发放总量配置(积分股) +/// 第1纪元: 100M / (2年 × 365天) ≈ 136,986 积分股/天 +const double dailyAllocationTotal = 136986.0; + +final estimatedEarningsProvider = Provider.family((ref, accountSequence) { + final contributionAsync = ref.watch(contributionProvider(accountSequence)); + final statsAsync = ref.watch(contributionStatsProvider); + + final contribution = contributionAsync.valueOrNull; + final stats = statsAsync.valueOrNull; + + if (contribution == null || stats == null) { + return EstimatedEarnings.zero; + } + + final userContribution = double.tryParse(contribution.totalContribution) ?? 0; + final totalContribution = double.tryParse(stats.totalContribution) ?? 0; + + if (totalContribution <= 0 || userContribution <= 0) { + return EstimatedEarnings.zero; + } + + // 用户占比 + final ratio = userContribution / totalContribution; + + // 每日预估 + final dailyShares = ratio * dailyAllocationTotal; + + // 每秒预估 + final perSecondShares = dailyShares / 86400; + + return EstimatedEarnings( + dailyShares: dailyShares.toStringAsFixed(4), + perSecondShares: perSecondShares.toStringAsFixed(8), + isValid: true, + ); +});