feat: admin simple 登录支持 账号@手机号
This commit is contained in:
@@ -46,7 +46,7 @@ public sealed class AuthController(IAdminAuthService authService) : BaseApiContr
|
|||||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<TokenResponse>> LoginSimple([FromBody] AdminLoginRequest request, CancellationToken cancellationToken)
|
public async Task<ApiResponse<TokenResponse>> LoginSimple([FromBody] AdminLoginRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var response = await authService.LoginAsync(request, cancellationToken);
|
var response = await authService.LoginSimpleAsync(request, cancellationToken);
|
||||||
return ApiResponse<TokenResponse>.Ok(response);
|
return ApiResponse<TokenResponse>.Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ public interface IAdminAuthService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 简化登录:支持使用“账号@手机号”自动解析租户后登录。
|
||||||
|
/// </summary>
|
||||||
|
Task<TokenResponse> LoginSimpleAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 刷新 Token。
|
/// 刷新 Token。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using TakeoutSaaS.Application.Identity.Abstractions;
|
|||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
using TakeoutSaaS.Domain.Identity.Entities;
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
@@ -23,9 +24,13 @@ public sealed class AdminAuthService(
|
|||||||
IPasswordHasher<IdentityUser> passwordHasher,
|
IPasswordHasher<IdentityUser> passwordHasher,
|
||||||
IJwtTokenService jwtTokenService,
|
IJwtTokenService jwtTokenService,
|
||||||
IRefreshTokenStore refreshTokenStore,
|
IRefreshTokenStore refreshTokenStore,
|
||||||
ITenantProvider tenantProvider) : IAdminAuthService
|
ITenantProvider tenantProvider,
|
||||||
|
ITenantContextAccessor tenantContextAccessor,
|
||||||
|
ITenantRepository tenantRepository) : IAdminAuthService
|
||||||
{
|
{
|
||||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||||
|
private readonly ITenantContextAccessor _tenantContextAccessor = tenantContextAccessor;
|
||||||
|
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||||
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
|
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
|
||||||
private readonly IRoleRepository _roleRepository = roleRepository;
|
private readonly IRoleRepository _roleRepository = roleRepository;
|
||||||
private readonly IPermissionRepository _permissionRepository = permissionRepository;
|
private readonly IPermissionRepository _permissionRepository = permissionRepository;
|
||||||
@@ -57,6 +62,60 @@ public sealed class AdminAuthService(
|
|||||||
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 简化登录:支持使用“账号@手机号”解析租户后登录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">登录请求</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌</param>
|
||||||
|
/// <returns>令牌响应</returns>
|
||||||
|
public async Task<TokenResponse> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
|
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -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<MenuNodeDto> BuildMenuTree(
|
private static IReadOnlyList<MenuNodeDto> BuildMenuTree(
|
||||||
IReadOnlyList<Domain.Identity.Entities.MenuDefinition> definitions,
|
IReadOnlyList<Domain.Identity.Entities.MenuDefinition> definitions,
|
||||||
IReadOnlyList<string> permissions)
|
IReadOnlyList<string> permissions)
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ public interface ITenantRepository
|
|||||||
/// <returns>存在返回 true,否则 false。</returns>
|
/// <returns>存在返回 true,否则 false。</returns>
|
||||||
Task<bool> ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default);
|
Task<bool> ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 依据联系人手机号查询租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="phone">联系人手机号。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>租户 ID,未找到返回 null。</returns>
|
||||||
|
Task<long?> FindTenantIdByContactPhoneAsync(string phone, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取实名资料。
|
/// 获取实名资料。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ public sealed class EfTenantRepository(TakeoutAppDbContext context) : ITenantRep
|
|||||||
return context.Tenants.AnyAsync(x => x.ContactPhone == normalized, cancellationToken);
|
return context.Tenants.AnyAsync(x => x.ContactPhone == normalized, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<long?> 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default)
|
public Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user