feat(mining-app): add estimated earnings and contribution stats API
- Add ContributionStats entity and model for network-wide statistics - Add /api/v2/contribution/stats endpoint - Implement estimatedEarningsProvider to calculate daily earnings - Formula: (user contribution / total contribution) × daily allocation - Update contribution page to display real estimated earnings - Add debug logs for contribution records API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6bcb4af028
commit
ed9f817fae
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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<ContributionStatsModel> getContributionStats();
|
||||
}
|
||||
|
||||
class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource {
|
||||
|
|
@ -60,6 +62,16 @@ class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ContributionStatsModel> 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:
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> json) {
|
||||
final contributionByType = json['contributionByType'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Either<Failure, ContributionStats>> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Object?> get props => [
|
||||
totalUsers,
|
||||
totalAccounts,
|
||||
accountsWithContribution,
|
||||
totalAdoptions,
|
||||
totalContribution,
|
||||
personalContribution,
|
||||
teamLevelContribution,
|
||||
teamBonusContribution,
|
||||
];
|
||||
}
|
||||
|
|
@ -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<Either<Failure, Contribution>> getUserContribution(String accountSequence);
|
||||
|
|
@ -12,4 +13,5 @@ abstract class ContributionRepository {
|
|||
int page = 1,
|
||||
int pageSize = 50,
|
||||
});
|
||||
Future<Either<Failure, ContributionStats>> getContributionStats();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<ContributionRecordsPage?, ContributionRecordsParams>(
|
||||
(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<ContributionRecordsPag
|
|||
pageSize: params.pageSize,
|
||||
);
|
||||
|
||||
print('[ContributionRecordsProvider] Result: ${result.isRight() ? "success" : "failed"}');
|
||||
result.fold(
|
||||
(failure) => 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<ContributionRecordsPag
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// 全网贡献值统计 Provider
|
||||
final contributionStatsProvider = FutureProvider<ContributionStats?>((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<EstimatedEarnings, String>((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,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue