/** * Kafka Event Publisher Service for Wallet Service * * Publishes domain events to Kafka for cross-service communication. * Used to notify blockchain-service about withdrawal requests. */ import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Kafka, Producer, logLevel } from 'kafkajs'; export interface EventPayload { eventId?: string; eventType: string; occurredAt?: Date; payload: { [key: string]: unknown }; } @Injectable() export class EventPublisherService implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(EventPublisherService.name); private kafka: Kafka; private producer: Producer; private isConnected = false; constructor(private readonly configService: ConfigService) {} async onModuleInit() { const brokers = this.configService.get('KAFKA_BROKERS')?.split(',') || ['localhost:9092']; const clientId = this.configService.get('KAFKA_CLIENT_ID') || 'wallet-service'; this.logger.log(`[INIT] Event Publisher initializing...`); this.logger.log(`[INIT] ClientId: ${clientId}`); this.logger.log(`[INIT] Brokers: ${brokers.join(', ')}`); this.kafka = new Kafka({ clientId, brokers, logLevel: logLevel.WARN, retry: { initialRetryTime: 100, retries: 8, }, }); this.producer = this.kafka.producer(); try { await this.producer.connect(); this.isConnected = true; this.logger.log(`[CONNECT] Kafka producer connected successfully`); } catch (error) { this.logger.error(`[ERROR] Failed to connect Kafka producer`, error); } } async onModuleDestroy() { if (this.isConnected) { await this.producer.disconnect(); this.logger.log('Kafka producer disconnected'); } } /** * Publish an event to Kafka */ async publish(event: EventPayload): Promise { if (!this.isConnected) { this.logger.warn(`[PUBLISH] Kafka not connected, skipping event: ${event.eventType}`); return; } const topic = this.getTopicForEvent(event.eventType); const eventId = event.eventId || this.generateEventId(); const occurredAt = event.occurredAt || new Date(); const message = { key: eventId, value: JSON.stringify({ eventId, eventType: event.eventType, occurredAt: occurredAt.toISOString(), payload: event.payload, }), headers: { eventType: event.eventType, source: 'wallet-service', }, }; try { await this.producer.send({ topic, messages: [message], }); this.logger.log(`[PUBLISH] Published event: ${event.eventType} to topic: ${topic}`); } catch (error) { this.logger.error(`[ERROR] Failed to publish event: ${event.eventType}`, error); throw error; } } private getTopicForEvent(eventType: string): string { const topicMap: Record = { 'wallet.withdrawal.requested': 'wallet.withdrawals', 'wallet.withdrawal.completed': 'wallet.withdrawals', 'wallet.withdrawal.failed': 'wallet.withdrawals', // ACK events - 确认消息 'wallet.deposit.credited': 'wallet.acks', }; return topicMap[eventType] || 'wallet.events'; } private generateEventId(): string { return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } }