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';
|
static const String transferHistory = '/api/v2/trading/transfers/history';
|
||||||
|
|
||||||
// Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
|
// Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
|
||||||
|
static const String contributionStats = '/api/v2/contribution/stats';
|
||||||
static String contribution(String accountSequence) =>
|
static String contribution(String accountSequence) =>
|
||||||
'/api/v2/contribution/accounts/$accountSequence';
|
'/api/v2/contribution/accounts/$accountSequence';
|
||||||
static String contributionRecords(String accountSequence) =>
|
static String contributionRecords(String accountSequence) =>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import '../../models/contribution_model.dart';
|
import '../../models/contribution_model.dart';
|
||||||
import '../../models/contribution_record_model.dart';
|
import '../../models/contribution_record_model.dart';
|
||||||
|
import '../../models/contribution_stats_model.dart';
|
||||||
import '../../../core/network/api_client.dart';
|
import '../../../core/network/api_client.dart';
|
||||||
import '../../../core/network/api_endpoints.dart';
|
import '../../../core/network/api_endpoints.dart';
|
||||||
import '../../../core/error/exceptions.dart';
|
import '../../../core/error/exceptions.dart';
|
||||||
|
|
@ -14,6 +15,7 @@ abstract class ContributionRemoteDataSource {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 50,
|
int pageSize = 50,
|
||||||
});
|
});
|
||||||
|
Future<ContributionStatsModel> getContributionStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource {
|
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) {
|
String _sourceTypeToString(ContributionSourceType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ContributionSourceType.personal:
|
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 'package:dartz/dartz.dart';
|
||||||
import '../../domain/entities/contribution.dart';
|
import '../../domain/entities/contribution.dart';
|
||||||
import '../../domain/entities/contribution_record.dart';
|
import '../../domain/entities/contribution_record.dart';
|
||||||
|
import '../../domain/entities/contribution_stats.dart';
|
||||||
import '../../domain/repositories/contribution_repository.dart';
|
import '../../domain/repositories/contribution_repository.dart';
|
||||||
import '../../core/error/exceptions.dart';
|
import '../../core/error/exceptions.dart';
|
||||||
import '../../core/error/failures.dart';
|
import '../../core/error/failures.dart';
|
||||||
|
|
@ -46,4 +47,16 @@ class ContributionRepositoryImpl implements ContributionRepository {
|
||||||
return Left(const NetworkFailure());
|
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 '../../core/error/failures.dart';
|
||||||
import '../entities/contribution.dart';
|
import '../entities/contribution.dart';
|
||||||
import '../entities/contribution_record.dart';
|
import '../entities/contribution_record.dart';
|
||||||
|
import '../entities/contribution_stats.dart';
|
||||||
|
|
||||||
abstract class ContributionRepository {
|
abstract class ContributionRepository {
|
||||||
Future<Either<Failure, Contribution>> getUserContribution(String accountSequence);
|
Future<Either<Failure, Contribution>> getUserContribution(String accountSequence);
|
||||||
|
|
@ -12,4 +13,5 @@ abstract class ContributionRepository {
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 50,
|
int pageSize = 50,
|
||||||
});
|
});
|
||||||
|
Future<Either<Failure, ContributionStats>> getContributionStats();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,16 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams));
|
final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams));
|
||||||
|
|
||||||
|
// 获取预估收益
|
||||||
|
final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence));
|
||||||
|
final statsAsync = ref.watch(contributionStatsProvider);
|
||||||
|
|
||||||
// Extract loading state and data from AsyncValue
|
// Extract loading state and data from AsyncValue
|
||||||
final isLoading = contributionAsync.isLoading;
|
final isLoading = contributionAsync.isLoading;
|
||||||
final contribution = contributionAsync.valueOrNull;
|
final contribution = contributionAsync.valueOrNull;
|
||||||
final hasError = contributionAsync.hasError;
|
final hasError = contributionAsync.hasError;
|
||||||
final error = contributionAsync.error;
|
final error = contributionAsync.error;
|
||||||
|
final isStatsLoading = statsAsync.isLoading;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
|
@ -46,6 +51,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(contributionProvider(accountSequence));
|
ref.invalidate(contributionProvider(accountSequence));
|
||||||
ref.invalidate(contributionRecordsProvider(recordsParams));
|
ref.invalidate(contributionRecordsProvider(recordsParams));
|
||||||
|
ref.invalidate(contributionStatsProvider);
|
||||||
},
|
},
|
||||||
child: hasError && contribution == null
|
child: hasError && contribution == null
|
||||||
? Center(
|
? Center(
|
||||||
|
|
@ -79,7 +85,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
_buildThreeColumnStats(contribution, isLoading),
|
_buildThreeColumnStats(contribution, isLoading),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 今日预估收益
|
// 今日预估收益
|
||||||
_buildTodayEstimateCard(contribution, isLoading),
|
_buildTodayEstimateCard(estimatedEarnings, isLoading || isStatsLoading),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 贡献值明细
|
// 贡献值明细
|
||||||
_buildContributionDetailCard(context, ref, recordsAsync),
|
_buildContributionDetailCard(context, ref, recordsAsync),
|
||||||
|
|
@ -259,13 +265,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) {
|
Widget _buildTodayEstimateCard(EstimatedEarnings earnings, bool isLoading) {
|
||||||
// 基于贡献值计算预估收益(暂时显示占位符,后续可接入实际计算API)
|
|
||||||
final totalContribution = double.tryParse(contribution?.totalContribution ?? '0') ?? 0;
|
|
||||||
// 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配
|
|
||||||
// 这里先显示"--"表示暂无数据,后续可接入实际计算
|
|
||||||
final hasContribution = totalContribution > 0;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -314,9 +314,9 @@ class ContributionPage extends ConsumerWidget {
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: hasContribution ? '计算中' : '--',
|
text: earnings.isValid ? formatAmount(earnings.dailyShares) : '--',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: hasContribution ? 14 : 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: _green,
|
color: _green,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/contribution.dart';
|
import '../../domain/entities/contribution.dart';
|
||||||
import '../../domain/entities/contribution_record.dart';
|
import '../../domain/entities/contribution_record.dart';
|
||||||
|
import '../../domain/entities/contribution_stats.dart';
|
||||||
import '../../domain/usecases/contribution/get_user_contribution.dart';
|
import '../../domain/usecases/contribution/get_user_contribution.dart';
|
||||||
import '../../domain/repositories/contribution_repository.dart';
|
import '../../domain/repositories/contribution_repository.dart';
|
||||||
import '../../core/di/injection.dart';
|
import '../../core/di/injection.dart';
|
||||||
|
|
@ -67,7 +68,12 @@ class ContributionRecordsParams {
|
||||||
final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPage?, ContributionRecordsParams>(
|
final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPage?, ContributionRecordsParams>(
|
||||||
(ref, params) async {
|
(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 repository = ref.watch(contributionRepositoryProvider);
|
||||||
final result = await repository.getContributionRecords(
|
final result = await repository.getContributionRecords(
|
||||||
|
|
@ -76,6 +82,12 @@ final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPag
|
||||||
pageSize: params.pageSize,
|
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 活跃
|
// 保持 provider 活跃
|
||||||
ref.keepAlive();
|
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