feat: finalize core modules and gateway
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Application.Sms.Abstractions;
|
||||
using TakeoutSaaS.Application.Sms.Contracts;
|
||||
using TakeoutSaaS.Application.Sms.Options;
|
||||
using TakeoutSaaS.Module.Sms.Abstractions;
|
||||
using TakeoutSaaS.Module.Sms.Models;
|
||||
using TakeoutSaaS.Module.Sms.Options;
|
||||
using TakeoutSaaS.Module.Sms;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Sms.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 短信验证码服务实现。
|
||||
/// </summary>
|
||||
public sealed class VerificationCodeService(
|
||||
ISmsSenderResolver senderResolver,
|
||||
IOptionsMonitor<SmsOptions> smsOptionsMonitor,
|
||||
IOptionsMonitor<VerificationCodeOptions> codeOptionsMonitor,
|
||||
ITenantProvider tenantProvider,
|
||||
IDistributedCache cache,
|
||||
ILogger<VerificationCodeService> logger) : IVerificationCodeService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<SendVerificationCodeResponse> SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.PhoneNumber))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "手机号不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Scene))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "场景不能为空");
|
||||
}
|
||||
|
||||
var smsOptions = smsOptionsMonitor.CurrentValue;
|
||||
var codeOptions = codeOptionsMonitor.CurrentValue;
|
||||
var templateCode = ResolveTemplate(request.Scene, smsOptions);
|
||||
var phone = NormalizePhoneNumber(request.PhoneNumber);
|
||||
var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N");
|
||||
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
|
||||
var cooldownKey = $"{cacheKey}:cooldown";
|
||||
|
||||
await EnsureCooldownAsync(cooldownKey, codeOptions.CooldownSeconds, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var code = GenerateCode(codeOptions.CodeLength);
|
||||
var variables = new Dictionary<string, string> { { "code", code } };
|
||||
var sender = senderResolver.Resolve(request.Provider);
|
||||
|
||||
var smsRequest = new SmsSendRequest(phone, templateCode, variables, smsOptions.DefaultSignName);
|
||||
var smsResult = await sender.SendAsync(smsRequest, cancellationToken).ConfigureAwait(false);
|
||||
if (!smsResult.Success)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.InternalServerError, $"短信发送失败:{smsResult.Message}");
|
||||
}
|
||||
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(codeOptions.ExpireMinutes);
|
||||
await cache.SetStringAsync(cacheKey, code, new DistributedCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpiration = expiresAt
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await cache.SetStringAsync(cooldownKey, "1", new DistributedCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(codeOptions.CooldownSeconds)
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("发送验证码成功,Phone:{Phone} Scene:{Scene} Tenant:{Tenant}", phone, request.Scene, tenantKey);
|
||||
return new SendVerificationCodeResponse
|
||||
{
|
||||
ExpiresAt = expiresAt,
|
||||
RequestId = smsResult.RequestId
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Code))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var codeOptions = codeOptionsMonitor.CurrentValue;
|
||||
var phone = NormalizePhoneNumber(request.PhoneNumber);
|
||||
var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N");
|
||||
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
|
||||
|
||||
var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(cachedCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var success = string.Equals(cachedCode, request.Code, StringComparison.Ordinal);
|
||||
if (success)
|
||||
{
|
||||
await cache.RemoveAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private static string ResolveTemplate(string scene, SmsOptions options)
|
||||
{
|
||||
if (options.SceneTemplates.TryGetValue(scene, out var template) && !string.IsNullOrWhiteSpace(template))
|
||||
{
|
||||
return template;
|
||||
}
|
||||
|
||||
throw new BusinessException(ErrorCodes.BadRequest, $"未配置场景 {scene} 的短信模板");
|
||||
}
|
||||
|
||||
private static string NormalizePhoneNumber(string phone)
|
||||
{
|
||||
var trimmed = phone.Trim();
|
||||
return trimmed.StartsWith("+", StringComparison.Ordinal) ? trimmed : $"+86{trimmed}";
|
||||
}
|
||||
|
||||
private static string GenerateCode(int length)
|
||||
{
|
||||
var buffer = new byte[length];
|
||||
RandomNumberGenerator.Fill(buffer);
|
||||
var builder = new StringBuilder(length);
|
||||
foreach (var b in buffer)
|
||||
{
|
||||
builder.Append((b % 10).ToString());
|
||||
}
|
||||
|
||||
return builder.ToString()[..length];
|
||||
}
|
||||
|
||||
private async Task EnsureCooldownAsync(string cooldownKey, int cooldownSeconds, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await cache.GetStringAsync(cooldownKey, cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(existing))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "请求过于频繁,请稍后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user