Sms 是全库唯一不拆包的成熟域。6 家供应商(阿里云 / 腾讯云 / 华为云 / 云片 / Twilio / Vonage)以子命名空间内嵌在单一包内。本页是域级手册。
能力边界
| 能力 | 是否支持 | 说明 |
|---|---|---|
| 发送短信 | 支持 | 模板短信与纯文本短信 |
| 模板参数 | 支持 | 各供应商格式不同(见 DTO 说明) |
| 国际短信 | 支持 | Twilio / Vonage 支持 E.164 号码 |
| 查询发送状态 | 不支持 | 统一接口不含查询能力 |
| 批量发送 | 不支持 | 单条发送 |
| 回调 / 上行 | 不支持 | 统一接口不含回调 |
域结构
Sms 采用单体包结构,与 Payment 的三层拆包形成对比:
Bitzsoft.Integrations.Sms/├── ISMS.cs ← 抽象接口 + SMSResult├── SmsMessage.cs ← 统一消息对象(胖 DTO)├── SmsErrorCodes.cs ← 内部错误码常量├── ServiceCollectionExtensions.cs ← 全部 6 家 DI 注册├── Aliyun/ { AliyunSmsOptions, AliyunSmsService }├── Tencent/ { TencentSmsOptions, TencentSmsService }├── Huawei/ { HuaweiSmsOptions, HuaweiSmsService }├── Yunpian/ { YunpianSmsOptions, YunpianSmsService }├── Twilio/ { TwilioSmsOptions, TwilioSmsService }└── Vonage/ { VonageSmsOptions, VonageSmsService }没有 .Sms.Aliyun 厂商包,没有 .All 聚合包。所有供应商共享同一个 ISMS 接口,同一时间只能注册一家。
统一接口
public interface ISMS{ string ProviderName { get; }
Task<SMSResult> SendAsync( SmsMessage message, CancellationToken cancellationToken = default);}ProviderName 返回供应商标识字符串(如 "Aliyun"、"Twilio")。注意与 Payment 域的 Code 属性不同——Sms 返回的是显示名而非路由常量。
胖 DTO 模式
SmsMessage 是”胖 DTO”,包含所有供应商可能用到的字段。每家供应商只读取自己需要的字段:
| 字段 | 类型 | 说明 | 使用方 |
|---|---|---|---|
To | string | 收件人号码。国内裸号或国际 E.164 | 全部 |
From | string? | 国际主叫号或发送服务标识 | Twilio / Vonage |
SignName | string? | 短信签名名称 | 阿里 / 腾讯 / 华为 |
TemplateId | string? | 短信模板标识 | 阿里 / 腾讯 / 华为 / 云片 / Twilio |
TemplateParams | string? | 模板变量(格式因供应商而异) | 全部(格式不同) |
Body | string? | 纯文本正文 | Twilio / Vonage / 云片 |
CountryCode | string? | 国家码 | 国内供应商归一化号码 |
Extra | Dictionary<string, string>? | 供应商特有扩展字段 | 华为(sender / extend) |
SMSResult 模式
public class SMSResult{ public bool Success { get; set; } public string? RequestId { get; set; } public string? BizId { get; set; } public string? ErrorCode { get; set; } public string? ErrorMessage { get; set; }
public static SMSResult Ok(string? requestId = null, string? bizId = null); public static SMSResult Fail(string errorCode, string errorMessage); public static SMSResult FromException(Exception ex);}工厂方法
| 方法 | 说明 |
|---|---|
Ok(requestId, bizId) | 成功结果,携带请求 ID 与业务流水号 |
Fail(errorCode, errorMessage) | 失败结果,携带错误码与消息 |
FromException(ex) | 异常转失败结果,错误码固定 "EXCEPTION",消息为 ex.Message |
内部错误码
SmsErrorCodes 为 internal static 类,定义本库自身生成的合成错误码:
| 常量 | 值 | 说明 |
|---|---|---|
Exception | "EXCEPTION" | 捕获到异常 |
ParseError | "PARSE_ERROR" | 响应解析失败 |
Unknown | "UNKNOWN" | 未知错误 |
NoResponse | "NO_RESPONSE" | 供应商无响应 |
NoMessage | "NO_MESSAGE" | 响应中无消息 |
这些区别于供应商返回的外部业务码(如阿里 OK、华为 000000)。
供应商清单
| 供应商 | HTTP 方式 | SDK | ProviderName | DI 方法 |
|---|---|---|---|---|
| 阿里云 | SDK 管道 | aliyun-net-sdk-core | "Aliyun" | AddAliyunSms() |
| 腾讯云 | SDK 管道 | TencentCloudSDK | "Tencent" | AddTencentSms() |
| 华为云 | Typed HttpClient | 无(WSSE 认证) | "Huawei" | AddHuaweiSms() |
| 云片 | Typed HttpClient | 无 | "Yunpian" | AddYunpianSms() |
| Twilio | Typed HttpClient | 无 | "Twilio" | AddTwilioSms() |
| Vonage | Typed HttpClient | 无 | "Vonage" | AddVonageSms() |
审计日志差异
SDK 管道与 Typed HttpClient 的审计方式不同:
| HTTP 方式 | 审计机制 |
|---|---|
| SDK 管道(阿里 / 腾讯) | IRequestLogRecorder 回调(SDK 自带 HTTP 管道无法通过 DelegatingHandler 拦截) |
| Typed HttpClient(华为 / 云片 / Twilio / Vonage) | .AddRequestLogging<T>() DelegatingHandler |
阿里云 / 腾讯云的注册方法会自动调用 AddRequestLogging()(幂等)注册审计基础设施。
DI 命名不一致
同时,Sms 域注册为 ISMS(Transient),不通过 AddIntegrationProviderCapability 注册,因此无法通过 IIntegrationProviderResolver<ISMS> 多厂商路由。同一时间只能有一家供应商注册。
注册
// 阿里云(使用 SDK 管道)builder.Services.AddAliyunSms(builder.Configuration.GetSection("AliyunSms"));
// 腾讯云(使用 SDK 管道)builder.Services.AddTencentSms(builder.Configuration.GetSection("TencentSms"));
// 华为云(使用 Typed HttpClient + WSSE 认证)builder.Services.AddHuaweiSms(builder.Configuration.GetSection("HuaweiSms"));
// 云片builder.Services.AddYunpianSms(builder.Configuration.GetSection("YunpianSms"));
// Twilio(国际短信)builder.Services.AddTwilioSms(builder.Configuration.GetSection("TwilioSms"));
// Vonage(国际短信)builder.Services.AddVonageSms(builder.Configuration.GetSection("VonageSms"));每个方法有 Action<TOptions> 和 IConfigurationSection 两个重载。
消费
public class NotificationService(ISMS sms){ public async Task SendVerificationCodeAsync(string phone, string code) { var result = await sms.SendAsync(new SmsMessage { To = phone, SignName = "Bitzsoft", TemplateId = "SMS_123456789", TemplateParams = """{"code":"1234"}""", // JSON 字符串 });
if (!result.Success) throw new InvalidOperationException( $"短信发送失败: {result.ErrorCode} - {result.ErrorMessage}"); }}
// 国际短信(Twilio)public class InternationalSmsService(ISMS sms){ public async Task SendAsync(string phone, string message) { var result = await sms.SendAsync(new SmsMessage { To = phone, // E.164 格式,如 +12025550123 Body = message, // 纯文本 }); }}已知问题
OperationCanceledException 吞没
部分供应商实现的 catch (OperationCanceledException) { throw; } 模式不完整。当前源码中 Aliyun / Twilio 已修复(显式 catch (OperationCanceledException) { throw; } 在 catch (Exception) 之前),但早期代码可能存在兜底 catch (Exception) 吞没取消异常的问题。
正确模式(当前已实现):
try{ // ... 发送逻辑}catch (OperationCanceledException){ throw; // 取消异常直接重抛,不被吞没}catch (Exception ex){ return SMSResult.FromException(ex);}SMSResult 属性可变
与 Payment 域的 init 属性不同,SMSResult 的属性为 set(可变),调用方理论上可以修改结果对象。这是遗留设计,新代码应避免依赖此特性。
单厂商限制
ISMS 注册为 AddTransient<ISMS, XxxService>(),同一时间只能注册一家供应商。需要多供应商切换时,需自行实现工厂模式或使用键值服务。
测试模式
SDK 管道供应商测试
阿里云 / 腾讯云使用 SDK 自带 HTTP 管道,测试时需 mock SDK 客户端或使用集成测试环境。
Typed HttpClient 供应商测试
华为 / 云片 / Twilio / Vonage 使用 Typed HttpClient,测试时注册 mock handler:
var handler = new MockHttpMessageHandler();services.AddHttpClient<TwilioSmsService>() .ConfigurePrimaryHttpMessageHandler(() => handler);Result 断言
var result = await sms.SendAsync(message);
Assert.True(result.Success);Assert.NotNull(result.BizId);