# 短信 (21_SMS)

最后更新: 2024-09-20


# 📚 概述

DarkM 框架的短信模块提供了统一的短信发送功能。支持多种短信服务商(阿里云、一心短信等),实现短信发送、批量发送、状态查询等功能。

源代码位置: DarkM/src/Framework/SMS


# 🏗️ 模块架构

# 项目结构

SMS/
├── SMS.Abstractions/                    # 抽象层(接口、模型、配置)
│   ├── ISms.cs                          # 短信发送接口
│   ├── SMSProvider.cs                   # 短信提供器枚举
│   └── Model/                           # 数据模型
│       ├── SmsConfig.cs                 # 短信配置
│       ├── SmsRequest.cs                # 短信请求
│       ├── SmsResponse.cs               # 短信响应
│       ├── OneXinXiSmsResponse.cs       # 一心短信响应
│       └── QueryDetailsResponse.cs      # 查询详情响应
│
├── SMS.Integration/                     # 集成层(DI 注册)
│   └── ServiceCollectionExtensions.cs    # DI 扩展
│
├── SMS.Aliyun/                          # 阿里云实现
│   └── Handler/
│       └── AliyunSMSSender.cs           # 阿里云发送器
│
├── SMS.OneXinXi/                        # 一心短信实现
│   └── Handler/
│       └── OneXinXiSender.cs            # 一心短信发送器
│
└── SMS.SystemNet/                       # 系统短信(预留)
    └── SystemNetSender.cs               # 系统短信发送器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 🔧 核心接口

# ISms(短信发送)

/// <summary>
/// 短信发送接口
/// </summary>
public interface ISms
{
    /// <summary>
    /// 发送短信(支持多手机号,模版相同/内容相同)
    /// </summary>
    /// <param name="phoneNumbers">
    /// 必填:待发送手机号。支持以逗号分隔的形式进行批量调用,
    /// 批量上限为 1000 个手机号码,批量调用相对于单条调用及时性稍有延迟,
    /// 验证码类型的短信推荐使用单条调用的方式
    /// </param>
    /// <param name="msg">短信内容</param>
    /// <param name="templateCode">
    /// 必填:短信模板 - 可在短信控制台中找到
    /// </param>
    /// <param name="templateParam">
    /// 可选:模板中的变量替换 JSON 串,
    /// 如模板内容为"亲爱的${name},您的验证码为${code}"时,
    /// 此处的值为:{"name":"Jack", "code":"456"}
    /// </param>
    /// <param name="outId">
    /// 可选:outId 为提供给业务方扩展字段,
    /// 最终在短信回执消息中将此值带回给调用者
    /// </param>
    /// <param name="signName">
    /// 必填:短信签名 - 可在短信控制台中找到
    /// </param>
    /// <returns>发送响应</returns>
    SmsResponse SendSms(
        string phoneNumbers,
        SmsRequest smsInfo,
        SmsConfig config);

    /// <summary>
    /// 批量发送短信(支持多手机号,模版相同,但是内容可以不同)
    /// </summary>
    /// <param name="phoneNumbers">
    /// 必填:待发送手机号。支持 JSON 格式的批量调用,
    /// 批量上限为 100 个手机号码,
    /// 格式如:["1500000000","1500000001"]
    /// </param>
    /// <param name="templateCode">
    /// 必填:短信模板 - 可在短信控制台中找到,如:SMS_1000000
    /// </param>
    /// <param name="templateParamJson">
    /// 必填:模板中的变量替换 JSON 串,
    /// 格式如:[{"name":"Tom", "code":"123"},{"name":"Jack", "code":"456"}]
    /// </param>
    /// <param name="smsUpExtendCodeJson">
    /// 可选:上行短信扩展码,
    /// 格式如:["90997","90998"]
    /// </param>
    /// <param name="signNameJson">
    /// 必填:短信签名 - 支持不同的号码发送不同的短信签名,
    /// 格式如:["云通信","云通信"]
    /// </param>
    /// <returns>发送响应</returns>
    SmsResponse SendBatchSms(
        string phoneNumbers,
        string templateCode,
        string templateParamJson,
        SmsConfig config,
        string smsUpExtendCodeJson = null,
        string signNameJson = null);

