diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index b12135c..22e9048 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -46,7 +46,7 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> LoginSimple([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) { - var response = await authService.LoginAsync(request, cancellationToken); + var response = await authService.LoginSimpleAsync(request, cancellationToken); return ApiResponse.Ok(response); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs index f49464c..15365df 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -13,6 +13,11 @@ public interface IAdminAuthService /// Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); + /// + /// 简化登录:支持使用“账号@手机号”自动解析租户后登录。 + /// + Task LoginSimpleAsync(AdminLoginRequest request, CancellationToken cancellationToken = default); + /// /// 刷新 Token。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 5b00f57..845fd7d 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -3,6 +3,7 @@ using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Results; @@ -23,9 +24,13 @@ public sealed class AdminAuthService( IPasswordHasher passwordHasher, IJwtTokenService jwtTokenService, IRefreshTokenStore refreshTokenStore, - ITenantProvider tenantProvider) : IAdminAuthService + ITenantProvider tenantProvider, + ITenantContextAccessor tenantContextAccessor, + ITenantRepository tenantRepository) : IAdminAuthService { private readonly ITenantProvider _tenantProvider = tenantProvider; + private readonly ITenantContextAccessor _tenantContextAccessor = tenantContextAccessor; + private readonly ITenantRepository _tenantRepository = tenantRepository; private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; private readonly IRoleRepository _roleRepository = roleRepository; private readonly IPermissionRepository _permissionRepository = permissionRepository; @@ -57,6 +62,60 @@ public sealed class AdminAuthService( return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); } + /// + /// 简化登录:支持使用“账号@手机号”解析租户后登录。 + /// + /// 登录请求 + /// 取消令牌 + /// 令牌响应 + public async Task LoginSimpleAsync(AdminLoginRequest request, CancellationToken cancellationToken = default) + { + // 1. 标准化输入 + var rawAccount = request.Account?.Trim() ?? string.Empty; + + // 2. 尝试解析 “账号@手机号” + var atIndex = rawAccount.LastIndexOf('@'); + if (atIndex > 0 && atIndex < rawAccount.Length - 1) + { + var accountPart = rawAccount[..atIndex].Trim(); + var phonePart = rawAccount[(atIndex + 1)..].Trim(); + + if (IsLikelyPhone(phonePart)) + { + if (string.IsNullOrWhiteSpace(accountPart)) + { + throw new BusinessException(ErrorCodes.BadRequest, "账号格式错误,应为 账号@手机号"); + } + + var tenantId = await _tenantRepository.FindTenantIdByContactPhoneAsync(phonePart, cancellationToken); + if (!tenantId.HasValue || tenantId.Value == 0) + { + throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); + } + + var originalTenant = _tenantContextAccessor.Current; + _tenantContextAccessor.Current = new TenantContext(tenantId.Value, null, "login:simple:contact_phone"); + try + { + return await LoginAsync(new AdminLoginRequest { Account = accountPart, Password = request.Password }, cancellationToken); + } + finally + { + _tenantContextAccessor.Current = originalTenant; + } + } + } + + // 3. 未携带手机号时,要求外部已解析租户(Header/Host 等) + if (_tenantProvider.GetCurrentTenantId() == 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请使用 账号@手机号 登录"); + } + + // 4. 走原有登录逻辑 + return await LoginAsync(new AdminLoginRequest { Account = rawAccount, Password = request.Password }, cancellationToken); + } + /// /// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。 /// @@ -214,6 +273,35 @@ public sealed class AdminAuthService( }; } + private static bool IsLikelyPhone(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var span = value.AsSpan(); + if (span[0] == '+') + { + span = span[1..]; + } + + if (span.Length < 6 || span.Length > 32) + { + return false; + } + + foreach (var ch in span) + { + if (!char.IsDigit(ch)) + { + return false; + } + } + + return true; + } + private static IReadOnlyList BuildMenuTree( IReadOnlyList definitions, IReadOnlyList permissions) diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs index 41b9136..a6ebbc0 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs @@ -60,6 +60,14 @@ public interface ITenantRepository /// 存在返回 true,否则 false。 Task ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default); + /// + /// 依据联系人手机号查询租户 ID。 + /// + /// 联系人手机号。 + /// 取消标记。 + /// 租户 ID,未找到返回 null。 + Task FindTenantIdByContactPhoneAsync(string phone, CancellationToken cancellationToken = default); + /// /// 获取实名资料。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs index a1bd911..61e7ff0 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs @@ -77,6 +77,18 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context) : ITenantRep return context.Tenants.AnyAsync(x => x.ContactPhone == normalized, cancellationToken); } + /// + public Task FindTenantIdByContactPhoneAsync(string phone, CancellationToken cancellationToken = default) + { + // 1. 标准化手机号 + var normalized = phone.Trim(); + // 2. 查询租户 ID + return context.Tenants.AsNoTracking() + .Where(x => x.ContactPhone == normalized) + .Select(x => (long?)x.Id) + .FirstOrDefaultAsync(cancellationToken); + } + /// public Task GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default) {