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 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 { [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; } }