feat(cdc): 添加 legacy 用户批量同步功能
auth-service: - 添加 AdminController 和 AdminSyncService - POST /admin/legacy-users/publish-all: 为所有 legacy 用户发布事件 - GET /admin/users/sync: 获取所有用户数据供同步 mining-admin-service: - 添加 user.legacy.migrated 事件处理器 - 添加 sync-users 和 sync-contribution-accounts API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
49b1571bba
commit
582beb4f81
|
|
@ -8,6 +8,7 @@ import {
|
||||||
KycController,
|
KycController,
|
||||||
UserController,
|
UserController,
|
||||||
HealthController,
|
HealthController,
|
||||||
|
AdminController,
|
||||||
} from './controllers';
|
} from './controllers';
|
||||||
import { ApplicationModule } from '@/application';
|
import { ApplicationModule } from '@/application';
|
||||||
import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
|
||||||
|
|
@ -33,6 +34,7 @@ import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
|
||||||
KycController,
|
KycController,
|
||||||
UserController,
|
UserController,
|
||||||
HealthController,
|
HealthController,
|
||||||
|
AdminController,
|
||||||
],
|
],
|
||||||
providers: [JwtAuthGuard],
|
providers: [JwtAuthGuard],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { AdminSyncService } from '@/application/services/admin-sync.service';
|
||||||
|
|
||||||
|
@ApiTags('Admin')
|
||||||
|
@Controller('admin')
|
||||||
|
export class AdminController {
|
||||||
|
constructor(private readonly adminSyncService: AdminSyncService) {}
|
||||||
|
|
||||||
|
@Get('users/sync')
|
||||||
|
@ApiOperation({ summary: '获取所有用户数据供 mining-admin-service 同步' })
|
||||||
|
async getAllUsersForSync() {
|
||||||
|
return this.adminSyncService.getAllUsersForSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('legacy-users/publish-all')
|
||||||
|
@ApiOperation({ summary: '为所有 synced_legacy_users 发布事件到 Kafka' })
|
||||||
|
async publishAllLegacyUsers() {
|
||||||
|
return this.adminSyncService.publishAllLegacyUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,4 @@ export * from './password.controller';
|
||||||
export * from './kyc.controller';
|
export * from './kyc.controller';
|
||||||
export * from './user.controller';
|
export * from './user.controller';
|
||||||
export * from './health.controller';
|
export * from './health.controller';
|
||||||
|
export * from './admin.controller';
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
KycService,
|
KycService,
|
||||||
UserService,
|
UserService,
|
||||||
OutboxService,
|
OutboxService,
|
||||||
|
AdminSyncService,
|
||||||
} from './services';
|
} from './services';
|
||||||
import { OutboxScheduler } from './schedulers';
|
import { OutboxScheduler } from './schedulers';
|
||||||
import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
||||||
|
|
@ -35,6 +36,7 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
||||||
KycService,
|
KycService,
|
||||||
UserService,
|
UserService,
|
||||||
OutboxService,
|
OutboxService,
|
||||||
|
AdminSyncService,
|
||||||
OutboxScheduler,
|
OutboxScheduler,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
|
@ -43,6 +45,7 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
||||||
SmsService,
|
SmsService,
|
||||||
KycService,
|
KycService,
|
||||||
UserService,
|
UserService,
|
||||||
|
AdminSyncService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApplicationModule {}
|
export class ApplicationModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||||
|
import { OutboxService } from './outbox.service';
|
||||||
|
import { LegacyUserMigratedEvent } from '@/domain';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminSyncService {
|
||||||
|
private readonly logger = new Logger(AdminSyncService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly outboxService: OutboxService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有用户数据供 mining-admin-service 同步
|
||||||
|
*/
|
||||||
|
async getAllUsersForSync(): Promise<{
|
||||||
|
users: Array<{
|
||||||
|
id: string;
|
||||||
|
accountSequence: string;
|
||||||
|
phone: string;
|
||||||
|
status: string;
|
||||||
|
kycStatus: string;
|
||||||
|
realName: string | null;
|
||||||
|
isLegacyUser: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
}> {
|
||||||
|
// 获取已注册到 2.0 的用户
|
||||||
|
const users = await this.prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
accountSequence: true,
|
||||||
|
phone: true,
|
||||||
|
status: true,
|
||||||
|
kycStatus: true,
|
||||||
|
realName: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取从 1.0 同步的但尚未迁移的用户
|
||||||
|
const legacyUsers = await this.prisma.syncedLegacyUser.findMany({
|
||||||
|
where: {
|
||||||
|
accountSequence: {
|
||||||
|
notIn: users.map((u) => u.accountSequence),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
legacyId: true,
|
||||||
|
accountSequence: true,
|
||||||
|
phone: true,
|
||||||
|
status: true,
|
||||||
|
legacyCreatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const allUsers = [
|
||||||
|
...users.map((u) => ({
|
||||||
|
id: String(u.id),
|
||||||
|
accountSequence: u.accountSequence,
|
||||||
|
phone: u.phone,
|
||||||
|
status: u.status,
|
||||||
|
kycStatus: u.kycStatus,
|
||||||
|
realName: u.realName,
|
||||||
|
isLegacyUser: false,
|
||||||
|
createdAt: u.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
...legacyUsers.map((u) => ({
|
||||||
|
id: String(u.legacyId),
|
||||||
|
accountSequence: u.accountSequence,
|
||||||
|
phone: u.phone || '',
|
||||||
|
status: u.status,
|
||||||
|
kycStatus: 'PENDING',
|
||||||
|
realName: null,
|
||||||
|
isLegacyUser: true,
|
||||||
|
createdAt: u.legacyCreatedAt?.toISOString() || new Date().toISOString(),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
return { users: allUsers };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为所有 synced_legacy_users 发布 user.legacy.migrated 事件
|
||||||
|
* 用于初始同步到 mining-admin-service
|
||||||
|
*/
|
||||||
|
async publishAllLegacyUsers(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
publishedCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
const legacyUsers = await this.prisma.syncedLegacyUser.findMany({
|
||||||
|
select: {
|
||||||
|
accountSequence: true,
|
||||||
|
phone: true,
|
||||||
|
legacyCreatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let publishedCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
|
for (const user of legacyUsers) {
|
||||||
|
try {
|
||||||
|
const event = new LegacyUserMigratedEvent(
|
||||||
|
user.accountSequence,
|
||||||
|
user.phone || '',
|
||||||
|
user.legacyCreatedAt || new Date(),
|
||||||
|
);
|
||||||
|
await this.outboxService.publish(event);
|
||||||
|
publishedCount++;
|
||||||
|
this.logger.debug(`Published event for legacy user: ${user.accountSequence}`);
|
||||||
|
} catch (error) {
|
||||||
|
failedCount++;
|
||||||
|
this.logger.error(`Failed to publish event for legacy user: ${user.accountSequence}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Published ${publishedCount} legacy user events, ${failedCount} failed`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: failedCount === 0,
|
||||||
|
publishedCount,
|
||||||
|
failedCount,
|
||||||
|
message: `Published ${publishedCount} events, ${failedCount} failed out of ${legacyUsers.length} total`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,4 @@ export * from './sms.service';
|
||||||
export * from './kyc.service';
|
export * from './kyc.service';
|
||||||
export * from './user.service';
|
export * from './user.service';
|
||||||
export * from './outbox.service';
|
export * from './outbox.service';
|
||||||
|
export * from './admin-sync.service';
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,16 @@ export class InitializationController {
|
||||||
async activateMining(@Req() req: any) {
|
async activateMining(@Req() req: any) {
|
||||||
return this.initService.activateMining(req.admin.id);
|
return this.initService.activateMining(req.admin.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('sync-users')
|
||||||
|
@ApiOperation({ summary: '同步所有用户数据(从auth-service初始同步)' })
|
||||||
|
async syncUsers(@Req() req: any) {
|
||||||
|
return this.initService.syncAllUsers(req.admin.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sync-contribution-accounts')
|
||||||
|
@ApiOperation({ summary: '同步所有算力账户(从contribution-service初始同步)' })
|
||||||
|
async syncContributionAccounts(@Req() req: any) {
|
||||||
|
return this.initService.syncAllContributionAccounts(req.admin.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,11 @@ export class CdcSyncService implements OnModuleInit {
|
||||||
'user.kyc_verified',
|
'user.kyc_verified',
|
||||||
this.handleKycStatusChanged.bind(this),
|
this.handleKycStatusChanged.bind(this),
|
||||||
);
|
);
|
||||||
|
// auth-service 发布的 user.legacy.migrated 事件 (1.0用户首次登录2.0时)
|
||||||
|
this.cdcConsumer.registerServiceHandler(
|
||||||
|
'user.legacy.migrated',
|
||||||
|
this.handleLegacyUserMigrated.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// 从 contribution-service 同步算力数据
|
// 从 contribution-service 同步算力数据
|
||||||
|
|
@ -322,6 +327,39 @@ export class CdcSyncService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 auth-service 发布的 user.legacy.migrated 事件
|
||||||
|
* payload: { accountSequence, phone, migratedAt }
|
||||||
|
*/
|
||||||
|
private async handleLegacyUserMigrated(event: ServiceEvent): Promise<void> {
|
||||||
|
const { payload } = event;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.prisma.syncedUser.upsert({
|
||||||
|
where: { accountSequence: payload.accountSequence },
|
||||||
|
create: {
|
||||||
|
originalUserId: payload.accountSequence,
|
||||||
|
accountSequence: payload.accountSequence,
|
||||||
|
phone: payload.phone,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
kycStatus: 'PENDING',
|
||||||
|
realName: null,
|
||||||
|
isLegacyUser: true,
|
||||||
|
createdAt: new Date(payload.migratedAt),
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
phone: payload.phone,
|
||||||
|
isLegacyUser: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.recordProcessedEvent(event);
|
||||||
|
this.logger.log(`Synced legacy migrated user: ${payload.accountSequence}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to sync legacy migrated user: ${payload.accountSequence}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleUserUpdated(event: ServiceEvent): Promise<void> {
|
private async handleUserUpdated(event: ServiceEvent): Promise<void> {
|
||||||
const { payload } = event;
|
const { payload } = event;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue