fix(contribution): 使用 upsert 替代 update 避免记录不存在错误

将 addContribution 方法改为 upsert,当系统账户不存在时自动创建,
存在时增加算力余额。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-20 08:43:37 -08:00
parent 5ec310124d
commit 1d5e3ebff2
4 changed files with 27 additions and 33 deletions

View File

@ -285,8 +285,6 @@ export class ContributionCalculationService {
(sum, u) => sum.add(u.amount), (sum, u) => sum.add(u.amount),
new ContributionAmount(0), new ContributionAmount(0),
); );
// 确保 HEADQUARTERS 账户存在
await this.systemAccountRepository.ensureSystemAccountsExist();
await this.systemAccountRepository.addContribution('HEADQUARTERS', totalUnallocatedAmount); await this.systemAccountRepository.addContribution('HEADQUARTERS', totalUnallocatedAmount);
// 发布 HEADQUARTERS 账户同步事件 // 发布 HEADQUARTERS 账户同步事件

View File

@ -81,9 +81,21 @@ export class SystemAccountRepository {
accountType: SystemAccountType, accountType: SystemAccountType,
amount: ContributionAmount, amount: ContributionAmount,
): Promise<void> { ): Promise<void> {
await this.client.systemAccount.update({ const accountNames: Record<SystemAccountType, string> = {
OPERATION: '运营账户',
PROVINCE: '省公司账户',
CITY: '市公司账户',
HEADQUARTERS: '总部账户',
};
await this.client.systemAccount.upsert({
where: { accountType }, where: { accountType },
data: { create: {
accountType,
name: accountNames[accountType],
contributionBalance: amount.value,
},
update: {
contributionBalance: { increment: amount.value }, contributionBalance: { increment: amount.value },
}, },
}); });

View File

@ -21,9 +21,8 @@ class ContributionPage extends ConsumerWidget {
final user = ref.watch(userNotifierProvider); final user = ref.watch(userNotifierProvider);
final accountSequence = user.accountSequence ?? ''; final accountSequence = user.accountSequence ?? '';
final contributionAsync = ref.watch(contributionProvider(accountSequence)); final contributionAsync = ref.watch(contributionProvider(accountSequence));
// //
final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence)); final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence));
final statsAsync = ref.watch(contributionStatsProvider);
// //
final sharePoolAsync = ref.watch(sharePoolBalanceProvider); final sharePoolAsync = ref.watch(sharePoolBalanceProvider);
@ -32,7 +31,6 @@ class ContributionPage extends ConsumerWidget {
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;
final isSharePoolLoading = sharePoolAsync.isLoading; final isSharePoolLoading = sharePoolAsync.isLoading;
final sharePoolBalance = sharePoolAsync.valueOrNull; final sharePoolBalance = sharePoolAsync.valueOrNull;
@ -43,7 +41,6 @@ class ContributionPage extends ConsumerWidget {
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
ref.invalidate(contributionProvider(accountSequence)); ref.invalidate(contributionProvider(accountSequence));
ref.invalidate(contributionStatsProvider);
ref.invalidate(sharePoolBalanceProvider); ref.invalidate(sharePoolBalanceProvider);
}, },
child: hasError && contribution == null child: hasError && contribution == null
@ -78,7 +75,7 @@ class ContributionPage extends ConsumerWidget {
_buildThreeColumnStats(context, ref, contribution, isLoading), _buildThreeColumnStats(context, ref, contribution, isLoading),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildTodayEstimateCard(context, ref, estimatedEarnings, isLoading || isStatsLoading), _buildTodayEstimateCard(context, ref, estimatedEarnings, isLoading),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildContributionDetailCard(context, ref, contribution, isLoading), _buildContributionDetailCard(context, ref, contribution, isLoading),

View File

@ -8,6 +8,7 @@ import '../../domain/repositories/contribution_repository.dart';
import '../../core/di/injection.dart'; import '../../core/di/injection.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 'mining_providers.dart';
final getUserContributionUseCaseProvider = Provider<GetUserContribution>((ref) { final getUserContributionUseCaseProvider = Provider<GetUserContribution>((ref) {
return getIt<GetUserContribution>(); return getIt<GetUserContribution>();
@ -118,8 +119,7 @@ final contributionStatsProvider = FutureProvider<ContributionStats?>((ref) async
}); });
/// ///
/// : ( / ) × /// : × 60 × 60 × 24
/// = × 86400
class EstimatedEarnings { class EstimatedEarnings {
final String dailyShares; final String dailyShares;
final String perSecondShares; final String perSecondShares;
@ -138,40 +138,27 @@ class EstimatedEarnings {
); );
} }
///
/// 1: 100M / (2 × 365) 136,986 /
const double dailyAllocationTotal = 136986.0;
final estimatedEarningsProvider = Provider.family<EstimatedEarnings, String>((ref, accountSequence) { final estimatedEarningsProvider = Provider.family<EstimatedEarnings, String>((ref, accountSequence) {
final contributionAsync = ref.watch(contributionProvider(accountSequence)); // mining-service
final statsAsync = ref.watch(contributionStatsProvider); final shareAccountAsync = ref.watch(shareAccountProvider(accountSequence));
final shareAccount = shareAccountAsync.valueOrNull;
final contribution = contributionAsync.valueOrNull; if (shareAccount == null) {
final stats = statsAsync.valueOrNull;
if (contribution == null || stats == null) {
return EstimatedEarnings.zero; return EstimatedEarnings.zero;
} }
final userContribution = double.tryParse(contribution.totalContribution) ?? 0; final perSecondEarning = double.tryParse(shareAccount.perSecondEarning) ?? 0;
final totalContribution = double.tryParse(stats.totalContribution) ?? 0;
if (totalContribution <= 0 || userContribution <= 0) { if (perSecondEarning <= 0) {
return EstimatedEarnings.zero; return EstimatedEarnings.zero;
} }
// // = × 8640060×60×24
final ratio = userContribution / totalContribution; final dailyShares = perSecondEarning * 86400;
//
final dailyShares = ratio * dailyAllocationTotal;
//
final perSecondShares = dailyShares / 86400;
return EstimatedEarnings( return EstimatedEarnings(
dailyShares: dailyShares.toStringAsFixed(4), dailyShares: dailyShares.toStringAsFixed(4),
perSecondShares: perSecondShares.toStringAsFixed(8), perSecondShares: perSecondEarning.toStringAsFixed(8),
isValid: true, isValid: true,
); );
}); });