Files
TakeoutSaaS.AdminApi/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs

196 lines
7.3 KiB
C#

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;
/// <summary>
/// 创建用户处理器。
/// </summary>
public sealed class CreateIdentityUserCommandHandler(
IIdentityUserRepository identityUserRepository,
IUserRoleRepository userRoleRepository,
IRoleRepository roleRepository,
IPasswordHasher<IdentityUser> passwordHasher,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService,
IIdentityOperationLogPublisher operationLogPublisher,
IIdGenerator idGenerator,
IMediator mediator)
: IRequestHandler<CreateIdentityUserCommand, UserDetailDto>
{
/// <inheritdoc />
public async Task<UserDetailDto> 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<string>(),
RoleIds = Array.Empty<string>(),
Permissions = Array.Empty<string>(),
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<long>();
}
// 2. 解析并去重
var ids = new List<long>(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();
}
}