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 一致(含验证码、登录日志事件)。 /// 门店数据来自 userlocationlocation 表。 /// /// 邮箱、密码;若系统开启验证码则需传 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); } }