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 || {};
|
const dc = raw.detailedContribution || {};
|
||||||
|
|
||||||
// 转换为前端期望的格式
|
// 转换为前端期望的格式
|
||||||
|
// 优先使用远程服务数据,因为 CDC 同步可能不完整
|
||||||
|
const remoteData = raw.remoteData || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 基础统计
|
// 基础统计
|
||||||
totalUsers: raw.users?.total || 0,
|
totalUsers: raw.users?.total || 0,
|
||||||
|
|
@ -39,9 +42,12 @@ export class DashboardController {
|
||||||
networkTotalContribution: raw.contribution?.totalContribution || '0',
|
networkTotalContribution: raw.contribution?.totalContribution || '0',
|
||||||
networkLevelPending: dc.levelContribution?.pending || '0',
|
networkLevelPending: dc.levelContribution?.pending || '0',
|
||||||
networkBonusPending: dc.bonusContribution?.pending || '0',
|
networkBonusPending: dc.bonusContribution?.pending || '0',
|
||||||
totalDistributed: raw.mining?.totalMined || '0',
|
// 已分配积分股:优先使用远程数据
|
||||||
totalBurned: raw.mining?.latestDailyStat?.totalBurned || '0',
|
totalDistributed: remoteData.totalDistributed || raw.mining?.totalMined || '0',
|
||||||
circulationPool: raw.trading?.circulationPool?.totalShares || '0',
|
// 已销毁积分股:优先使用远程数据
|
||||||
|
totalBurned: remoteData.totalBurned || raw.mining?.latestDailyStat?.totalBurned || '0',
|
||||||
|
// 流通池:优先使用远程数据
|
||||||
|
circulationPool: remoteData.circulationPool || raw.trading?.circulationPool?.totalShares || '0',
|
||||||
currentPrice: raw.latestPrice?.close || '1',
|
currentPrice: raw.latestPrice?.close || '1',
|
||||||
priceChange24h,
|
priceChange24h,
|
||||||
totalOrders: raw.trading?.totalAccounts || 0,
|
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_LEVEL_TOTAL = new Decimal('0.075');
|
||||||
const RATE_BONUS_TOTAL = new Decimal('0.075');
|
const RATE_BONUS_TOTAL = new Decimal('0.075');
|
||||||
|
|
||||||
|
// 远程服务数据缓存
|
||||||
|
interface RemoteServiceData {
|
||||||
|
totalDistributed: string;
|
||||||
|
totalBurned: string;
|
||||||
|
circulationPool: string;
|
||||||
|
fetchedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DashboardService {
|
export class DashboardService {
|
||||||
private readonly logger = new Logger(DashboardService.name);
|
private readonly logger = new Logger(DashboardService.name);
|
||||||
|
private remoteDataCache: RemoteServiceData | null = null;
|
||||||
|
private readonly CACHE_TTL_MS = 30000; // 30秒缓存
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
|
|
@ -34,6 +44,7 @@ export class DashboardService {
|
||||||
latestReport,
|
latestReport,
|
||||||
latestKLine,
|
latestKLine,
|
||||||
detailedContributionStats,
|
detailedContributionStats,
|
||||||
|
remoteData,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getUserStats(),
|
this.getUserStats(),
|
||||||
this.getContributionStats(),
|
this.getContributionStats(),
|
||||||
|
|
@ -42,13 +53,40 @@ export class DashboardService {
|
||||||
this.prisma.dailyReport.findFirst({ orderBy: { reportDate: 'desc' } }),
|
this.prisma.dailyReport.findFirst({ orderBy: { reportDate: 'desc' } }),
|
||||||
this.prisma.syncedDayKLine.findFirst({ orderBy: { klineDate: 'desc' } }),
|
this.prisma.syncedDayKLine.findFirst({ orderBy: { klineDate: 'desc' } }),
|
||||||
this.getDetailedContributionStats(),
|
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 {
|
return {
|
||||||
users: userStats,
|
users: userStats,
|
||||||
contribution: contributionStats,
|
contribution: contributionStats,
|
||||||
mining: miningStats,
|
mining: {
|
||||||
trading: tradingStats,
|
...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,
|
detailedContribution: detailedContributionStats,
|
||||||
latestReport: latestReport
|
latestReport: latestReport
|
||||||
? this.formatDailyReport(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