Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

Sms 短信域手册

Sms 域单体包结构、胖 DTO 模式、6 家供应商 HTTP 方式表、SMSResult 模式、已知问题与测试模式。

Last updated

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”,包含所有供应商可能用到的字段。每家供应商只读取自己需要的字段:

字段类型说明使用方
Tostring收件人号码。国内裸号或国际 E.164全部
Fromstring?国际主叫号或发送服务标识Twilio / Vonage
SignNamestring?短信签名名称阿里 / 腾讯 / 华为
TemplateIdstring?短信模板标识阿里 / 腾讯 / 华为 / 云片 / Twilio
TemplateParamsstring?模板变量(格式因供应商而异)全部(格式不同)
Bodystring?纯文本正文Twilio / Vonage / 云片
CountryCodestring?国家码国内供应商归一化号码
ExtraDictionary<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

内部错误码

SmsErrorCodesinternal static 类,定义本库自身生成的合成错误码:

常量说明
Exception"EXCEPTION"捕获到异常
ParseError"PARSE_ERROR"响应解析失败
Unknown"UNKNOWN"未知错误
NoResponse"NO_RESPONSE"供应商无响应
NoMessage"NO_MESSAGE"响应中无消息

这些区别于供应商返回的外部业务码(如阿里 OK、华为 000000)。

供应商清单

供应商HTTP 方式SDKProviderNameDI 方法
阿里云SDK 管道aliyun-net-sdk-core"Aliyun"AddAliyunSms()
腾讯云SDK 管道TencentCloudSDK"Tencent"AddTencentSms()
华为云Typed HttpClient无(WSSE 认证)"Huawei"AddHuaweiSms()
云片Typed HttpClient"Yunpian"AddYunpianSms()
TwilioTyped HttpClient"Twilio"AddTwilioSms()
VonageTyped 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);

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%