diff --git a/backend/src/Health.Infrastructure/Auth/AuthService.cs b/backend/src/Health.Infrastructure/Auth/AuthService.cs index 9e11f2e..2c662f9 100644 --- a/backend/src/Health.Infrastructure/Auth/AuthService.cs +++ b/backend/src/Health.Infrastructure/Auth/AuthService.cs @@ -20,6 +20,17 @@ public sealed class AuthService( public async Task SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct) { + // 60 秒冷却(AdminPhone 豁免) + if (phone != AdminPhone) + { + var last = await _db.VerificationCodes + .Where(x => x.Phone == phone) + .OrderByDescending(x => x.CreatedAt) + .FirstOrDefaultAsync(ct); + if (last != null && DateTime.UtcNow - last.CreatedAt < TimeSpan.FromSeconds(60)) + return Error(40009, "请求过于频繁,请 1 分钟后重试"); + } + var code = phone == AdminPhone ? AdminSmsCode : _sms.GenerateCode(); await _db.VerificationCodes.AddAsync(new VerificationCode { @@ -27,7 +38,12 @@ public sealed class AuthService( ExpiresAt = DateTime.UtcNow.AddMinutes(5), }, ct); await _db.SaveChangesAsync(ct); - if (phone != AdminPhone) await _sms.SendCodeAsync(phone, code); + + if (phone != AdminPhone) + { + var sent = await _sms.SendCodeAsync(phone, code); + if (!sent) return Error(40010, "短信发送失败,请稍后重试"); + } return new AuthResult(0, new { success = true, devCode = revealDevelopmentCode ? code : null }); } diff --git a/backend/src/Health.Infrastructure/Services/sms_service.cs b/backend/src/Health.Infrastructure/Services/sms_service.cs index fa1c2d0..3c0bad1 100644 --- a/backend/src/Health.Infrastructure/Services/sms_service.cs +++ b/backend/src/Health.Infrastructure/Services/sms_service.cs @@ -1,26 +1,27 @@ +using Microsoft.Extensions.Logging; + namespace Health.Infrastructure.Services; /// -/// 短信验证码服务(开发阶段直接返回成功) +/// 短信验证码服务(调用百度云 SMS v3 真实发送) /// -public sealed class SmsService +public sealed class SmsService(BceSmsClient bce, ILogger log) { - /// - /// 发送验证码(开发阶段不做真实发送) - /// - public Task SendCodeAsync(string phone, string code) + private readonly BceSmsClient _bce = bce; + private readonly ILogger _log = log; + + public async Task SendCodeAsync(string phone, string code) { - // 开发阶段:直接在控制台输出,不做真实发送 - Console.WriteLine($"[SMS DEV] 发送验证码到 {phone}: {code}"); - return Task.FromResult(true); + try + { + return await _bce.SendAsync(phone, code, CancellationToken.None); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _log.LogError(ex, "百度短信发送异常 phone={Phone}", phone); + return false; + } } - /// - /// 生成 6 位随机数字验证码 - /// - public string GenerateCode() - { - // Next(min, max) 的 max 是 exclusive 的,所以用 1000000 保证 6 位 - return Random.Shared.Next(100000, 1000000).ToString(); - } + public string GenerateCode() => Random.Shared.Next(100000, 1000000).ToString(); } diff --git a/backend/src/Health.WebApi/Program.cs b/backend/src/Health.WebApi/Program.cs index bbbb4e0..d9408d1 100644 --- a/backend/src/Health.WebApi/Program.cs +++ b/backend/src/Health.WebApi/Program.cs @@ -108,10 +108,17 @@ builder.Services.AddAuthorization(); // ---- 业务服务 ---- builder.Services.AddSingleton(); +builder.Services.AddHttpClient(client => +{ + var host = builder.Configuration["BAIDU_SMS_HOST"] ?? "smsv3.bj.baidubce.com"; + client.BaseAddress = new Uri($"https://{host}"); + client.Timeout = TimeSpan.FromSeconds(10); +}); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Apple Sign-In JWT 验证 builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/backend/tests/Health.Tests/auth_tests.cs b/backend/tests/Health.Tests/auth_tests.cs index 3278890..9cc6ad6 100644 --- a/backend/tests/Health.Tests/auth_tests.cs +++ b/backend/tests/Health.Tests/auth_tests.cs @@ -5,6 +5,7 @@ using Health.Infrastructure.Data; using Health.Infrastructure.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; namespace Health.Tests; @@ -32,12 +33,16 @@ public class AuthTests return new ConfigurationBuilder().AddInMemoryCollection(settings).Build(); } + private static SmsService CreateSmsService() => + new(new BceSmsClient(new HttpClient(), new ConfigurationBuilder().Build(), NullLogger.Instance), + NullLogger.Instance); + [Fact] public async Task SendSms_Should_Create_VerificationCode() { // Arrange using var db = CreateDbContext(); - var sms = new SmsService(); + var sms = CreateSmsService(); // Act var code = sms.GenerateCode(); @@ -148,7 +153,7 @@ public class AuthTests var service = new AuthService( db, jwt, - new SmsService(), + CreateSmsService(), new AppleTokenValidator(config)); var result = await service.RefreshAsync(oldRefresh, CancellationToken.None);