不能直接裸调vendor包,因各厂商SDK在调用方式、错误码、重试逻辑、签名模板格式上差异大,硬编码导致耦合高、维护难;需通过接口抽象(SmsSender)、统一请求/错误结构、按需加载、注册表机制、分级重试、UUID幂等、YAML配置+环境变量隔离敏感信息等设计实现可插拔、可降级、可扩展的短信模块。

为什么不能直接用 vendor 包裸调短信接口
不同短信服务商(如阿里云、腾讯云、华为云)的 SDK 调用方式、错误码结构、重试逻辑、签名/模板参数格式差异极大,硬编码对接多个平台会导致业务层被耦合死。一旦某家服务下线或接口变更,就得改一堆 SendSms 调用点。
统一模块的核心不是“写个通用函数”,而是把「渠道抽象」和「失败兜底」提前设计好。比如:AliyunSmsClient 和 TencentSmsClient 都实现同一个 SmsSender 接口,上层只依赖接口,不关心具体实现。
- 必须定义统一的请求结构体:
SmsRequest{Phone, TemplateID, Params map[string]string, SignName string},所有厂商适配器负责把它转成自家 SDK 所需格式 - 错误要归一化:把
aliyun: InvalidParameter、tencent: 1004都映射为自定义错误类型ErrSmsTemplateInvalid,业务层用 switch 判断即可,不用查各家文档 - 别在初始化时就 new SDK client —— 要支持按需加载,避免启动失败(比如配置错了一个渠道,不该让整个服务起不来)
如何设计可插拔的渠道注册机制
硬写 if-else 判断 provider == "aliyun" 是反模式。Go 里更稳妥的方式是用 map + 函数注册表,类似 HTTP mux 的思路。
声明一个全局变量:var senders = make(map[string]SmsSender),然后提供注册函数:
立即学习“go语言免费学习笔记(深入)”;
func RegisterSender(name string, sender SmsSender) {
senders[name] = sender
}
各厂商初始化逻辑放在各自包的 init() 函数里,例如 aliyun/sms.go:
func init() {
RegisterSender("aliyun", &AliyunSmsClient{
Config: &aliyunConfig,
})
}
业务调用时只需:sender := senders["aliyun"],不存在就 panic 或返回 error —— 这比字符串硬匹配更易测试和 mock。
- 注册名必须小写且唯一,建议和配置项 key 保持一致(如配置里写
provider: aliyun,代码里就注册"aliyun") - 不要在注册函数里做 heavy 初始化(如连接池创建),放到
SmsSender.Send()第一次调用时懒加载更安全 - 测试时可直接
RegisterSender("mock", &MockSmsSender{}),无需改配置
重试与降级怎么写才不踩坑
短信发送失败不等于业务失败,但也不能无脑重试。常见错误包括:模板未审核、余额不足、频次超限 —— 这些重试 10 次也没用,反而压垮下游。
推荐分三级处理:
- 网络层失败(timeout / connection refused):立即重试,最多 2 次,间隔 100ms
- 服务商返回明确错误码(如阿里云
isv.BUSINESS_LIMIT_CONTROL):记录日志并直接返回,不重试 - 未知错误(空响应 / status=500):进异步队列,由后台 worker 以指数退避(1s → 3s → 9s)重发,最多 3 轮
关键点:重试逻辑必须封装在 SmsSender.Send() 内部,而不是让业务层自己循环调用。否则每个调用方都要重复写判断,且容易漏掉幂等性检查(比如重试时没带 OutID 导致重复发)。
阿里云 SDK 支持传 OutId 做去重,腾讯云叫 SessionContext,统一模块里应强制生成 UUID 并透传到底层,避免业务方遗漏。
配置热加载与敏感信息隔离
短信密钥绝不能写死在代码里,也不能全靠环境变量 —— 环境变量无法区分不同环境(dev/staging/prod)的模板 ID,也难做灰度切换。
推荐方案:用 YAML 配置文件 + viper 加载,结构示例:
sms:
default_provider: aliyun
providers:
aliyun:
access_key_id: ${ALIYUN_ACCESS_KEY_ID}
access_key_secret: ${ALIYUN_ACCESS_KEY_SECRET}
region_id: cn-hangzhou
template_ids:
register: "SMS_12345678"
reset_pwd: "SMS_87654321"
注意两点:
- 敏感字段(
access_key_secret)必须用环境变量注入,配置文件里只留占位符,避免误提交 -
template_ids按业务场景拆分,而不是写死在代码里 —— 某个模板被驳回时,只需改配置重启,不动代码 - 别用
viper.WatchConfig()自动热重载 —— 短信模块对配置一致性要求极高,热更新期间可能出现旧模板 ID + 新密钥的错配,建议重启生效
真正麻烦的是多租户场景:不同客户要用不同签名和模板。这时候模板 ID 就不能只靠配置文件静态定义,得在 SmsRequest 里动态传入,并校验白名单 —— 这部分逻辑容易漏,上线前务必压测验证。


















