import { Controller, Get, Post, Logger } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository'; import { UnitOfWork } from '../../infrastructure/persistence/unit-of-work/unit-of-work'; import { ContributionRateService } from '../../application/services/contribution-rate.service'; import { ContributionAccountSyncedEvent, ReferralSyncedEvent, AdoptionSyncedEvent, ContributionRecordSyncedEvent, NetworkProgressUpdatedEvent, } from '../../domain/events'; import { Public } from '../../shared/guards/jwt-auth.guard'; @ApiTags('Admin') @Controller('admin') export class AdminController { private readonly logger = new Logger(AdminController.name); constructor( private readonly prisma: PrismaService, private readonly outboxRepository: OutboxRepository, private readonly unitOfWork: UnitOfWork, private readonly contributionRateService: ContributionRateService, ) {} @Get('accounts/sync') @Public() @ApiOperation({ summary: '获取所有贡献值账户用于同步' }) async getAllAccountsForSync() { const accounts = await this.prisma.contributionAccount.findMany({ select: { accountSequence: true, personalContribution: true, totalLevelPending: true, totalBonusPending: true, totalUnlocked: true, effectiveContribution: true, hasAdopted: true, directReferralAdoptedCount: true, unlockedLevelDepth: true, unlockedBonusTiers: true, createdAt: true, updatedAt: true, }, }); return { accounts: accounts.map((acc) => ({ accountSequence: acc.accountSequence, personalContribution: acc.personalContribution.toString(), teamLevelContribution: acc.totalLevelPending.toString(), teamBonusContribution: acc.totalBonusPending.toString(), totalContribution: acc.effectiveContribution.toString(), effectiveContribution: acc.effectiveContribution.toString(), hasAdopted: acc.hasAdopted, directReferralAdoptedCount: acc.directReferralAdoptedCount, unlockedLevelDepth: acc.unlockedLevelDepth, unlockedBonusTiers: acc.unlockedBonusTiers, createdAt: acc.createdAt, updatedAt: acc.updatedAt, })), total: accounts.length, }; } @Post('contribution-accounts/publish-all') @Public() @ApiOperation({ summary: '发布所有贡献值账户事件到 outbox,用于初始同步到 mining-admin-service' }) async publishAllContributionAccounts(): Promise<{ success: boolean; publishedCount: number; failedCount: number; message: string; }> { const accounts = await this.prisma.contributionAccount.findMany({ select: { accountSequence: true, personalContribution: true, totalLevelPending: true, totalBonusPending: true, effectiveContribution: true, hasAdopted: true, directReferralAdoptedCount: true, unlockedLevelDepth: true, unlockedBonusTiers: true, createdAt: true, }, }); let publishedCount = 0; let failedCount = 0; // 批量处理,每批 100 条 const batchSize = 100; for (let i = 0; i < accounts.length; i += batchSize) { const batch = accounts.slice(i, i + batchSize); try { await this.unitOfWork.executeInTransaction(async () => { const events = batch.map((acc) => { const event = new ContributionAccountSyncedEvent( acc.accountSequence, acc.personalContribution.toString(), acc.totalLevelPending.toString(), acc.totalBonusPending.toString(), acc.effectiveContribution.toString(), acc.effectiveContribution.toString(), acc.hasAdopted, acc.directReferralAdoptedCount, acc.unlockedLevelDepth, acc.unlockedBonusTiers, acc.createdAt, ); return { aggregateType: ContributionAccountSyncedEvent.AGGREGATE_TYPE, aggregateId: acc.accountSequence, eventType: ContributionAccountSyncedEvent.EVENT_TYPE, payload: event.toPayload(), }; }); await this.outboxRepository.saveMany(events); }); publishedCount += batch.length; this.logger.debug(`Published batch ${Math.floor(i / batchSize) + 1}: ${batch.length} events`); } catch (error) { failedCount += batch.length; this.logger.error(`Failed to publish batch ${Math.floor(i / batchSize) + 1}`, error); } } this.logger.log(`Published ${publishedCount} contribution account events, ${failedCount} failed`); return { success: failedCount === 0, publishedCount, failedCount, message: `Published ${publishedCount} events, ${failedCount} failed out of ${accounts.length} total`, }; } @Post('referrals/publish-all') @Public() @ApiOperation({ summary: '发布所有推荐关系事件到 outbox,用于同步到 mining-admin-service' }) async publishAllReferrals(): Promise<{ success: boolean; publishedCount: number; failedCount: number; message: string; }> { const referrals = await this.prisma.syncedReferral.findMany({ select: { accountSequence: true, referrerAccountSequence: true, referrerUserId: true, originalUserId: true, ancestorPath: true, depth: true, }, }); let publishedCount = 0; let failedCount = 0; const batchSize = 100; for (let i = 0; i < referrals.length; i += batchSize) { const batch = referrals.slice(i, i + batchSize); try { await this.unitOfWork.executeInTransaction(async () => { const events = batch.map((ref) => { const event = new ReferralSyncedEvent( ref.accountSequence, ref.referrerAccountSequence, ref.referrerUserId, ref.originalUserId, ref.ancestorPath, ref.depth, ); return { aggregateType: ReferralSyncedEvent.AGGREGATE_TYPE, aggregateId: ref.accountSequence, eventType: ReferralSyncedEvent.EVENT_TYPE, payload: event.toPayload(), }; }); await this.outboxRepository.saveMany(events); }); publishedCount += batch.length; this.logger.debug(`Published referral batch ${Math.floor(i / batchSize) + 1}: ${batch.length} events`); } catch (error) { failedCount += batch.length; this.logger.error(`Failed to publish referral batch ${Math.floor(i / batchSize) + 1}`, error); } } this.logger.log(`Published ${publishedCount} referral events, ${failedCount} failed`); return { success: failedCount === 0, publishedCount, failedCount, message: `Published ${publishedCount} events, ${failedCount} failed out of ${referrals.length} total`, }; } @Post('adoptions/publish-all') @Public() @ApiOperation({ summary: '发布所有认种记录事件到 outbox,用于同步到 mining-admin-service' }) async publishAllAdoptions(): Promise<{ success: boolean; publishedCount: number; failedCount: number; message: string; }> { const adoptions = await this.prisma.syncedAdoption.findMany({ select: { originalAdoptionId: true, accountSequence: true, treeCount: true, adoptionDate: true, status: true, contributionPerTree: true, }, }); let publishedCount = 0; let failedCount = 0; const batchSize = 100; for (let i = 0; i < adoptions.length; i += batchSize) { const batch = adoptions.slice(i, i + batchSize); try { await this.unitOfWork.executeInTransaction(async () => { const events = batch.map((adoption) => { const event = new AdoptionSyncedEvent( adoption.originalAdoptionId, adoption.accountSequence, adoption.treeCount, adoption.adoptionDate, adoption.status, adoption.contributionPerTree.toString(), ); return { aggregateType: AdoptionSyncedEvent.AGGREGATE_TYPE, aggregateId: adoption.originalAdoptionId.toString(), eventType: AdoptionSyncedEvent.EVENT_TYPE, payload: event.toPayload(), }; }); await this.outboxRepository.saveMany(events); }); publishedCount += batch.length; this.logger.debug(`Published adoption batch ${Math.floor(i / batchSize) + 1}: ${batch.length} events`); } catch (error) { failedCount += batch.length; this.logger.error(`Failed to publish adoption batch ${Math.floor(i / batchSize) + 1}`, error); } } this.logger.log(`Published ${publishedCount} adoption events, ${failedCount} failed`); return { success: failedCount === 0, publishedCount, failedCount, message: `Published ${publishedCount} events, ${failedCount} failed out of ${adoptions.length} total`, }; } @Get('network-progress') @Public() @ApiOperation({ summary: '获取全网认种进度和算力系数' }) async getNetworkProgress() { const progress = await this.contributionRateService.getNetworkProgress(); return { totalTreeCount: progress.totalTreeCount, totalAdoptionOrders: progress.totalAdoptionOrders, totalAdoptedUsers: progress.totalAdoptedUsers, currentUnit: progress.currentUnit, currentMultiplier: progress.currentMultiplier.toString(), currentContributionPerTree: progress.currentContributionPerTree.toString(), nextUnitTreeCount: progress.nextUnitTreeCount, // 计算下一个单位还需要多少棵 treesToNextUnit: progress.nextUnitTreeCount - progress.totalTreeCount, }; } @Post('contribution-records/publish-all') @Public() @ApiOperation({ summary: '发布所有算力记录事件到 outbox,用于初始同步到 mining-admin-service' }) async publishAllContributionRecords(): Promise<{ success: boolean; publishedCount: number; failedCount: number; message: string; }> { const records = await this.prisma.contributionRecord.findMany({ select: { id: true, accountSequence: true, sourceType: true, sourceAdoptionId: true, sourceAccountSequence: true, treeCount: true, baseContribution: true, distributionRate: true, levelDepth: true, bonusTier: true, amount: true, effectiveDate: true, expireDate: true, isExpired: true, createdAt: true, }, }); let publishedCount = 0; let failedCount = 0; const batchSize = 100; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); try { await this.unitOfWork.executeInTransaction(async () => { const events = batch.map((record) => { const event = new ContributionRecordSyncedEvent( record.id, record.accountSequence, record.sourceType, record.sourceAdoptionId, record.sourceAccountSequence, record.treeCount, record.baseContribution.toString(), record.distributionRate.toString(), record.levelDepth, record.bonusTier, record.amount.toString(), record.effectiveDate, record.expireDate, record.isExpired, record.createdAt, ); return { aggregateType: ContributionRecordSyncedEvent.AGGREGATE_TYPE, aggregateId: record.id.toString(), eventType: ContributionRecordSyncedEvent.EVENT_TYPE, payload: event.toPayload(), }; }); await this.outboxRepository.saveMany(events); }); publishedCount += batch.length; this.logger.debug(`Published contribution record batch ${Math.floor(i / batchSize) + 1}: ${batch.length} events`); } catch (error) { failedCount += batch.length; this.logger.error(`Failed to publish contribution record batch ${Math.floor(i / batchSize) + 1}`, error); } } this.logger.log(`Published ${publishedCount} contribution record events, ${failedCount} failed`); return { success: failedCount === 0, publishedCount, failedCount, message: `Published ${publishedCount} events, ${failedCount} failed out of ${records.length} total`, }; } @Post('network-progress/publish') @Public() @ApiOperation({ summary: '发布当前全网进度事件' }) async publishNetworkProgress(): Promise<{ success: boolean; message: string }> { try { const progress = await this.contributionRateService.getNetworkProgress(); const event = new NetworkProgressUpdatedEvent( progress.totalTreeCount, progress.totalAdoptionOrders, progress.totalAdoptedUsers, progress.currentUnit, progress.currentMultiplier.toString(), progress.currentContributionPerTree.toString(), progress.nextUnitTreeCount, ); await this.outboxRepository.save({ aggregateType: NetworkProgressUpdatedEvent.AGGREGATE_TYPE, aggregateId: 'network', eventType: NetworkProgressUpdatedEvent.EVENT_TYPE, payload: event.toPayload(), }); return { success: true, message: `Published network progress: trees=${progress.totalTreeCount}, unit=${progress.currentUnit}, multiplier=${progress.currentMultiplier.toString()}`, }; } catch (error) { this.logger.error('Failed to publish network progress', error); return { success: false, message: `Failed: ${error.message}`, }; } } }