using FoodLabeling.Th.Application.Contracts.Dtos.Auth;
using FoodLabeling.Th.Application.Contracts.IServices;
using FoodLabeling.Th.Application.MultiTenancy;
using Lazy.Captcha.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.MultiTenancy;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.Rbac.Domain.Managers;
using Yi.Framework.Rbac.Domain.Shared.Options;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace FoodLabeling.Th.Application.Services;
///
/// 泰额版 Web 管理端登录:租户库校验 + RBAC 全量 Token(含 TenantId Claim)
///
public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService
{
private readonly IAccountManager _accountManager;
private readonly ISqlSugarRepository _userRepository;
private readonly ICaptcha _captcha;
private readonly RbacOptions _rbacOptions;
private readonly ITenantStore _tenantStore;
public ThWebAuthAppService(
IAccountManager accountManager,
ISqlSugarRepository userRepository,
ICaptcha captcha,
IOptions rbacOptions,
ITenantStore tenantStore)
{
_accountManager = accountManager;
_userRepository = userRepository;
_captcha = captcha;
_rbacOptions = rbacOptions.Value;
_tenantStore = tenantStore;
}
///
/// Web 管理端登录
///
///
/// 校验租户后在租户独立库验证账号,签发含 TenantId 与 RBAC 权限的 JWT。
///
/// 泰额 H5 与公司 Web 共用此接口。选 **Default** 或 tenantId 为空时,若邮箱账号存在于主库则走**平台登录**(JWT 无 TenantId);
/// 选具体公司 tenantId 时在该公司业务库校验。Default 业务库(如 US)仅在主库无该邮箱时作为回落。
///
/// 示例请求(平台,选 Default):
/// ```json
/// {
/// "tenantId": "11111111-1111-1111-1111-111111111111",
/// "userName": "admin@example.com",
/// "password": "123456"
/// }
/// ```
///
/// 参数说明:
/// - tenantId: 可选,租户 Guid(与 tenantName 二选一)
/// - tenantName: 可选,租户名称(与 tenantId 二选一)
/// - userName: 登录账号或邮箱
/// - password: 明文密码
///
/// 登录参数
/// Token 与租户信息
/// 登录成功
/// 参数缺失、验证码错误或账号密码错误
/// 服务器错误
[AllowAnonymous]
[HttpPost("th-web-auth/login")]
public virtual async Task LoginAsync(ThWebLoginInputVo input)
{
if (string.IsNullOrWhiteSpace(input.Password)
|| string.IsNullOrWhiteSpace(input.UserName))
{
throw new UserFriendlyException("请输入账号与密码");
}
ValidationImageCaptcha(input.Uuid, input.Code);
// H5 登录页选 Default / 空 tenantId:优先主库平台账号(admin@example.com),避免误入 Default 业务库(如 US)
if (ThWebPlatformLoginHelper.ShouldTryPlatformLogin(input.TenantId, input.TenantName))
{
var platformLogin = await TryPlatformLoginAsync(input);
if (platformLogin != null)
{
return platformLogin;
}
if (!input.TenantId.HasValue || input.TenantId.Value == Guid.Empty)
{
input.TenantId = ThWebPlatformLoginHelper.DefaultTenantId;
}
}
TenantConfiguration tenantConfig;
try
{
tenantConfig = await TenantResolveHelper.ResolveTenantAsync(
_tenantStore, CurrentTenant, input.TenantId, input.TenantName);
}
catch (UserFriendlyException)
{
throw;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "解析租户失败");
throw new UserFriendlyException($"租户解析失败:{ex.GetBaseException().Message}");
}
var tenantId = tenantConfig.Id;
try
{
using (CurrentTenant.Change(tenantId, tenantConfig.Name))
{
var user = await FindActiveUserByEmailAsync(input.UserName.Trim());
if (user == null)
{
throw new UserFriendlyException("登录失败:账号不存在或已禁用");
}
if (!user.JudgePassword(input.Password))
{
throw new UserFriendlyException("登录失败:账号或密码错误");
}
var accessToken = await _accountManager.GetTokenByUserIdAsync(user.Id);
var refreshToken = _accountManager.CreateRefreshToken(user.Id);
return new ThWebLoginOutputDto
{
Token = accessToken,
RefreshToken = refreshToken,
TenantId = tenantId,
TenantName = tenantConfig.Name ?? string.Empty
};
}
}
catch (UserFriendlyException)
{
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, "租户 {TenantId}/{TenantName} Web 登录异常", tenantId, tenantConfig.Name);
var detail = ex.GetBaseException().Message;
throw new UserFriendlyException(
$"登录失败:租户业务库不可用或未初始化完成。{detail}");
}
}
///
/// 主库平台登录:CurrentTenant 为空签发无 TenantId 的 JWT,供 Vue3Router/vben5 使用。
/// 主库无对应邮箱用户时返回 null,由调用方回落 Default/公司租户库登录。
///
private async Task TryPlatformLoginAsync(ThWebLoginInputVo input)
{
if (!ThWebPlatformLoginHelper.IsPlausiblePlatformEmail(input.UserName))
{
return null;
}
using (CurrentTenant.Change(null))
{
var user = await FindActiveUserByEmailAsync(input.UserName.Trim());
if (user == null)
{
return null;
}
if (!user.JudgePassword(input.Password))
{
throw new UserFriendlyException("登录失败:账号或密码错误");
}
var accessToken = await _accountManager.GetTokenByUserIdAsync(user.Id);
var refreshToken = _accountManager.CreateRefreshToken(user.Id);
return new ThWebLoginOutputDto
{
Token = accessToken,
RefreshToken = refreshToken,
TenantId = Guid.Empty,
TenantName = ThWebPlatformLoginHelper.PlatformTenantDisplayName
};
}
}
private void ValidationImageCaptcha(string? uuid, string? code)
{
if (!_rbacOptions.EnableCaptcha)
{
return;
}
if (!_captcha.Validate(uuid, code))
{
throw new UserFriendlyException("验证码错误或已过期");
}
}
private async Task FindActiveUserByEmailAsync(string email)
{
var normalized = email.Trim().ToLowerInvariant();
var users = await _userRepository._DbQueryable
.Where(u => !u.IsDeleted && u.State)
.Where(u =>
(u.Email != null && SqlFunc.ToLower(u.Email) == normalized) ||
SqlFunc.ToLower(u.UserName) == normalized)
.ToListAsync();
return users.FirstOrDefault(u =>
u.Email != null &&
string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase))
?? users.FirstOrDefault();
}
}