rwadurian/backend/services/wallet-service/src/infrastructure/kafka/reward-event-consumer.contr...

143 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Controller, Logger } from '@nestjs/common';
import { MessagePattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices';
import { PrismaService } from '../persistence/prisma/prisma.service';
import { EventAckPublisher } from './event-ack.publisher';
import Decimal from 'decimal.js';
/**
* 奖励事件消息结构
*
* 注意OutboxPublisherService 发送的消息格式是将 payload 字段直接展开在顶层
* 所以 accountSequence 等字段直接在消息根级别
*/
interface RewardSummaryUpdatedEvent {
// payload 字段直接展开在顶层
accountSequence: string;
userId: string;
pendingUsdt: number;
pendingHashpower: number;
pendingExpireAt: string | null;
settleableUsdt: number;
settleableHashpower: number;
settledTotalUsdt: number;
settledTotalHashpower: number;
expiredTotalUsdt: number;
expiredTotalHashpower: number;
// outbox 元数据
_outbox?: {
id: string;
aggregateId: string;
eventType: string;
};
}
/**
* 奖励事件 Kafka 控制器
*
* 消费 reward-service 发布的奖励汇总更新事件
* 使用幂等性检查确保不重复处理
*/
@Controller()
export class RewardEventConsumerController {
private readonly logger = new Logger(RewardEventConsumerController.name);
constructor(
private readonly prisma: PrismaService,
private readonly eventAckPublisher: EventAckPublisher,
) {}
/**
* 处理奖励汇总更新事件
*/
@MessagePattern('reward.summary.updated')
async handleRewardSummaryUpdated(
@Payload() message: RewardSummaryUpdatedEvent,
@Ctx() context: KafkaContext,
): Promise<void> {
const partition = context.getPartition();
const offset = context.getMessage().offset;
const outboxInfo = message._outbox;
// eventId 和 eventType 来自 _outbox 元数据,或者使用 accountSequence 作为备用
const eventId = outboxInfo?.aggregateId || message.accountSequence;
const eventType = outboxInfo?.eventType || 'reward.summary.updated';
this.logger.log(
`[REWARD-EVENT] Received reward.summary.updated for ${eventId} ` +
`[partition=${partition}, offset=${offset}]`,
);
this.logger.debug(`[REWARD-EVENT] Message content: ${JSON.stringify(message)}`);
try {
// 1. 幂等性检查
const alreadyProcessed = await this.prisma.processedEvent.findUnique({
where: {
eventId_eventType: {
eventId,
eventType,
},
},
});
if (alreadyProcessed) {
this.logger.warn(`[REWARD-EVENT] Event ${eventId} (${eventType}) already processed, skipping`);
// 仍然发送确认,避免重复发送
if (outboxInfo) {
await this.eventAckPublisher.sendSuccess(eventId, eventType);
}
return;
}
// 2. 更新 wallet_accounts 表的 rewards 数据
// 注意payload 字段直接展开在消息顶层
const accountSequence = BigInt(message.accountSequence);
await this.prisma.$transaction(async (tx) => {
// 更新钱包账户的奖励数据
await tx.walletAccount.update({
where: { accountSequence },
data: {
pendingUsdt: new Decimal(message.pendingUsdt),
pendingHashpower: new Decimal(message.pendingHashpower),
pendingExpireAt: message.pendingExpireAt ? new Date(message.pendingExpireAt) : null,
settleableUsdt: new Decimal(message.settleableUsdt),
settleableHashpower: new Decimal(message.settleableHashpower),
settledTotalUsdt: new Decimal(message.settledTotalUsdt),
settledTotalHashpower: new Decimal(message.settledTotalHashpower),
expiredTotalUsdt: new Decimal(message.expiredTotalUsdt),
expiredTotalHashpower: new Decimal(message.expiredTotalHashpower),
},
});
// 记录已处理事件(幂等性)
await tx.processedEvent.create({
data: {
eventId,
eventType,
sourceService: 'reward-service',
},
});
});
this.logger.log(
`[REWARD-EVENT] ✓ Updated rewards for accountSequence ${accountSequence}: ` +
`settleable=${message.settleableUsdt}, pending=${message.pendingUsdt}`,
);
// 3. 发送确认
if (outboxInfo) {
await this.eventAckPublisher.sendSuccess(eventId, eventType);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`[REWARD-EVENT] Error processing event ${eventId}: ${errorMessage}`);
// 发送失败确认
if (outboxInfo) {
await this.eventAckPublisher.sendFailure(eventId, eventType, errorMessage);
}
throw error;
}
}
}