feat(trading-service): sync trading account creation with wallet service
- Add CDC consumer to listen for UserWalletCreated events from mining-wallet-service - Create trading accounts when user contribution wallets are created (lazy creation) - Add WalletSystemAccountCreated handler for province/city system accounts - Add seed script for core system accounts (HQ, operation, cost, pool) - Keep auth.user.registered listener for V2 new user registration This ensures trading accounts are created in sync with wallet accounts, supporting both V2 new users and V1 migrated users. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
183b2bef59
commit
19428a8cb7
|
|
@ -15,7 +15,11 @@
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:migrate:prod": "prisma migrate deploy"
|
"prisma:migrate:prod": "prisma migrate deploy",
|
||||||
|
"prisma:seed": "ts-node prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Seeding trading-service database...');
|
||||||
|
|
||||||
|
// 1. 初始化系统账户的交易账户
|
||||||
|
// 这些账户序列号对应 mining-wallet-service 中的系统账户
|
||||||
|
const systemAccounts = [
|
||||||
|
{ accountSequence: 'S0000000001', name: '总部社区' },
|
||||||
|
{ accountSequence: 'S0000000002', name: '成本费账户' },
|
||||||
|
{ accountSequence: 'S0000000003', name: '运营费账户' },
|
||||||
|
{ accountSequence: 'S0000000004', name: 'RWAD底池账户' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const account of systemAccounts) {
|
||||||
|
const existing = await prisma.tradingAccount.findUnique({
|
||||||
|
where: { accountSequence: account.accountSequence },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const created = await prisma.tradingAccount.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: account.accountSequence,
|
||||||
|
shareBalance: new Decimal(0),
|
||||||
|
cashBalance: new Decimal(0),
|
||||||
|
frozenShares: new Decimal(0),
|
||||||
|
frozenCash: new Decimal(0),
|
||||||
|
totalBought: new Decimal(0),
|
||||||
|
totalSold: new Decimal(0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发布交易账户创建事件到 Outbox(用于 mining-admin-service 同步)
|
||||||
|
await prisma.outboxEvent.create({
|
||||||
|
data: {
|
||||||
|
aggregateType: 'TradingAccount',
|
||||||
|
aggregateId: created.id,
|
||||||
|
eventType: 'TradingAccountCreated',
|
||||||
|
topic: 'cdc.trading.outbox',
|
||||||
|
key: created.accountSequence,
|
||||||
|
payload: {
|
||||||
|
accountId: created.id,
|
||||||
|
accountSequence: created.accountSequence,
|
||||||
|
shareBalance: '0',
|
||||||
|
cashBalance: '0',
|
||||||
|
frozenShares: '0',
|
||||||
|
frozenCash: '0',
|
||||||
|
totalBought: '0',
|
||||||
|
totalSold: '0',
|
||||||
|
createdAt: created.createdAt.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Created trading account for system: ${account.name} (${account.accountSequence})`);
|
||||||
|
} else {
|
||||||
|
console.log(`Trading account already exists: ${account.name} (${account.accountSequence})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化交易配置
|
||||||
|
const existingConfig = await prisma.tradingConfig.findFirst();
|
||||||
|
if (!existingConfig) {
|
||||||
|
await prisma.tradingConfig.create({
|
||||||
|
data: {
|
||||||
|
totalShares: new Decimal('100020000000'), // 100.02B 总积分股
|
||||||
|
burnTarget: new Decimal('10000000000'), // 100亿目标销毁量
|
||||||
|
burnPeriodMinutes: 2102400, // 4年 = 365*4*1440 分钟
|
||||||
|
minuteBurnRate: new Decimal('4756.468797564687'), // 每分钟销毁率
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Created trading config');
|
||||||
|
} else {
|
||||||
|
console.log('Trading config already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 初始化黑洞账户
|
||||||
|
const existingBlackHole = await prisma.blackHole.findFirst();
|
||||||
|
if (!existingBlackHole) {
|
||||||
|
await prisma.blackHole.create({
|
||||||
|
data: {
|
||||||
|
totalBurned: new Decimal(0),
|
||||||
|
targetBurn: new Decimal('10000000000'), // 100亿目标销毁
|
||||||
|
remainingBurn: new Decimal('10000000000'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Created black hole account');
|
||||||
|
} else {
|
||||||
|
console.log('Black hole account already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 初始化积分股池
|
||||||
|
const existingSharePool = await prisma.sharePool.findFirst();
|
||||||
|
if (!existingSharePool) {
|
||||||
|
await prisma.sharePool.create({
|
||||||
|
data: {
|
||||||
|
greenPoints: new Decimal(0), // 初始绿积分为 0
|
||||||
|
totalInflow: new Decimal(0),
|
||||||
|
totalOutflow: new Decimal(0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Created share pool');
|
||||||
|
} else {
|
||||||
|
console.log('Share pool already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 初始化流通池
|
||||||
|
const existingCirculationPool = await prisma.circulationPool.findFirst();
|
||||||
|
if (!existingCirculationPool) {
|
||||||
|
await prisma.circulationPool.create({
|
||||||
|
data: {
|
||||||
|
totalShares: new Decimal(0),
|
||||||
|
totalCash: new Decimal(0),
|
||||||
|
totalInflow: new Decimal(0),
|
||||||
|
totalOutflow: new Decimal(0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Created circulation pool');
|
||||||
|
} else {
|
||||||
|
console.log('Circulation pool already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Seeding completed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Seeding failed:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
@ -14,6 +14,7 @@ import { ProcessedEventRepository } from './persistence/repositories/processed-e
|
||||||
import { RedisService } from './redis/redis.service';
|
import { RedisService } from './redis/redis.service';
|
||||||
import { KafkaProducerService } from './kafka/kafka-producer.service';
|
import { KafkaProducerService } from './kafka/kafka-producer.service';
|
||||||
import { UserRegisteredConsumer } from './kafka/consumers/user-registered.consumer';
|
import { UserRegisteredConsumer } from './kafka/consumers/user-registered.consumer';
|
||||||
|
import { CdcConsumerService } from './kafka/cdc-consumer.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
|
@ -51,6 +52,7 @@ import { UserRegisteredConsumer } from './kafka/consumers/user-registered.consum
|
||||||
PriceSnapshotRepository,
|
PriceSnapshotRepository,
|
||||||
ProcessedEventRepository,
|
ProcessedEventRepository,
|
||||||
KafkaProducerService,
|
KafkaProducerService,
|
||||||
|
CdcConsumerService,
|
||||||
{
|
{
|
||||||
provide: 'REDIS_OPTIONS',
|
provide: 'REDIS_OPTIONS',
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Kafka, Consumer, EachMessagePayload } from 'kafkajs';
|
||||||
|
import { TradingAccountRepository } from '../persistence/repositories/trading-account.repository';
|
||||||
|
import { OutboxRepository } from '../persistence/repositories/outbox.repository';
|
||||||
|
import { ProcessedEventRepository } from '../persistence/repositories/processed-event.repository';
|
||||||
|
import { RedisService } from '../redis/redis.service';
|
||||||
|
import { TradingAccountAggregate } from '../../domain/aggregates/trading-account.aggregate';
|
||||||
|
import {
|
||||||
|
TradingEventTypes,
|
||||||
|
TradingTopics,
|
||||||
|
TradingAccountCreatedPayload,
|
||||||
|
} from '../../domain/events/trading.events';
|
||||||
|
|
||||||
|
// 4小时 TTL(秒)
|
||||||
|
const IDEMPOTENCY_TTL_SECONDS = 4 * 60 * 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDC Consumer Service
|
||||||
|
* 监听 mining-wallet-service 的 outbox 事件,在用户钱包创建时同步创建交易账户
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CdcConsumerService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(CdcConsumerService.name);
|
||||||
|
private kafka: Kafka;
|
||||||
|
private consumer: Consumer;
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly tradingAccountRepository: TradingAccountRepository,
|
||||||
|
private readonly outboxRepository: OutboxRepository,
|
||||||
|
private readonly processedEventRepository: ProcessedEventRepository,
|
||||||
|
private readonly redis: RedisService,
|
||||||
|
) {
|
||||||
|
const brokers = this.configService
|
||||||
|
.get<string>('KAFKA_BROKERS', 'localhost:9092')
|
||||||
|
.split(',');
|
||||||
|
|
||||||
|
this.kafka = new Kafka({
|
||||||
|
clientId: 'trading-service-cdc',
|
||||||
|
brokers,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.consumer = this.kafka.consumer({
|
||||||
|
groupId: this.configService.get<string>(
|
||||||
|
'CDC_CONSUMER_GROUP',
|
||||||
|
'trading-service-cdc-group',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.logger.warn('CDC consumer is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletTopic = this.configService.get<string>(
|
||||||
|
'CDC_TOPIC_WALLET_OUTBOX',
|
||||||
|
'cdc.mining-wallet.outbox',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.consumer.connect();
|
||||||
|
this.logger.log('CDC consumer connected');
|
||||||
|
|
||||||
|
await this.consumer.subscribe({
|
||||||
|
topics: [walletTopic],
|
||||||
|
fromBeginning: false, // 不需要处理历史消息
|
||||||
|
});
|
||||||
|
this.logger.log(`Subscribed to topic: ${walletTopic}`);
|
||||||
|
|
||||||
|
await this.consumer.run({
|
||||||
|
eachMessage: async (payload: EachMessagePayload) => {
|
||||||
|
await this.handleMessage(payload);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
this.logger.log('CDC consumer started - listening for UserWalletCreated and WalletSystemAccountCreated events');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to start CDC consumer', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.consumer.disconnect();
|
||||||
|
this.isRunning = false;
|
||||||
|
this.logger.log('CDC consumer stopped');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to stop CDC consumer', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMessage(payload: EachMessagePayload): Promise<void> {
|
||||||
|
const { topic, message } = payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!message.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = JSON.parse(message.value.toString());
|
||||||
|
|
||||||
|
// 忽略心跳消息
|
||||||
|
if (this.isHeartbeatMessage(eventData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Debezium outbox 事件
|
||||||
|
if (this.isDebeziumOutboxEvent(eventData)) {
|
||||||
|
const op = eventData.payload?.op;
|
||||||
|
// 只处理 create 操作
|
||||||
|
if (op !== 'c' && op !== 'r') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = eventData.payload.after;
|
||||||
|
await this.handleOutboxEvent(after);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error processing message from topic ${topic}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isHeartbeatMessage(data: any): boolean {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
return keys.length === 1 && keys[0] === 'ts_ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDebeziumOutboxEvent(data: any): boolean {
|
||||||
|
const after = data.payload?.after;
|
||||||
|
if (!after) return false;
|
||||||
|
return after.event_type && after.aggregate_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleOutboxEvent(data: any): Promise<void> {
|
||||||
|
const eventType = data.event_type;
|
||||||
|
const eventId = data.id || data.outbox_id;
|
||||||
|
|
||||||
|
// 处理不同类型的事件
|
||||||
|
if (eventType === 'UserWalletCreated') {
|
||||||
|
await this.handleUserWalletCreated(eventId, data);
|
||||||
|
} else if (eventType === 'WalletSystemAccountCreated') {
|
||||||
|
await this.handleSystemAccountCreated(eventId, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理用户钱包创建事件 - 创建用户交易账户
|
||||||
|
*/
|
||||||
|
private async handleUserWalletCreated(eventId: string, data: any): Promise<void> {
|
||||||
|
|
||||||
|
const payload = typeof data.payload === 'string'
|
||||||
|
? JSON.parse(data.payload)
|
||||||
|
: data.payload;
|
||||||
|
|
||||||
|
const accountSequence = payload?.accountSequence;
|
||||||
|
if (!accountSequence) {
|
||||||
|
this.logger.warn(`UserWalletCreated event ${eventId} missing accountSequence`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理 CONTRIBUTION 类型的钱包
|
||||||
|
if (payload?.walletType !== 'CONTRIBUTION') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Processing UserWalletCreated event: ${eventId}, accountSequence: ${accountSequence}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 幂等性检查
|
||||||
|
if (await this.isEventProcessed(eventId)) {
|
||||||
|
this.logger.debug(`Event ${eventId} already processed, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查交易账户是否已存在
|
||||||
|
const existingAccount =
|
||||||
|
await this.tradingAccountRepository.findByAccountSequence(accountSequence);
|
||||||
|
|
||||||
|
if (existingAccount) {
|
||||||
|
this.logger.debug(`Trading account ${accountSequence} already exists`);
|
||||||
|
await this.markEventProcessed(eventId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建交易账户
|
||||||
|
const account = TradingAccountAggregate.create(accountSequence);
|
||||||
|
const accountId = await this.tradingAccountRepository.save(account);
|
||||||
|
|
||||||
|
// 发布交易账户创建事件
|
||||||
|
await this.publishAccountCreatedEvent(accountId, accountSequence);
|
||||||
|
|
||||||
|
// 标记为已处理
|
||||||
|
await this.markEventProcessed(eventId);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Trading account created for user ${accountSequence} (triggered by UserWalletCreated)`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('Unique constraint')) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Trading account already exists for ${accountSequence}, marking as processed`,
|
||||||
|
);
|
||||||
|
await this.markEventProcessed(eventId, 'UserWalletCreated');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to create trading account for ${accountSequence}`,
|
||||||
|
error instanceof Error ? error.stack : error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理系统账户创建事件 - 为省/市系统账户创建交易账户
|
||||||
|
* 系统账户的 accountSequence 格式为 code(如 PROV-440000, CITY-440100)
|
||||||
|
*/
|
||||||
|
private async handleSystemAccountCreated(eventId: string, data: any): Promise<void> {
|
||||||
|
const payload = typeof data.payload === 'string'
|
||||||
|
? JSON.parse(data.payload)
|
||||||
|
: data.payload;
|
||||||
|
|
||||||
|
// 系统账户使用 code 作为 accountSequence
|
||||||
|
const accountCode = payload?.code;
|
||||||
|
if (!accountCode) {
|
||||||
|
this.logger.warn(`WalletSystemAccountCreated event ${eventId} missing code`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理省/市级系统账户
|
||||||
|
const accountType = payload?.accountType;
|
||||||
|
if (accountType !== 'PROVINCE' && accountType !== 'CITY') {
|
||||||
|
this.logger.debug(
|
||||||
|
`Skipping non-regional system account: ${accountCode}, type: ${accountType}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Processing WalletSystemAccountCreated event: ${eventId}, code: ${accountCode}, type: ${accountType}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 幂等性检查
|
||||||
|
if (await this.isEventProcessed(eventId)) {
|
||||||
|
this.logger.debug(`Event ${eventId} already processed, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查交易账户是否已存在
|
||||||
|
const existingAccount =
|
||||||
|
await this.tradingAccountRepository.findByAccountSequence(accountCode);
|
||||||
|
|
||||||
|
if (existingAccount) {
|
||||||
|
this.logger.debug(`Trading account ${accountCode} already exists`);
|
||||||
|
await this.markEventProcessed(eventId, 'WalletSystemAccountCreated');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建交易账户
|
||||||
|
const account = TradingAccountAggregate.create(accountCode);
|
||||||
|
const accountId = await this.tradingAccountRepository.save(account);
|
||||||
|
|
||||||
|
// 发布交易账户创建事件
|
||||||
|
await this.publishAccountCreatedEvent(accountId, accountCode);
|
||||||
|
|
||||||
|
// 标记为已处理
|
||||||
|
await this.markEventProcessed(eventId, 'WalletSystemAccountCreated');
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Trading account created for system ${accountType}: ${accountCode} (triggered by WalletSystemAccountCreated)`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('Unique constraint')) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Trading account already exists for ${accountCode}, marking as processed`,
|
||||||
|
);
|
||||||
|
await this.markEventProcessed(eventId, 'WalletSystemAccountCreated');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to create trading account for system ${accountCode}`,
|
||||||
|
error instanceof Error ? error.stack : error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isEventProcessed(eventId: string): Promise<boolean> {
|
||||||
|
const redisKey = `trading:processed-event:wallet:${eventId}`;
|
||||||
|
|
||||||
|
const cached = await this.redis.get(redisKey);
|
||||||
|
if (cached) return true;
|
||||||
|
|
||||||
|
const dbRecord = await this.processedEventRepository.findByEventId(String(eventId));
|
||||||
|
if (dbRecord) {
|
||||||
|
await this.redis.set(redisKey, '1', IDEMPOTENCY_TTL_SECONDS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markEventProcessed(
|
||||||
|
eventId: string,
|
||||||
|
eventType: string = 'UserWalletCreated',
|
||||||
|
): Promise<void> {
|
||||||
|
const redisKey = `trading:processed-event:wallet:${eventId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.processedEventRepository.create({
|
||||||
|
eventId: String(eventId),
|
||||||
|
eventType,
|
||||||
|
sourceService: 'mining-wallet-service',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error && error.message.includes('Unique constraint'))) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.redis.set(redisKey, '1', IDEMPOTENCY_TTL_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async publishAccountCreatedEvent(
|
||||||
|
accountId: string,
|
||||||
|
accountSequence: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payload: TradingAccountCreatedPayload = {
|
||||||
|
accountId,
|
||||||
|
accountSequence,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.outboxRepository.create({
|
||||||
|
aggregateType: 'TradingAccount',
|
||||||
|
aggregateId: accountId,
|
||||||
|
eventType: TradingEventTypes.TRADING_ACCOUNT_CREATED,
|
||||||
|
payload,
|
||||||
|
topic: TradingTopics.ACCOUNTS,
|
||||||
|
key: accountSequence,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.debug(`Published TradingAccountCreated event for ${accountSequence}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to publish TradingAccountCreated event: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue