using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Security.Cryptography; using System.Text; 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.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Sms.Services; /// /// 短信验证码服务实现。 /// public sealed class VerificationCodeService( ISmsSenderResolver senderResolver, IOptionsMonitor smsOptionsMonitor, IOptionsMonitor codeOptionsMonitor, ITenantProvider tenantProvider, IDistributedCache cache, ILogger logger) : IVerificationCodeService { /// public async Task SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default) { // 1. 参数校验 if (string.IsNullOrWhiteSpace(request.PhoneNumber)) { throw new BusinessException(ErrorCodes.BadRequest, "手机号不能为空"); } if (string.IsNullOrWhiteSpace(request.Scene)) { throw new BusinessException(ErrorCodes.BadRequest, "场景不能为空"); } // 2. 解析模板与缓存键 var smsOptions = smsOptionsMonitor.CurrentValue; var codeOptions = codeOptionsMonitor.CurrentValue; var templateCode = ResolveTemplate(request.Scene, smsOptions); var phone = NormalizePhoneNumber(request.PhoneNumber); var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString(); var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; var cooldownKey = $"{cacheKey}:cooldown"; // 3. 检查冷却期 await EnsureCooldownAsync(cooldownKey, codeOptions.CooldownSeconds, cancellationToken).ConfigureAwait(false); // 4. 生成验证码并发送短信 var code = GenerateCode(codeOptions.CodeLength); var variables = new Dictionary { { "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}"); } // 5. 写入验证码与冷却缓存 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 }; } /// public async Task VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default) { // 1. 基础校验 if (string.IsNullOrWhiteSpace(request.Code)) { return false; } // 2. 读取验证码 var codeOptions = codeOptionsMonitor.CurrentValue; var phone = NormalizePhoneNumber(request.PhoneNumber); var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString(); var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(cachedCode)) { return false; } // 3. 比对成功后清除缓存 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, "请求过于频繁,请稍后再试"); } } }