import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; import { FundAllocationDTO } from '../../domain/value-objects/fund-allocation.vo'; export interface DeductForPlantingRequest { userId: string; amount: number; orderId: string; } export interface AllocateFundsRequest { orderId: string; allocations: FundAllocationDTO[]; } export interface WalletBalance { userId: string; available: number; locked: number; currency: string; } export interface FreezeForPlantingRequest { userId: string; accountSequence?: string; // 跨服务关联标识(优先使用) amount: number; orderId: string; } export interface ConfirmPlantingDeductionRequest { userId: string; accountSequence?: string; // 跨服务关联标识(优先使用) orderId: string; } export interface UnfreezeForPlantingRequest { userId: string; accountSequence?: string; // 跨服务关联标识(优先使用) orderId: string; } export interface FreezeResult { success: boolean; frozenAmount: number; } /** * HTTP 重试配置 */ interface RetryConfig { maxRetries: number; baseDelayMs: number; maxDelayMs: number; } const DEFAULT_RETRY_CONFIG: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000, }; @Injectable() export class WalletServiceClient { private readonly logger = new Logger(WalletServiceClient.name); private readonly baseUrl: string; private readonly retryConfig: RetryConfig; constructor( private readonly configService: ConfigService, private readonly httpService: HttpService, ) { this.baseUrl = this.configService.get('WALLET_SERVICE_URL') || 'http://localhost:3002'; this.retryConfig = DEFAULT_RETRY_CONFIG; } /** * 带重试的 HTTP 请求包装器 * - 使用指数退避策略 * - 只对网络错误和 5xx 错误进行重试 * - 4xx 错误(客户端错误)不重试 */ private async withRetry( operation: string, fn: () => Promise, config: RetryConfig = this.retryConfig, ): Promise { let lastError: Error | undefined; for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { return await fn(); } catch (error: unknown) { lastError = error as Error; // 判断是否应该重试 const shouldRetry = this.shouldRetry(error, attempt, config.maxRetries); if (!shouldRetry) { throw error; } // 计算退避延迟(指数退避 + 随机抖动) const delay = this.calculateBackoffDelay(attempt, config); this.logger.warn( `${operation} failed (attempt ${attempt + 1}/${config.maxRetries + 1}), ` + `retrying in ${delay}ms: ${(error as Error).message}`, ); await this.delay(delay); } } throw lastError; } /** * 判断是否应该重试 */ private shouldRetry( error: unknown, attempt: number, maxRetries: number, ): boolean { // 已达到最大重试次数 if (attempt >= maxRetries) { return false; } // 检查是否是 HTTP 响应错误 const axiosError = error as { response?: { status?: number }; code?: string }; // 网络错误(无响应)- 应该重试 if (!axiosError.response) { // 常见的网络错误码 const retryableCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND']; if (axiosError.code && retryableCodes.includes(axiosError.code)) { return true; } // 超时错误 if ((error as Error).message?.includes('timeout')) { return true; } return true; // 网络问题默认重试 } // 5xx 服务器错误 - 应该重试 if (axiosError.response.status && axiosError.response.status >= 500) { return true; } // 4xx 客户端错误 - 不重试(业务错误) // 429 Too Many Requests - 可以重试 if (axiosError.response.status === 429) { return true; } return false; } /** * 计算指数退避延迟 */ private calculateBackoffDelay(attempt: number, config: RetryConfig): number { // 基础延迟 * 2^attempt + 随机抖动 const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt); const jitter = Math.random() * config.baseDelayMs * 0.5; return Math.min(exponentialDelay + jitter, config.maxDelayMs); } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 获取用户钱包余额 */ async getBalance(userId: string): Promise { try { return await this.withRetry(`getBalance(${userId})`, async () => { const response = await firstValueFrom( this.httpService.get( `${this.baseUrl}/api/v1/wallets/${userId}/balance`, ), ); return response.data; }); } catch (error) { this.logger.error(`Failed to get balance for user ${userId}`, error); // 在开发环境返回模拟数据 if (this.configService.get('NODE_ENV') === 'development') { return { userId, available: 100000, locked: 0, currency: 'USDT', }; } throw error; } } /** * 认种扣款(幂等,支持重试) */ async deductForPlanting(request: DeductForPlantingRequest): Promise { try { return await this.withRetry( `deductForPlanting(${request.orderId})`, async () => { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/api/v1/wallets/deduct-for-planting`, request, ), ); return response.data.success; }, ); } catch (error) { this.logger.error( `Failed to deduct for planting: ${request.orderId}`, error, ); // 在开发环境模拟成功 if (this.configService.get('NODE_ENV') === 'development') { this.logger.warn('Development mode: simulating successful deduction'); return true; } throw error; } } /** * 冻结资金用于认种 */ async freezeForPlanting(request: FreezeForPlantingRequest): Promise { try { return await this.withRetry( `freezeForPlanting(${request.orderId})`, async () => { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/api/v1/wallets/freeze-for-planting`, request, ), ); return response.data; }, ); } catch (error) { this.logger.error( `Failed to freeze for planting: ${request.orderId}`, error, ); // 在开发环境模拟成功 if (this.configService.get('NODE_ENV') === 'development') { this.logger.warn('Development mode: simulating successful freeze'); return { success: true, frozenAmount: request.amount }; } throw error; } } /** * 确认认种扣款(从冻结金额扣除) */ async confirmPlantingDeduction(request: ConfirmPlantingDeductionRequest): Promise { try { return await this.withRetry( `confirmPlantingDeduction(${request.orderId})`, async () => { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/api/v1/wallets/confirm-planting-deduction`, request, ), ); return response.data.success; }, ); } catch (error) { this.logger.error( `Failed to confirm planting deduction: ${request.orderId}`, error, ); // 在开发环境模拟成功 if (this.configService.get('NODE_ENV') === 'development') { this.logger.warn('Development mode: simulating successful confirmation'); return true; } throw error; } } /** * 解冻资金(认种失败时回滚) */ async unfreezeForPlanting(request: UnfreezeForPlantingRequest): Promise { try { return await this.withRetry( `unfreezeForPlanting(${request.orderId})`, async () => { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/api/v1/wallets/unfreeze-for-planting`, request, ), ); return response.data.success; }, ); } catch (error) { this.logger.error( `Failed to unfreeze for planting: ${request.orderId}`, error, ); // 在开发环境模拟成功 if (this.configService.get('NODE_ENV') === 'development') { this.logger.warn('Development mode: simulating successful unfreeze'); return true; } throw error; } } /** * 执行资金分配(幂等,支持重试) */ async allocateFunds(request: AllocateFundsRequest): Promise { try { return await this.withRetry( `allocateFunds(${request.orderId})`, async () => { const response = await firstValueFrom( this.httpService.post( `${this.baseUrl}/api/v1/wallets/allocate-funds`, request, ), ); return response.data.success; }, ); } catch (error) { this.logger.error( `Failed to allocate funds for order: ${request.orderId}`, error, ); // 在开发环境模拟成功 if (this.configService.get('NODE_ENV') === 'development') { this.logger.warn('Development mode: simulating successful allocation'); return true; } throw error; } } /** * 注入底池 */ async injectToPool( batchId: string, amount: number, ): Promise<{ txHash: string }> { try { return await this.withRetry(`injectToPool(${batchId})`, async () => { const response = await firstValueFrom( this.httpService.post<{ txHash: string }>( `${this.baseUrl}/api/v1/pool/inject`, { batchId, amount }, ), ); return response.data; }); } catch (error) { this.logger.error(`Failed to inject to pool: batch ${batchId}`, error); // 在开发环境返回模拟交易哈希 if (this.configService.get('NODE_ENV') === 'development') { this.logger.warn('Development mode: simulating pool injection'); return { txHash: `0x${Date.now().toString(16)}${Math.random().toString(16).substring(2)}`, }; } throw error; } } }