Аватар
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Profiles.Domain;
|
||||
using Knot.Contracts.Profiles.Application.DTOs;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
|
||||
|
||||
@@ -15,7 +10,8 @@ public sealed record CropAvatarCommand(
|
||||
Stream FileStream,
|
||||
string FileName,
|
||||
string ContentType,
|
||||
int X, int Y, int Width, int Height) : ICommand<UserProfileDto>;
|
||||
int X, int Y, int Width, int Height,
|
||||
int SourceWidth, int SourceHeight) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarCommand, UserProfileDto>
|
||||
{
|
||||
@@ -53,11 +49,16 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarComma
|
||||
private static async Task<MemoryStream> CropAndResizeAsync(CropAvatarCommand request, CancellationToken ct)
|
||||
{
|
||||
using var image = await Image.LoadAsync(request.FileStream, ct);
|
||||
image.Mutate(x => x.AutoOrient());
|
||||
|
||||
var startX = Math.Clamp(request.X, 0, image.Width - 1);
|
||||
var startY = Math.Clamp(request.Y, 0, image.Height - 1);
|
||||
var width = Math.Clamp(request.Width, 1, image.Width - startX);
|
||||
var height = Math.Clamp(request.Height, 1, image.Height - startY);
|
||||
// Рассчитываем коэффициент масштабирования между тем, что видел фронтенд и тем, что загрузил бэкенд
|
||||
double scaleX = (double)image.Width / request.SourceWidth;
|
||||
double scaleY = (double)image.Height / request.SourceHeight;
|
||||
|
||||
var startX = Math.Clamp((int)(request.X * scaleX), 0, image.Width - 1);
|
||||
var startY = Math.Clamp((int)(request.Y * scaleY), 0, image.Height - 1);
|
||||
var width = Math.Clamp((int)(request.Width * scaleX), 1, image.Width - startX);
|
||||
var height = Math.Clamp((int)(request.Height * scaleY), 1, image.Height - startY);
|
||||
|
||||
image.Mutate(ctx => ctx
|
||||
.Crop(new Rectangle(startX, startY, width, height))
|
||||
|
||||
@@ -18,11 +18,16 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
|
||||
{
|
||||
private readonly IProfileRepository _repository;
|
||||
private readonly IAvatarStorageService _avatarStorage;
|
||||
private readonly Knot.Contracts.Auth.Domain.IUserRepository _userRepository;
|
||||
|
||||
public UploadAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
||||
public UploadAvatarCommandHandler(
|
||||
IProfileRepository repository,
|
||||
IAvatarStorageService avatarStorage,
|
||||
Knot.Contracts.Auth.Domain.IUserRepository userRepository)
|
||||
{
|
||||
_repository = repository;
|
||||
_avatarStorage = avatarStorage;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
||||
@@ -37,6 +42,14 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
|
||||
var fileId = await _avatarStorage.UploadAsync(request.FileStream, request.FileName, request.ContentType, cancellationToken);
|
||||
var avatarUrl = $"/api/files/{fileId}";
|
||||
|
||||
// Синхронизируем с основным модулем пользователей (Auth/Postgres)
|
||||
var userContract = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (userContract != null)
|
||||
{
|
||||
userContract.Avatar = avatarUrl;
|
||||
await _userRepository.UpdateAsync(userContract, cancellationToken);
|
||||
}
|
||||
|
||||
profile.Avatar = avatarUrl;
|
||||
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
||||
if (result.IsFailure)
|
||||
@@ -52,11 +65,16 @@ internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarC
|
||||
{
|
||||
private readonly IProfileRepository _repository;
|
||||
private readonly IAvatarStorageService _avatarStorage;
|
||||
private readonly Knot.Contracts.Auth.Domain.IUserRepository _userRepository;
|
||||
|
||||
public DeleteAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
||||
public DeleteAvatarCommandHandler(
|
||||
IProfileRepository repository,
|
||||
IAvatarStorageService avatarStorage,
|
||||
Knot.Contracts.Auth.Domain.IUserRepository userRepository)
|
||||
{
|
||||
_repository = repository;
|
||||
_avatarStorage = avatarStorage;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
|
||||
@@ -68,6 +86,14 @@ internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarC
|
||||
if (!string.IsNullOrEmpty(profile.Avatar))
|
||||
await _avatarStorage.DeleteAsync(profile.Avatar, cancellationToken);
|
||||
|
||||
// Синхронизируем с основным модулем пользователей (Auth/Postgres)
|
||||
var userContract = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (userContract != null)
|
||||
{
|
||||
userContract.Avatar = null;
|
||||
await _userRepository.UpdateAsync(userContract, cancellationToken);
|
||||
}
|
||||
|
||||
profile.Avatar = null;
|
||||
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
||||
if (result.IsFailure)
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Contracts.Profiles.Application.DTOs;
|
||||
using Knot.Contracts.Profiles.Domain;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
using Knot.Modules.Profiles.Infrastructure.Mappings;
|
||||
using Knot.Shared.Kernel;
|
||||
using MongoDB.Bson;
|
||||
@@ -76,17 +70,19 @@ internal class ProfileRepository : IProfileRepository
|
||||
|
||||
public async Task<Result<UserProfileDto>> UpdateAsync(UserProfileDto dto, CancellationToken ct = default)
|
||||
{
|
||||
var profile = await _profiles.Find(p => p.Id == dto.UserId).FirstOrDefaultAsync(ct);
|
||||
if (profile is null)
|
||||
var document = await _profiles.Find(p => p.Id == dto.UserId).FirstOrDefaultAsync(ct);
|
||||
if (document is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
profile.UpdateProfile(
|
||||
dto.DisplayName ?? profile.DisplayName,
|
||||
dto.About ?? profile.Bio,
|
||||
document.UpdateProfile(
|
||||
dto.DisplayName ?? document.DisplayName,
|
||||
dto.About ?? document.Bio,
|
||||
dto.Birthday);
|
||||
|
||||
await _profiles.ReplaceOneAsync(p => p.Id == dto.UserId, profile, new ReplaceOptions { IsUpsert = true }, ct);
|
||||
return Result.Success(profile.ToDto());
|
||||
document.UpdateAvatar(dto.Avatar);
|
||||
|
||||
await _profiles.ReplaceOneAsync(p => p.Id == dto.UserId, document, cancellationToken: ct);
|
||||
return Result.Success(document.ToDto());
|
||||
}
|
||||
|
||||
public async Task<Result> UpdateStatusAsync(Guid userId, bool isBanned, bool isDeleted, CancellationToken ct = default)
|
||||
|
||||
@@ -53,9 +53,11 @@ public static class ProfilesEndpoints
|
||||
int.TryParse(form["y"], out int y);
|
||||
int.TryParse(form["width"], out int width);
|
||||
int.TryParse(form["height"], out int height);
|
||||
int.TryParse(form["sw"], out int sw);
|
||||
int.TryParse(form["sh"], out int sh);
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var result = await sender.Send(new CropAvatarCommand(userContext.UserId, stream, file.FileName ?? "avatar.jpg", file.ContentType, x, y, width, height), ct);
|
||||
var result = await sender.Send(new CropAvatarCommand(userContext.UserId, stream, file.FileName ?? "avatar.jpg", file.ContentType, x, y, width, height, sw, sh), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
||||
}).DisableAntiforgery();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user