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:
parent
b4dafc9e38
commit
e5e4e3512b
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue