using MediatR; 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.Models; 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.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; /// /// 批量用户操作处理器。 /// public sealed class BatchIdentityUserOperationCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// public async Task Handle(BatchIdentityUserOperationCommand 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, "禁止跨租户批量操作用户"); } if (isSuperAdmin && !request.TenantId.HasValue) { throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户"); } // 3. (空行后) 解析用户 ID 列表 var tenantId = request.TenantId ?? currentTenantId; var userIds = ParseIds(request.UserIds, "用户"); if (userIds.Length == 0) { return new BatchIdentityUserOperationResult { SuccessCount = 0, FailureCount = 0, Failures = Array.Empty(), ExportItems = Array.Empty() }; } // 4. (空行后) 查询目标用户集合 var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore; var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken); var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer.Default); // 5. (空行后) 预计算租户管理员约束 var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); var tenantAdminUserIds = tenantAdminRole == null ? Array.Empty() : (await userRoleRepository.GetByUserIdsAsync(tenantId, usersById.Keys, cancellationToken)) .Where(x => x.RoleId == tenantAdminRole.Id) .Select(x => x.UserId) .Distinct() .ToArray(); var activeAdminCount = tenantAdminRole == null ? 0 : (await identityUserRepository.SearchPagedAsync(new IdentityUserSearchFilter { TenantId = tenantId, RoleId = tenantAdminRole.Id, Status = IdentityUserStatus.Active, IncludeDeleted = false, Page = 1, PageSize = 1 }, isSuperAdmin, cancellationToken)).Total; var remainingActiveAdmins = activeAdminCount; // 6. (空行后) 执行批量操作 var failures = new List(); var successCount = 0; var exportItems = new List(); foreach (var userId in userIds) { if (!usersById.TryGetValue(userId, out var user)) { failures.Add(new BatchIdentityUserFailureItem { UserId = userId, Reason = "用户不存在" }); continue; } try { switch (request.Operation) { case IdentityUserBatchOperation.Enable: user.Status = IdentityUserStatus.Active; user.LockedUntil = null; user.FailedLoginCount = 0; await identityUserRepository.SaveChangesAsync(cancellationToken); successCount++; break; case IdentityUserBatchOperation.Disable: if (user.Status == IdentityUserStatus.Active && tenantAdminUserIds.Contains(user.Id) && remainingActiveAdmins <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); } if (user.Status == IdentityUserStatus.Active && tenantAdminUserIds.Contains(user.Id)) { remainingActiveAdmins--; } user.Status = IdentityUserStatus.Disabled; user.LockedUntil = null; await identityUserRepository.SaveChangesAsync(cancellationToken); successCount++; break; case IdentityUserBatchOperation.Delete: if (user.Status == IdentityUserStatus.Active && tenantAdminUserIds.Contains(user.Id) && remainingActiveAdmins <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); } if (user.Status == IdentityUserStatus.Active && tenantAdminUserIds.Contains(user.Id)) { remainingActiveAdmins--; } await identityUserRepository.RemoveAsync(user, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); successCount++; break; case IdentityUserBatchOperation.Restore: if (!user.DeletedAt.HasValue) { throw new BusinessException(ErrorCodes.BadRequest, "用户未删除"); } user.DeletedAt = null; user.DeletedBy = null; await identityUserRepository.SaveChangesAsync(cancellationToken); successCount++; break; case IdentityUserBatchOperation.Export: successCount++; break; default: throw new BusinessException(ErrorCodes.BadRequest, "无效的批量操作类型"); } } catch (Exception ex) when (IsConcurrencyException(ex)) { failures.Add(new BatchIdentityUserFailureItem { UserId = userId, Reason = "用户数据已被修改,请刷新后重试" }); } catch (BusinessException ex) { failures.Add(new BatchIdentityUserFailureItem { UserId = userId, Reason = ex.Message }); } } // 6.1 (空行后) 处理导出数据 if (request.Operation == IdentityUserBatchOperation.Export) { var roleCodesLookup = await ResolveRoleCodesAsync(users, userRoleRepository, roleRepository, cancellationToken); var now = DateTime.UtcNow; exportItems.AddRange(users.Select(user => new UserListItemDto { UserId = user.Id, TenantId = user.TenantId, Account = user.Account, DisplayName = user.DisplayName, Phone = user.Phone, Email = user.Email, Status = user.Status, IsLocked = user.Status == IdentityUserStatus.Locked || (user.LockedUntil.HasValue && user.LockedUntil.Value > now), IsDeleted = user.DeletedAt.HasValue, Roles = roleCodesLookup.TryGetValue(user.Id, out var codes) ? codes : Array.Empty(), CreatedAt = user.CreatedAt, LastLoginAt = user.LastLoginAt })); } // 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:batch", TargetType = "identity_user", TargetIds = JsonSerializer.Serialize(userIds), OperatorId = currentUserAccessor.UserId.ToString(), OperatorName = operatorName, Parameters = JsonSerializer.Serialize(new { tenantId, operation = request.Operation.ToString() }), Result = JsonSerializer.Serialize(new { successCount, failureCount = failures.Count }), Success = failures.Count == 0 }; // 8. (空行后) 写入 Outbox 并保存变更 await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); return new BatchIdentityUserOperationResult { SuccessCount = successCount, FailureCount = failures.Count, Failures = failures, ExportItems = exportItems }; } 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(); } private static async Task> ResolveRoleCodesAsync( IReadOnlyList users, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, CancellationToken cancellationToken) { // 1. 预分配字典容量 var result = new Dictionary(users.Count); // 2. (空行后) 按租户分组,降低角色查询次数 foreach (var group in users.GroupBy(user => user.TenantId)) { var tenantId = group.Key; var userIds = group.Select(user => user.Id).Distinct().ToArray(); if (userIds.Length == 0) { continue; } // 3. (空行后) 查询用户角色映射 var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); if (relations.Count == 0) { continue; } // 4. (空行后) 查询角色并构建映射 var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); var roles = roleIds.Length == 0 ? Array.Empty() : await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer.Default); // 5. (空行后) 组装用户角色编码列表 foreach (var relationGroup in relations.GroupBy(x => x.UserId)) { var codes = relationGroup .Select(x => roleCodeMap.TryGetValue(x.RoleId, out var code) ? code : null) .OfType() .Where(code => !string.IsNullOrWhiteSpace(code)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); result[relationGroup.Key] = codes.Length == 0 ? Array.Empty() : codes; } } return result; } private static bool IsConcurrencyException(Exception exception) => string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal); }