Миграции, сборка

This commit is contained in:
Халимов Рустам
2026-03-27 23:09:16 +03:00
parent 030ae1e4e4
commit ba5c5210d4
43 changed files with 406 additions and 194 deletions

View File

@@ -1,22 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Profiles.Application.Abstractions;
/// <summary>
/// Интерфейс для хранения аватаров в S3 (MinIO).
/// Отдельный от IFileStorageService модуля Storage,
/// чтобы не создавать прямой зависимости на Storage модуль.
/// </summary>
public interface IAvatarStorageService
{
/// <summary>
/// Загружает файл в S3-бакет аватаров и возвращает fileId (ключ объекта).
/// </summary>
Task<string> UploadAsync(System.IO.Stream stream, string fileName, string contentType, CancellationToken ct = default);
/// <summary>
/// Удаляет файл аватара из S3 по fileId.
/// </summary>
Task DeleteAsync(string fileId, CancellationToken ct = default);
}

View File

@@ -1,14 +0,0 @@
using Knot.Modules.Profiles.Domain;
using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Profiles.Application.Abstractions;
namespace Knot.Modules.Profiles.Application.Abstractions;
public interface IProfilesDbContext
{
DbSet<Profile> Profiles { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Application.Profiles.DTOs;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
@@ -34,10 +34,10 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarComma
if (profile is null)
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
// Обрезать и ресайзнуть изображение до 400×400
// Обрезать и ресайзнуть изображение до 400×400
using var ms = await CropAndResizeAsync(request, cancellationToken);
// Удалить старый аватар из S3
// Удалить старый аватар из S3
if (!string.IsNullOrEmpty(profile.AvatarUrl))
await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken);

View File

@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Application.Profiles.DTOs;
using System;
using System.IO;
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
// ─── Upload ────────────────────────────────────────────────────────────────
// в”Ђв”Ђв”Ђ Upload в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
public sealed record UploadAvatarCommand(
Guid UserId,
@@ -33,7 +33,7 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
if (profile is null)
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
// Удалить старый аватар из S3, если был
// Удалить старый аватар из S3, если был
if (!string.IsNullOrEmpty(profile.AvatarUrl))
await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken);
@@ -47,7 +47,7 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
}
}
// ─── Delete ────────────────────────────────────────────────────────────────
// в”Ђв”Ђв”Ђ Delete в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
public sealed record DeleteAvatarCommand(Guid UserId) : ICommand<UserProfileDto>;

View File

@@ -1,4 +1,4 @@
using System;
using System;
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Knot.Modules.Profiles.Domain;
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;

View File

@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Application.Profiles.DTOs;
using System;
using System.Threading;

View File

