From 25a4078688470a925a7b68dc763bd9b7bd0de141 Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Tue, 28 Jul 2026 15:01:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=AF=E7=94=A8=E7=99=BE=E5=BA=A6?= =?UTF-8?q?=E4=BA=91=20SMS=20=E7=9C=9F=E5=AE=9E=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SmsService: 从开发桩改为调用 BceSmsClient.SendAsync 真实发送 - AuthService.SendCodeAsync: 加 60 秒冷却 + 发送结果检查(失败返回 40010) - Program.cs: 注册 BceSmsClient 到 DI(AddHttpClient + 10s 超时) - auth_tests.cs: 加 CreateSmsService helper 适配构造注入 移动号实测可收到验证码,联通号运营商环节待报备 --- .../Health.Infrastructure/Auth/AuthService.cs | 18 +++++++++- .../Services/sms_service.cs | 35 ++++++++++--------- backend/src/Health.WebApi/Program.cs | 7 ++++ backend/tests/Health.Tests/auth_tests.cs | 9 +++-- 4 files changed, 49 insertions(+), 20 deletions(-) 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);