import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Kafka, Consumer, logLevel, EachMessagePayload } from 'kafkajs'; import { PrismaService } from '../persistence/prisma/prisma.service'; import { IUserQueryRepository, USER_QUERY_REPOSITORY } from '../../domain/repositories/user-query.repository'; import { Inject } from '@nestjs/common'; /** * 用户事件 Payload 类型定义 */ interface UserAccountCreatedPayload { userId: string; accountSequence: string; referralCode: string; phoneNumber?: string; initialDeviceId: string; inviterSequence: string | null; registeredAt: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } interface UserAccountAutoCreatedPayload { userId: string; accountSequence: string; referralCode: string; initialDeviceId: string; inviterSequence: string | null; registeredAt: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } interface UserProfileUpdatedPayload { userId: string; accountSequence: string; nickname: string | null; avatarUrl: string | null; updatedAt: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } interface KYCVerifiedPayload { userId: string; verifiedAt: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } interface KYCRejectedPayload { userId: string; reason: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } interface UserAccountFrozenPayload { userId: string; reason: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } interface UserAccountDeactivatedPayload { userId: string; deactivatedAt: string; _outbox?: { id: string; aggregateId: string; eventType: string; }; } /** * 用户事件消费者服务 * * 消费 identity-service 发布的用户相关事件,同步更新本地 UserQueryView */ @Injectable() export class UserEventConsumerService implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(UserEventConsumerService.name); private kafka: Kafka; private consumer: Consumer; private isRunning = false; // 配置 private readonly topics: string[]; private readonly consumerGroup: string; private readonly ackTopic: string; constructor( private readonly configService: ConfigService, private readonly prisma: PrismaService, @Inject(USER_QUERY_REPOSITORY) private readonly userQueryRepository: IUserQueryRepository, ) { const brokers = (this.configService.get('KAFKA_BROKERS', 'localhost:9092')).split(','); const clientId = this.configService.get('KAFKA_CLIENT_ID', 'admin-service'); this.consumerGroup = this.configService.get('KAFKA_CONSUMER_GROUP', 'admin-service-user-sync'); this.ackTopic = 'identity.events.ack'; // 订阅的主题 (与 identity-service 的 IDENTITY_TOPICS 保持一致) this.topics = [ 'identity.UserAccountCreated', 'identity.UserAccountAutoCreated', 'identity.PhoneBound', 'identity.KYCSubmitted', 'identity.KYCVerified', 'identity.KYCRejected', 'identity.UserAccountFrozen', 'identity.UserAccountDeactivated', ]; this.kafka = new Kafka({ clientId, brokers, logLevel: logLevel.WARN, }); this.consumer = this.kafka.consumer({ groupId: this.consumerGroup }); this.logger.log(`[UserEventConsumer] Configured with topics: ${this.topics.join(', ')}`); } async onModuleInit() { await this.start(); } async onModuleDestroy() { await this.stop(); } async start(): Promise { if (this.isRunning) { this.logger.warn('[UserEventConsumer] Already running'); return; } try { this.logger.log('[UserEventConsumer] Connecting to Kafka...'); await this.consumer.connect(); for (const topic of this.topics) { await this.consumer.subscribe({ topic, fromBeginning: false }); this.logger.log(`[UserEventConsumer] Subscribed to topic: ${topic}`); } await this.consumer.run({ eachMessage: async (payload: EachMessagePayload) => { await this.handleMessage(payload); }, }); this.isRunning = true; this.logger.log('[UserEventConsumer] Started successfully'); } catch (error) { this.logger.error('[UserEventConsumer] Failed to start:', error); } } async stop(): Promise { if (!this.isRunning) return; try { await this.consumer.disconnect(); this.isRunning = false; this.logger.log('[UserEventConsumer] Stopped'); } catch (error) { this.logger.error('[UserEventConsumer] Failed to stop:', error); } } private async handleMessage(payload: EachMessagePayload): Promise { const { topic, partition, message } = payload; if (!message.value) { this.logger.warn(`[UserEventConsumer] Empty message from ${topic}:${partition}`); return; } try { const eventData = JSON.parse(message.value.toString()); const eventType = eventData._outbox?.eventType || eventData.eventType; const eventId = eventData._outbox?.id || message.key?.toString(); this.logger.debug(`[UserEventConsumer] Received event: ${eventType} (${eventId})`); // 幂等性检查 if (eventId && await this.isEventProcessed(eventId)) { this.logger.debug(`[UserEventConsumer] Event ${eventId} already processed, skipping`); return; } // 处理事件 (eventData.payload 包含实际的业务数据) await this.processEvent(eventType, eventData.payload); // 记录已处理事件 if (eventId) { await this.markEventProcessed(eventId, eventType); } // 发送确认消息 (B方案) if (eventData._outbox?.id) { await this.sendAck(eventData._outbox.id, eventType); } this.logger.log(`[UserEventConsumer] ✓ Processed event: ${eventType}`); } catch (error) { this.logger.error(`[UserEventConsumer] Failed to process message:`, error); // 不抛出错误,避免阻塞消费 } } private async processEvent(eventType: string, payload: unknown): Promise { switch (eventType) { case 'UserAccountCreated': await this.handleUserAccountCreated(payload as UserAccountCreatedPayload); break; case 'UserAccountAutoCreated': await this.handleUserAccountAutoCreated(payload as UserAccountAutoCreatedPayload); break; case 'UserProfileUpdated': await this.handleUserProfileUpdated(payload as UserProfileUpdatedPayload); break; case 'KYCVerified': await this.handleKYCVerified(payload as KYCVerifiedPayload); break; case 'KYCRejected': await this.handleKYCRejected(payload as KYCRejectedPayload); break; case 'UserAccountFrozen': await this.handleUserAccountFrozen(payload as UserAccountFrozenPayload); break; case 'UserAccountDeactivated': await this.handleUserAccountDeactivated(payload as UserAccountDeactivatedPayload); break; default: this.logger.debug(`[UserEventConsumer] Unknown event type: ${eventType}, skipping`); } } // ==================== Event Handlers ==================== private async handleUserAccountCreated(payload: UserAccountCreatedPayload): Promise { const phoneNumberMasked = payload.phoneNumber ? this.maskPhoneNumber(payload.phoneNumber) : null; await this.userQueryRepository.upsert({ userId: BigInt(payload.userId), accountSequence: payload.accountSequence, phoneNumberMasked, inviterSequence: payload.inviterSequence, registeredAt: new Date(payload.registeredAt), }); this.logger.log(`[UserEventConsumer] Created user: ${payload.accountSequence}`); } private async handleUserAccountAutoCreated(payload: UserAccountAutoCreatedPayload): Promise { await this.userQueryRepository.upsert({ userId: BigInt(payload.userId), accountSequence: payload.accountSequence, inviterSequence: payload.inviterSequence, registeredAt: new Date(payload.registeredAt), }); this.logger.log(`[UserEventConsumer] Auto-created user: ${payload.accountSequence}`); } private async handleUserProfileUpdated(payload: UserProfileUpdatedPayload): Promise { const userId = BigInt(payload.userId); // 检查用户是否存在 const exists = await this.userQueryRepository.exists(userId); if (!exists) { this.logger.warn(`[UserEventConsumer] User ${userId} not found, skipping profile update`); return; } await this.userQueryRepository.updateProfile(userId, { nickname: payload.nickname, avatarUrl: payload.avatarUrl, }); this.logger.log(`[UserEventConsumer] Updated profile for user: ${payload.accountSequence}`); } private async handleKYCVerified(payload: KYCVerifiedPayload): Promise { const userId = BigInt(payload.userId); const exists = await this.userQueryRepository.exists(userId); if (!exists) { this.logger.warn(`[UserEventConsumer] User ${userId} not found, skipping KYC update`); return; } await this.userQueryRepository.updateKycStatus(userId, 'VERIFIED'); this.logger.log(`[UserEventConsumer] KYC verified for user: ${userId}`); } private async handleKYCRejected(payload: KYCRejectedPayload): Promise { const userId = BigInt(payload.userId); const exists = await this.userQueryRepository.exists(userId); if (!exists) { this.logger.warn(`[UserEventConsumer] User ${userId} not found, skipping KYC update`); return; } await this.userQueryRepository.updateKycStatus(userId, 'REJECTED'); this.logger.log(`[UserEventConsumer] KYC rejected for user: ${userId}`); } private async handleUserAccountFrozen(payload: UserAccountFrozenPayload): Promise { const userId = BigInt(payload.userId); const exists = await this.userQueryRepository.exists(userId); if (!exists) { this.logger.warn(`[UserEventConsumer] User ${userId} not found, skipping status update`); return; } await this.userQueryRepository.updateStatus(userId, 'FROZEN'); this.logger.log(`[UserEventConsumer] User frozen: ${userId}`); } private async handleUserAccountDeactivated(payload: UserAccountDeactivatedPayload): Promise { const userId = BigInt(payload.userId); const exists = await this.userQueryRepository.exists(userId); if (!exists) { this.logger.warn(`[UserEventConsumer] User ${userId} not found, skipping status update`); return; } await this.userQueryRepository.updateStatus(userId, 'DEACTIVATED'); this.logger.log(`[UserEventConsumer] User deactivated: ${userId}`); } // ==================== Helper Methods ==================== private maskPhoneNumber(phone: string): string { if (phone.length < 7) return phone; return phone.slice(0, 3) + '****' + phone.slice(-4); } private async isEventProcessed(eventId: string): Promise { const count = await this.prisma.processedEvent.count({ where: { eventId }, }); return count > 0; } private async markEventProcessed(eventId: string, eventType: string): Promise { await this.prisma.processedEvent.create({ data: { eventId, eventType, processedAt: new Date(), }, }); } private async sendAck(outboxId: string, eventType: string): Promise { try { const producer = this.kafka.producer(); await producer.connect(); await producer.send({ topic: this.ackTopic, messages: [ { key: outboxId, value: JSON.stringify({ outboxId, eventType, consumerId: this.consumerGroup, confirmedAt: new Date().toISOString(), }), }, ], }); await producer.disconnect(); this.logger.debug(`[UserEventConsumer] Sent ACK for outbox event ${outboxId}`); } catch (error) { this.logger.error(`[UserEventConsumer] Failed to send ACK:`, error); } } }