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:
hailin 2026-01-11 20:17:46 -08:00
parent 49b1571bba
commit 582beb4f81
8 changed files with 209 additions and 0 deletions

View File

@ -8,6 +8,7 @@ import {
KycController,
UserController,
HealthController,
AdminController,
} from './controllers';
import { ApplicationModule } from '@/application';
import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
@ -33,6 +34,7 @@ import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard';
KycController,
UserController,
HealthController,
AdminController,
],
providers: [JwtAuthGuard],
})

View File

@ -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();
}
}

View File

@ -4,3 +4,4 @@ export * from './password.controller';
export * from './kyc.controller';
export * from './user.controller';
export * from './health.controller';
export * from './admin.controller';

View File

@ -9,6 +9,7 @@ import {
KycService,
UserService,
OutboxService,
AdminSyncService,
} from './services';
import { OutboxScheduler } from './schedulers';
import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
@ -35,6 +36,7 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
KycService,
UserService,
OutboxService,
AdminSyncService,
OutboxScheduler,
],
exports: [
@ -43,6 +45,7 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
SmsService,
KycService,
UserService,
AdminSyncService,
],
})
export class ApplicationModule {}

View File

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

View File

@ -4,3 +4,4 @@ export * from './sms.service';
export * from './kyc.service';
export * from './user.service';
export * from './outbox.service';
export * from './admin-sync.service';

View File

@ -32,4 +32,16 @@ export class InitializationController {
async activateMining(@Req() req: any) {
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);
}
}

View File

@ -59,6 +59,11 @@ export class CdcSyncService implements OnModuleInit {
'user.kyc_verified',
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 同步算力数据
@ -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> {
const { payload } = event;