rwadurian/backend/services/admin-service/src/infrastructure/kafka/wallet-cdc-consumer.service.ts

739 lines
26 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 { 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 { Decimal } from '@prisma/client/runtime/library';
/**
* Debezium CDC 事件结构 (经过 ExtractNewRecordState 转换后)
*/
// wallet_accounts 表
interface CdcWalletAccountPayload {
wallet_id: string;
account_sequence: string;
user_id: string;
usdt_available: string;
usdt_frozen: string;
dst_available: string;
dst_frozen: string;
bnb_available: string;
bnb_frozen: string;
og_available: string;
og_frozen: string;
rwad_available: string;
rwad_frozen: string;
hashpower: string;
pending_usdt: string;
pending_hashpower: string;
settleable_usdt: string;
settleable_hashpower: string;
settled_total_usdt: string;
settled_total_hashpower: string;
expired_total_usdt: string;
expired_total_hashpower: string;
status: string;
has_planted: boolean;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// withdrawal_orders 表
interface CdcWithdrawalOrderPayload {
order_id: string;
order_no: string;
account_sequence: string;
user_id: string;
amount: string;
fee: string;
chain_type: string;
to_address: string;
tx_hash?: string | null;
is_internal_transfer: boolean;
to_account_sequence?: string | null;
to_user_id?: string | null;
status: string;
error_message?: string | null;
frozen_at?: string | null;
broadcasted_at?: string | null;
confirmed_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// fiat_withdrawal_orders 表
interface CdcFiatWithdrawalOrderPayload {
order_id: string;
order_no: string;
account_sequence: string;
user_id: string;
amount: string;
fee: string;
payment_method: string;
bank_name?: string | null;
bank_card_no?: string | null;
card_holder_name?: string | null;
alipay_account?: string | null;
alipay_real_name?: string | null;
wechat_account?: string | null;
wechat_real_name?: string | null;
status: string;
error_message?: string | null;
reviewed_by?: string | null;
reviewed_at?: string | null;
review_remark?: string | null;
paid_by?: string | null;
paid_at?: string | null;
frozen_at?: string | null;
completed_at?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
// wallet_ledger_entries 表 (分类账流水)
interface CdcWalletLedgerEntryPayload {
entry_id: string;
account_sequence: string;
user_id: string;
entry_type: string;
amount: string;
asset_type: string;
balance_after?: string | null;
ref_order_id?: string | null;
ref_tx_hash?: string | null;
memo?: string | null;
created_at: string;
__op: 'c' | 'u' | 'd' | 'r';
__table: string;
__source_ts_ms: number;
__deleted?: string;
}
type CdcWalletPayload = CdcWalletAccountPayload | CdcWithdrawalOrderPayload | CdcFiatWithdrawalOrderPayload | CdcWalletLedgerEntryPayload;
/**
* Wallet CDC 消费者服务
*
* 消费 Debezium 从 wallet-service PostgreSQL 捕获的数据变更
*
* Topics:
* - cdc.wallet.public.wallet_accounts
* - cdc.wallet.public.withdrawal_orders
* - cdc.wallet.public.fiat_withdrawal_orders
*/
@Injectable()
export class WalletCdcConsumerService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(WalletCdcConsumerService.name);
private kafka: Kafka;
private consumer: Consumer;
private isRunning = false;
// CDC Topics
private readonly cdcTopics = [
'cdc.wallet.public.wallet_accounts',
'cdc.wallet.public.withdrawal_orders',
'cdc.wallet.public.fiat_withdrawal_orders',
'cdc.wallet.public.wallet_ledger_entries',
];
private readonly consumerGroup: string;
constructor(
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
) {
const brokers = (this.configService.get<string>('KAFKA_BROKERS', 'localhost:9092')).split(',');
const clientId = this.configService.get<string>('KAFKA_CLIENT_ID', 'admin-service');
this.consumerGroup = this.configService.get<string>('KAFKA_WALLET_CDC_GROUP', 'admin-service-wallet-cdc');
this.kafka = new Kafka({
clientId: `${clientId}-wallet-cdc`,
brokers,
logLevel: logLevel.WARN,
});
this.consumer = this.kafka.consumer({ groupId: this.consumerGroup });
this.logger.log(`[Wallet-CDC] Configured to consume topics: ${this.cdcTopics.join(', ')}`);
}
async onModuleInit() {
await this.start();
}
async onModuleDestroy() {
await this.stop();
}
async start(): Promise<void> {
if (this.isRunning) {
this.logger.warn('[Wallet-CDC] Consumer already running');
return;
}
try {
this.logger.log('[Wallet-CDC] Connecting to Kafka...');
await this.consumer.connect();
for (const topic of this.cdcTopics) {
await this.consumer.subscribe({
topic,
fromBeginning: false,
});
}
this.logger.log(`[Wallet-CDC] Subscribed to topics: ${this.cdcTopics.join(', ')}`);
await this.consumer.run({
eachMessage: async (payload: EachMessagePayload) => {
await this.handleMessage(payload);
},
});
this.isRunning = true;
this.logger.log('[Wallet-CDC] Consumer started successfully');
} catch (error) {
this.logger.error('[Wallet-CDC] Failed to start consumer:', error);
}
}
async stop(): Promise<void> {
if (!this.isRunning) return;
try {
await this.consumer.disconnect();
this.isRunning = false;
this.logger.log('[Wallet-CDC] Consumer stopped');
} catch (error) {
this.logger.error('[Wallet-CDC] Failed to stop consumer:', error);
}
}
private async handleMessage(payload: EachMessagePayload): Promise<void> {
const { topic, partition, message } = payload;
if (!message.value) {
this.logger.warn(`[Wallet-CDC] Empty message from ${topic}:${partition}`);
return;
}
try {
const data = JSON.parse(message.value.toString()) as CdcWalletPayload;
const operation = data.__op;
const table = data.__table;
this.logger.debug(
`[Wallet-CDC] Received ${operation} event for table ${table}`
);
// 幂等性检查
const eventId = `wallet-cdc:${topic}:${partition}:${message.offset}`;
if (await this.isEventProcessed(eventId)) {
this.logger.debug(`[Wallet-CDC] Event ${eventId} already processed, skipping`);
return;
}
// 根据表名处理不同的事件
switch (table) {
case 'wallet_accounts':
await this.processWalletAccountEvent(data as CdcWalletAccountPayload);
break;
case 'withdrawal_orders':
await this.processWithdrawalOrderEvent(data as CdcWithdrawalOrderPayload);
break;
case 'fiat_withdrawal_orders':
await this.processFiatWithdrawalOrderEvent(data as CdcFiatWithdrawalOrderPayload);
break;
case 'wallet_ledger_entries':
await this.processWalletLedgerEntryEvent(data as CdcWalletLedgerEntryPayload);
break;
default:
this.logger.warn(`[Wallet-CDC] Unknown table: ${table}`);
}
// 记录已处理
await this.markEventProcessed(eventId, `wallet-cdc:${table}:${operation}`);
this.logger.log(
`[Wallet-CDC] ✓ Processed ${operation} for table: ${table}`
);
} catch (error) {
this.logger.error(`[Wallet-CDC] Failed to process message:`, error);
throw error; // 让 KafkaJS 重试
}
}
// ==================== wallet_accounts 处理 ====================
private async processWalletAccountEvent(data: CdcWalletAccountPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleWalletAccountDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleWalletAccountCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleWalletAccountUpdate(data);
}
}
private async handleWalletAccountCreateOrSnapshot(data: CdcWalletAccountPayload): Promise<void> {
await this.prisma.walletAccountQueryView.upsert({
where: { id: BigInt(data.wallet_id) },
create: {
id: BigInt(data.wallet_id),
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
usdtAvailable: new Decimal(data.usdt_available || '0'),
usdtFrozen: new Decimal(data.usdt_frozen || '0'),
dstAvailable: new Decimal(data.dst_available || '0'),
dstFrozen: new Decimal(data.dst_frozen || '0'),
bnbAvailable: new Decimal(data.bnb_available || '0'),
bnbFrozen: new Decimal(data.bnb_frozen || '0'),
ogAvailable: new Decimal(data.og_available || '0'),
ogFrozen: new Decimal(data.og_frozen || '0'),
rwadAvailable: new Decimal(data.rwad_available || '0'),
rwadFrozen: new Decimal(data.rwad_frozen || '0'),
hashpower: new Decimal(data.hashpower || '0'),
pendingUsdt: new Decimal(data.pending_usdt || '0'),
pendingHashpower: new Decimal(data.pending_hashpower || '0'),
settleableUsdt: new Decimal(data.settleable_usdt || '0'),
settleableHashpower: new Decimal(data.settleable_hashpower || '0'),
settledTotalUsdt: new Decimal(data.settled_total_usdt || '0'),
settledTotalHashpower: new Decimal(data.settled_total_hashpower || '0'),
expiredTotalUsdt: new Decimal(data.expired_total_usdt || '0'),
expiredTotalHashpower: new Decimal(data.expired_total_hashpower || '0'),
status: data.status,
hasPlanted: data.has_planted,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
usdtAvailable: new Decimal(data.usdt_available || '0'),
usdtFrozen: new Decimal(data.usdt_frozen || '0'),
dstAvailable: new Decimal(data.dst_available || '0'),
dstFrozen: new Decimal(data.dst_frozen || '0'),
bnbAvailable: new Decimal(data.bnb_available || '0'),
bnbFrozen: new Decimal(data.bnb_frozen || '0'),
ogAvailable: new Decimal(data.og_available || '0'),
ogFrozen: new Decimal(data.og_frozen || '0'),
rwadAvailable: new Decimal(data.rwad_available || '0'),
rwadFrozen: new Decimal(data.rwad_frozen || '0'),
hashpower: new Decimal(data.hashpower || '0'),
pendingUsdt: new Decimal(data.pending_usdt || '0'),
pendingHashpower: new Decimal(data.pending_hashpower || '0'),
settleableUsdt: new Decimal(data.settleable_usdt || '0'),
settleableHashpower: new Decimal(data.settleable_hashpower || '0'),
settledTotalUsdt: new Decimal(data.settled_total_usdt || '0'),
settledTotalHashpower: new Decimal(data.settled_total_hashpower || '0'),
expiredTotalUsdt: new Decimal(data.expired_total_usdt || '0'),
expiredTotalHashpower: new Decimal(data.expired_total_hashpower || '0'),
status: data.status,
hasPlanted: data.has_planted,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created/Snapshot wallet account: ${data.account_sequence}`);
}
private async handleWalletAccountUpdate(data: CdcWalletAccountPayload): Promise<void> {
const id = BigInt(data.wallet_id);
const exists = await this.prisma.walletAccountQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleWalletAccountCreateOrSnapshot(data);
return;
}
await this.prisma.walletAccountQueryView.update({
where: { id },
data: {
usdtAvailable: new Decimal(data.usdt_available || '0'),
usdtFrozen: new Decimal(data.usdt_frozen || '0'),
dstAvailable: new Decimal(data.dst_available || '0'),
dstFrozen: new Decimal(data.dst_frozen || '0'),
bnbAvailable: new Decimal(data.bnb_available || '0'),
bnbFrozen: new Decimal(data.bnb_frozen || '0'),
ogAvailable: new Decimal(data.og_available || '0'),
ogFrozen: new Decimal(data.og_frozen || '0'),
rwadAvailable: new Decimal(data.rwad_available || '0'),
rwadFrozen: new Decimal(data.rwad_frozen || '0'),
hashpower: new Decimal(data.hashpower || '0'),
pendingUsdt: new Decimal(data.pending_usdt || '0'),
pendingHashpower: new Decimal(data.pending_hashpower || '0'),
settleableUsdt: new Decimal(data.settleable_usdt || '0'),
settleableHashpower: new Decimal(data.settleable_hashpower || '0'),
settledTotalUsdt: new Decimal(data.settled_total_usdt || '0'),
settledTotalHashpower: new Decimal(data.settled_total_hashpower || '0'),
expiredTotalUsdt: new Decimal(data.expired_total_usdt || '0'),
expiredTotalHashpower: new Decimal(data.expired_total_hashpower || '0'),
status: data.status,
hasPlanted: data.has_planted,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Updated wallet account: ${data.account_sequence}`);
}
private async handleWalletAccountDelete(data: CdcWalletAccountPayload): Promise<void> {
const id = BigInt(data.wallet_id);
try {
await this.prisma.walletAccountQueryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted wallet account: ${data.account_sequence}`);
} catch {
this.logger.warn(`[Wallet-CDC] Wallet account not found for delete: ${data.account_sequence}`);
}
}
// ==================== withdrawal_orders 处理 ====================
private async processWithdrawalOrderEvent(data: CdcWithdrawalOrderPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleWithdrawalOrderDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleWithdrawalOrderCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleWithdrawalOrderUpdate(data);
}
}
private async handleWithdrawalOrderCreateOrSnapshot(data: CdcWithdrawalOrderPayload): Promise<void> {
await this.prisma.withdrawalOrderQueryView.upsert({
where: { id: BigInt(data.order_id) },
create: {
id: BigInt(data.order_id),
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
chainType: data.chain_type,
toAddress: data.to_address,
txHash: data.tx_hash || null,
isInternalTransfer: data.is_internal_transfer,
toAccountSequence: data.to_account_sequence || null,
toUserId: data.to_user_id ? BigInt(data.to_user_id) : null,
status: data.status,
errorMessage: data.error_message || null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
broadcastedAt: data.broadcasted_at ? new Date(data.broadcasted_at) : null,
confirmedAt: data.confirmed_at ? new Date(data.confirmed_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
chainType: data.chain_type,
toAddress: data.to_address,
txHash: data.tx_hash || null,
isInternalTransfer: data.is_internal_transfer,
toAccountSequence: data.to_account_sequence || null,
toUserId: data.to_user_id ? BigInt(data.to_user_id) : null,
status: data.status,
errorMessage: data.error_message || null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
broadcastedAt: data.broadcasted_at ? new Date(data.broadcasted_at) : null,
confirmedAt: data.confirmed_at ? new Date(data.confirmed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created/Snapshot withdrawal order: ${data.order_no}`);
}
private async handleWithdrawalOrderUpdate(data: CdcWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
const exists = await this.prisma.withdrawalOrderQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleWithdrawalOrderCreateOrSnapshot(data);
return;
}
await this.prisma.withdrawalOrderQueryView.update({
where: { id },
data: {
txHash: data.tx_hash || null,
status: data.status,
errorMessage: data.error_message || null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
broadcastedAt: data.broadcasted_at ? new Date(data.broadcasted_at) : null,
confirmedAt: data.confirmed_at ? new Date(data.confirmed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Updated withdrawal order: ${data.order_no}`);
}
private async handleWithdrawalOrderDelete(data: CdcWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
try {
await this.prisma.withdrawalOrderQueryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted withdrawal order: ${data.order_no}`);
} catch {
this.logger.warn(`[Wallet-CDC] Withdrawal order not found for delete: ${data.order_no}`);
}
}
// ==================== fiat_withdrawal_orders 处理 ====================
private async processFiatWithdrawalOrderEvent(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
if (operation === 'd' || isDeleted) {
await this.handleFiatWithdrawalOrderDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleFiatWithdrawalOrderCreateOrSnapshot(data);
} else if (operation === 'u') {
await this.handleFiatWithdrawalOrderUpdate(data);
}
}
private maskBankCardNo(cardNo: string | null | undefined): string | null {
if (!cardNo) return null;
if (cardNo.length <= 8) return cardNo;
return cardNo.slice(0, 4) + '****' + cardNo.slice(-4);
}
private maskAccount(account: string | null | undefined): string | null {
if (!account) return null;
if (account.length <= 4) return account;
const visible = Math.min(3, Math.floor(account.length / 3));
return account.slice(0, visible) + '****' + account.slice(-visible);
}
private async handleFiatWithdrawalOrderCreateOrSnapshot(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
await this.prisma.fiatWithdrawalOrderQueryView.upsert({
where: { id: BigInt(data.order_id) },
create: {
id: BigInt(data.order_id),
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
paymentMethod: data.payment_method,
bankName: data.bank_name || null,
bankCardNoMasked: this.maskBankCardNo(data.bank_card_no),
cardHolderName: data.card_holder_name || null,
alipayAccountMasked: this.maskAccount(data.alipay_account),
wechatAccountMasked: this.maskAccount(data.wechat_account),
status: data.status,
errorMessage: data.error_message || null,
reviewedBy: data.reviewed_by || null,
reviewedAt: data.reviewed_at ? new Date(data.reviewed_at) : null,
reviewRemark: data.review_remark || null,
paidBy: data.paid_by || null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
orderNo: data.order_no,
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
amount: new Decimal(data.amount),
fee: new Decimal(data.fee),
paymentMethod: data.payment_method,
bankName: data.bank_name || null,
bankCardNoMasked: this.maskBankCardNo(data.bank_card_no),
cardHolderName: data.card_holder_name || null,
alipayAccountMasked: this.maskAccount(data.alipay_account),
wechatAccountMasked: this.maskAccount(data.wechat_account),
status: data.status,
errorMessage: data.error_message || null,
reviewedBy: data.reviewed_by || null,
reviewedAt: data.reviewed_at ? new Date(data.reviewed_at) : null,
reviewRemark: data.review_remark || null,
paidBy: data.paid_by || null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created/Snapshot fiat withdrawal order: ${data.order_no}`);
}
private async handleFiatWithdrawalOrderUpdate(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
const exists = await this.prisma.fiatWithdrawalOrderQueryView.findUnique({
where: { id },
select: { id: true },
});
if (!exists) {
await this.handleFiatWithdrawalOrderCreateOrSnapshot(data);
return;
}
await this.prisma.fiatWithdrawalOrderQueryView.update({
where: { id },
data: {
status: data.status,
errorMessage: data.error_message || null,
reviewedBy: data.reviewed_by || null,
reviewedAt: data.reviewed_at ? new Date(data.reviewed_at) : null,
reviewRemark: data.review_remark || null,
paidBy: data.paid_by || null,
paidAt: data.paid_at ? new Date(data.paid_at) : null,
frozenAt: data.frozen_at ? new Date(data.frozen_at) : null,
completedAt: data.completed_at ? new Date(data.completed_at) : null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Updated fiat withdrawal order: ${data.order_no}`);
}
private async handleFiatWithdrawalOrderDelete(data: CdcFiatWithdrawalOrderPayload): Promise<void> {
const id = BigInt(data.order_id);
try {
await this.prisma.fiatWithdrawalOrderQueryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted fiat withdrawal order: ${data.order_no}`);
} catch {
this.logger.warn(`[Wallet-CDC] Fiat withdrawal order not found for delete: ${data.order_no}`);
}
}
// ==================== wallet_ledger_entries 处理 ====================
private async processWalletLedgerEntryEvent(data: CdcWalletLedgerEntryPayload): Promise<void> {
const operation = data.__op;
const isDeleted = data.__deleted === 'true';
// 分类账流水是 append-only通常只有 create 和 snapshot
// 但也处理更新和删除情况以保持完整性
if (operation === 'd' || isDeleted) {
await this.handleWalletLedgerEntryDelete(data);
} else if (operation === 'c' || operation === 'r') {
await this.handleWalletLedgerEntryCreate(data);
} else if (operation === 'u') {
await this.handleWalletLedgerEntryUpdate(data);
}
}
private async handleWalletLedgerEntryCreate(data: CdcWalletLedgerEntryPayload): Promise<void> {
await this.prisma.walletLedgerEntryView.upsert({
where: { id: BigInt(data.entry_id) },
create: {
id: BigInt(data.entry_id),
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
entryType: data.entry_type,
amount: new Decimal(data.amount),
assetType: data.asset_type,
balanceAfter: data.balance_after ? new Decimal(data.balance_after) : null,
refOrderId: data.ref_order_id || null,
refTxHash: data.ref_tx_hash || null,
memo: data.memo || null,
createdAt: new Date(data.created_at),
syncedAt: new Date(),
},
update: {
accountSequence: data.account_sequence,
userId: BigInt(data.user_id),
entryType: data.entry_type,
amount: new Decimal(data.amount),
assetType: data.asset_type,
balanceAfter: data.balance_after ? new Decimal(data.balance_after) : null,
refOrderId: data.ref_order_id || null,
refTxHash: data.ref_tx_hash || null,
memo: data.memo || null,
syncedAt: new Date(),
},
});
this.logger.log(`[Wallet-CDC] Created ledger entry: ${data.entry_id} (${data.entry_type})`);
}
private async handleWalletLedgerEntryUpdate(data: CdcWalletLedgerEntryPayload): Promise<void> {
// 理论上分类账不应该被更新,但为了完整性还是处理
await this.handleWalletLedgerEntryCreate(data);
this.logger.log(`[Wallet-CDC] Updated ledger entry: ${data.entry_id}`);
}
private async handleWalletLedgerEntryDelete(data: CdcWalletLedgerEntryPayload): Promise<void> {
const id = BigInt(data.entry_id);
try {
await this.prisma.walletLedgerEntryView.delete({
where: { id },
});
this.logger.log(`[Wallet-CDC] Deleted ledger entry: ${data.entry_id}`);
} catch {
this.logger.warn(`[Wallet-CDC] Ledger entry not found for delete: ${data.entry_id}`);
}
}
// ==================== Helper Methods ====================
private async isEventProcessed(eventId: string): Promise<boolean> {
const count = await this.prisma.processedEvent.count({
where: { eventId },
});
return count > 0;
}
private async markEventProcessed(eventId: string, eventType: string): Promise<void> {
await this.prisma.processedEvent.create({
data: {
eventId,
eventType,
processedAt: new Date(),
},
});
}
/**
* 获取消费者状态
*/
getStatus(): { isRunning: boolean; topics: string[]; consumerGroup: string } {
return {
isRunning: this.isRunning,
topics: this.cdcTopics,
consumerGroup: this.consumerGroup,
};
}
}