forked from gongxuegit/tiku-backend.net
feat: add phase five operations foundation
This commit is contained in:
169
Tiku.Infrastructure/Auth/AliyunSmsProvider.cs
Normal file
169
Tiku.Infrastructure/Auth/AliyunSmsProvider.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Text.Json;
|
||||
using AlibabaCloud.OpenApiClient.Models;
|
||||
using AlibabaCloud.SDK.Dysmsapi20170525.Models;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using DysmsClient = AlibabaCloud.SDK.Dysmsapi20170525.Client;
|
||||
|
||||
namespace Tiku.Infrastructure.Auth;
|
||||
|
||||
internal sealed class AliyunSmsProvider(
|
||||
ITenantExternalProviderConfigService providerConfigService) : ISmsProvider
|
||||
{
|
||||
private const string ProviderCode = "aliyun_sms";
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<SmsProviderSendResult> SendAsync(
|
||||
SmsProviderSendRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
TenantExternalProviderAccount account;
|
||||
try
|
||||
{
|
||||
account = await providerConfigService.GetActiveProviderAsync(
|
||||
request.TenantId,
|
||||
TenantExternalProviderCapability.Sms,
|
||||
null,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (TenantExternalProviderException)
|
||||
{
|
||||
return new SmsProviderSendResult("noop", "accepted");
|
||||
}
|
||||
|
||||
if (string.Equals(account.Provider, "noop", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(account.Provider, "local_dev", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SmsProviderSendResult(account.Provider, "accepted");
|
||||
}
|
||||
|
||||
if (!string.Equals(account.Provider, ProviderCode, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
$"SMS provider '{account.Provider}' is not supported.",
|
||||
"sms_provider_unsupported");
|
||||
}
|
||||
|
||||
var signName = Required(account.ConfigPublic, "signName", "smsSignName");
|
||||
var templateCode = ResolveTemplateCode(account.ConfigPublic, request.Purpose);
|
||||
var accessKeyId = Required(account.SecretPayload, "accessKeyId");
|
||||
var accessKeySecret = Required(account.SecretPayload, "accessKeySecret");
|
||||
var endpoint = Optional(account.ConfigPublic, "endpoint") ?? "dysmsapi.aliyuncs.com";
|
||||
var regionId = Optional(account.ConfigPublic, "regionId", "region") ?? "cn-hangzhou";
|
||||
|
||||
var client = new DysmsClient(new Config
|
||||
{
|
||||
AccessKeyId = accessKeyId,
|
||||
AccessKeySecret = accessKeySecret,
|
||||
Endpoint = endpoint,
|
||||
RegionId = regionId
|
||||
});
|
||||
|
||||
var sendRequest = new SendSmsRequest
|
||||
{
|
||||
PhoneNumbers = request.Phone,
|
||||
SignName = signName,
|
||||
TemplateCode = templateCode,
|
||||
TemplateParam = BuildTemplateParam(account.ConfigPublic, request),
|
||||
OutId = request.TenantId.ToString("N")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendSmsAsync(sendRequest).WaitAsync(cancellationToken);
|
||||
var body = response.Body;
|
||||
if (body is null)
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
"Aliyun SMS returned an empty response.",
|
||||
"aliyun_sms_empty_response");
|
||||
}
|
||||
|
||||
if (!string.Equals(body.Code, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
$"Aliyun SMS send failed: {body.Code}.",
|
||||
"aliyun_sms_send_rejected");
|
||||
}
|
||||
|
||||
return new SmsProviderSendResult(
|
||||
ProviderCode,
|
||||
body.Code ?? "OK",
|
||||
body.BizId ?? body.RequestId);
|
||||
}
|
||||
catch (SmsProviderException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
throw new SmsProviderException(
|
||||
"Aliyun SMS send failed.",
|
||||
"aliyun_sms_send_failed",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveTemplateCode(JsonElement config, SmsPurpose purpose)
|
||||
{
|
||||
if (config.ValueKind == JsonValueKind.Object &&
|
||||
config.TryGetProperty("templateCodes", out var templateCodes) &&
|
||||
templateCodes.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var purposeKey = purpose.ToString().ToLowerInvariant();
|
||||
if (templateCodes.TryGetProperty(purposeKey, out var purposeTemplate) &&
|
||||
purposeTemplate.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(purposeTemplate.GetString()))
|
||||
{
|
||||
return purposeTemplate.GetString()!;
|
||||
}
|
||||
}
|
||||
|
||||
return Required(config, "templateCode", "smsTemplateCode");
|
||||
}
|
||||
|
||||
private static string BuildTemplateParam(JsonElement config, SmsProviderSendRequest request)
|
||||
{
|
||||
var codeField = Optional(config, "codeField") ?? "code";
|
||||
return JsonSerializer.Serialize(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
[codeField] = request.Code
|
||||
},
|
||||
JsonOptions);
|
||||
}
|
||||
|
||||
private static string Required(JsonElement element, params string[] keys)
|
||||
{
|
||||
var value = Optional(element, keys);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new SmsProviderException(
|
||||
"Aliyun SMS provider configuration is incomplete.",
|
||||
"aliyun_sms_config_incomplete");
|
||||
}
|
||||
|
||||
private static string? Optional(JsonElement element, params string[] keys)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (element.TryGetProperty(key, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrWhiteSpace(property.GetString()))
|
||||
{
|
||||
return property.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user