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;
}
// 重试配置
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<SmsSendResult> {
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<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));
}
/**