diff --git a/backend/services/identity-service/src/infrastructure/external/sms/sms.service.ts b/backend/services/identity-service/src/infrastructure/external/sms/sms.service.ts index a6c82985..5977f692 100644 --- a/backend/services/identity-service/src/infrastructure/external/sms/sms.service.ts +++ b/backend/services/identity-service/src/infrastructure/external/sms/sms.service.ts @@ -12,6 +12,21 @@ export interface SmsSendResult { message?: string; } +// 重试配置 +const RETRY_CONFIG = { + maxRetries: 3, // 最大重试次数 + baseDelay: 1000, // 基础延迟 1 秒 + maxDelay: 5000, // 最大延迟 5 秒 + retryableErrors: [ + 'ConnectTimeout', + 'ReadTimeout', + 'ETIMEDOUT', + 'ECONNRESET', + 'ECONNREFUSED', + 'ServiceUnavailable', + ], +}; + @Injectable() export class SmsService implements OnModuleInit { private readonly logger = new Logger(SmsService.name); @@ -68,7 +83,7 @@ export class SmsService implements OnModuleInit { } /** - * 发送验证码短信 + * 发送验证码短信(带重试机制) * * @param phoneNumber 手机号码(支持国际格式,如 +86xxx) * @param code 验证码 @@ -99,61 +114,141 @@ export class SmsService implements OnModuleInit { }; } - try { - const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({ - phoneNumbers: normalizedPhone, - signName: this.signName, - templateCode: this.templateCode, - templateParam: JSON.stringify({ code }), - }); + // 带重试的发送 + return this.sendWithRetry(normalizedPhone, code); + } - const runtime = new $Util.RuntimeOptions({ - connectTimeout: 10000, // 连接超时 10 秒 - readTimeout: 10000, // 读取超时 10 秒 - }); - const response = await this.client.sendSmsWithOptions( - sendSmsRequest, - runtime, - ); + /** + * 带重试机制的短信发送 + */ + private async sendWithRetry( + phoneNumber: string, + code: string, + ): Promise { + let lastError: any = null; - const body = response.body; - const result: SmsSendResult = { - success: body?.code === 'OK', - requestId: body?.requestId, - bizId: body?.bizId, - code: body?.code, - message: body?.message, - }; + for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) { + try { + if (attempt > 0) { + // 指数退避延迟 + const delay = Math.min( + RETRY_CONFIG.baseDelay * Math.pow(2, attempt - 1), + RETRY_CONFIG.maxDelay, + ); + this.logger.log( + `[SMS] 第 ${attempt} 次重试,延迟 ${delay}ms...`, + ); + await this.sleep(delay); + } - if (result.success) { - this.logger.log( - `[SMS] 发送成功: requestId=${result.requestId}, bizId=${result.bizId}`, - ); - } else { - this.logger.error( - `[SMS] 发送失败: code=${result.code}, message=${result.message}`, + const result = await this.doSendSms(phoneNumber, code); + + if (result.success) { + if (attempt > 0) { + this.logger.log( + `[SMS] 重试成功 (第 ${attempt} 次): requestId=${result.requestId}`, + ); + } + return result; + } + + // 业务错误(如频率限制)不重试 + if (!this.isRetryableError(result.code || '')) { + return result; + } + + lastError = new Error(result.message || 'SMS send failed'); + (lastError as any).code = result.code; + } catch (error: any) { + lastError = error; + + // 检查是否是可重试的错误 + if (!this.isRetryableError(error.message || error.code || '')) { + this.logger.error(`[SMS] 不可重试错误: ${error.message}`); + return { + success: false, + code: error.code || 'UNKNOWN_ERROR', + message: error.message || '短信发送失败', + }; + } + + this.logger.warn( + `[SMS] 发送失败 (尝试 ${attempt + 1}/${RETRY_CONFIG.maxRetries + 1}): ${error.message}`, ); } - - return result; - } catch (error: any) { - this.logger.error(`[SMS] 发送异常: ${error.message}`, error.stack); - - // 解析阿里云错误 - if (error.code) { - return { - success: false, - code: error.code, - message: error.message || '短信发送失败', - }; - } - - return { - success: false, - code: 'UNKNOWN_ERROR', - message: error.message || '短信发送失败', - }; } + + // 所有重试都失败 + this.logger.error( + `[SMS] 发送失败,已重试 ${RETRY_CONFIG.maxRetries} 次: ${lastError?.message}`, + ); + return { + success: false, + code: lastError?.code || 'RETRY_EXHAUSTED', + message: `短信发送失败,已重试 ${RETRY_CONFIG.maxRetries} 次: ${lastError?.message || '未知错误'}`, + }; + } + + /** + * 实际发送短信 + */ + private async doSendSms( + phoneNumber: string, + code: string, + ): Promise { + const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({ + phoneNumbers: phoneNumber, + signName: this.signName, + templateCode: this.templateCode, + templateParam: JSON.stringify({ code }), + }); + + const runtime = new $Util.RuntimeOptions({ + connectTimeout: 15000, // 连接超时 15 秒 + readTimeout: 15000, // 读取超时 15 秒 + }); + + const response = await this.client!.sendSmsWithOptions( + sendSmsRequest, + runtime, + ); + + const body = response.body; + const result: SmsSendResult = { + success: body?.code === 'OK', + requestId: body?.requestId, + bizId: body?.bizId, + code: body?.code, + message: body?.message, + }; + + if (result.success) { + this.logger.log( + `[SMS] 发送成功: requestId=${result.requestId}, bizId=${result.bizId}`, + ); + } else { + this.logger.error( + `[SMS] 发送失败: code=${result.code}, message=${result.message}`, + ); + } + + return result; + } + + /** + * 检查是否是可重试的错误 + */ + private isRetryableError(errorInfo: string): boolean { + return RETRY_CONFIG.retryableErrors.some( + (retryable) => errorInfo.includes(retryable), + ); + } + + /** + * 睡眠函数 + */ + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); } /**