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}";
}
}