119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
/**
|
|
* 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<string>('KAFKA_BROKERS')?.split(',') || ['localhost:9092'];
|
|
const clientId = this.configService.get<string>('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<void> {
|
|
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<string, string> = {
|
|
'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)}`;
|
|
}
|
|
}
|