    /// <summary>
    /// 查询短信发送状态
    /// </summary>
    /// <param name="phoneNumber">必填:号码</param>
    /// <param name="bizId">可选:流水号</param>
    /// <param name="sendDate">
    /// 必填:发送日期 支持 30 天内记录查询,格式 yyyyMMdd
    /// </param>
    /// <param name="pageSize">必填:页大小</param>
    /// <param name="currentPage">必填:当前页码从 1 开始计数</param>
    /// <returns>查询响应</returns>
    QueryDetailsResponse QuerySendDetails(
        string phoneNumber,
        string bizId,
        SmsConfig config,
        string sendDate = null,
        int pageSize = 30,
        int currentPage = 1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

# SMSProvider(短信提供器)

[Description("SMS 处理器")]
public enum SMSProvider
{
    /// <summary>
    /// 阿里云短信
    /// </summary>
    [Description("Aliyun")]
    Aliyun,

    /// <summary>
    /// 一心短信
    /// </summary>
    [Description("OneXinXi")]
    OneXinXi,

    /// <summary>
    /// 自定义 SMS 服务
    /// </summary>
    [Description("自定义")]
    Custome
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# SmsConfig(短信配置)

/// <summary>
/// 发送短信的账号配置
/// </summary>
public class SmsConfig
{
    /// <summary>
    /// 接口账号
    /// </summary>
    public string AccessKeyId { get; set; }

    /// <summary>
    /// 接口账号密码
    /// </summary>
    public string AccessKeySecret { get; set; }

    /// <summary>
    /// 接口默认签名
    /// </summary>
    public string DefaultSignName { get; set; }

    /// <summary>
    /// 接口一个默认的模版
    /// </summary>
    public string DefaultTemplateCode { get; set; }

    /// <summary>
    /// 是否开启测试模式
    /// 默认 false
    /// </summary>
    public bool EnableTest { get; set; } = false;

    /// <summary>
    /// 开启测试模式时,发送的测试手机号
    /// </summary>
    public string TestMobile { get; set; }

    /// <summary>
    /// 请求对应的:产品名称:云通信短信 API 产品,开发者无需替换
    /// 默认:Dysmsapi,请不要赋值
    /// </summary>
    public string Product = "Dysmsapi";

    /// <summary>
    /// 请求地址
    /// 默认:dysmsapi.aliyuncs.com,请不要赋值
    /// </summary>
    public string BaseAddress = "dysmsapi.aliyuncs.com";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# SmsRequest(短信请求)

/// <summary>
/// 短信请求数据
/// </summary>
public class SmsRequest
{
    /// <summary>
    /// 短信模版编号
    /// </summary>
    public string TemplateCode { get; set; }

    /// <summary>
    /// 短信内容
    /// </summary>
    public string Message { get; set; }

    /// <summary>
    /// 短信模版参数
    /// </summary>
    public string TemplateParam { get; set; }

    /// <summary>
    /// 短信关联 ID
    /// </summary>
    public string OutId { get; set; }

    /// <summary>
    /// 短信签名
    /// </summary>
    public string SignName { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# SmsResponse(短信响应)

public class SmsResponse
{
    /// <summary>
    /// 请求 ID
    /// </summary>
    public string RequestId { get; set; }

    /// <summary>
    /// 业务 ID(用于查询)
    /// </summary>
    public string BizId { get; set; }

    /// <summary>
    /// 响应码
    /// </summary>
    public string Code { get; set; }

    /// <summary>
    /// 响应消息
    /// </summary>
    public string Message { get; set; }

    /// <summary>
    /// 错误码
    /// </summary>
    public int ErrorCode { get; set; }

    /// <summary>
    /// 无效号码数量
    /// </summary>
    public int InvalidCount { get; set; }

    /// <summary>
    /// 成功数量
    /// </summary>
    public int SuccessCount { get; set; }

    /// <summary>
    /// 黑名单数量
    /// </summary>
    public int BlackCount { get; set; }

    /// <summary>
    /// 是否发送成功
    /// </summary>
    public bool IsSuccess()
    {
        return "OK".Equals(Code);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 🏗️ 依赖注入

# 自动注册机制

// SMS.Integration/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// 添加短信发送服务
    /// </summary>
    public static IServiceCollection AddSMS(
        this IServiceCollection services, 
        IConfiguration cfg)
    {
        var providerName = string.Empty;
        var list = EnumExtensions.ToResult<SMSProvider>(false, true);

        if (list != null && list.Count > 0)
        {
            foreach (var one in list)
            {
                // 加载所有短信实现程序集
                var assembly = AssemblyHelper.LoadByNameEndString(
                    $".Lib.SMS.{one.Value}");
                
                if (assembly != null)
                {
                    // 注册 ISms 实现
                    var handlerType = assembly.GetTypes()
                        .FirstOrDefault(m => typeof(ISms).IsAssignableFrom(m));
                    
                    if (handlerType != null)
                    {
                        services.AddSingleton(typeof(ISms), handlerType);
                    }
                }
            }
        }

        return services;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 💡 使用示例

# 1. 配置短信模块

// appsettings.json
{
  "SMS": {
    "Provider": "Aliyun",
    "Aliyun": {
      "AccessKeyId": "your-access-key-id",
      "AccessKeySecret": "your-access-key-secret",
      "DefaultSignName": "您的签名",
      "DefaultTemplateCode": "SMS_123456789",
      "EnableTest": false,
      "TestMobile": "13800138000"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2. 注册服务

// Program.cs 或 Startup.cs
var builder = WebApplication.CreateBuilder(args);

// 添加短信服务
builder.Services.AddSMS(builder.Configuration);

var app = builder.Build();
1
2
3
4
5
6
7

# 3. 发送单条短信

public class SmsService
{
    private readonly ISms _sms;
    private readonly SmsConfig _smsConfig;
    private readonly ILogger<SmsService> _logger;

    public SmsService(
        ISms sms,
        IOptions<SmsConfig> smsConfig,
        ILogger<SmsService> logger)
    {
        _sms = sms;
        _smsConfig = smsConfig.Value;
        _logger = logger;
    }

    /// <summary>
    /// 发送验证码短信
    /// </summary>
    public async Task<SmsResponse> SendVerificationCodeAsync(
        string phoneNumber,
        string code)
    {
        var request = new SmsRequest
        {
            TemplateCode = _smsConfig.DefaultTemplateCode,
            SignName = _smsConfig.DefaultSignName,
            TemplateParam = $"{{\"code\":\"{code}\"}}"
        };

        var response = _sms.SendSms(
            phoneNumbers: phoneNumber,
            smsInfo: request,
            config: _smsConfig);

        if (response.IsSuccess())
        {
            _logger.LogInformation(
                "验证码短信发送成功:{Phone}, BizId: {BizId}",
                phoneNumber,
                response.BizId);
        }
        else
        {
            _logger.LogWarning(
                "验证码短信发送失败:{Phone}, Code: {Code}, Message: {Message}",
                phoneNumber,
                response.Code,
                response.Message);
        }

        return response;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 4. 发送通知短信

public class NotificationSmsService
{
    private readonly ISms _sms;
    private readonly SmsConfig _smsConfig;

    public NotificationSmsService(
        ISms sms,
        IOptions<SmsConfig> smsConfig)
    {
        _sms = sms;
        _smsConfig = smsConfig.Value;
    }

    /// <summary>
    /// 发送订单通知
    /// </summary>
    public SmsResponse SendOrderNotification(
        string phoneNumber,
        string orderNo,
        decimal amount,
        string productName)
    {
        var request = new SmsRequest
        {
            TemplateCode = "SMS_ORDER_NOTIFY", // 订单通知模板
            SignName = _smsConfig.DefaultSignName,
            TemplateParam = JsonSerializer.Serialize(new
            {
                orderNo = orderNo,
                amount = amount.ToString("F2"),
                productName = productName
            })
        };

        return _sms.SendSms(
            phoneNumbers: phoneNumber,
            smsInfo: request,
            config: _smsConfig);
    }

    /// <summary>
    /// 发送物流通知
    /// </summary>
    public SmsResponse SendShippingNotification(
        string phoneNumber,
        string orderNo,
        string courierCompany,
        string trackingNo)
    {
        var request = new SmsRequest
        {
            TemplateCode = "SMS_SHIPPING_NOTIFY", // 物流通知模板
            SignName = _smsConfig.DefaultSignName,
            TemplateParam = JsonSerializer.Serialize(new
            {
                orderNo = orderNo,
                courierCompany = courierCompany,
                trackingNo = trackingNo
            })
        };

        return _sms.SendSms(
            phoneNumbers: phoneNumber,
            smsInfo: request,
            config: _smsConfig);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 5. 批量发送短信

public class BatchSmsService
{
    private readonly ISms _sms;
    private readonly SmsConfig _smsConfig;
    private readonly ILogger<BatchSmsService> _logger;

    public BatchSmsService(
        ISms sms,
        IOptions<SmsConfig> smsConfig,
        ILogger<BatchSmsService> logger)
    {
        _sms = sms;
        _smsConfig = smsConfig.Value;
        _logger = logger;
    }

    /// <summary>
    /// 批量发送相同内容短信
    /// </summary>
    public SmsResponse SendBatchSameContent(
        List<string> phoneNumbers,
        string templateCode,
        Dictionary<string, string> templateParams)
    {
        var request = new SmsRequest
        {
            TemplateCode = templateCode,
            SignName = _smsConfig.DefaultSignName,
            TemplateParam = JsonSerializer.Serialize(templateParams)
        };

        var response = _sms.SendSms(
            phoneNumbers: string.Join(",", phoneNumbers),
            smsInfo: request,
            config: _smsConfig);

        _logger.LogInformation(
            "批量短信发送完成 - 总数:{Total}, 成功:{Success}, 失败:{Invalid}, 黑名单:{Black}",
            phoneNumbers.Count,
            response.SuccessCount,
            response.InvalidCount,
            response.BlackCount);

        return response;
    }

    /// <summary>
    /// 批量发送不同内容短信
    /// </summary>
    public SmsResponse SendBatchDifferentContent(
        List<(string phone, Dictionary<string, string> @params)> recipients,
        string templateCode)
    {
        var phoneNumbersJson = JsonSerializer.Serialize(
            recipients.Select(r => r.phone).ToList());

        var templateParamJson = JsonSerializer.Serialize(
            recipients.Select(r => r.@params).ToList());

        var response = _sms.SendBatchSms(
            phoneNumbers: phoneNumbersJson,
            templateCode: templateCode,
            templateParamJson: templateParamJson,
            config: _smsConfig);

        _logger.LogInformation(
            "批量个性化短信发送完成 - 总数:{Total}, 成功:{Success}",
            recipients.Count,
            response.SuccessCount);

        return response;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 6. 查询短信状态

public class SmsQueryService
{
    private readonly ISms _sms;
    private readonly SmsConfig _smsConfig;

    public SmsQueryService(
        ISms sms,
        IOptions<SmsConfig> smsConfig)
    {
        _sms = sms;
        _smsConfig = smsConfig.Value;
    }

    /// <summary>
    /// 查询短信发送详情
    /// </summary>
    public QueryDetailsResponse QuerySmsStatus(
        string phoneNumber,
        string bizId,
        DateTime sendDate)
    {
        return _sms.QuerySendDetails(
            phoneNumber: phoneNumber,
            bizId: bizId,
            config: _smsConfig,
            sendDate: sendDate.ToString("yyyyMMdd"),
            pageSize: 10,
            currentPage: 1);
    }

    /// <summary>
    /// 查询今日发送统计
    /// </summary>
    public async Task<SmsStatistics> GetTodayStatisticsAsync()
    {
        // 实现统计逻辑
        return new SmsStatistics
        {
            Date = DateTime.Today,
            TotalSent = 0,
            SuccessCount = 0,
            FailedCount = 0
        };
    }
}

public class SmsStatistics
{
    public DateTime Date { get; set; }
    public int TotalSent { get; set; }
    public int SuccessCount { get; set; }
    public int FailedCount { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 7. 短信控制器

[Route("api/[controller]")]
[ApiController]
public class SmsController : ModuleController
{
    private readonly SmsService _smsService;
    private readonly NotificationSmsService _notificationService;
    private readonly BatchSmsService _batchService;

    public SmsController(
        SmsService smsService,
        NotificationSmsService notificationService,
        BatchSmsService batchService)
    {
        _smsService = smsService;
        _notificationService = notificationService;
        _batchService = batchService;
    }

    /// <summary>
    /// 发送验证码
    /// </summary>
    [HttpPost("send-code")]
    public async Task<IResultModel> SendCode(
        [FromBody] SendCodeRequest request)
    {
        // 生成验证码
        var code = new Random().Next(100000, 999999).ToString();

        // 缓存验证码(5 分钟有效期)
        var cacheKey = $"sms_code_{request.Phone}";
        await _cache.SetAsync(
            cacheKey,
            code,
            TimeSpan.FromMinutes(5));

        // 发送短信
        var response = await _smsService.SendVerificationCodeAsync(
            request.Phone,
            code);

        if (response.IsSuccess())
        {
            return ResultModel.Success("验证码已发送");
        }
        else
        {
            return ResultModel.Error($"发送失败:{response.Message}");
        }
    }

    /// <summary>
    /// 发送订单通知
    /// </summary>
    [HttpPost("notify-order")]
    public IResultModel NotifyOrder([FromBody] NotifyOrderRequest request)
    {
        var response = _notificationService.SendOrderNotification(
            request.Phone,
            request.OrderNo,
            request.Amount,
            request.ProductName);

        if (response.IsSuccess())
        {
            return ResultModel.Success("通知已发送");
        }
        else
        {
            return ResultModel.Error($"发送失败:{response.Message}");
        }
    }

    /// <summary>
    /// 批量发送
    /// </summary>
    [HttpPost("send-batch")]
    public IResultModel SendBatch([FromBody] SendBatchRequest request)
    {
        var recipients = request.Phones
            .Select(p => (p, new Dictionary<string, string>
            {
                { "code", request.Code }
            }))
            .ToList();

        var response = _batchService.SendBatchDifferentContent(
            recipients,
            request.TemplateCode);

        if (response.IsSuccess())
        {
            return ResultModel.Success(
                $"发送完成 - 成功:{response.SuccessCount}/{recipients.Count}");
        }
        else
        {
            return ResultModel.Error($"发送失败:{response.Message}");
        }
    }

    /// <summary>
    /// 查询发送状态
    /// </summary>
    [HttpGet("query-status")]
    public IResultModel QueryStatus(
        [FromQuery] string phone,
        [FromQuery] string bizId,
        [FromQuery] DateTime sendDate)
    {
        var result = _smsQueryService.QuerySmsStatus(
            phone,
            bizId,
            sendDate);

        return ResultModel.Success(result);
    }
}

public class SendCodeRequest
{
    public string Phone { get; set; }
}

public class NotifyOrderRequest
{
    public string Phone { get; set; }
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
    public string ProductName { get; set; }
}

public class SendBatchRequest
{
    public List<string> Phones { get; set; }
    public string TemplateCode { get; set; }
    public string Code { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

# 🔧 短信提供器对比

# SMS.Aliyun(阿里云短信)

特点:

  • ✅ 国内覆盖率高
  • ✅ 到达率高(99%+)
  • ✅ 支持验证码、通知、营销短信
  • ✅ 完善的 API 文档
  • ✅ 支持签名和模板管理
  • ⚠️ 需要实名认证
  • ⚠️ 营销短信审核严格

适用场景:

  • 验证码短信
  • 订单通知
  • 物流通知
  • 会员营销

配置示例:

{
  "SMS": {
    "Aliyun": {
      "AccessKeyId": "LTAI5t...",
      "AccessKeySecret": "...",
      "DefaultSignName": "您的签名",
      "DefaultTemplateCode": "SMS_123456789"
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# SMS.OneXinXi(一心短信)

特点:

  • ✅ 价格相对较低
  • ✅ 支持三网合一
  • ✅ 发送速度快
  • ⚠️ 品牌知名度较低
  • ⚠️ 文档相对简单

适用场景:

  • 成本敏感场景
  • 大批量发送
  • 通知类短信

# 💡 最佳实践

# 1. 验证码防刷

public class AntiSpamSmsService
{
    private readonly ISms _sms;
    private readonly SmsConfig _smsConfig;
    private readonly IMemoryCache _cache;

    public AntiSpamSmsService(
        ISms sms,
        IOptions<SmsConfig> smsConfig,
        IMemoryCache cache)
    {
        _sms = sms;
        _smsConfig = smsConfig.Value;
        _cache = cache;
    }

    /// <summary>
    /// 发送验证码(带防刷限制)
    /// </summary>
    public async Task<SmsResponse> SendCodeWithAntiSpamAsync(
        string phoneNumber)
    {
        // 检查频率限制
        var sendKey = $"sms_send_limit_{phoneNumber}";
        if (_cache.TryGetValue(sendKey, out _))
        {
            throw new SmsFrequencyLimitException(
                "发送过于频繁,请稍后再试");
        }

        // 检查每日发送次数
        var dailyKey = $"sms_daily_count_{phoneNumber}";
        var dailyCount = _cache.Get<int>(dailyKey);
        if (dailyCount >= 5)
        {
            throw new SmsDailyLimitException(
                "今日发送次数已达上限");
        }

        // 生成验证码
        var code = new Random().Next(100000, 999999).ToString();

        // 发送短信
        var response = await SendVerificationCodeAsync(phoneNumber, code);

        if (response.IsSuccess())
        {
            // 设置发送频率限制(60 秒)
            _cache.Set(sendKey, true, TimeSpan.FromSeconds(60));

            // 更新每日计数
            _cache.Set(dailyKey, dailyCount + 1, TimeSpan.FromDays(1));

            // 缓存验证码(5 分钟)
            _cache.Set($"sms_code_{phoneNumber}", code, TimeSpan.FromMinutes(5));
        }

        return response;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 2. 短信模板管理

public class SmsTemplateService
{
    private readonly Dictionary<string, SmsTemplate> _templates;

    public SmsTemplateService()
    {
        _templates = new Dictionary<string, SmsTemplate>
        {
            ["VERIFY_CODE"] = new SmsTemplate
            {
                Code = "SMS_123456789",
                SignName = "您的签名",
                Content = "您的验证码为${code},5 分钟内有效",
                Params = new[] { "code" }
            },
            ["ORDER_NOTIFY"] = new SmsTemplate
            {
                Code = "SMS_987654321",
                SignName = "您的签名",
                Content = "您的订单${orderNo}已发货,快递公司:${courier},单号:${trackingNo}",
                Params = new[] { "orderNo", "courier", "trackingNo" }
            }
        };
    }

    public SmsTemplate GetTemplate(string templateKey)
    {
        if (!_templates.ContainsKey(templateKey))
            throw new TemplateNotFoundException($"模板不存在:{templateKey}");

        return _templates[templateKey];
    }
}

public class SmsTemplate
{
    public string Code { get; set; }
    public string SignName { get; set; }
    public string Content { get; set; }
    public string[] Params { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 3. 短信发送日志

public class SmsLogService
{
    private readonly ILogger<SmsLogService> _logger;

    public SmsLogService(ILogger<SmsLogService> logger)
    {
        _logger = logger;
    }

    public void LogSmsSent(
        string phoneNumber,
        string templateCode,
        string content,
        SmsResponse response,
        string businessType = null,
        string businessId = null)
    {
        _logger.LogInformation(
            "短信发送 - 手机:{Phone}, 模板:{Template}, 业务类型:{BizType}, 业务 ID: {BizId}, 结果:{Code}, 消息:{Message}, 请求 ID: {RequestId}",
            phoneNumber,
            templateCode,
            businessType,
            businessId,
            response.Code,
            response.Message,
            response.RequestId);
    }

    public void LogSmsFailed(
        string phoneNumber,
        string templateCode,
        Exception ex,
        string businessType = null,
        string businessId = null)
    {
        _logger.LogError(
            ex,
            "短信发送失败 - 手机:{Phone}, 模板:{Template}, 业务类型:{BizType}, 业务 ID: {BizId}",
            phoneNumber,
            templateCode,
            businessType,
            businessId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 🔍 常见问题

# Q1: 短信发送失败?

检查清单:

  1. ✅ 确认 AccessKeyId 和 AccessKeySecret 正确
  2. ✅ 确认短信签名已审核通过
  3. ✅ 确认短信模板已审核通过
  4. ✅ 确认手机号格式正确
  5. ✅ 确认账户余额充足
  6. ✅ 查看错误码和错误信息

常见错误码:

  • isv.BUSINESS_LIMIT_CONTROL - 业务限制(频率控制)
  • isv.INVALID_PHONE_NUMBER - 手机号格式错误
  • isv.SIGN_NAME_ILLEGAL - 签名不合法
  • isv.TEMPLATE_MISSING_PARAMETERS - 模板缺少参数

# Q2: 如何选择合适的短信服务商?

选择建议:

场景 推荐服务商 理由
验证码 阿里云 到达率高、速度快
订单通知 阿里云 稳定可靠
营销短信 一心短信 价格较低
国际短信 阿里云 覆盖国家多
成本敏感 一心短信 单价较低

# Q3: 如何处理短信回执?

解决方案:

// 阿里云短信回执需要通过 MNS 消息服务接收
public class SmsReceiptService
{
    public async Task ProcessReceiptAsync(SmsReceipt receipt)
    {
        // 更新发送状态
        await _smsLogRepository.UpdateStatusAsync(
            receipt.BizId,
            receipt.Status,
            receipt.ReceiveTime);

        // 处理失败重发
        if (receipt.Status == "DELIVERED_FAILED")
        {
            await HandleFailedReceiptAsync(receipt);
        }
    }

    private async Task HandleFailedReceiptAsync(SmsReceipt receipt)
    {
        // 检查重发次数
        var retryCount = await _smsLogRepository.GetRetryCountAsync(
            receipt.BizId);

        if (retryCount < 3)
        {
            // 重发短信
            await _smsService.RetrySendAsync(receipt.BizId);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20