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.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 ResetIdentityUserPasswordCommandHandler( IAdminPasswordResetTokenStore tokenStore, IIdentityUserRepository identityUserRepository, ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// public async Task Handle(ResetIdentityUserPasswordCommand 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) { throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); } if (!isSuperAdmin && user.TenantId != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码"); } // 4. (空行后) 签发重置令牌(1 小时有效) var expiresAt = DateTime.UtcNow.AddHours(1); var token = await tokenStore.IssueAsync(user.Id, expiresAt, cancellationToken); // 5. (空行后) 标记用户需重置密码 user.MustChangePassword = true; user.FailedLoginCount = 0; user.LockedUntil = null; if (user.Status == IdentityUserStatus.Locked) { user.Status = IdentityUserStatus.Active; } // 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:password-reset", 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 }), Result = JsonSerializer.Serialize(new { userId = user.Id, expiresAt }), Success = true }; // 7. (空行后) 写入 Outbox 并保存变更 await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); return new ResetIdentityUserPasswordResult { Token = token, ExpiresAt = expiresAt }; } }