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;
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue