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:
parent
4292d5da66
commit
feb871bcf1
|
|
@ -6,6 +6,7 @@ import { InitializationService } from './services/initialization.service';
|
||||||
import { DashboardService } from './services/dashboard.service';
|
import { DashboardService } from './services/dashboard.service';
|
||||||
import { UsersService } from './services/users.service';
|
import { UsersService } from './services/users.service';
|
||||||
import { SystemAccountsService } from './services/system-accounts.service';
|
import { SystemAccountsService } from './services/system-accounts.service';
|
||||||
|
import { DailyReportService } from './services/daily-report.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [InfrastructureModule],
|
imports: [InfrastructureModule],
|
||||||
|
|
@ -16,6 +17,7 @@ import { SystemAccountsService } from './services/system-accounts.service';
|
||||||
DashboardService,
|
DashboardService,
|
||||||
UsersService,
|
UsersService,
|
||||||
SystemAccountsService,
|
SystemAccountsService,
|
||||||
|
DailyReportService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AuthService,
|
AuthService,
|
||||||
|
|
@ -24,6 +26,7 @@ import { SystemAccountsService } from './services/system-accounts.service';
|
||||||
DashboardService,
|
DashboardService,
|
||||||
UsersService,
|
UsersService,
|
||||||
SystemAccountsService,
|
SystemAccountsService,
|
||||||
|
DailyReportService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApplicationModule implements OnModuleInit {
|
export class ApplicationModule implements OnModuleInit {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue