feat(dashboard): add detailed contribution breakdown by category
Backend (contribution-service): - Add getDetailedContributionStats() to repository - Add getUnallocatedByLevelTier/BonusTier() to repository - Extend stats API with level/bonus breakdown by tier - Add getTotalTrees() to synced-data repository Backend (mining-admin-service): - Add detailed contribution stats calculation - Calculate theoretical vs actual values per category - Return level/bonus breakdown with unlocked/pending amounts Frontend (mining-admin-web): - Add ContributionBreakdown component showing: - Personal (70%), Operation (12%), Province (1%), City (2%) - Level contribution (7.5%) by tier: 1-5, 6-10, 11-15 - Bonus contribution (7.5%) by tier: T1, T2, T3 - Update DashboardStats type definition - Integrate breakdown component into dashboard page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1f15daa6c5
commit
cfbf1b21f3
|
|
@ -1,4 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import Decimal from 'decimal.js';
|
||||
import { ContributionAccountRepository } from '../../infrastructure/persistence/repositories/contribution-account.repository';
|
||||
import { ContributionRecordRepository } from '../../infrastructure/persistence/repositories/contribution-record.repository';
|
||||
import { UnallocatedContributionRepository } from '../../infrastructure/persistence/repositories/unallocated-contribution.repository';
|
||||
|
|
@ -6,6 +7,15 @@ import { SystemAccountRepository } from '../../infrastructure/persistence/reposi
|
|||
import { SyncedDataRepository } from '../../infrastructure/persistence/repositories/synced-data.repository';
|
||||
import { ContributionSourceType } from '../../domain/aggregates/contribution-account.aggregate';
|
||||
|
||||
// 基准算力常量
|
||||
const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617');
|
||||
const RATE_PERSONAL = new Decimal('0.70');
|
||||
const RATE_OPERATION = new Decimal('0.12');
|
||||
const RATE_PROVINCE = new Decimal('0.01');
|
||||
const RATE_CITY = new Decimal('0.02');
|
||||
const RATE_LEVEL_TOTAL = new Decimal('0.075');
|
||||
const RATE_BONUS_TOTAL = new Decimal('0.075');
|
||||
|
||||
export interface ContributionStatsDto {
|
||||
// 用户统计
|
||||
totalUsers: number;
|
||||
|
|
@ -16,17 +26,57 @@ export interface ContributionStatsDto {
|
|||
totalAdoptions: number;
|
||||
processedAdoptions: number;
|
||||
unprocessedAdoptions: number;
|
||||
totalTrees: number;
|
||||
|
||||
// 算力统计
|
||||
totalContribution: string;
|
||||
|
||||
// 算力分布
|
||||
// 算力分布(基础)
|
||||
contributionByType: {
|
||||
personal: string;
|
||||
teamLevel: string;
|
||||
teamBonus: string;
|
||||
};
|
||||
|
||||
// ========== 详细算力分解(按用户需求) ==========
|
||||
// 全网算力 = 总认种树 * 22617
|
||||
networkTotalContribution: string;
|
||||
// 个人用户总算力 = 总认种树 * (22617 * 70%)
|
||||
personalTotalContribution: string;
|
||||
// 运营账户总算力 = 总认种树 * (22617 * 12%)
|
||||
operationTotalContribution: string;
|
||||
// 省公司总算力 = 总认种树 * (22617 * 1%)
|
||||
provinceTotalContribution: string;
|
||||
// 市公司总算力 = 总认种树 * (22617 * 2%)
|
||||
cityTotalContribution: string;
|
||||
|
||||
// 层级算力详情 (7.5%)
|
||||
levelContribution: {
|
||||
total: string;
|
||||
unlocked: string;
|
||||
pending: string;
|
||||
byTier: {
|
||||
// 1档: 1-5级
|
||||
tier1: { unlocked: string; pending: string };
|
||||
// 2档: 6-10级
|
||||
tier2: { unlocked: string; pending: string };
|
||||
// 3档: 11-15级
|
||||
tier3: { unlocked: string; pending: string };
|
||||
};
|
||||
};
|
||||
|
||||
// 团队奖励算力详情 (7.5%)
|
||||
bonusContribution: {
|
||||
total: string;
|
||||
unlocked: string;
|
||||
pending: string;
|
||||
byTier: {
|
||||
tier1: { unlocked: string; pending: string };
|
||||
tier2: { unlocked: string; pending: string };
|
||||
tier3: { unlocked: string; pending: string };
|
||||
};
|
||||
};
|
||||
|
||||
// 系统账户
|
||||
systemAccounts: {
|
||||
accountType: string;
|
||||
|
|
@ -61,6 +111,10 @@ export class GetContributionStatsQuery {
|
|||
systemAccounts,
|
||||
totalUnallocated,
|
||||
unallocatedByType,
|
||||
detailedStats,
|
||||
unallocatedByLevelTier,
|
||||
unallocatedByBonusTier,
|
||||
totalTrees,
|
||||
] = await Promise.all([
|
||||
this.syncedDataRepository.countUsers(),
|
||||
this.accountRepository.countAccounts(),
|
||||
|
|
@ -72,8 +126,33 @@ export class GetContributionStatsQuery {
|
|||
this.systemAccountRepository.findAll(),
|
||||
this.unallocatedRepository.getTotalUnallocated(),
|
||||
this.unallocatedRepository.getTotalUnallocatedByType(),
|
||||
this.accountRepository.getDetailedContributionStats(),
|
||||
this.unallocatedRepository.getUnallocatedByLevelTier(),
|
||||
this.unallocatedRepository.getUnallocatedByBonusTier(),
|
||||
this.syncedDataRepository.getTotalTrees(),
|
||||
]);
|
||||
|
||||
// 计算理论算力(基于总认种树 * 基准算力)
|
||||
const networkTotal = BASE_CONTRIBUTION_PER_TREE.mul(totalTrees);
|
||||
const personalTotal = networkTotal.mul(RATE_PERSONAL);
|
||||
const operationTotal = networkTotal.mul(RATE_OPERATION);
|
||||
const provinceTotal = networkTotal.mul(RATE_PROVINCE);
|
||||
const cityTotal = networkTotal.mul(RATE_CITY);
|
||||
const levelTotal = networkTotal.mul(RATE_LEVEL_TOTAL);
|
||||
const bonusTotal = networkTotal.mul(RATE_BONUS_TOTAL);
|
||||
|
||||
// 层级算力: 已解锁 + 未解锁
|
||||
const levelUnlocked = new Decimal(detailedStats.levelUnlocked);
|
||||
const levelPending = new Decimal(unallocatedByLevelTier.tier1)
|
||||
.plus(unallocatedByLevelTier.tier2)
|
||||
.plus(unallocatedByLevelTier.tier3);
|
||||
|
||||
// 团队奖励算力: 已解锁 + 未解锁
|
||||
const bonusUnlocked = new Decimal(detailedStats.bonusUnlocked);
|
||||
const bonusPending = new Decimal(unallocatedByBonusTier.tier1)
|
||||
.plus(unallocatedByBonusTier.tier2)
|
||||
.plus(unallocatedByBonusTier.tier3);
|
||||
|
||||
return {
|
||||
totalUsers,
|
||||
totalAccounts,
|
||||
|
|
@ -81,12 +160,63 @@ export class GetContributionStatsQuery {
|
|||
totalAdoptions,
|
||||
processedAdoptions: totalAdoptions - undistributedAdoptions,
|
||||
unprocessedAdoptions: undistributedAdoptions,
|
||||
totalTrees,
|
||||
totalContribution: totalContribution.value.toString(),
|
||||
contributionByType: {
|
||||
personal: (contributionByType.get(ContributionSourceType.PERSONAL)?.value || 0).toString(),
|
||||
teamLevel: (contributionByType.get(ContributionSourceType.TEAM_LEVEL)?.value || 0).toString(),
|
||||
teamBonus: (contributionByType.get(ContributionSourceType.TEAM_BONUS)?.value || 0).toString(),
|
||||
},
|
||||
|
||||
// 详细算力分解
|
||||
networkTotalContribution: networkTotal.toString(),
|
||||
personalTotalContribution: personalTotal.toString(),
|
||||
operationTotalContribution: operationTotal.toString(),
|
||||
provinceTotalContribution: provinceTotal.toString(),
|
||||
cityTotalContribution: cityTotal.toString(),
|
||||
|
||||
// 层级算力详情
|
||||
levelContribution: {
|
||||
total: levelTotal.toString(),
|
||||
unlocked: levelUnlocked.toString(),
|
||||
pending: levelPending.toString(),
|
||||
byTier: {
|
||||
tier1: {
|
||||
unlocked: detailedStats.levelByTier.tier1.unlocked,
|
||||
pending: unallocatedByLevelTier.tier1,
|
||||
},
|
||||
tier2: {
|
||||
unlocked: detailedStats.levelByTier.tier2.unlocked,
|
||||
pending: unallocatedByLevelTier.tier2,
|
||||
},
|
||||
tier3: {
|
||||
unlocked: detailedStats.levelByTier.tier3.unlocked,
|
||||
pending: unallocatedByLevelTier.tier3,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 团队奖励算力详情
|
||||
bonusContribution: {
|
||||
total: bonusTotal.toString(),
|
||||
unlocked: bonusUnlocked.toString(),
|
||||
pending: bonusPending.toString(),
|
||||
byTier: {
|
||||
tier1: {
|
||||
unlocked: detailedStats.bonusByTier.tier1.unlocked,
|
||||
pending: unallocatedByBonusTier.tier1,
|
||||
},
|
||||
tier2: {
|
||||
unlocked: detailedStats.bonusByTier.tier2.unlocked,
|
||||
pending: unallocatedByBonusTier.tier2,
|
||||
},
|
||||
tier3: {
|
||||
unlocked: detailedStats.bonusByTier.tier3.unlocked,
|
||||
pending: unallocatedByBonusTier.tier3,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
systemAccounts: systemAccounts.map((a) => ({
|
||||
accountType: a.accountType,
|
||||
name: a.name,
|
||||
|
|
@ -98,4 +228,5 @@ export class GetContributionStatsQuery {
|
|||
),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,6 +233,107 @@ export class ContributionAccountRepository implements IContributionAccountReposi
|
|||
return records.map((r) => this.toDomain(r));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细算力汇总(按类型分解)
|
||||
*/
|
||||
async getDetailedContributionStats(): Promise<{
|
||||
// 个人算力总计
|
||||
personalTotal: string;
|
||||
// 层级算力 - 已解锁(已分配给上线)
|
||||
levelUnlocked: string;
|
||||
// 层级算力 - 未解锁(待解锁的pending)
|
||||
levelPending: string;
|
||||
// 层级按档位分解
|
||||
levelByTier: {
|
||||
tier1: { unlocked: string; pending: string }; // 1-5级
|
||||
tier2: { unlocked: string; pending: string }; // 6-10级
|
||||
tier3: { unlocked: string; pending: string }; // 11-15级
|
||||
};
|
||||
// 团队奖励算力 - 已解锁
|
||||
bonusUnlocked: string;
|
||||
// 团队奖励算力 - 未解锁
|
||||
bonusPending: string;
|
||||
// 团队奖励按档位分解
|
||||
bonusByTier: {
|
||||
tier1: { unlocked: string; pending: string };
|
||||
tier2: { unlocked: string; pending: string };
|
||||
tier3: { unlocked: string; pending: string };
|
||||
};
|
||||
}> {
|
||||
const result = await this.client.contributionAccount.aggregate({
|
||||
_sum: {
|
||||
personalContribution: true,
|
||||
// 层级 1-5
|
||||
level1Pending: true,
|
||||
level2Pending: true,
|
||||
level3Pending: true,
|
||||
level4Pending: true,
|
||||
level5Pending: true,
|
||||
// 层级 6-10
|
||||
level6Pending: true,
|
||||
level7Pending: true,
|
||||
level8Pending: true,
|
||||
level9Pending: true,
|
||||
level10Pending: true,
|
||||
// 层级 11-15
|
||||
level11Pending: true,
|
||||
level12Pending: true,
|
||||
level13Pending: true,
|
||||
level14Pending: true,
|
||||
level15Pending: true,
|
||||
// 团队奖励
|
||||
bonusTier1Pending: true,
|
||||
bonusTier2Pending: true,
|
||||
bonusTier3Pending: true,
|
||||
// 汇总
|
||||
totalLevelPending: true,
|
||||
totalBonusPending: true,
|
||||
totalUnlocked: true,
|
||||
},
|
||||
});
|
||||
|
||||
const sum = result._sum;
|
||||
|
||||
// 层级 1-5 已解锁(在pending字段中存储的是已分配给该用户的层级算力)
|
||||
const level1to5 = new Decimal(sum.level1Pending || 0)
|
||||
.plus(sum.level2Pending || 0)
|
||||
.plus(sum.level3Pending || 0)
|
||||
.plus(sum.level4Pending || 0)
|
||||
.plus(sum.level5Pending || 0);
|
||||
|
||||
// 层级 6-10
|
||||
const level6to10 = new Decimal(sum.level6Pending || 0)
|
||||
.plus(sum.level7Pending || 0)
|
||||
.plus(sum.level8Pending || 0)
|
||||
.plus(sum.level9Pending || 0)
|
||||
.plus(sum.level10Pending || 0);
|
||||
|
||||
// 层级 11-15
|
||||
const level11to15 = new Decimal(sum.level11Pending || 0)
|
||||
.plus(sum.level12Pending || 0)
|
||||
.plus(sum.level13Pending || 0)
|
||||
.plus(sum.level14Pending || 0)
|
||||
.plus(sum.level15Pending || 0);
|
||||
|
||||
return {
|
||||
personalTotal: (sum.personalContribution || new Decimal(0)).toString(),
|
||||
levelUnlocked: (sum.totalLevelPending || new Decimal(0)).toString(),
|
||||
levelPending: '0', // 未解锁的存储在 unallocated 表中
|
||||
levelByTier: {
|
||||
tier1: { unlocked: level1to5.toString(), pending: '0' },
|
||||
tier2: { unlocked: level6to10.toString(), pending: '0' },
|
||||
tier3: { unlocked: level11to15.toString(), pending: '0' },
|
||||
},
|
||||
bonusUnlocked: (sum.totalBonusPending || new Decimal(0)).toString(),
|
||||
bonusPending: '0', // 未解锁的存储在 unallocated 表中
|
||||
bonusByTier: {
|
||||
tier1: { unlocked: (sum.bonusTier1Pending || new Decimal(0)).toString(), pending: '0' },
|
||||
tier2: { unlocked: (sum.bonusTier2Pending || new Decimal(0)).toString(), pending: '0' },
|
||||
tier3: { unlocked: (sum.bonusTier3Pending || new Decimal(0)).toString(), pending: '0' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private toDomain(record: any): ContributionAccountAggregate {
|
||||
return ContributionAccountAggregate.fromPersistence({
|
||||
id: record.id,
|
||||
|
|
|
|||
|
|
@ -461,6 +461,16 @@ export class SyncedDataRepository implements ISyncedDataRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getTotalTrees(): Promise<number> {
|
||||
const result = await this.client.syncedAdoption.aggregate({
|
||||
where: {
|
||||
status: 'MINING_ENABLED', // 只统计最终成功的认种订单
|
||||
},
|
||||
_sum: { treeCount: true },
|
||||
});
|
||||
return result._sum.treeCount ?? 0;
|
||||
}
|
||||
|
||||
// ========== 私有方法 ==========
|
||||
|
||||
private toSyncedUser(record: any): SyncedUser {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,81 @@ export class UnallocatedContributionRepository {
|
|||
return records.map((r) => this.toDomain(r));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分层级的未分配算力统计
|
||||
*/
|
||||
async getUnallocatedByLevelTier(): Promise<{
|
||||
tier1: string; // 1-5级未分配
|
||||
tier2: string; // 6-10级未分配
|
||||
tier3: string; // 11-15级未分配
|
||||
}> {
|
||||
const results = await this.client.unallocatedContribution.groupBy({
|
||||
by: ['levelDepth'],
|
||||
where: {
|
||||
levelDepth: { not: null },
|
||||
status: 'PENDING',
|
||||
},
|
||||
_sum: { amount: true },
|
||||
});
|
||||
|
||||
let tier1 = new ContributionAmount(0);
|
||||
let tier2 = new ContributionAmount(0);
|
||||
let tier3 = new ContributionAmount(0);
|
||||
|
||||
for (const item of results) {
|
||||
const depth = item.levelDepth!;
|
||||
const amount = new ContributionAmount(item._sum.amount || 0);
|
||||
if (depth >= 1 && depth <= 5) {
|
||||
tier1 = tier1.add(amount);
|
||||
} else if (depth >= 6 && depth <= 10) {
|
||||
tier2 = tier2.add(amount);
|
||||
} else if (depth >= 11 && depth <= 15) {
|
||||
tier3 = tier3.add(amount);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tier1: tier1.value.toString(),
|
||||
tier2: tier2.value.toString(),
|
||||
tier3: tier3.value.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分档位的未分配奖励统计
|
||||
*/
|
||||
async getUnallocatedByBonusTier(): Promise<{
|
||||
tier1: string;
|
||||
tier2: string;
|
||||
tier3: string;
|
||||
}> {
|
||||
const results = await this.client.unallocatedContribution.groupBy({
|
||||
by: ['unallocType'],
|
||||
where: {
|
||||
unallocType: { startsWith: 'BONUS_TIER_' },
|
||||
status: 'PENDING',
|
||||
},
|
||||
_sum: { amount: true },
|
||||
});
|
||||
|
||||
let tier1 = '0';
|
||||
let tier2 = '0';
|
||||
let tier3 = '0';
|
||||
|
||||
for (const item of results) {
|
||||
const amount = (item._sum.amount || 0).toString();
|
||||
if (item.unallocType === 'BONUS_TIER_1') {
|
||||
tier1 = amount;
|
||||
} else if (item.unallocType === 'BONUS_TIER_2') {
|
||||
tier2 = amount;
|
||||
} else if (item.unallocType === 'BONUS_TIER_3') {
|
||||
tier3 = amount;
|
||||
}
|
||||
}
|
||||
|
||||
return { tier1, tier2, tier3 };
|
||||
}
|
||||
|
||||
private toDomain(record: any): UnallocatedContribution {
|
||||
return {
|
||||
id: record.id,
|
||||
|
|
|
|||
|
|
@ -26,15 +26,19 @@ export class DashboardController {
|
|||
priceChange24h = (close - open) / open;
|
||||
}
|
||||
|
||||
// 详细算力分解数据
|
||||
const dc = raw.detailedContribution || {};
|
||||
|
||||
// 转换为前端期望的格式
|
||||
return {
|
||||
// 基础统计
|
||||
totalUsers: raw.users?.total || 0,
|
||||
adoptedUsers: raw.users?.adopted || 0,
|
||||
totalTrees: raw.contribution?.totalTrees || 0,
|
||||
networkEffectiveContribution: raw.contribution?.effectiveContribution || '0',
|
||||
networkTotalContribution: raw.contribution?.totalContribution || '0',
|
||||
networkLevelPending: raw.contribution?.teamLevelContribution || '0',
|
||||
networkBonusPending: raw.contribution?.teamBonusContribution || '0',
|
||||
networkLevelPending: dc.levelContribution?.pending || '0',
|
||||
networkBonusPending: dc.bonusContribution?.pending || '0',
|
||||
totalDistributed: raw.mining?.totalMined || '0',
|
||||
totalBurned: raw.mining?.latestDailyStat?.totalBurned || '0',
|
||||
circulationPool: raw.trading?.circulationPool?.totalShares || '0',
|
||||
|
|
@ -42,6 +46,47 @@ export class DashboardController {
|
|||
priceChange24h,
|
||||
totalOrders: raw.trading?.totalAccounts || 0,
|
||||
totalTrades: raw.trading?.totalAccounts || 0,
|
||||
|
||||
// ========== 详细算力分解 ==========
|
||||
detailedContribution: {
|
||||
totalTrees: dc.totalTrees || 0,
|
||||
// 全网算力(理论值)= 总树数 * 22617
|
||||
networkTotalTheory: dc.networkTotalTheory || '0',
|
||||
// 个人算力(70%)
|
||||
personalTheory: dc.personalTheory || '0',
|
||||
personalActual: raw.contribution?.personalContribution || '0',
|
||||
// 运营账户(12%)
|
||||
operationTheory: dc.operationTheory || '0',
|
||||
operationActual: dc.operationActual || '0',
|
||||
// 省公司(1%)
|
||||
provinceTheory: dc.provinceTheory || '0',
|
||||
provinceActual: dc.provinceActual || '0',
|
||||
// 市公司(2%)
|
||||
cityTheory: dc.cityTheory || '0',
|
||||
cityActual: dc.cityActual || '0',
|
||||
|
||||
// 层级算力(7.5%)
|
||||
level: {
|
||||
theory: dc.levelTheory || '0',
|
||||
unlocked: dc.levelContribution?.unlocked || '0',
|
||||
pending: dc.levelContribution?.pending || '0',
|
||||
// 分档详情
|
||||
tier1: dc.levelContribution?.byTier?.tier1 || { unlocked: '0', pending: '0' },
|
||||
tier2: dc.levelContribution?.byTier?.tier2 || { unlocked: '0', pending: '0' },
|
||||
tier3: dc.levelContribution?.byTier?.tier3 || { unlocked: '0', pending: '0' },
|
||||
},
|
||||
|
||||
// 团队奖励算力(7.5%)
|
||||
bonus: {
|
||||
theory: dc.bonusTheory || '0',
|
||||
unlocked: dc.bonusContribution?.unlocked || '0',
|
||||
pending: dc.bonusContribution?.pending || '0',
|
||||
// 分档详情
|
||||
tier1: dc.bonusContribution?.byTier?.tier1 || { unlocked: '0', pending: '0' },
|
||||
tier2: dc.bonusContribution?.byTier?.tier2 || { unlocked: '0', pending: '0' },
|
||||
tier3: dc.bonusContribution?.byTier?.tier3 || { unlocked: '0', pending: '0' },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Decimal } from 'decimal.js';
|
||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||
|
||||
// 基准算力常量
|
||||
const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617');
|
||||
const RATE_PERSONAL = new Decimal('0.70');
|
||||
const RATE_OPERATION = new Decimal('0.12');
|
||||
const RATE_PROVINCE = new Decimal('0.01');
|
||||
const RATE_CITY = new Decimal('0.02');
|
||||
const RATE_LEVEL_TOTAL = new Decimal('0.075');
|
||||
const RATE_BONUS_TOTAL = new Decimal('0.075');
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
private readonly logger = new Logger(DashboardService.name);
|
||||
|
|
@ -23,6 +33,7 @@ export class DashboardService {
|
|||
tradingStats,
|
||||
latestReport,
|
||||
latestKLine,
|
||||
detailedContributionStats,
|
||||
] = await Promise.all([
|
||||
this.getUserStats(),
|
||||
this.getContributionStats(),
|
||||
|
|
@ -30,6 +41,7 @@ export class DashboardService {
|
|||
this.getTradingStats(),
|
||||
this.prisma.dailyReport.findFirst({ orderBy: { reportDate: 'desc' } }),
|
||||
this.prisma.syncedDayKLine.findFirst({ orderBy: { klineDate: 'desc' } }),
|
||||
this.getDetailedContributionStats(),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
@ -37,6 +49,7 @@ export class DashboardService {
|
|||
contribution: contributionStats,
|
||||
mining: miningStats,
|
||||
trading: tradingStats,
|
||||
detailedContribution: detailedContributionStats,
|
||||
latestReport: latestReport
|
||||
? this.formatDailyReport(latestReport)
|
||||
: null,
|
||||
|
|
@ -128,6 +141,7 @@ export class DashboardService {
|
|||
_count: true,
|
||||
}),
|
||||
this.prisma.syncedAdoption.aggregate({
|
||||
where: { status: 'MINING_ENABLED' },
|
||||
_sum: { treeCount: true },
|
||||
_count: true,
|
||||
}),
|
||||
|
|
@ -152,6 +166,137 @@ export class DashboardService {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细算力分解统计(按用户需求)
|
||||
*/
|
||||
private async getDetailedContributionStats() {
|
||||
// 获取总树数
|
||||
const adoptionStats = await this.prisma.syncedAdoption.aggregate({
|
||||
where: { status: 'MINING_ENABLED' },
|
||||
_sum: { treeCount: true },
|
||||
});
|
||||
const totalTrees = adoptionStats._sum.treeCount || 0;
|
||||
|
||||
// 按层级统计已分配的层级算力
|
||||
const levelRecords = await this.prisma.syncedContributionRecord.groupBy({
|
||||
by: ['levelDepth'],
|
||||
where: {
|
||||
sourceType: 'TEAM_LEVEL',
|
||||
levelDepth: { not: null },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
});
|
||||
|
||||
// 按档位统计已分配的团队奖励算力
|
||||
const bonusRecords = await this.prisma.syncedContributionRecord.groupBy({
|
||||
by: ['bonusTier'],
|
||||
where: {
|
||||
sourceType: 'TEAM_BONUS',
|
||||
bonusTier: { not: null },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
});
|
||||
|
||||
// 获取系统账户按类型的算力
|
||||
const systemAccounts = await this.prisma.syncedSystemContribution.findMany();
|
||||
|
||||
// 汇总层级1-5, 6-10, 11-15
|
||||
let levelTier1 = new Decimal(0);
|
||||
let levelTier2 = new Decimal(0);
|
||||
let levelTier3 = new Decimal(0);
|
||||
for (const record of levelRecords) {
|
||||
const depth = record.levelDepth!;
|
||||
const amount = new Decimal(record._sum.amount || 0);
|
||||
if (depth >= 1 && depth <= 5) levelTier1 = levelTier1.plus(amount);
|
||||
else if (depth >= 6 && depth <= 10) levelTier2 = levelTier2.plus(amount);
|
||||
else if (depth >= 11 && depth <= 15) levelTier3 = levelTier3.plus(amount);
|
||||
}
|
||||
|
||||
// 汇总团队奖励档位
|
||||
let bonusTier1 = new Decimal(0);
|
||||
let bonusTier2 = new Decimal(0);
|
||||
let bonusTier3 = new Decimal(0);
|
||||
for (const record of bonusRecords) {
|
||||
const tier = record.bonusTier!;
|
||||
const amount = new Decimal(record._sum.amount || 0);
|
||||
if (tier === 1) bonusTier1 = amount;
|
||||
else if (tier === 2) bonusTier2 = amount;
|
||||
else if (tier === 3) bonusTier3 = amount;
|
||||
}
|
||||
|
||||
const levelUnlocked = levelTier1.plus(levelTier2).plus(levelTier3);
|
||||
const bonusUnlocked = bonusTier1.plus(bonusTier2).plus(bonusTier3);
|
||||
|
||||
// 计算理论值
|
||||
const networkTotal = BASE_CONTRIBUTION_PER_TREE.mul(totalTrees);
|
||||
const personalTheory = networkTotal.mul(RATE_PERSONAL);
|
||||
const operationTheory = networkTotal.mul(RATE_OPERATION);
|
||||
const provinceTheory = networkTotal.mul(RATE_PROVINCE);
|
||||
const cityTheory = networkTotal.mul(RATE_CITY);
|
||||
const levelTheory = networkTotal.mul(RATE_LEVEL_TOTAL);
|
||||
const bonusTheory = networkTotal.mul(RATE_BONUS_TOTAL);
|
||||
|
||||
// 计算未解锁(理论 - 已解锁)
|
||||
const levelPending = levelTheory.minus(levelUnlocked).greaterThan(0)
|
||||
? levelTheory.minus(levelUnlocked)
|
||||
: new Decimal(0);
|
||||
const bonusPending = bonusTheory.minus(bonusUnlocked).greaterThan(0)
|
||||
? bonusTheory.minus(bonusUnlocked)
|
||||
: new Decimal(0);
|
||||
|
||||
// 系统账户按类型汇总
|
||||
let operationActual = new Decimal(0);
|
||||
let provinceActual = new Decimal(0);
|
||||
let cityActual = new Decimal(0);
|
||||
for (const account of systemAccounts) {
|
||||
const balance = new Decimal(account.contributionBalance || 0);
|
||||
if (account.accountType === 'OPERATION') operationActual = operationActual.plus(balance);
|
||||
else if (account.accountType === 'PROVINCE') provinceActual = provinceActual.plus(balance);
|
||||
else if (account.accountType === 'CITY') cityActual = cityActual.plus(balance);
|
||||
}
|
||||
|
||||
return {
|
||||
totalTrees,
|
||||
// 理论值(基于总树数计算)
|
||||
networkTotalTheory: networkTotal.toString(),
|
||||
personalTheory: personalTheory.toString(),
|
||||
operationTheory: operationTheory.toString(),
|
||||
provinceTheory: provinceTheory.toString(),
|
||||
cityTheory: cityTheory.toString(),
|
||||
levelTheory: levelTheory.toString(),
|
||||
bonusTheory: bonusTheory.toString(),
|
||||
|
||||
// 实际值(从数据库统计)
|
||||
operationActual: operationActual.toString(),
|
||||
provinceActual: provinceActual.toString(),
|
||||
cityActual: cityActual.toString(),
|
||||
|
||||
// 层级算力详情
|
||||
levelContribution: {
|
||||
total: levelTheory.toString(),
|
||||
unlocked: levelUnlocked.toString(),
|
||||
pending: levelPending.toString(),
|
||||
byTier: {
|
||||
tier1: { unlocked: levelTier1.toString(), pending: '0' },
|
||||
tier2: { unlocked: levelTier2.toString(), pending: '0' },
|
||||
tier3: { unlocked: levelTier3.toString(), pending: '0' },
|
||||
},
|
||||
},
|
||||
|
||||
// 团队奖励算力详情
|
||||
bonusContribution: {
|
||||
total: bonusTheory.toString(),
|
||||
unlocked: bonusUnlocked.toString(),
|
||||
pending: bonusPending.toString(),
|
||||
byTier: {
|
||||
tier1: { unlocked: bonusTier1.toString(), pending: '0' },
|
||||
tier2: { unlocked: bonusTier2.toString(), pending: '0' },
|
||||
tier3: { unlocked: bonusTier3.toString(), pending: '0' },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取挖矿统计
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { PageHeader } from '@/components/layout/page-header';
|
|||
import { StatsCards } from '@/features/dashboard/components/stats-cards';
|
||||
import { RealtimePanel } from '@/features/dashboard/components/realtime-panel';
|
||||
import { PriceOverview } from '@/features/dashboard/components/price-overview';
|
||||
import { ContributionBreakdown } from '@/features/dashboard/components/contribution-breakdown';
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
|
|
@ -12,6 +13,9 @@ export default function DashboardPage() {
|
|||
|
||||
<StatsCards />
|
||||
|
||||
{/* 详细算力分解 */}
|
||||
<ContributionBreakdown />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<PriceOverview />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useDashboardStats } from '../hooks/use-dashboard-stats';
|
||||
import { formatCompactNumber } from '@/lib/utils/format';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine } from 'lucide-react';
|
||||
|
||||
function ContributionBreakdownSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div key={i} className="flex justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BreakdownRowProps {
|
||||
label: string;
|
||||
theory?: string;
|
||||
actual?: string;
|
||||
unlocked?: string;
|
||||
pending?: string;
|
||||
icon?: React.ElementType;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function BreakdownRow({ label, theory, actual, unlocked, pending, icon: Icon, color = 'text-gray-600' }: BreakdownRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2 border-b last:border-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{Icon && <Icon className={`h-4 w-4 ${color}`} />}
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{theory && actual && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">{formatCompactNumber(actual)}</span>
|
||||
<span className="text-muted-foreground text-xs ml-1">/ {formatCompactNumber(theory)}</span>
|
||||
</div>
|
||||
)}
|
||||
{unlocked !== undefined && pending !== undefined && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-green-600">{formatCompactNumber(unlocked)}</span>
|
||||
<span className="text-muted-foreground mx-1">/</span>
|
||||
<span className="font-medium text-orange-500">{formatCompactNumber(pending)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContributionBreakdown() {
|
||||
const { data: stats, isLoading } = useDashboardStats();
|
||||
|
||||
if (isLoading) {
|
||||
return <ContributionBreakdownSkeleton />;
|
||||
}
|
||||
|
||||
const dc = stats?.detailedContribution;
|
||||
if (!dc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* 基础算力分配 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<TreePine className="h-5 w-5 text-green-600" />
|
||||
算力分配概览
|
||||
</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
总认种树: {dc.totalTrees} 棵 | 全网理论算力: {formatCompactNumber(dc.networkTotalTheory)}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<BreakdownRow
|
||||
label="个人算力 (70%)"
|
||||
theory={dc.personalTheory}
|
||||
actual={dc.personalActual}
|
||||
icon={Users}
|
||||
color="text-blue-500"
|
||||
/>
|
||||
<BreakdownRow
|
||||
label="运营账户 (12%)"
|
||||
theory={dc.operationTheory}
|
||||
actual={dc.operationActual}
|
||||
icon={Building2}
|
||||
color="text-purple-500"
|
||||
/>
|
||||
<BreakdownRow
|
||||
label="省公司 (1%)"
|
||||
theory={dc.provinceTheory}
|
||||
actual={dc.provinceActual}
|
||||
icon={Landmark}
|
||||
color="text-indigo-500"
|
||||
/>
|
||||
<BreakdownRow
|
||||
label="市公司 (2%)"
|
||||
theory={dc.cityTheory}
|
||||
actual={dc.cityActual}
|
||||
icon={Building2}
|
||||
color="text-cyan-500"
|
||||
/>
|
||||
<div className="pt-2 mt-2 border-t">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">层级算力 (7.5%)</span>
|
||||
<span className="font-medium">{formatCompactNumber(dc.level.theory)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">团队奖励 (7.5%)</span>
|
||||
<span className="font-medium">{formatCompactNumber(dc.bonus.theory)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 层级算力详情 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Layers className="h-5 w-5 text-blue-600" />
|
||||
层级算力详情 (7.5%)
|
||||
</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
理论: {formatCompactNumber(dc.level.theory)} |
|
||||
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.level.unlocked)}</span> |
|
||||
<span className="text-orange-500"> 待解锁: {formatCompactNumber(dc.level.pending)}</span>
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
(已解锁 / 待解锁)
|
||||
</div>
|
||||
<BreakdownRow
|
||||
label="1档 (1-5级, 需1直推)"
|
||||
unlocked={dc.level.tier1.unlocked}
|
||||
pending={dc.level.tier1.pending}
|
||||
icon={Activity}
|
||||
color="text-green-500"
|
||||
/>
|
||||
<BreakdownRow
|
||||
label="2档 (6-10级, 需3直推)"
|
||||
unlocked={dc.level.tier2.unlocked}
|
||||
pending={dc.level.tier2.pending}
|
||||
icon={Activity}
|
||||
color="text-yellow-500"
|
||||
/>
|
||||
<BreakdownRow
|
||||
label="3档 (11-15级, 需5直推)"
|
||||
unlocked={dc.level.tier3.unlocked}
|
||||
pending={dc.level.tier3.pending}
|
||||
icon={Activity}
|
||||
color="text-orange-500"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 团队奖励详情 */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Gift className="h-5 w-5 text-purple-600" />
|
||||
团队奖励详情 (7.5%)
|
||||
</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
理论: {formatCompactNumber(dc.bonus.theory)} |
|
||||
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.bonus.unlocked)}</span> |
|
||||
<span className="text-orange-500"> 待解锁: {formatCompactNumber(dc.bonus.pending)}</span>
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-3 bg-green-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-green-800">1档 (2.5%)</div>
|
||||
<div className="text-xs text-green-600 mb-2">条件: 自己认种</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-muted-foreground">已解锁</span>
|
||||
<span className="text-sm font-medium text-green-700">{formatCompactNumber(dc.bonus.tier1.unlocked)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-muted-foreground">待解锁</span>
|
||||
<span className="text-sm font-medium text-orange-600">{formatCompactNumber(dc.bonus.tier1.pending)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-yellow-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-yellow-800">2档 (2.5%)</div>
|
||||
<div className="text-xs text-yellow-600 mb-2">条件: 2直推认种</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-muted-foreground">已解锁</span>
|
||||
<span className="text-sm font-medium text-green-700">{formatCompactNumber(dc.bonus.tier2.unlocked)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-muted-foreground">待解锁</span>
|
||||
<span className="text-sm font-medium text-orange-600">{formatCompactNumber(dc.bonus.tier2.pending)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-orange-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-orange-800">3档 (2.5%)</div>
|
||||
<div className="text-xs text-orange-600 mb-2">条件: 4直推认种</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-muted-foreground">已解锁</span>
|
||||
<span className="text-sm font-medium text-green-700">{formatCompactNumber(dc.bonus.tier3.unlocked)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-muted-foreground">待解锁</span>
|
||||
<span className="text-sm font-medium text-orange-600">{formatCompactNumber(dc.bonus.tier3.pending)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,39 @@
|
|||
export interface TierContribution {
|
||||
unlocked: string;
|
||||
pending: string;
|
||||
}
|
||||
|
||||
export interface ContributionBreakdown {
|
||||
theory: string;
|
||||
unlocked: string;
|
||||
pending: string;
|
||||
tier1: TierContribution;
|
||||
tier2: TierContribution;
|
||||
tier3: TierContribution;
|
||||
}
|
||||
|
||||
export interface DetailedContribution {
|
||||
totalTrees: number;
|
||||
// 全网算力(理论值)
|
||||
networkTotalTheory: string;
|
||||
// 个人算力(70%)
|
||||
personalTheory: string;
|
||||
personalActual: string;
|
||||
// 运营账户(12%)
|
||||
operationTheory: string;
|
||||
operationActual: string;
|
||||
// 省公司(1%)
|
||||
provinceTheory: string;
|
||||
provinceActual: string;
|
||||
// 市公司(2%)
|
||||
cityTheory: string;
|
||||
cityActual: string;
|
||||
// 层级算力(7.5%)
|
||||
level: ContributionBreakdown;
|
||||
// 团队奖励算力(7.5%)
|
||||
bonus: ContributionBreakdown;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
totalUsers: number;
|
||||
adoptedUsers: number;
|
||||
|
|
@ -13,6 +49,8 @@ export interface DashboardStats {
|
|||
priceChange24h: number;
|
||||
totalOrders: number;
|
||||
totalTrades: number;
|
||||
// 详细算力分解
|
||||
detailedContribution?: DetailedContribution;
|
||||
}
|
||||
|
||||
export interface RealtimeData {
|
||||
|
|
|
|||
Loading…
Reference in New Issue