diff --git a/backend/services/mining-admin-service/src/application/application.module.ts b/backend/services/mining-admin-service/src/application/application.module.ts index 37104688..ea15f370 100644 --- a/backend/services/mining-admin-service/src/application/application.module.ts +++ b/backend/services/mining-admin-service/src/application/application.module.ts @@ -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 { diff --git a/backend/services/mining-admin-service/src/application/services/daily-report.service.ts b/backend/services/mining-admin-service/src/application/services/daily-report.service.ts new file mode 100644 index 00000000..ab217b02 --- /dev/null +++ b/backend/services/mining-admin-service/src/application/services/daily-report.service.ts @@ -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 { + 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 { + 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, + }; + } +}