163 lines
6.1 KiB
C#
163 lines
6.1 KiB
C#
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.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.Security;
|
|
|
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
|
|
|
/// <summary>
|
|
/// 更新用户处理器。
|
|
/// </summary>
|
|
public sealed class UpdateIdentityUserCommandHandler(
|
|
IIdentityUserRepository identityUserRepository,
|
|
IUserRoleRepository userRoleRepository,
|
|
IRoleRepository roleRepository,
|
|
ICurrentUserAccessor currentUserAccessor,
|
|
IAdminAuthService adminAuthService,
|
|
IIdentityOperationLogPublisher operationLogPublisher,
|
|
IMediator mediator)
|
|
: IRequestHandler<UpdateIdentityUserCommand, UserDetailDto?>
|
|
{
|
|
/// <inheritdoc />
|
|
public async Task<UserDetailDto?> Handle(UpdateIdentityUserCommand request, CancellationToken cancellationToken)
|
|
{
|
|
// 1. 获取操作者档案(用于操作日志)
|
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
|
|
|
// 2. (空行后) 获取用户实体
|
|
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
|
|
if (user == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// 3. (空行后) 限定仅允许更新平台管理员账号
|
|
if (user.Portal != PortalType.Admin || user.TenantId is not null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// 4. (空行后) 规范化输入并校验唯一性
|
|
var portal = PortalType.Admin;
|
|
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 = request.RoleIds == null ? null : ParseIds(request.RoleIds, "角色");
|
|
|
|
if (!string.IsNullOrWhiteSpace(phone)
|
|
&& !string.Equals(phone, user.Phone, StringComparison.OrdinalIgnoreCase)
|
|
&& await identityUserRepository.ExistsByPhoneAsync(portal, null, phone, user.Id, cancellationToken))
|
|
{
|
|
throw new BusinessException(ErrorCodes.Conflict, "手机号已存在");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(email)
|
|
&& !string.Equals(email, user.Email, StringComparison.OrdinalIgnoreCase)
|
|
&& await identityUserRepository.ExistsByEmailAsync(portal, null, email, user.Id, cancellationToken))
|
|
{
|
|
throw new BusinessException(ErrorCodes.Conflict, "邮箱已存在");
|
|
}
|
|
|
|
if (roleIds is { Length: > 0 })
|
|
{
|
|
var roles = await roleRepository.GetByIdsAsync(portal, null, roleIds, cancellationToken);
|
|
if (roles.Count != roleIds.Length)
|
|
{
|
|
throw new BusinessException(ErrorCodes.BadRequest, "角色列表包含无效项");
|
|
}
|
|
}
|
|
|
|
// 5. 更新用户字段
|
|
user.DisplayName = displayName;
|
|
user.Phone = phone;
|
|
user.Email = email;
|
|
user.Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim();
|
|
user.RowVersion = request.RowVersion;
|
|
|
|
// 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:update",
|
|
TargetType = "identity_user",
|
|
TargetIds = JsonSerializer.Serialize(new[] { user.Id }),
|
|
OperatorId = currentUserAccessor.UserId.ToString(),
|
|
OperatorName = operatorName,
|
|
Parameters = JsonSerializer.Serialize(new
|
|
{
|
|
userId = user.Id,
|
|
portal = portal.ToString(),
|
|
displayName,
|
|
phone,
|
|
email,
|
|
roleIds
|
|
}),
|
|
Result = JsonSerializer.Serialize(new { userId = user.Id }),
|
|
Success = true
|
|
};
|
|
|
|
// 7. 持久化用户更新并写入 Outbox
|
|
try
|
|
{
|
|
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
|
|
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex) when (IsConcurrencyException(ex))
|
|
{
|
|
throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试");
|
|
}
|
|
|
|
// 8. 覆盖角色绑定(仅当显式传入时)
|
|
if (roleIds != null)
|
|
{
|
|
await userRoleRepository.ReplaceUserRolesAsync(portal, null, user.Id, roleIds, cancellationToken);
|
|
}
|
|
|
|
// 9. 返回用户详情
|
|
return await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
private static bool IsConcurrencyException(Exception exception)
|
|
=> string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal);
|
|
}
|