using System.Globalization; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; 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; /// /// 腾讯云短信发送实现(TC3-HMAC 签名)。 /// public sealed class TencentSmsSender(IHttpClientFactory httpClientFactory, IOptionsMonitor optionsMonitor, ILogger logger) : ISmsSender { private const string Service = "sms"; private const string Action = "SendSms"; private const string Version = "2021-01-11"; /// public SmsProviderKind Provider => SmsProviderKind.Tencent; /// public async Task 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(); } }