Аватар

This commit is contained in:
Халимов Рустам
2026-04-05 01:27:58 +03:00
parent 53f193970c
commit e11240f78f
13 changed files with 133 additions and 47 deletions

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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();