feat: 添加短信发送重试机制提高可靠性

- 最多重试 3 次(共 4 次尝试)
- 指数退避延迟(1s, 2s, 4s)
- 超时时间增加到 15 秒
- 只对网络错误重试,业务错误不重试
- 可重试错误:ConnectTimeout, ReadTimeout, ETIMEDOUT, ECONNRESET 等

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-21 20:32:45 -08:00
parent b4dafc9e38
commit e5e4e3512b
1 changed files with 145 additions and 50 deletions

View File

@ -12,6 +12,21 @@ export interface SmsSendResult {
message?: string; message?: string;
} }
// 重试配置
const RETRY_CONFIG = {
maxRetries: 3, // 最大重试次数
baseDelay: 1000, // 基础延迟 1 秒
maxDelay: 5000, // 最大延迟 5 秒
retryableErrors: [
'ConnectTimeout',
'ReadTimeout',
'ETIMEDOUT',
'ECONNRESET',
'ECONNREFUSED',
'ServiceUnavailable',
],
};
@Injectable() @Injectable()
export class SmsService implements OnModuleInit { export class SmsService implements OnModuleInit {
private readonly logger = new Logger(SmsService.name); private readonly logger = new Logger(SmsService.name);
@ -68,7 +83,7 @@ export class SmsService implements OnModuleInit {
} }
/** /**
* *
* *
* @param phoneNumber +86xxx * @param phoneNumber +86xxx
* @param code * @param code
@ -99,61 +114,141 @@ export class SmsService implements OnModuleInit {
}; };
} }
try { // 带重试的发送
const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({ return this.sendWithRetry(normalizedPhone, code);
phoneNumbers: normalizedPhone, }
signName: this.signName,
templateCode: this.templateCode,
templateParam: JSON.stringify({ code }),
});
const runtime = new $Util.RuntimeOptions({ /**
connectTimeout: 10000, // 连接超时 10 秒 *
readTimeout: 10000, // 读取超时 10 秒 */
}); private async sendWithRetry(
const response = await this.client.sendSmsWithOptions( phoneNumber: string,
sendSmsRequest, code: string,
runtime, ): Promise<SmsSendResult> {
); let lastError: any = null;
const body = response.body; for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
const result: SmsSendResult = { try {
success: body?.code === 'OK', if (attempt > 0) {
requestId: body?.requestId, // 指数退避延迟
bizId: body?.bizId, const delay = Math.min(
code: body?.code, RETRY_CONFIG.baseDelay * Math.pow(2, attempt - 1),
message: body?.message, RETRY_CONFIG.maxDelay,
}; );
this.logger.log(
`[SMS] 第 ${attempt} 次重试,延迟 ${delay}ms...`,
);
await this.sleep(delay);
}
if (result.success) { const result = await this.doSendSms(phoneNumber, code);
this.logger.log(
`[SMS] 发送成功: requestId=${result.requestId}, bizId=${result.bizId}`, if (result.success) {
); if (attempt > 0) {
} else { this.logger.log(
this.logger.error( `[SMS] 重试成功 (第 ${attempt} 次): requestId=${result.requestId}`,
`[SMS] 发送失败: code=${result.code}, message=${result.message}`, );
}
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<SmsSendResult> {
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<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
} }
/** /**