424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
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}`,
|
||
};
|
||
}
|
||
}
|
||
}
|