rwadurian/backend/services/contribution-service/src/api/controllers/admin.controller.ts

424 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}`,
};
}
}
}