Фронт
This commit is contained in:
@@ -1,14 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Abstractions;
|
||||
|
||||
public interface IProfileRepository
|
||||
{
|
||||
Task<ProfileDocument?> GetByIdAsync(Guid userId, 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);
|
||||
Task<List<ProfileDocument>> SearchAsync(string query, CancellationToken ct = default);
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
using Knot.Shared.Kernel.Events;
|
||||
using MediatR;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Реакция модуля Profiles на создание пользователя в модуле Auth.
|
||||
/// Создает соответствующий документ в MongoDB.
|
||||
/// Обработчик события из модуля Auth.
|
||||
/// Создаёт пустой профиль при регистрации пользователя.
|
||||
/// </summary>
|
||||
internal sealed class UserRegisteredDomainEventHandler : INotificationHandler<UserRegisteredDomainEvent>
|
||||
public class UserRegisteredDomainEventHandler : INotificationHandler<UserRegisteredDomainEvent>
|
||||
{
|
||||
private readonly IProfileRepository _repository;
|
||||
|
||||
@@ -22,17 +15,12 @@ internal sealed class UserRegisteredDomainEventHandler : INotificationHandler<Us
|
||||
|
||||
public async Task Handle(UserRegisteredDomainEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
// Проверяем, существует ли уже профиль (защита от дублей)
|
||||
var existing = await _repository.GetByIdAsync(notification.UserId, cancellationToken);
|
||||
if (existing is not null) return;
|
||||
|
||||
var profile = ProfileDocument.Create(
|
||||
notification.UserId,
|
||||
notification.Username,
|
||||
notification.DisplayName,
|
||||
notification.Bio
|
||||
notification.DisplayName ?? notification.Username
|
||||
);
|
||||
|
||||
await _repository.AddAsync(profile, cancellationToken);
|
||||
await _repository.AddAsync(profile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Infrastructure.Database;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace Knot.Modules.Profiles;
|
||||
|
||||
public static class DependencyInjection
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: профиль пользователя создан при регистрации.
|
||||
/// Профиль пользователя создан.
|
||||
/// </summary>
|
||||
public sealed record ProfileCreatedDomainEvent(Guid UserId, string Username) : IDomainEvent;
|
||||
public record ProfileCreatedDomainEvent(Guid ProfileId) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Доменное событие: аватар профиля был изменён (для возможной инвалидации CDN-кэша).
|
||||
/// Профиль пользователя обновлен.
|
||||
/// </summary>
|
||||
public sealed record ProfileAvatarChangedDomainEvent(Guid UserId, string? OldAvatarFileId, string? NewAvatarFileId) : IDomainEvent;
|
||||
public record ProfileUpdatedDomainEvent(Guid ProfileId) : IDomainEvent;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Domain;
|
||||
|
||||
public sealed class Profile : AggregateRoot<Guid>
|
||||
@@ -13,13 +10,17 @@ public sealed class Profile : AggregateRoot<Guid>
|
||||
public bool HideStoryViews { get; private set; }
|
||||
public string Profilename => Username;
|
||||
public string Name => DisplayName;
|
||||
public string AvatarUrl => Avatar;
|
||||
public string? AvatarUrl => Avatar;
|
||||
public bool IsPrivate => false;
|
||||
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
|
||||
public int FollowersCount => 0;
|
||||
public int FollowingCount => 0;
|
||||
public int FriendsCount => 0;
|
||||
|
||||
#pragma warning disable CS8618
|
||||
private Profile() : base(Guid.Empty) { }
|
||||
#pragma warning restore CS8618
|
||||
|
||||
private Profile(Guid id, string username, string displayName, string? bio = null)
|
||||
: base(id)
|
||||
{
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// MongoDB-документ профиля пользователя.
|
||||
/// Id совпадает с UserId из модуля Auth (Postgres).
|
||||
/// Аватар хранится в S3 — здесь лежит только ссылка.
|
||||
/// </summary>
|
||||
public sealed class ProfileDocument
|
||||
{
|
||||
@@ -21,23 +16,17 @@ public sealed class ProfileDocument
|
||||
|
||||
public string? Bio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL или ключ объекта в S3-хранилище (MinIO).
|
||||
/// Пример: "/api/files/{fileId}"
|
||||
/// </summary>
|
||||
public string? AvatarUrl { get; private set; }
|
||||
|
||||
public DateTime? Birthday { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Скрывать ли просмотры сторис от других пользователей.
|
||||
/// </summary>
|
||||
public bool HideStoryViews { get; private set; }
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
// Для MongoDB — protected-конструктор через BSON-десериализацию
|
||||
protected ProfileDocument() { }
|
||||
#pragma warning disable CS8618
|
||||
private ProfileDocument() { }
|
||||
#pragma warning restore CS8618
|
||||
|
||||
private ProfileDocument(Guid id, string username, string displayName, string? bio)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
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.Application.Profiles;
|
||||
global using Knot.Modules.Profiles.Domain;
|
||||
global using Knot.Modules.Profiles.Domain.Events;
|
||||
global using Knot.Modules.Profiles.Infrastructure.Database;
|
||||
global using MongoDB.Bson;
|
||||
global using MongoDB.Bson.Serialization.Attributes;
|
||||
global using MongoDB.Driver;
|
||||
global using MediatR;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Адаптер к S3-хранилищу для аватаров.
|
||||
/// Инжектирует общий IFileStorageService и передает ему управление.
|
||||
/// </summary>
|
||||
internal sealed class AvatarStorageService : IAvatarStorageService
|
||||
public class AvatarStorageService : IAvatarStorageService
|
||||
{
|
||||
private readonly IFileStorageService _storage;
|
||||
private readonly IFileStorageService _fileStorage;
|
||||
|
||||
public AvatarStorageService(IFileStorageService storage)
|
||||
public AvatarStorageService(IFileStorageService fileStorage)
|
||||
{
|
||||
_storage = storage;
|
||||
_fileStorage = fileStorage;
|
||||
}
|
||||
|
||||
public async Task<string> UploadAsync(Stream stream, string fileName, string contentType, CancellationToken ct = default)
|
||||
public async Task<string> UploadAsync(Stream content, string fileName, string contentType, CancellationToken ct = default)
|
||||
{
|
||||
return await _storage.UploadFileAsync(stream, fileName, contentType);
|
||||
// IFileStorageService не принимает CancellationToken в UploadFileAsync
|
||||
return await _fileStorage.UploadFileAsync(content, fileName, contentType);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string fileId, CancellationToken ct = default)
|
||||
{
|
||||
// Если fileId содержит "/api/files/", обрезаем его до чистого ID
|
||||
var cleanId = fileId.Replace("/api/files/", "");
|
||||
await _storage.DeleteFileAsync(cleanId);
|
||||
// IFileStorageService не принимает CancellationToken в DeleteFileAsync
|
||||
await _fileStorage.DeleteFileAsync(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
using MongoDB.Driver;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Database;
|
||||
|
||||
internal sealed class ProfileRepository : IProfileRepository
|
||||
public class ProfileRepository : IProfileRepository
|
||||
{
|
||||
private readonly IMongoCollection<ProfileDocument> _profiles;
|
||||
|
||||
@@ -17,9 +9,14 @@ internal sealed class ProfileRepository : IProfileRepository
|
||||
_profiles = database.GetCollection<ProfileDocument>("profiles");
|
||||
}
|
||||
|
||||
public async Task<ProfileDocument?> GetByIdAsync(Guid userId, CancellationToken ct = default)
|
||||
public async Task<ProfileDocument?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
return await _profiles.Find(p => p.Id == userId).FirstOrDefaultAsync(ct);
|
||||
return await _profiles.Find(p => p.Id == id).FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<ProfileDocument?> GetByUsernameAsync(string username, CancellationToken ct = default)
|
||||
{
|
||||
return await _profiles.Find(p => p.Username == username).FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
public async Task AddAsync(ProfileDocument profile, CancellationToken ct = default)
|
||||
@@ -29,20 +26,19 @@ internal sealed class ProfileRepository : IProfileRepository
|
||||
|
||||
public async Task UpdateAsync(ProfileDocument profile, CancellationToken ct = default)
|
||||
{
|
||||
await _profiles.ReplaceOneAsync(p => p.Id == profile.Id, profile, new ReplaceOptions { IsUpsert = false }, ct);
|
||||
await _profiles.ReplaceOneAsync(p => p.Id == profile.Id, profile, new ReplaceOptions { IsUpsert = true }, ct);
|
||||
}
|
||||
|
||||
public async Task<List<ProfileDocument>> SearchAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return new List<ProfileDocument>();
|
||||
return await _profiles.Find(_ => true).Limit(50).ToListAsync(ct);
|
||||
|
||||
// Простой регистронезависимый поиск по Regex (в реальной системе лучше использовать Text Index)
|
||||
var filter = Builders<ProfileDocument>.Filter.Or(
|
||||
Builders<ProfileDocument>.Filter.Regex(p => p.Username, new MongoDB.Bson.BsonRegularExpression(query, "i")),
|
||||
Builders<ProfileDocument>.Filter.Regex(p => p.DisplayName, new MongoDB.Bson.BsonRegularExpression(query, "i"))
|
||||
Builders<ProfileDocument>.Filter.Regex(p => p.Username, new BsonRegularExpression(query, "i")),
|
||||
Builders<ProfileDocument>.Filter.Regex(p => p.DisplayName, new BsonRegularExpression(query, "i"))
|
||||
);
|
||||
|
||||
return await _profiles.Find(filter).Limit(20).ToListAsync(ct);
|
||||
return await _profiles.Find(filter).Limit(50).ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
using Knot.Modules.Profiles.Application.Abstractions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Database;
|
||||
|
||||
internal sealed class ProfilesUnitOfWork : IProfilesUnitOfWork
|
||||
public class ProfilesUnitOfWork : IProfilesUnitOfWork
|
||||
{
|
||||
// MongoDB updates are atomic per document by default in the driver,
|
||||
// so for simple ProfileDocument updates, we don't need distributed transactions.
|
||||
// MongoDB не поддерживает транзакции без репликации в простом виде,
|
||||
// поэтому UnitOfWork здесь формальный для соответствия интерфейсу.
|
||||
public Task SaveChangesAsync(CancellationToken ct = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user