using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using FoodLabeling.Th.Application.Contracts; using FoodLabeling.Th.Application.Contracts.Dtos.Auth; using FoodLabeling.Th.Application.Contracts.IServices; using Lazy.Captcha.Core; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.MultiTenancy; using Volo.Abp.Security.Claims; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.Rbac.Domain.Helpers; using Yi.Framework.Rbac.Domain.Managers; using Yi.Framework.Rbac.Domain.Shared.Consts; using Yi.Framework.Rbac.Domain.Shared.Options; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Th.Application.Services; /// /// 泰额版 App 登录:先解析租户,再在租户独立库校验用户,JWT 写入 TenantId /// public class ThAppAuthAppService : ApplicationService, IThAppAuthAppService { private readonly IAccountManager _accountManager; private readonly ISqlSugarRepository _userRepository; private readonly ISugarDbContextProvider _dbContextProvider; private readonly ICaptcha _captcha; private readonly RbacOptions _rbacOptions; private readonly JwtOptions _jwtOptions; private readonly ITenantStore _tenantStore; public ThAppAuthAppService( IAccountManager accountManager, ISqlSugarRepository userRepository, ISugarDbContextProvider dbContextProvider, ICaptcha captcha, IOptions jwtOptions, IOptions rbacOptions, ITenantStore tenantStore) { _accountManager = accountManager; _userRepository = userRepository; _dbContextProvider = dbContextProvider; _captcha = captcha; _jwtOptions = jwtOptions.Value; _rbacOptions = rbacOptions.Value; _tenantStore = tenantStore; } /// [AllowAnonymous] [HttpPost("th-app-auth/login")] public virtual async Task LoginAsync(ThAppLoginInputVo input) { if (input.TenantId == Guid.Empty || string.IsNullOrWhiteSpace(input.Password) || string.IsNullOrWhiteSpace(input.Email)) { throw new UserFriendlyException("请输入租户、邮箱与密码!"); } ValidationImageCaptcha(input.Uuid, input.Code); var tenantConfig = await ResolveTenantAsync(input.TenantId); UserAggregateRoot user; List locations; using (CurrentTenant.Change(input.TenantId, tenantConfig.Name)) { user = await FindActiveUserByEmailAsync(input.Email.Trim()) ?? throw new UserFriendlyException("登录失败!邮箱不存在!"); if (!UserPasswordHelper.VerifyPlainPassword(user, input.Password)) { throw new UserFriendlyException(UserConst.Login_Error); } locations = await LoadBoundLocationsAsync(user.Id); } var accessToken = CreateAppAccessToken(user, input.TenantId); var refreshToken = _accountManager.CreateRefreshToken(user.Id); // 不发布 LoginEvent:事件在 UoW 完成时于「当前连接」写 LoginLog;泰额 App 校验在租户库, // 完成阶段连接为主库 antis-foodlabeling-host,无 LoginLog 表会导致登录接口 500。 return new ThAppLoginOutputDto { Token = accessToken, RefreshToken = refreshToken, TenantId = input.TenantId, TenantName = tenantConfig.Name ?? string.Empty, Locations = locations }; } /// [Authorize] [HttpGet("th-app-auth/my-locations")] public virtual async Task> GetMyLocationsAsync() { if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } if (!CurrentTenant.Id.HasValue) { throw new UserFriendlyException("未识别租户,请重新登录或携带 __tenant"); } using (CurrentTenant.Change(CurrentTenant.Id.Value, CurrentTenant.Name)) { return await LoadBoundLocationsAsync(CurrentUser.Id.Value); } } private async Task ResolveTenantAsync(Guid tenantId) { TenantConfiguration? config; using (CurrentTenant.Change(null)) { config = await _tenantStore.FindAsync(tenantId); } if (config is null) { throw new UserFriendlyException("租户不存在或已停用"); } if (string.IsNullOrWhiteSpace(config.ConnectionStrings?.Default)) { throw new UserFriendlyException("租户未配置业务库连接串"); } return config; } 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(); } private string CreateAppAccessToken(UserAggregateRoot user, Guid tenantId) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var tenantIdStr = tenantId.ToString(); var claims = new List { new(AbpClaimTypes.UserId, user.Id.ToString()), new(AbpClaimTypes.UserName, user.UserName), new(AbpClaimTypes.TenantId, tenantIdStr), new(TokenTypeConst.TenantId, tenantIdStr), new(ThAppJwtClaims.ClientKind, ThAppJwtClaims.ClientKindThApp) }; if (!string.IsNullOrWhiteSpace(user.Email)) { claims.Add(new Claim(AbpClaimTypes.Email, user.Email)); } var token = new JwtSecurityToken( _jwtOptions.Issuer, _jwtOptions.Audience, claims, expires: DateTime.Now.AddMinutes(_jwtOptions.ExpiresMinuteTime), notBefore: DateTime.Now, signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } private async Task> LoadBoundLocationsAsync(Guid userId) { var db = (await _dbContextProvider.GetDbContextAsync()).SqlSugarClient; var userIdStr = userId.ToString(); var links = await db.Queryable() .Where(x => !x.IsDeleted && x.UserId == userIdStr) .Select(x => x.LocationId) .ToListAsync(); if (links.Count == 0) { return new List(); } var wanted = links.Distinct().ToList(); var locations = (await db.Queryable() .Where(x => !x.IsDeleted) .Where(x => wanted.Contains(x.Id.ToString())) .ToListAsync()) .OrderBy(x => x.OrderNum) .ThenBy(x => x.LocationName) .ToList(); return locations.Select(x => new UsAppBoundLocationDto { Id = x.Id.ToString(), LocationCode = x.LocationCode ?? string.Empty, LocationName = x.LocationName ?? string.Empty, FullAddress = BuildFullAddress(x), State = x.State }).ToList(); } private static string BuildFullAddress(LocationAggregateRoot loc) { var street = loc.Street?.Trim(); var city = loc.City?.Trim(); var state = loc.StateCode?.Trim(); var zip = loc.ZipCode?.Trim(); var line2Parts = new List(); if (!string.IsNullOrEmpty(city)) { line2Parts.Add(city); } if (!string.IsNullOrEmpty(state)) { line2Parts.Add(state); } if (!string.IsNullOrEmpty(zip)) { line2Parts.Add(zip); } var line2 = string.Join(", ", line2Parts); if (string.IsNullOrEmpty(street)) { return line2; } return string.IsNullOrEmpty(line2) ? street : $"{street}, {line2}"; } }