Files
license-manager/backend/docs/modules_design/2026.1.10-短信发送服务.md
T

9.0 KiB
Raw Blame History

短信发送模块设计

概述

基于阿里云短信服务,实现验证码发送功能,支持注册、登录、密码重置等场景。

技术栈

  • 短信服务商: 阿里云短信服务
  • SDK: github.com/alibabacloud-go/dysmsapi-20170525/v5
  • 缓存: Redis (存储验证码)
  • 频率限制: Redis (限制发送频率)

核心功能

  1. 验证码发送: 支持手机号验证
  2. 频率控制: 防止短信轰炸
  3. 验证码缓存: Redis存储和管理
  4. 错误处理: 完善的错误响应

架构设计

1. 配置结构

// internal/config/config.go
type SMSConfig struct {
    Enabled         bool   `mapstructure:"enabled"`          // 是否启用短信服务
    AccessKeyID     string `mapstructure:"access_key_id"`    // 阿里云AccessKey ID
    AccessKeySecret string `mapstructure:"access_key_secret"`// 阿里云AccessKey Secret
    SignName        string `mapstructure:"sign_name"`        // 短信签名
    RegionID        string `mapstructure:"region_id"`        // 地域ID,默认cn-hangzhou
    Endpoint        string `mapstructure:"endpoint"`         // 服务端点
}

// 添加到主配置
type Config struct {
    // ... 其他配置
    SMS SMSConfig `mapstructure:"sms"`
}

2. 服务接口

// pkg/utils/sms.go
type SMSService interface {
    SendVerificationCode(phone, phoneCountryCode, templateCode string) error
    VerifyCode(phone, phoneCountryCode, code string) (bool, error)
    IsRateLimited(phone, phoneCountryCode string) bool
}

type smsService struct {
    client   *dysmsapi20170525.Client
    redis    *redis.Client // 验证码缓存和频率限制
    config   *SMSConfig
    logger   *log.Logger
}

3. 配置与常量设计

为什么不完全使用配置文件?

不推荐完全配置化的原因:

  1. 模板代码相对稳定:阿里云模板一旦创建,不会频繁变化
  2. 类型安全:代码常量有编译时检查,避免运行时错误
  3. 重构友好:IDE可以自动重构所有引用
  4. 性能优化:常量在编译时确定,无运行时查找开销

推荐的混合方案

// pkg/utils/sms.go

// 默认短信模板常量(生产环境使用)
const (
    DefaultTemplateRegister  = "SMS_330275014"
    DefaultTemplateResetPwd  = "SMS_330275015"
    DefaultTemplateLogin     = "SMS_330275016"
)

// 缓存键常量
const (
    SMSCodeKeyPrefix  = "sms:code"
    SMSLimitKeyPrefix = "sms:limit"
    SMSCodeExpireTime = 5 * time.Minute
    SMSLimitTimeWindow = time.Hour
    SMSLimitMaxCount = 5
)

// SMS模板配置(支持运行时覆盖)
type SMSTemplates struct {
    Register  string `mapstructure:"register"`
    ResetPwd  string `mapstructure:"reset_pwd"`
    Login     string `mapstructure:"login"`
}

// 使用方法:优先使用配置,否则使用默认常量
func (s *smsService) getTemplateCode(templateType string) string {
    var templateCode string

    switch templateType {
    case "register":
        templateCode = s.templates.Register
        if templateCode == "" {
            templateCode = DefaultTemplateRegister
        }
    case "reset_pwd":
        templateCode = s.templates.ResetPwd
        if templateCode == "" {
            templateCode = DefaultTemplateResetPwd
        }
    case "login":
        templateCode = s.templates.Login
        if templateCode == "" {
            templateCode = DefaultTemplateLogin
        }
    }

    return templateCode
}

配置文件示例

# configs/config.yaml
sms:
  enabled: true
  access_key_id: "your-access-key-id"
  access_key_secret: "your-access-key-secret"
  sign_name: "惠州顺视智能科技"
  region_id: "cn-hangzhou"
  endpoint: "dysmsapi.aliyuncs.com"

  # 模板配置(可选,不配置则使用代码中的默认值)
  templates:
    register: "SMS_330275014"      # 注册模板
    reset_pwd: "SMS_330275015"     # 重置密码模板
    login: "SMS_330275016"         # 登录模板
    current_phone: "SMS_330275017" # 当前手机号验证模板
    new_phone: "SMS_330275018"     # 新手机号验证模板

注意:所有模板ID都必须在阿里云SMS控制台中存在且审核通过,否则会报错"isv.SMS_TEMPLATE_ILLEGAL"

4. 缓存设计

验证码存储

  • Key: sms:code:{country_code}:{phone}
  • Value: 6位数字验证码
  • TTL: 5分钟

频率限制

  • Key: sms:limit:{country_code}:{phone}
  • Value: 发送次数计数器
  • TTL: 1小时
  • 限制: 每小时最多5次

