fix(mining-admin): fetch dashboard data from remote services
Dashboard now fetches totalDistributed and totalBurned directly from mining-service and trading-service APIs instead of relying solely on CDC sync which may not have data. - Add fetchRemoteServiceData() to get real-time data - Use mining-service /admin/status for totalDistributed - Use trading-service /asset/market for totalBurned and circulationPool - Add 30-second cache to reduce API calls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3096297198
commit
9d65eef1b1
|
|
@ -30,6 +30,9 @@ export class DashboardController {
|
|||
const dc = raw.detailedContribution || {};
|
||||
|
||||
// 转换为前端期望的格式
|
||||
// 优先使用远程服务数据,因为 CDC 同步可能不完整
|
||||
const remoteData = raw.remoteData || {};
|
||||
|
||||
return {
|
||||
// 基础统计
|
||||
totalUsers: raw.users?.total || 0,
|
||||
|
|
@ -39,9 +42,12 @@ export class DashboardController {
|
|||
networkTotalContribution: raw.contribution?.totalContribution || '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',
|
||||
// 已分配积分股:优先使用远程数据
|
||||
totalDistributed: remoteData.totalDistributed || raw.mining?.totalMined || '0',
|
||||
// 已销毁积分股:优先使用远程数据
|
||||
totalBurned: remoteData.totalBurned || raw.mining?.latestDailyStat?.totalBurned || '0',
|
||||
// 流通池:优先使用远程数据
|
||||
circulationPool: remoteData.circulationPool || raw.trading?.circulationPool?.totalShares || '0',
|
||||
currentPrice: raw.latestPrice?.close || '1',
|
||||
priceChange24h,
|
||||
totalOrders: raw.trading?.totalAccounts || 0,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,19 @@ const RATE_CITY = new Decimal('0.02');
|
|||
const RATE_LEVEL_TOTAL = new Decimal('0.075');
|
||||
const RATE_BONUS_TOTAL = new Decimal('0.075');
|
||||
|
||||
// 远程服务数据缓存
|
||||
interface RemoteServiceData {
|
||||
totalDistributed: string;
|
||||
totalBurned: string;
|
||||
circulationPool: string;
|
||||
fetchedAt: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
private readonly logger = new Logger(DashboardService.name);
|
||||
private remoteDataCache: RemoteServiceData | null = null;
|
||||
private readonly CACHE_TTL_MS = 30000; // 30秒缓存
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
|
|
@ -34,6 +44,7 @@ export class DashboardService {
|
|||
latestReport,
|
||||
latestKLine,
|
||||
detailedContributionStats,
|
||||
remoteData,
|
||||
] = await Promise.all([
|
||||
this.getUserStats(),
|
||||
this.getContributionStats(),
|
||||
|
|
@ -42,13 +53,40 @@ export class DashboardService {
|
|||
this.prisma.dailyReport.findFirst({ orderBy: { reportDate: 'desc' } }),
|
||||
this.prisma.syncedDayKLine.findFirst({ orderBy: { klineDate: 'desc' } }),
|
||||
this.getDetailedContributionStats(),
|
||||
this.fetchRemoteServiceData(),
|
||||
]);
|
||||
|
||||
// 合并远程服务数据(如果本地数据为空或为0则使用远程数据)
|
||||
const totalMined = miningStats.totalMined !== '0'
|
||||
? miningStats.totalMined
|
||||
: remoteData.totalDistributed;
|
||||
|
||||
const totalBurned = miningStats.latestDailyStat?.totalBurned || remoteData.totalBurned;
|
||||
|
||||
const circulationPoolShares = tradingStats.circulationPool?.totalShares !== '0'
|
||||
? tradingStats.circulationPool?.totalShares
|
||||
: remoteData.circulationPool;
|
||||
|
||||
return {
|
||||
users: userStats,
|
||||
contribution: contributionStats,
|
||||
mining: miningStats,
|
||||
trading: tradingStats,
|
||||
mining: {
|
||||
...miningStats,
|
||||
totalMined, // 使用合并后的已分配数据
|
||||
},
|
||||
trading: {
|
||||
...tradingStats,
|
||||
circulationPool: {
|
||||
totalShares: circulationPoolShares || '0',
|
||||
totalCash: tradingStats.circulationPool?.totalCash || '0',
|
||||
},
|
||||
},
|
||||
// 直接提供远程数据用于仪表盘显示
|
||||
remoteData: {
|
||||
totalDistributed: remoteData.totalDistributed,
|
||||
totalBurned: remoteData.totalBurned,
|
||||
circulationPool: remoteData.circulationPool,
|
||||
},
|
||||
detailedContribution: detailedContributionStats,
|
||||
latestReport: latestReport
|
||||
? this.formatDailyReport(latestReport)
|
||||
|
|
@ -460,6 +498,86 @@ export class DashboardService {
|
|||
};
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 远程服务数据获取(实时数据备选方案)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* 从 mining-service 和 trading-service 获取实时数据
|
||||
* 用于补充 CDC 同步数据的不足
|
||||
*/
|
||||
private async fetchRemoteServiceData(): Promise<RemoteServiceData> {
|
||||
// 检查缓存
|
||||
if (
|
||||
this.remoteDataCache &&
|
||||
Date.now() - this.remoteDataCache.fetchedAt.getTime() < this.CACHE_TTL_MS
|
||||
) {
|
||||
return this.remoteDataCache;
|
||||
}
|
||||
|
||||
const miningServiceUrl = this.configService.get<string>(
|
||||
'MINING_SERVICE_URL',
|
||||
'http://localhost:3021',
|
||||
);
|
||||
const tradingServiceUrl = this.configService.get<string>(
|
||||
'TRADING_SERVICE_URL',
|
||||
'http://localhost:3022',
|
||||
);
|
||||
|
||||
let totalDistributed = '0';
|
||||
let totalBurned = '0';
|
||||
let circulationPool = '0';
|
||||
|
||||
try {
|
||||
// 从 mining-service 获取已分配积分股
|
||||
const miningResponse = await fetch(
|
||||
`${miningServiceUrl}/api/v2/admin/status`,
|
||||
);
|
||||
if (miningResponse.ok) {
|
||||
const miningResult = await miningResponse.json();
|
||||
const miningData = miningResult.data || miningResult;
|
||||
// 使用 remainingDistribution 计算已分配
|
||||
// 总量 50亿 - 剩余 = 已分配
|
||||
const distributionPool = new Decimal(
|
||||
miningData.distributionPool || '5000000000',
|
||||
);
|
||||
const remaining = new Decimal(
|
||||
miningData.remainingDistribution || '5000000000',
|
||||
);
|
||||
totalDistributed = distributionPool.minus(remaining).toString();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to fetch mining service data: ${error.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// 从 trading-service 获取市场概览(包含销毁和流通池数据)
|
||||
const marketResponse = await fetch(
|
||||
`${tradingServiceUrl}/api/v2/asset/market`,
|
||||
);
|
||||
if (marketResponse.ok) {
|
||||
const marketResult = await marketResponse.json();
|
||||
const marketData = marketResult.data || marketResult;
|
||||
// blackHoleAmount 是已销毁总量
|
||||
totalBurned = marketData.blackHoleAmount || '0';
|
||||
// circulationPool 是流通池余额
|
||||
circulationPool = marketData.circulationPool || '0';
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to fetch market overview: ${error.message}`);
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
this.remoteDataCache = {
|
||||
totalDistributed,
|
||||
totalBurned,
|
||||
circulationPool,
|
||||
fetchedAt: new Date(),
|
||||
};
|
||||
|
||||
return this.remoteDataCache;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 辅助方法
|
||||
// ===========================================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue