using MediatR; using Microsoft.AspNetCore.Identity; using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; /// /// 创建用户处理器。 /// public sealed class CreateIdentityUserCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IPasswordHasher passwordHasher, ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher, IIdGenerator idGenerator, IMediator mediator) : IRequestHandler { /// public async Task Handle(CreateIdentityUserCommand request, CancellationToken cancellationToken) { // 1. 获取操作者档案并判断权限 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); // 2. 校验跨租户访问权限 if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户"); } // 3. 规范化输入并准备校验 var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId; var account = request.Account.Trim(); var displayName = request.DisplayName.Trim(); var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim(); var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim(); var roleIds = ParseIds(request.RoleIds, "角色"); // 4. 唯一性校验 if (await identityUserRepository.ExistsByAccountAsync(tenantId, account, null, cancellationToken)) { throw new BusinessException(ErrorCodes.Conflict, "账号已存在"); } if (!string.IsNullOrWhiteSpace(phone) && await identityUserRepository.ExistsByPhoneAsync(tenantId, phone, null, cancellationToken)) { throw new BusinessException(ErrorCodes.Conflict, "手机号已存在"); } if (!string.IsNullOrWhiteSpace(email) && await identityUserRepository.ExistsByEmailAsync(tenantId, email, null, cancellationToken)) { throw new BusinessException(ErrorCodes.Conflict, "邮箱已存在"); } // 5. 校验角色合法性 if (roleIds.Length > 0) { var portal = PortalType.Tenant; var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); if (roles.Count != roleIds.Length) { throw new BusinessException(ErrorCodes.BadRequest, "角色列表包含无效项"); } } // 6. 创建用户实体 var userPortal = PortalType.Tenant; var user = new IdentityUser { Id = idGenerator.NextId(), Portal = userPortal, TenantId = tenantId, Account = account, DisplayName = displayName, Phone = phone, Email = email, Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim(), Status = request.Status ?? IdentityUserStatus.Active, FailedLoginCount = 0, LockedUntil = null, LastLoginAt = null, MustChangePassword = false, PasswordHash = string.Empty }; user.PasswordHash = passwordHasher.HashPassword(user, request.Password); // 7. 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; if (string.IsNullOrWhiteSpace(operatorName)) { operatorName = $"user:{currentUserAccessor.UserId}"; } var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:create", TargetType = "identity_user", TargetIds = JsonSerializer.Serialize(new[] { user.Id }), OperatorId = currentUserAccessor.UserId.ToString(), OperatorName = operatorName, Parameters = JsonSerializer.Serialize(new { tenantId, account, displayName, phone, email, roleIds }), Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; // 8. 持久化用户并写入 Outbox await identityUserRepository.AddAsync(user, cancellationToken); await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); // 9. 绑定角色 if (roleIds.Length > 0) { await userRoleRepository.ReplaceUserRolesAsync(userPortal, tenantId, user.Id, roleIds, cancellationToken); } // 10. 返回用户详情 var detail = await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken); return detail ?? new UserDetailDto { UserId = user.Id, Portal = user.Portal, TenantId = user.TenantId, MerchantId = user.MerchantId, Account = user.Account, DisplayName = user.DisplayName, Phone = user.Phone, Email = user.Email, Status = user.Status, IsLocked = false, Roles = Array.Empty(), RoleIds = Array.Empty(), Permissions = Array.Empty(), CreatedAt = user.CreatedAt, LastLoginAt = user.LastLoginAt, Avatar = user.Avatar, RowVersion = user.RowVersion }; } private static long[] ParseIds(string[] values, string name) { // 1. 空数组直接返回 if (values.Length == 0) { return Array.Empty(); } // 2. 解析并去重 var ids = new List(values.Length); foreach (var value in values) { if (!long.TryParse(value, out var id) || id <= 0) { throw new BusinessException(ErrorCodes.BadRequest, $"{name} ID 无效"); } ids.Add(id); } // 3. 返回去重结果 return ids.Distinct().ToArray(); } }