@@ -1,8 +1,14 @@
using MediatR;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Profiles.Domain;
using Knot.Shared.Kernel.Events;
namespace Knot.Modules.Profiles.Application.Profiles.Integration;
/// <summary>
/// Обработчик события из модуля Auth.
/// Создаёт пустой профиль при регистрации пользователя.
/// Обработчик события из модуля Auth.
/// Создаёт пустой профиль при регистрации пользователя.
/// </summary>
public class UserRegisteredDomainEventHandler : INotificationHandler<UserRegisteredDomainEvent>
{

View File

@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Application.Profiles.DTOs;
using System.Collections.Generic;
using System.Linq;

View File

@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Application.Profiles.DTOs;
using System;
using System.Threading;

View File

@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Application.Profiles.DTOs;
using System;
using System.Threading;

View File

@@ -1,3 +1,9 @@
using Knot.Modules.Profiles.Domain;
using Knot.Modules.Profiles.Infrastructure.Database;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
namespace Knot.Modules.Profiles;
public static class DependencyInjection
@@ -11,10 +17,11 @@ public static class DependencyInjection
// MongoDB Registration
var mongoConnection = configuration.GetConnectionString("MongoConnection")
?? configuration["MONGO_URL"]
?? "mongodb://localhost:27017";
?? "mongodb://mongo:27017";
var mongoClient = new MongoClient(mongoConnection);
var database = mongoClient.GetDatabase("knot_messager");
var databaseName = new MongoUrl(mongoConnection).DatabaseName ?? "knot_messager";
var database = mongoClient.GetDatabase(databaseName);
services.AddSingleton<IMongoDatabase>(database);
services.AddMediatR(config =>

View File

@@ -1,11 +1,11 @@
namespace Knot.Modules.Profiles.Domain.Events;
namespace Knot.Modules.Profiles.Domain.Events;
/// <summary>
/// Профиль пользователя создан.
/// Профиль пользователя создан.
/// </summary>
public record ProfileCreatedDomainEvent(Guid ProfileId) : IDomainEvent;
/// <summary>
/// Профиль пользователя обновлен.
/// Профиль пользователя обновлен.
/// </summary>
public record ProfileUpdatedDomainEvent(Guid ProfileId) : IDomainEvent;

View File

@@ -0,0 +1,11 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Profiles.Domain;
public interface IAvatarStorageService
{
Task<string> UploadAsync(Stream content, string fileName, string contentType, CancellationToken ct = default);
Task DeleteAsync(string fileId, CancellationToken ct = default);
}

View File

@@ -1,8 +1,13 @@
namespace Knot.Modules.Profiles.Application.Abstractions;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Profiles.Domain;
public interface IProfileRepository
{
Task<ProfileDocument?> GetByIdAsync(Guid userId, CancellationToken ct = default);
Task<ProfileDocument?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<ProfileDocument?> GetByUsernameAsync(string username, CancellationToken ct = default);
Task AddAsync(ProfileDocument profile, CancellationToken ct = default);
Task UpdateAsync(ProfileDocument profile, CancellationToken ct = default);

View File

@@ -1,7 +1,7 @@
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Profiles.Application.Abstractions;
namespace Knot.Modules.Profiles.Domain;
public interface IProfilesUnitOfWork
{

View File

@@ -1,4 +1,4 @@
namespace Knot.Modules.Profiles.Domain;
namespace Knot.Modules.Profiles.Domain;
public sealed class Profile : AggregateRoot<Guid>
{

View File

@@ -1,8 +1,11 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Knot.Modules.Profiles.Domain;
/// <summary>
/// MongoDB-документ профиля пользователя.
/// Id совпадает с UserId из модуля Auth (Postgres).
/// MongoDB-документ профиля пользователя.
/// Id совпадает с UserId из модуля Auth (Postgres).
/// </summary>
public sealed class ProfileDocument
{

View File

@@ -1,4 +1,4 @@
namespace Knot.Modules.Profiles.Domain;
namespace Knot.Modules.Profiles.Domain;
using Knot.Shared.Kernel;
public static class ProfilesErrors
{

View File

@@ -1,7 +1,6 @@
global using Knot.Shared.Kernel;
global using Knot.Shared.Kernel.Events;
global using Knot.Shared.Kernel.Storage;
global using Knot.Modules.Profiles.Application.Abstractions;
global using Knot.Modules.Profiles.Domain;
global using Knot.Modules.Profiles.Domain.Events;
global using Knot.Modules.Profiles.Infrastructure.Database;

View File

@@ -1,3 +1,9 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Profiles.Domain;
using Knot.Shared.Kernel.Storage;
namespace Knot.Modules.Profiles.Infrastructure.Database;
public class AvatarStorageService : IAvatarStorageService
@@ -11,13 +17,13 @@ public class AvatarStorageService : IAvatarStorageService
public async Task<string> UploadAsync(Stream content, string fileName, string contentType, CancellationToken ct = default)
{
// IFileStorageService не принимает CancellationToken в UploadFileAsync
// IFileStorageService не принимает CancellationToken в UploadFileAsync
return await _fileStorage.UploadFileAsync(content, fileName, contentType);
}
public async Task DeleteAsync(string fileId, CancellationToken ct = default)
{
// IFileStorageService не принимает CancellationToken в DeleteFileAsync
// IFileStorageService не принимает CancellationToken в DeleteFileAsync
await _fileStorage.DeleteFileAsync(fileId);
}
}

View File

@@ -1,3 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Profiles.Domain;
using MongoDB.Bson;
using MongoDB.Driver;
namespace Knot.Modules.Profiles.Infrastructure.Database;
public class ProfileRepository : IProfileRepository

View File

@@ -1,8 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Profiles.Domain;
namespace Knot.Modules.Profiles.Infrastructure.Database;
public class ProfilesUnitOfWork : IProfilesUnitOfWork
{
// MongoDB не поддерживает транзакции без репликации в простом виде,
// поэтому UnitOfWork здесь формальный для соответствия интерфейсу.
public Task SaveChangesAsync(CancellationToken ct = default) => Task.CompletedTask;
}

View File

@@ -1,4 +1,4 @@
using Carter;
using Carter;
using Knot.Shared.Kernel;
using Knot.Modules.Profiles.Application.Profiles.Avatar;
using Knot.Modules.Profiles.Application.Profiles.GetProfile;