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:
hailin 2026-01-16 08:25:07 -08:00
parent 3096297198
commit 9d65eef1b1
2 changed files with 129 additions and 5 deletions

View File

@ -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,

View File

@ -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;
}
// ===========================================================================
// 辅助方法
// ===========================================================================