refactor: 清理租户API旧模块代码
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
using TakeoutSaaS.Module.Sms.Models;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 短信发送抽象。
|
||||
/// </summary>
|
||||
public interface ISmsSender
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务商类型。
|
||||
/// </summary>
|
||||
SmsProviderKind Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送短信。
|
||||
/// </summary>
|
||||
Task<SmsSendResult> SendAsync(SmsSendRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace TakeoutSaaS.Module.Sms.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 短信服务商解析器。
|
||||
/// </summary>
|
||||
public interface ISmsSenderResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取指定服务商的发送器。
|
||||
/// </summary>
|
||||
ISmsSender Resolve(SmsProviderKind? provider = null);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TakeoutSaaS.Module.Sms.Abstractions;
|
||||
using TakeoutSaaS.Module.Sms.Options;
|
||||
using TakeoutSaaS.Module.Sms.Services;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 短信模块 DI 注册扩展。
|
||||
/// </summary>
|
||||
public static class SmsServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册短信模块(包含腾讯云、阿里云实现)。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSmsModule(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<SmsOptions>()
|
||||
.Bind(configuration.GetSection("Sms"))
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddHttpClient(nameof(TencentSmsSender));
|
||||
services.AddHttpClient(nameof(AliyunSmsSender));
|
||||
|
||||
services.AddSingleton<ISmsSender, TencentSmsSender>();
|
||||
services.AddSingleton<ISmsSender, AliyunSmsSender>();
|
||||
services.AddSingleton<ISmsSenderResolver, SmsSenderResolver>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
namespace TakeoutSaaS.Module.Sms.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 短信发送请求。
|
||||
/// </summary>
|
||||
public sealed class SmsSendRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化短信发送请求。
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber">目标手机号码(含国家码,如 +86xxxxxxxxxxx)。</param>
|
||||
/// <param name="templateCode">模版编号。</param>
|
||||
/// <param name="variables">模版变量。</param>
|
||||
/// <param name="signName">短信签名。</param>
|
||||
public SmsSendRequest(string phoneNumber, string templateCode, IDictionary<string, string> variables, string? signName = null)
|
||||
{
|
||||
PhoneNumber = phoneNumber;
|
||||
TemplateCode = templateCode;
|
||||
Variables = new Dictionary<string, string>(variables);
|
||||
SignName = signName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标手机号。
|
||||
/// </summary>
|
||||
public string PhoneNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 模版编号。
|
||||
/// </summary>
|
||||
public string TemplateCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 模版变量。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Variables { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选的签名。
|
||||
/// </summary>
|
||||
public string? SignName { get; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace TakeoutSaaS.Module.Sms.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 短信发送结果。
|
||||
/// </summary>
|
||||
public sealed class SmsSendResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否发送成功。
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商返回的请求标识。
|
||||
/// </summary>
|
||||
public string? RequestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 描述信息。
|
||||
/// </summary>
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云短信配置。
|
||||
/// </summary>
|
||||
public sealed class AliyunSmsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// AccessKeyId。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string AccessKeyId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// AccessKeySecret。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string AccessKeySecret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 短信服务域名。
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = "dysmsapi.aliyuncs.com";
|
||||
|
||||
/// <summary>
|
||||
/// 默认签名。
|
||||
/// </summary>
|
||||
public string? SignName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地域 ID。
|
||||
/// </summary>
|
||||
public string Region { get; set; } = "cn-hangzhou";
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 短信模块配置。
|
||||
/// </summary>
|
||||
public sealed class SmsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认服务商,默认为腾讯云。
|
||||
/// </summary>
|
||||
public SmsProviderKind Provider { get; set; } = SmsProviderKind.Tencent;
|
||||
|
||||
/// <summary>
|
||||
/// 默认签名。
|
||||
/// </summary>
|
||||
public string? DefaultSignName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用模拟发送(仅日志,不实际调用),方便开发环境。
|
||||
/// </summary>
|
||||
public bool UseMock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云短信配置。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public TencentSmsOptions Tencent { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云短信配置。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public AliyunSmsOptions Aliyun { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 场景与模板映射(如 login: TEMPLATE_ID)。
|
||||
/// </summary>
|
||||
public Dictionary<string, string> SceneTemplates { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云短信配置。
|
||||
/// </summary>
|
||||
public sealed class TencentSmsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// SecretId。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string SecretId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SecretKey。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 应用 SdkAppId。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string SdkAppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 默认签名。
|
||||
/// </summary>
|
||||
public string? SignName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认地域。
|
||||
/// </summary>
|
||||
public string Region { get; set; } = "ap-guangzhou";
|
||||
|
||||
/// <summary>
|
||||
/// 接口域名。
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = "https://sms.tencentcloudapi.com";
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Module.Sms.Abstractions;
|
||||
using TakeoutSaaS.Module.Sms.Models;
|
||||
using TakeoutSaaS.Module.Sms.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云短信发送实现(简化版,占位可扩展正式签名流程)。
|
||||
/// </summary>
|
||||
public sealed class AliyunSmsSender(IHttpClientFactory httpClientFactory, IOptionsMonitor<SmsOptions> optionsMonitor, ILogger<AliyunSmsSender> logger)
|
||||
: ISmsSender
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SmsProviderKind Provider => SmsProviderKind.Aliyun;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SmsSendResult> SendAsync(SmsSendRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
if (options.UseMock)
|
||||
{
|
||||
logger.LogInformation("Mock 发送阿里云短信到 {Phone}, Template:{Template}", request.PhoneNumber, request.TemplateCode);
|
||||
return Task.FromResult(new SmsSendResult { Success = true, Message = "Mocked" });
|
||||
}
|
||||
// 预留 HttpClient,便于后续接入阿里云正式签名请求
|
||||
using var httpClient = httpClientFactory.CreateClient(nameof(AliyunSmsSender));
|
||||
|
||||
// 占位:保留待接入阿里云正式签名流程,当前返回未实现。
|
||||
logger.LogWarning("阿里云短信尚未启用,请配置腾讯云或开启 UseMock。");
|
||||
return Task.FromResult(new SmsSendResult { Success = false, Message = "Aliyun SMS not enabled" });
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Module.Sms.Abstractions;
|
||||
using TakeoutSaaS.Module.Sms.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 短信服务商解析器。
|
||||
/// </summary>
|
||||
public sealed class SmsSenderResolver(IOptionsMonitor<SmsOptions> optionsMonitor, IEnumerable<ISmsSender> senders) : ISmsSenderResolver
|
||||
{
|
||||
private readonly IReadOnlyDictionary<SmsProviderKind, ISmsSender> _map = senders.ToDictionary(x => x.Provider);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISmsSender Resolve(SmsProviderKind? provider = null)
|
||||
{
|
||||
var key = provider ?? optionsMonitor.CurrentValue.Provider;
|
||||
if (_map.TryGetValue(key, out var sender))
|
||||
{
|
||||
return sender;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"未注册短信服务商:{key}");
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Module.Sms.Abstractions;
|
||||
using TakeoutSaaS.Module.Sms.Models;
|
||||
using TakeoutSaaS.Module.Sms.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Sms.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云短信发送实现(TC3-HMAC 签名)。
|
||||
/// </summary>
|
||||
public sealed class TencentSmsSender(IHttpClientFactory httpClientFactory, IOptionsMonitor<SmsOptions> optionsMonitor, ILogger<TencentSmsSender> logger)
|
||||
: ISmsSender
|
||||
{
|
||||
private const string Service = "sms";
|
||||
private const string Action = "SendSms";
|
||||
private const string Version = "2021-01-11";
|
||||
|
||||
/// <inheritdoc />
|
||||
public SmsProviderKind Provider => SmsProviderKind.Tencent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SmsSendResult> SendAsync(SmsSendRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 读取配置并处理 Mock
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
if (options.UseMock)
|
||||
{
|
||||
logger.LogInformation("Mock 发送短信到 {Phone}, Template:{Template}, Vars:{Vars}", request.PhoneNumber, request.TemplateCode, JsonSerializer.Serialize(request.Variables));
|
||||
return new SmsSendResult { Success = true, Message = "Mocked" };
|
||||
}
|
||||
|
||||
// 2. 构建请求负载与签名所需字段
|
||||
var tencent = options.Tencent;
|
||||
var payload = BuildPayload(request, tencent);
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var date = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
|
||||
var host = new Uri(tencent.Endpoint).Host;
|
||||
var canonicalRequest = BuildCanonicalRequest(payload, host, tencent.Endpoint.StartsWith("https", StringComparison.OrdinalIgnoreCase));
|
||||
var stringToSign = BuildStringToSign(canonicalRequest, timestamp, date);
|
||||
var signature = Sign(stringToSign, tencent.SecretKey, date);
|
||||
|
||||
// 3. 构建 HTTP 请求
|
||||
using var httpClient = httpClientFactory.CreateClient(nameof(TencentSmsSender));
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, tencent.Endpoint)
|
||||
{
|
||||
Content = new StringContent(payload, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
httpRequest.Headers.Add("Host", host);
|
||||
httpRequest.Headers.Add("X-TC-Action", Action);
|
||||
httpRequest.Headers.Add("X-TC-Version", Version);
|
||||
httpRequest.Headers.Add("X-TC-Timestamp", timestamp.ToString(CultureInfo.InvariantCulture));
|
||||
httpRequest.Headers.Add("X-TC-Region", tencent.Region);
|
||||
httpRequest.Headers.Add("Authorization",
|
||||
$"TC3-HMAC-SHA256 Credential={tencent.SecretId}/{date}/{Service}/tc3_request, SignedHeaders=content-type;host, Signature={signature}");
|
||||
|
||||
// 4. 发送请求并读取响应
|
||||
var response = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogWarning("腾讯云短信发送失败:{Status} {Content}", response.StatusCode, content);
|
||||
return new SmsSendResult { Success = false, Message = content };
|
||||
}
|
||||
|
||||
// 5. 解析响应
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
var root = doc.RootElement.GetProperty("Response");
|
||||
var status = root.GetProperty("SendStatusSet")[0];
|
||||
var code = status.GetProperty("Code").GetString();
|
||||
var message = status.GetProperty("Message").GetString();
|
||||
var requestId = root.GetProperty("RequestId").GetString();
|
||||
|
||||
var success = string.Equals(code, "Ok", StringComparison.OrdinalIgnoreCase);
|
||||
return new SmsSendResult
|
||||
{
|
||||
Success = success,
|
||||
RequestId = requestId,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildPayload(SmsSendRequest request, TencentSmsOptions options)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
PhoneNumberSet = new[] { request.PhoneNumber },
|
||||
SmsSdkAppId = options.SdkAppId,
|
||||
SignName = request.SignName ?? options.SignName,
|
||||
TemplateId = request.TemplateCode,
|
||||
TemplateParamSet = request.Variables.Values.ToArray()
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(payload);
|
||||
}
|
||||
|
||||
private static string BuildCanonicalRequest(string payload, string host, bool useHttps)
|
||||
{
|
||||
_ = useHttps;
|
||||
var hashedPayload = HashSha256(payload);
|
||||
var canonicalHeaders = $"content-type:application/json\nhost:{host}\n";
|
||||
return $"POST\n/\n\n{canonicalHeaders}\ncontent-type;host\n{hashedPayload}";
|
||||
}
|
||||
|
||||
private static string BuildStringToSign(string canonicalRequest, long timestamp, string date)
|
||||
{
|
||||
var hashedRequest = HashSha256(canonicalRequest);
|
||||
return $"TC3-HMAC-SHA256\n{timestamp}\n{date}/{Service}/tc3_request\n{hashedRequest}";
|
||||
}
|
||||
|
||||
private static string Sign(string stringToSign, string secretKey, string date)
|
||||
{
|
||||
static byte[] HmacSha256(byte[] key, string msg) => new HMACSHA256(key).ComputeHash(Encoding.UTF8.GetBytes(msg));
|
||||
|
||||
var secretDate = HmacSha256(Encoding.UTF8.GetBytes($"TC3{secretKey}"), date);
|
||||
var secretService = HmacSha256(secretDate, Service);
|
||||
var secretSigning = HmacSha256(secretService, "tc3_request");
|
||||
var signatureBytes = new HMACSHA256(secretSigning).ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
|
||||
return Convert.ToHexString(signatureBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string HashSha256(string raw)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
|
||||
var builder = new StringBuilder(bytes.Length * 2);
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
builder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace TakeoutSaaS.Module.Sms;
|
||||
|
||||
/// <summary>
|
||||
/// 短信服务商类型。
|
||||
/// </summary>
|
||||
public enum SmsProviderKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 腾讯云短信。
|
||||
/// </summary>
|
||||
Tencent = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 阿里云短信。
|
||||
/// </summary>
|
||||
Aliyun = 2
|
||||
}
|
||||
Reference in New Issue
Block a user