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(); } }