ThWebAuthAppService.cs 8.03 KB
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;

/// <summary>
/// 泰额版 Web 管理端登录:租户库校验 + RBAC 全量 Token(含 TenantId Claim)
/// </summary>
public class ThWebAuthAppService : ApplicationService, IThWebAuthAppService
{
    private readonly IAccountManager _accountManager;
    private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
    private readonly ICaptcha _captcha;
    private readonly RbacOptions _rbacOptions;
    private readonly ITenantStore _tenantStore;

    public ThWebAuthAppService(
        IAccountManager accountManager,
        ISqlSugarRepository<UserAggregateRoot, Guid> userRepository,
        ICaptcha captcha,
        IOptions<RbacOptions> rbacOptions,
        ITenantStore tenantStore)
    {
        _accountManager = accountManager;
        _userRepository = userRepository;
        _captcha = captcha;
        _rbacOptions = rbacOptions.Value;
        _tenantStore = tenantStore;
    }

    /// <summary>
    /// Web 管理端登录
    /// </summary>
    /// <remarks>
    /// 校验租户后在租户独立库验证账号,签发含 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: 明文密码
    /// </remarks>
    /// <param name="input">登录参数</param>
    /// <returns>Token 与租户信息</returns>
    /// <response code="200">登录成功</response>
    /// <response code="400">参数缺失、验证码错误或账号密码错误</response>
    /// <response code="500">服务器错误</response>
    [AllowAnonymous]
    [HttpPost("th-web-auth/login")]
    public virtual async Task<ThWebLoginOutputDto> 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}");
        }
    }

    /// <summary>
    /// 主库平台登录:CurrentTenant 为空签发无 TenantId 的 JWT,供 Vue3Router/vben5 使用。
    /// 主库无对应邮箱用户时返回 null,由调用方回落 Default/公司租户库登录。
    /// </summary>
    private async Task<ThWebLoginOutputDto?> 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<UserAggregateRoot?> 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();
    }
}