121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||
import { Cron } from '@nestjs/schedule';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { C2cOrderRepository } from '../../infrastructure/persistence/repositories/c2c-order.repository';
|
||
import { C2cBotService } from '../services/c2c-bot.service';
|
||
import { RedisService } from '../../infrastructure/redis/redis.service';
|
||
|
||
/**
|
||
* C2C Bot 定时任务
|
||
* 定期扫描待处理的卖单并自动购买
|
||
*/
|
||
@Injectable()
|
||
export class C2cBotScheduler implements OnModuleInit {
|
||
private readonly logger = new Logger(C2cBotScheduler.name);
|
||
private readonly LOCK_KEY = 'c2c:bot:scheduler:lock';
|
||
static readonly ENABLED_KEY = 'c2c:bot:enabled';
|
||
|
||
constructor(
|
||
private readonly c2cOrderRepository: C2cOrderRepository,
|
||
private readonly c2cBotService: C2cBotService,
|
||
private readonly redis: RedisService,
|
||
private readonly configService: ConfigService,
|
||
) {}
|
||
|
||
async onModuleInit() {
|
||
// 如果 Redis 中没有设置,用环境变量初始化
|
||
const existing = await this.redis.get(C2cBotScheduler.ENABLED_KEY);
|
||
if (existing === null) {
|
||
const envEnabled = this.configService.get<string>('C2C_BOT_ENABLED', 'false');
|
||
await this.redis.set(C2cBotScheduler.ENABLED_KEY, envEnabled === 'true' ? 'true' : 'false');
|
||
}
|
||
|
||
const enabled = await this.isEnabled();
|
||
this.logger.log(`C2C Bot Scheduler initialized, enabled: ${enabled}`);
|
||
|
||
if (enabled) {
|
||
const isAvailable = await this.c2cBotService.isAvailable();
|
||
if (isAvailable) {
|
||
const balance = await this.c2cBotService.getHotWalletBalance();
|
||
this.logger.log(`Hot wallet balance: ${balance} dUSDT`);
|
||
} else {
|
||
this.logger.warn('Mining blockchain service not available, Bot will not process orders');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查 Bot 是否启用(读 Redis,回退到环境变量)
|
||
*/
|
||
async isEnabled(): Promise<boolean> {
|
||
const value = await this.redis.get(C2cBotScheduler.ENABLED_KEY);
|
||
if (value === null) {
|
||
return this.configService.get<string>('C2C_BOT_ENABLED', 'false') === 'true';
|
||
}
|
||
return value === 'true';
|
||
}
|
||
|
||
/**
|
||
* 每10秒扫描待处理的卖单
|
||
*/
|
||
@Cron('*/10 * * * * *')
|
||
async processPendingSellOrders(): Promise<void> {
|
||
if (!(await this.isEnabled())) {
|
||
return;
|
||
}
|
||
|
||
const lockValue = await this.redis.acquireLock(this.LOCK_KEY, 30); // 30秒锁(留足链上转账时间)
|
||
if (!lockValue) {
|
||
return; // 其他实例正在处理
|
||
}
|
||
|
||
try {
|
||
// 每个周期只处理1个订单,确保顺序执行
|
||
const orders = await this.c2cOrderRepository.findPendingSellOrdersForBot(1);
|
||
|
||
if (orders.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const order = orders[0];
|
||
this.logger.log(`[SCHEDULER] Processing order ${order.orderNo} (amount: ${order.totalAmount})`);
|
||
|
||
try {
|
||
const success = await this.c2cBotService.purchaseOrder(order);
|
||
this.logger.log(`[SCHEDULER] Order ${order.orderNo}: ${success ? 'success' : 'failed'}`);
|
||
} catch (error: any) {
|
||
this.logger.error(`[SCHEDULER] Error processing order ${order.orderNo}: ${error.message}`);
|
||
}
|
||
} catch (error: any) {
|
||
this.logger.error(`[SCHEDULER] Error in processPendingSellOrders: ${error.message}`);
|
||
} finally {
|
||
await this.redis.releaseLock(this.LOCK_KEY, lockValue);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 每分钟检查热钱包余额
|
||
*/
|
||
@Cron('0 * * * * *')
|
||
async checkHotWalletBalance(): Promise<void> {
|
||
if (!(await this.isEnabled())) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const balance = await this.c2cBotService.getHotWalletBalance();
|
||
if (balance) {
|
||
// 如果余额低于阈值,记录警告
|
||
const threshold = this.configService.get<number>('C2C_BOT_BALANCE_THRESHOLD', 1000);
|
||
const balanceNum = parseFloat(balance);
|
||
|
||
if (balanceNum < threshold) {
|
||
this.logger.warn(`[SCHEDULER] Hot wallet balance (${balance}) is below threshold (${threshold})`);
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
this.logger.error(`[SCHEDULER] Error checking hot wallet balance: ${error.message}`);
|
||
}
|
||
}
|
||
}
|