5. 阿里云SMS API集成

已实现功能 ✅

  • ✅ AccessKey认证方式
  • ✅ 短信发送API调用
  • ✅ 错误处理和响应解析
  • ✅ 缓存集成(验证码存储和频率限制)
  • ✅ 多环境配置支持

API调用流程

  1. 客户端创建: 使用AccessKeyID和AccessKeySecret创建阿里云SMS客户端
  2. 请求构建: 构建SendSmsRequest,包含签名、模板、手机号和参数
  3. API调用: 调用阿里云SMS API发送短信
  4. 响应处理: 解析响应,检查发送结果
  5. 缓存存储: 成功发送后将验证码存储到缓存

错误处理

  • 认证错误: InvalidAccessKeyId.NotFound 等
  • 参数错误: 手机号格式、模板不存在等
  • 网络错误: 超时、重试等
  • 配额错误: 发送频率限制等

测试验证

# 运行SMS服务测试
go test ./pkg/utils/ -v -run TestSMS

# 验证阿里云API调用
# 使用有效AccessKey时会成功发送短信
# 使用无效AccessKey时会返回认证错误

接口设计

发送验证码

// 发送注册验证码
func (s *smsService) SendRegisterCode(phone, phoneCountryCode string) error {
    return s.SendVerificationCode(phone, phoneCountryCode, TemplateRegister)
}

// 发送重置密码验证码
func (s *smsService) SendResetPwdCode(phone, phoneCountryCode string) error {
    return s.SendVerificationCode(phone, phoneCountryCode, TemplateResetPwd)
}

验证码验证

// 验证验证码
func (s *smsService) VerifyCode(phone, phoneCountryCode, code string) (bool, error) {
    key := fmt.Sprintf("sms:code:%s:%s", phoneCountryCode, phone)

    storedCode, err := s.redis.Get(key).Result()
    if err == redis.Nil {
        return false, errors.New("验证码不存在或已过期")
    }
    if err != nil {
        return false, err
    }

    // 验证成功后删除验证码(一次性使用)
    if storedCode == code {
        s.redis.Del(key)
        return true, nil
    }

    return false, nil
}

频率限制

// 检查频率限制
func (s *smsService) IsRateLimited(phone, phoneCountryCode string) bool {
    key := fmt.Sprintf("sms:limit:%s:%s", phoneCountryCode, phone)

    count, err := s.redis.Incr(key).Result()
    if err != nil {
        return true // Redis错误时限制发送
    }

    // 首次发送,设置过期时间
    if count == 1 {
        s.redis.Expire(key, time.Hour)
    }

    // 每小时最多5次
    return count > 5
}

集成方式

1. Service层集成

// internal/service/cu_user_service.go
func (s *cuUserService) SendRegisterSms(ctx context.Context, req *models.CuUserSendRegisterSmsRequest) error {
    // 1. 参数验证
    if req == nil {
        return i18n.NewI18nError("900001", lang)
    }

    // 2. 检查手机号是否已注册
    exists, err := s.repo.CheckPhoneExists(req.Phone, req.PhoneCountryCode, "")
    if err != nil {
        return i18n.NewI18nError("900004", lang, err.Error())
    }
    if exists {
        return i18n.NewI18nError("200002", lang)
    }

    // 3. 调用短信服务
    err = s.smsService.SendRegisterCode(req.Phone, req.PhoneCountryCode)
    if err != nil {
        return i18n.NewI18nError("200004", lang) // 短信发送失败
    }

    return nil
}

2. 依赖注入

// internal/api/routes/router.go
func SetupRouter() *gin.Engine {
    // ... 其他代码

    // 初始化短信服务
    smsService := utils.NewSMSService(&cfg.SMS, redisClient, logger)

    // 初始化用户服务时注入短信服务
    cuUserService := service.NewCuUserService(cuUserRepo, customerRepo, db, smsService)

    // ... 其他代码
}

错误码扩展

在语言包中添加短信相关错误码:

# customer模块扩展
customer:
  "200003": "请求过于频繁,请稍后再试"
  "200004": "短信发送失败"

安全考虑

  1. 验证码保护

    • 验证码一次性使用
    • 过期时间控制(5分钟)
    • Redis存储,防止内存泄露
  2. 频率控制

    • 基于手机号的发送频率限制
    • 防止短信轰炸攻击
    • 支持国际号码
  3. 日志记录

    • 发送成功/失败日志
    • 敏感信息脱敏
    • 便于问题排查

实施计划

第一阶段:基础功能

  1. 实现SMS服务基础类
  2. 集成阿里云短信SDK
  3. 实现验证码发送和验证
  4. 添加频率限制

第二阶段:完善功能

  1. 添加多种验证码模板
  2. 完善错误处理和日志
  3. 添加监控指标
  4. 性能优化

第三阶段:高级功能

  1. 支持国际短信
  2. 模板动态配置
  3. 批量发送功能
  4. 失败重试机制