using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
using Lazy.Captcha.Core;
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Security.Claims;
using Volo.Abp.Users;
using Yi.Framework.Core.Helper;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.Rbac.Domain.Managers;
using Yi.Framework.Rbac.Domain.Shared.Consts;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.Rbac.Domain.Shared.Etos;
using Yi.Framework.Rbac.Domain.Shared.Options;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace FoodLabeling.Application.Services;
///
/// 美国版 App 登录:邮箱 + 密码(与 AccountManager 相同盐值哈希)签发 JWT,并返回 userlocation 绑定门店
///
public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
{
private readonly IAccountManager _accountManager;
private readonly ISqlSugarRepository _userRepository;
private readonly ISqlSugarDbContext _dbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ICaptcha _captcha;
private readonly RbacOptions _rbacOptions;
private readonly JwtOptions _jwtOptions;
public UsAppAuthAppService(
IAccountManager accountManager,
ISqlSugarRepository userRepository,
ISqlSugarDbContext dbContext,
IHttpContextAccessor httpContextAccessor,
ICaptcha captcha,
IOptions jwtOptions,
IOptions rbacOptions)
{
_accountManager = accountManager;
_userRepository = userRepository;
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
_captcha = captcha;
_jwtOptions = jwtOptions.Value;
_rbacOptions = rbacOptions.Value;
}
protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService();
///
/// App 登录:签发 Token / RefreshToken,并返回当前账号绑定的门店列表
///
///
/// 行为与系统 AccountService.PostLoginAsync 一致(含验证码、登录日志事件)。
/// 门店数据来自 userlocation 与 location 表。
///
/// 邮箱、密码;若系统开启验证码则需传 Uuid、Code
/// Token、RefreshToken 与绑定门店
/// 登录成功
/// 参数或验证码错误
/// 服务器错误
[AllowAnonymous]
public virtual async Task LoginAsync(UsAppLoginInputVo input)
{
if (string.IsNullOrWhiteSpace(input.Password) || string.IsNullOrWhiteSpace(input.Email))
{
throw new UserFriendlyException("请输入合理数据!");
}
ValidationImageCaptcha(input.Uuid, input.Code);
var user = await FindActiveUserByEmailAsync(input.Email.Trim());
if (user is null)
{
throw new UserFriendlyException("登录失败!邮箱不存在!");
}
if (user.EncryPassword.Password != MD5Helper.SHA2Encode(input.Password, user.EncryPassword.Salt))
{
throw new UserFriendlyException(UserConst.Login_Error);
}
// App 端不依赖 RBAC 权限体系:允许“无权限账号”登录拿 Token(H5 再做权限控制)
var accessToken = CreateAppAccessToken(user);
var refreshToken = _accountManager.CreateRefreshToken(user.Id);
if (_httpContextAccessor.HttpContext is not null)
{
var loginEntity = new LoginLogAggregateRoot().GetInfoByHttpContext(_httpContextAccessor.HttpContext);
var loginEto = loginEntity.Adapt();
loginEto.UserName = user.UserName;
loginEto.UserId = user.Id;
await LocalEventBus.PublishAsync(loginEto);
}
var locations = await LoadBoundLocationsAsync(user.Id);
return new UsAppLoginOutputDto
{
Token = accessToken,
RefreshToken = refreshToken,
Locations = locations
};
}
///
/// 获取当前登录用户已绑定的门店(切换门店时可重新拉取)
///
[Authorize]
public virtual async Task> GetMyLocationsAsync()
{
if (!CurrentUser.Id.HasValue)
{
throw new UserFriendlyException("用户未登录");
}
return await LoadBoundLocationsAsync(CurrentUser.Id.Value);
}
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 == true)
.Where(u => u.Email != null && SqlFunc.ToLower(u.Email) == normalized)
.ToListAsync();
return users.FirstOrDefault();
}
private string CreateAppAccessToken(UserAggregateRoot user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new List
{
new(AbpClaimTypes.UserId, user.Id.ToString()),
new(AbpClaimTypes.UserName, user.UserName)
};
if (!string.IsNullOrWhiteSpace(user.Email))
{
claims.Add(new Claim(AbpClaimTypes.Email, user.Email));
}
var token = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(_jwtOptions.ExpiresMinuteTime),
notBefore: DateTime.Now,
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private async Task> LoadBoundLocationsAsync(Guid userId)
{
var userIdStr = userId.ToString();
var links = await _dbContext.SqlSugarClient.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 _dbContext.SqlSugarClient.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);
}
var line2 = line2Parts.Count > 0 ? string.Join(", ", line2Parts) : string.Empty;
if (!string.IsNullOrEmpty(zip))
{
line2 = string.IsNullOrEmpty(line2) ? zip : $"{line2} {zip}";
}
var segments = new List();
if (!string.IsNullOrEmpty(street))
{
segments.Add(street);
}
if (!string.IsNullOrEmpty(line2))
{
segments.Add(line2);
}
return segments.Count == 0 ? "无" : string.Join(", ", segments);
}
}