196 lines
7.3 KiB
C#
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();
|
|
}
|
|
}
|