feat(mining-admin): add daily report generation service

Add DailyReportService that:
- Generates daily reports on startup
- Updates reports every hour
- Collects stats from synced tables (users, adoptions, contributions, mining, trading)
- Supports historical report generation for backfilling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-14 02:03:21 -08:00
parent 4292d5da66
commit feb871bcf1
2 changed files with 267 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import { InitializationService } from './services/initialization.service';
import { DashboardService } from './services/dashboard.service';
import { UsersService } from './services/users.service';
import { SystemAccountsService } from './services/system-accounts.service';
import { DailyReportService } from './services/daily-report.service';
@Module({
imports: [InfrastructureModule],
@ -16,6 +17,7 @@ import { SystemAccountsService } from './services/system-accounts.service';
DashboardService,
UsersService,
SystemAccountsService,
DailyReportService,
],
exports: [
AuthService,
@ -24,6 +26,7 @@ import { SystemAccountsService } from './services/system-accounts.service';
DashboardService,
UsersService,
SystemAccountsService,
DailyReportService,
],
})
export class ApplicationModule implements OnModuleInit {

View File

@ -0,0 +1,264 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
import Decimal from 'decimal.js';
@Injectable()
export class DailyReportService implements OnModuleInit {
private readonly logger = new Logger(DailyReportService.name);
private reportInterval: NodeJS.Timeout | null = null;
constructor(private readonly prisma: PrismaService) {}
async onModuleInit() {
// 启动时先生成一次报表
await this.generateTodayReport();
// 每小时检查并更新当日报表
this.reportInterval = setInterval(
() => this.generateTodayReport(),
60 * 60 * 1000, // 1 hour
);
this.logger.log('Daily report service initialized');
}
/**
*
*/
async generateTodayReport(): Promise<void> {
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
this.logger.log(`Generating daily report for ${today.toISOString().split('T')[0]}`);
// 收集各项统计数据
const [
userStats,
adoptionStats,
contributionStats,
miningStats,
tradingStats,
priceStats,
] = await Promise.all([
this.getUserStats(today),
this.getAdoptionStats(today),
this.getContributionStats(today),
this.getMiningStats(),
this.getTradingStats(today),
this.getPriceStats(today),
]);
// 更新或创建今日报表
await this.prisma.dailyReport.upsert({
where: { reportDate: today },
create: {
reportDate: today,
...userStats,
...adoptionStats,
...contributionStats,
...miningStats,
...tradingStats,
...priceStats,
},
update: {
...userStats,
...adoptionStats,
...contributionStats,
...miningStats,
...tradingStats,
...priceStats,
},
});
this.logger.log(`Daily report generated successfully for ${today.toISOString().split('T')[0]}`);
} catch (error) {
this.logger.error('Failed to generate daily report', error);
}
}
/**
*
*/
async generateHistoricalReport(date: Date): Promise<void> {
const reportDate = new Date(date);
reportDate.setHours(0, 0, 0, 0);
const [
userStats,
adoptionStats,
contributionStats,
miningStats,
tradingStats,
priceStats,
] = await Promise.all([
this.getUserStats(reportDate),
this.getAdoptionStats(reportDate),
this.getContributionStats(reportDate),
this.getMiningStats(),
this.getTradingStats(reportDate),
this.getPriceStats(reportDate),
]);
await this.prisma.dailyReport.upsert({
where: { reportDate },
create: {
reportDate,
...userStats,
...adoptionStats,
...contributionStats,
...miningStats,
...tradingStats,
...priceStats,
},
update: {
...userStats,
...adoptionStats,
...contributionStats,
...miningStats,
...tradingStats,
...priceStats,
},
});
}
/**
*
*/
private async getUserStats(date: Date) {
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
const [totalUsers, newUsers] = await Promise.all([
this.prisma.syncedUser.count({
where: { createdAt: { lt: nextDay } },
}),
this.prisma.syncedUser.count({
where: {
createdAt: { gte: date, lt: nextDay },
},
}),
]);
// 活跃用户暂时用总用户数(需要有活跃度跟踪才能准确计算)
const activeUsers = totalUsers;
return {
totalUsers,
newUsers,
activeUsers,
};
}
/**
*
*/
private async getAdoptionStats(date: Date) {
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
const [totalAdoptions, newAdoptions, treesResult] = await Promise.all([
this.prisma.syncedAdoption.count({
where: { adoptionDate: { lt: nextDay } },
}),
this.prisma.syncedAdoption.count({
where: {
adoptionDate: { gte: date, lt: nextDay },
},
}),
this.prisma.syncedAdoption.aggregate({
where: { adoptionDate: { lt: nextDay } },
_sum: { treeCount: true },
}),
]);
return {
totalAdoptions,
newAdoptions,
totalTrees: treesResult._sum.treeCount || 0,
};
}
/**
*
*/
private async getContributionStats(date: Date) {
// 获取全网算力进度
const networkProgress = await this.prisma.syncedNetworkProgress.findFirst();
// 获取用户算力汇总
const userContribution = await this.prisma.syncedContributionAccount.aggregate({
_sum: {
totalContribution: true,
effectiveContribution: true,
},
});
const totalContribution = new Decimal(
userContribution._sum.totalContribution?.toString() || '0',
);
// 获取昨日报表计算增长
const yesterday = new Date(date);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayReport = await this.prisma.dailyReport.findUnique({
where: { reportDate: yesterday },
});
const contributionGrowth = yesterdayReport
? totalContribution.minus(new Decimal(yesterdayReport.totalContribution.toString()))
: totalContribution;
return {
totalContribution,
contributionGrowth: contributionGrowth.gt(0) ? contributionGrowth : new Decimal(0),
};
}
/**
*
*/
private async getMiningStats() {
const dailyStat = await this.prisma.syncedDailyMiningStat.findFirst({
orderBy: { statDate: 'desc' },
});
return {
totalDistributed: dailyStat?.totalDistributed || new Decimal(0),
totalBurned: dailyStat?.totalBurned || new Decimal(0),
};
}
/**
*
*/
private async getTradingStats(date: Date) {
const kline = await this.prisma.syncedDayKLine.findUnique({
where: { klineDate: date },
});
return {
tradingVolume: kline?.volume || new Decimal(0),
tradingAmount: kline?.amount || new Decimal(0),
tradeCount: kline?.tradeCount || 0,
};
}
/**
*
*/
private async getPriceStats(date: Date) {
const kline = await this.prisma.syncedDayKLine.findUnique({
where: { klineDate: date },
});
const defaultPrice = new Decimal(1);
return {
openPrice: kline?.open || defaultPrice,
closePrice: kline?.close || defaultPrice,
highPrice: kline?.high || defaultPrice,
lowPrice: kline?.low || defaultPrice,
};
}
}