150 lines
5.7 KiB
C#
150 lines
5.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 更新用户状态处理器。
|
|
/// </summary>
|
|
public sealed class ChangeIdentityUserStatusCommandHandler(
|
|
IIdentityUserRepository identityUserRepository,
|
|
IUserRoleRepository userRoleRepository,
|
|
IRoleRepository roleRepository,
|
|
ITenantProvider tenantProvider,
|
|
ICurrentUserAccessor currentUserAccessor,
|
|
IAdminAuthService adminAuthService,
|
|
IIdentityOperationLogPublisher operationLogPublisher)
|
|
: IRequestHandler<ChangeIdentityUserStatusCommand, bool>
|
|
{
|
|
/// <inheritdoc />
|
|
public async Task<bool> 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, "至少保留一个管理员");
|
|
}
|
|
}
|
|
}
|