using MediatR; using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Events; 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 ChangeIdentityUserStatusCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// public async Task Handle(ChangeIdentityUserStatusCommand 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 user = isSuperAdmin ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; } if (!isSuperAdmin && user.TenantId != currentTenantId) { return false; } // 4. 校验租户管理员保留规则 if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active) { await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken); } // 5. 更新状态 var previousStatus = user.Status; switch (request.Status) { case IdentityUserStatus.Active: user.Status = IdentityUserStatus.Active; user.LockedUntil = null; user.FailedLoginCount = 0; break; case IdentityUserStatus.Disabled: user.Status = IdentityUserStatus.Disabled; user.LockedUntil = null; break; case IdentityUserStatus.Locked: user.Status = IdentityUserStatus.Locked; user.LockedUntil = null; break; default: throw new BusinessException(ErrorCodes.BadRequest, "无效的用户状态"); } // 6. 构建操作日志消息 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:status-change", TargetType = "identity_user", TargetIds = JsonSerializer.Serialize(new[] { user.Id }), OperatorId = currentUserAccessor.UserId.ToString(), OperatorName = operatorName, Parameters = JsonSerializer.Serialize(new { userId = user.Id, tenantId = user.TenantId, previousStatus = previousStatus.ToString(), currentStatus = user.Status.ToString() }), Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; // 7. 写入 Outbox 并保存变更 await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); return true; } private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken) { // 1. 获取租户管理员角色 var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); if (tenantAdminRole == null) { return; } // 2. 判断用户是否为租户管理员 var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); if (!relations.Any(x => x.RoleId == tenantAdminRole.Id)) { return; } // 3. 统计活跃管理员数量 var filter = new IdentityUserSearchFilter { TenantId = tenantId, RoleId = tenantAdminRole.Id, Status = IdentityUserStatus.Active, IncludeDeleted = false, Page = 1, PageSize = 1 }; var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken); if (result.Total <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); } } }