From d172b6408bbccbf5e13718cd13f8045e12423607 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 03:41:27 +0300 Subject: [PATCH 1/8] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D0=B4=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=84=D0=B0=D0=B1=D1=80=D0=B8=D0=BA=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D0=BF=D1=80=D1=8F=D0=BC=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20new?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Создание сервисов с порт-интерфейсом перенесено в IXxxFactory: шифр секретов, хранилище лимитов, ИИ-классификатор, ИИ-инструменты, файловое хранилище (Local/MinIO). Регистраторы и DiscoveryWorkerService больше не создают реализации напрямую. Добавлен тест FileStorageFactory. --- .../Abstractions/IAiClassifierFactory.cs | 15 ++++++ .../Abstractions/IAiToolsFactory.cs | 15 ++++++ .../Services/AiClassifierFactory.cs | 50 +++++++++++++++++++ .../Integrations/Services/AiToolsFactory.cs | 44 ++++++++++++++++ .../Abstractions/IFileStorageFactory.cs | 15 ++++++ .../Storage/Options/LocalStorageRoot.cs | 7 +++ .../Storage/Services/FileStorageFactory.cs | 42 ++++++++++++++++ .../Storage/Services/FileStorageRegistrar.cs | 15 +++--- .../Abstractions/ITenantLimitStoreFactory.cs | 15 ++++++ .../Services/TenantLimitStoreFactory.cs | 31 ++++++++++++ .../Abstractions/ISecretCipherFactory.cs | 15 ++++++ .../Security/Services/SecretCipherFactory.cs | 25 ++++++++++ .../ServiceCollectionExtensions.cs | 31 ++++++------ .../Registrars/DiscoveryModuleRegistrar.cs | 3 +- .../Services/DiscoveryWorkerService.cs | 7 +-- .../Contracts/DiscoveryWorkerServiceTests.cs | 3 +- .../Support/FileStorageFactoryTests.cs | 49 ++++++++++++++++++ 17 files changed, 350 insertions(+), 32 deletions(-) create mode 100644 src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs create mode 100644 src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs create mode 100644 src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs create mode 100644 src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs create mode 100644 src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs create mode 100644 src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs create mode 100644 src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs create mode 100644 src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs create mode 100644 src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs create mode 100644 src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs create mode 100644 src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs create mode 100644 src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs diff --git a/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs new file mode 100644 index 0000000..d6646b0 --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs @@ -0,0 +1,15 @@ +using Deal.Contracts.Integrations.Abstractions; + +namespace Deal.Infrastructure.Integrations.Abstractions; + +/// +/// Фабрика ИИ-классификатора с бюджетным гейтом +/// +public interface IAiClassifierFactory +{ + /// + /// Создаёт ИИ-классификатор + /// + /// Готовый порт классификации. + public IAiClassifier Create(); +} diff --git a/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs new file mode 100644 index 0000000..0952945 --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs @@ -0,0 +1,15 @@ +using Deal.Contracts.Integrations.Abstractions; + +namespace Deal.Infrastructure.Integrations.Abstractions; + +/// +/// Фабрика ИИ-инструментов с бюджетным гейтом +/// +public interface IAiToolsFactory +{ + /// + /// Создаёт ИИ-инструменты + /// + /// Готовый порт ИИ-инструментов. + public IAiTools Create(); +} diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs new file mode 100644 index 0000000..fc0f698 --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs @@ -0,0 +1,50 @@ +using Deal.Contracts.Integrations.Abstractions; +using Deal.Infrastructure.Integrations.Abstractions; +using Deal.Modules.Tenants.Application.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Deal.Infrastructure.Integrations.Services; + +/// +/// Фабрика декоратора бюджетного гейта классификатора +/// +public sealed class AiClassifierFactory : IAiClassifierFactory +{ + private readonly GrpcAiClassifier _paidClassifier; + private readonly LocalAiClassifier _localClassifier; + private readonly ITenantLimitStore _tenantLimits; + private readonly ITenantContext _tenantContext; + private readonly ILogger _logger; + + /// + /// Создаёт фабрику классификатора + /// + /// Платный исполнитель (gRPC-адаптер ai-service). + /// Бесплатный локальный разбор/фильтр (fallback). + /// Хранилище лимитов бюджета. + /// Контекст текущего тенанта. + /// Логгер переходов на локальный путь. + public AiClassifierFactory( + GrpcAiClassifier paidClassifier, + LocalAiClassifier localClassifier, + ITenantLimitStore tenantLimits, + ITenantContext tenantContext, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(paidClassifier); + ArgumentNullException.ThrowIfNull(localClassifier); + ArgumentNullException.ThrowIfNull(tenantLimits); + ArgumentNullException.ThrowIfNull(tenantContext); + ArgumentNullException.ThrowIfNull(logger); + _paidClassifier = paidClassifier; + _localClassifier = localClassifier; + _tenantLimits = tenantLimits; + _tenantContext = tenantContext; + _logger = logger; + } + + /// + public IAiClassifier Create() => + new BudgetedAiClassifier(_paidClassifier, _localClassifier, _tenantLimits, _tenantContext, _logger); +} diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs new file mode 100644 index 0000000..3815c42 --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs @@ -0,0 +1,44 @@ +using Deal.Contracts.Integrations.Abstractions; +using Deal.Infrastructure.Integrations.Abstractions; +using Deal.Modules.Tenants.Application.Abstractions; +using Deal.SharedKernel.Tenants.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Deal.Infrastructure.Integrations.Services; + +/// +/// Фабрика декоратора бюджетного гейта ИИ-инструментов +/// +public sealed class AiToolsFactory : IAiToolsFactory +{ + private readonly GrpcAiTools _paidTools; + private readonly ITenantLimitStore _tenantLimits; + private readonly ITenantContext _tenantContext; + private readonly ILogger _logger; + + /// + /// Создаёт фабрику ИИ-инструментов + /// + /// Платный исполнитель (gRPC-адаптер ai-service). + /// Хранилище лимитов бюджета. + /// Контекст текущего тенанта. + /// Логгер переходов на локальный путь. + public AiToolsFactory( + GrpcAiTools paidTools, + ITenantLimitStore tenantLimits, + ITenantContext tenantContext, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(paidTools); + ArgumentNullException.ThrowIfNull(tenantLimits); + ArgumentNullException.ThrowIfNull(tenantContext); + ArgumentNullException.ThrowIfNull(logger); + _paidTools = paidTools; + _tenantLimits = tenantLimits; + _tenantContext = tenantContext; + _logger = logger; + } + + /// + public IAiTools Create() => new BudgetedAiTools(_paidTools, _tenantLimits, _tenantContext, _logger); +} diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs new file mode 100644 index 0000000..1760129 --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs @@ -0,0 +1,15 @@ +using Deal.Contracts.Integrations.Abstractions; + +namespace Deal.Infrastructure.Integrations.Storage.Abstractions; + +/// +/// Фабрика файлового хранилища вложений (Local или MinIO по конфигурации) +/// +public interface IFileStorageFactory +{ + /// + /// Создаёт файловое хранилище + /// + /// Готовый порт файлового хранилища. + public IFileStorage Create(); +} diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs new file mode 100644 index 0000000..363fc4b --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs @@ -0,0 +1,7 @@ +namespace Deal.Infrastructure.Integrations.Storage.Options; + +/// +/// Абсолютный путь корня локального файлового хранилища +/// +/// Абсолютный путь каталога вложений. +public sealed record LocalStorageRoot(string Path); diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs new file mode 100644 index 0000000..42f73cd --- /dev/null +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs @@ -0,0 +1,42 @@ +using Deal.Contracts.Integrations.Abstractions; +using Deal.Infrastructure.Integrations.Storage.Abstractions; +using Deal.Infrastructure.Integrations.Storage.Extensions; +using Deal.Infrastructure.Integrations.Storage.Options; +using Microsoft.Extensions.Logging; + +namespace Deal.Infrastructure.Integrations.Storage.Services; + +/// +/// Фабрика файлового хранилища: MinIO при заполненной секции, иначе локальный каталог +/// +public sealed class FileStorageFactory : IFileStorageFactory +{ + private readonly StorageOptions _options; + private readonly LocalStorageRoot _localRoot; + private readonly ILogger _minioLogger; + + /// + /// Создаёт фабрику файлового хранилища + /// + /// Настройки секции Storage. + /// Абсолютный путь корня локального режима. + /// Логгер MinIO-адаптера. + public FileStorageFactory( + StorageOptions options, + LocalStorageRoot localRoot, + ILogger minioLogger) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(localRoot); + ArgumentNullException.ThrowIfNull(minioLogger); + _options = options; + _localRoot = localRoot; + _minioLogger = minioLogger; + } + + /// + public IFileStorage Create() => + _options.Minio.IsConfigured() + ? new MinioFileStorage(_options.Minio, _minioLogger) + : new LocalFileStorage(_localRoot.Path); +} diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs index 007b84c..b993afb 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs @@ -1,4 +1,5 @@ using Deal.Contracts.Integrations.Abstractions; +using Deal.Infrastructure.Integrations.Storage.Abstractions; using Deal.Infrastructure.Integrations.Storage.Extensions; using Deal.Infrastructure.Integrations.Storage.Options; using Microsoft.Extensions.Configuration; @@ -48,16 +49,12 @@ public static class FileStorageRegistrar ArgumentNullException.ThrowIfNull(configuration); StorageOptions options = ReadOptions(configuration); - - if (options.Minio.IsConfigured()) - { - services.AddSingleton(serviceProvider => - new MinioFileStorage(options.Minio, serviceProvider.GetRequiredService>())); - return services; - } - string rootPath = ResolveLocalRoot(options.Local, contentRootPath); - services.AddSingleton(new LocalFileStorage(rootPath)); + services.AddSingleton(options); + services.AddSingleton(new LocalStorageRoot(rootPath)); + services.AddSingleton(); + services.AddSingleton(serviceProvider => + serviceProvider.GetRequiredService().Create()); return services; } diff --git a/src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs new file mode 100644 index 0000000..f8fadbd --- /dev/null +++ b/src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs @@ -0,0 +1,15 @@ +using Deal.Modules.Tenants.Application.Abstractions; + +namespace Deal.Infrastructure.Persistence.Abstractions; + +/// +/// Фабрика хранилища лимитов ИИ-бюджета +/// +public interface ITenantLimitStoreFactory +{ + /// + /// Создаёт хранилище лимитов + /// + /// Готовый порт лимитов тенанта. + public ITenantLimitStore Create(); +} diff --git a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs new file mode 100644 index 0000000..f6a9cd3 --- /dev/null +++ b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs @@ -0,0 +1,31 @@ +using Deal.Infrastructure.Persistence.Abstractions; +using Deal.Infrastructure.Persistence.Repositories; +using Deal.Modules.Tenants.Application.Abstractions; +using Deal.Modules.Tenants.Application.Models; + +namespace Deal.Infrastructure.Persistence.Services; + +/// +/// Фабрика EF-адаптера хранилища лимитов с дефолт-бюджетом из конфигурации +/// +public sealed class TenantLimitStoreFactory : ITenantLimitStoreFactory +{ + private readonly DealDbContext _dbContext; + private readonly TokenLimitDefaults _defaults; + + /// + /// Создаёт фабрику хранилища лимитов + /// + /// Системный контекст (public-схема). + /// Дефолт-параметры лениво создаваемой строки. + public TenantLimitStoreFactory(DealDbContext dbContext, TokenLimitDefaults defaults) + { + ArgumentNullException.ThrowIfNull(dbContext); + ArgumentNullException.ThrowIfNull(defaults); + _dbContext = dbContext; + _defaults = defaults; + } + + /// + public ITenantLimitStore Create() => new TenantLimitStore(_dbContext, _defaults); +} diff --git a/src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs new file mode 100644 index 0000000..e79b491 --- /dev/null +++ b/src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs @@ -0,0 +1,15 @@ +using Deal.Modules.Settings.Application.Abstractions; + +namespace Deal.Infrastructure.Security.Abstractions; + +/// +/// Фабрика шифра секретов тенанта +/// +public interface ISecretCipherFactory +{ + /// + /// Создаёт шифр секретов + /// + /// Готовый порт симметричного шифрования. + public ISecretCipher Create(); +} diff --git a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs new file mode 100644 index 0000000..b6aeaad --- /dev/null +++ b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs @@ -0,0 +1,25 @@ +using Deal.Infrastructure.Security.Abstractions; +using Deal.Modules.Settings.Application.Abstractions; + +namespace Deal.Infrastructure.Security.Services; + +/// +/// Фабрика AES-256-GCM-шифра по ключу приложения +/// +public sealed class SecretCipherFactory : ISecretCipherFactory +{ + private readonly EncryptionKeyProvider _keyProvider; + + /// + /// Создаёт фабрику шифра + /// + /// Источник ключа шифрования приложения. + public SecretCipherFactory(EncryptionKeyProvider keyProvider) + { + ArgumentNullException.ThrowIfNull(keyProvider); + _keyProvider = keyProvider; + } + + /// + public ISecretCipher Create() => new AesGcmSecretCipher(_keyProvider.GetKey()); +} diff --git a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs index 84c5734..7d466c2 100644 --- a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs +++ b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs @@ -5,8 +5,12 @@ using Deal.Infrastructure.Integrations.Options; using Deal.Infrastructure.Integrations.Services; using Deal.Infrastructure.Integrations.Sources; using Deal.Infrastructure.Persistence; +using Deal.Infrastructure.Persistence.Abstractions; using Deal.Infrastructure.Persistence.Repositories; +using Deal.Infrastructure.Persistence.Services; using Deal.Infrastructure.Security; +using Deal.Infrastructure.Security.Abstractions; +using Deal.Infrastructure.Security.Services; using Deal.Infrastructure.Services; using Deal.Infrastructure.Tenancy; using Deal.Modules.Cards.Application.Abstractions; @@ -20,7 +24,6 @@ using Deal.Modules.Tenants.Application.Abstractions; using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel.Tenants.Abstractions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Deal.Infrastructure; @@ -46,9 +49,9 @@ public static class ServiceCollectionExtensions services.AddScoped(); - services.AddScoped(provider => new TenantLimitStore( - provider.GetRequiredService(), - tenantLimitDefaults ?? TokenBudgetDefaults.Default)); + services.AddSingleton(tenantLimitDefaults ?? TokenBudgetDefaults.Default); + services.AddScoped(); + services.AddScoped(provider => provider.GetRequiredService().Create()); services.AddScoped(); @@ -117,18 +120,11 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(provider => new BudgetedAiClassifier( - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService>())); + services.AddScoped(); + services.AddScoped(provider => provider.GetRequiredService().Create()); services.AddScoped(); - services.AddScoped(provider => new BudgetedAiTools( - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService>())); + services.AddScoped(); + services.AddScoped(provider => provider.GetRequiredService().Create()); } if (telegramOptions.UseLocal) @@ -152,8 +148,9 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddDealSecurity(this IServiceCollection services, string contentRootPath) { EncryptionKeyProvider keyProvider = new(contentRootPath); - byte[] key = keyProvider.GetKey(); - services.AddSingleton(new AesGcmSecretCipher(key)); + services.AddSingleton(keyProvider); + services.AddSingleton(); + services.AddSingleton(provider => provider.GetRequiredService().Create()); return services; } } diff --git a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs index d87810f..848908b 100644 --- a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs +++ b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs @@ -21,8 +21,7 @@ public static class DiscoveryModuleRegistrar services.AddScoped(); services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(sp => new DiscoveryBanGuard( diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs index a5e66ea..13a9291 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs @@ -31,7 +31,7 @@ public sealed partial class DiscoveryWorkerService /// Квоты дня/flood/стоп-кран (DiscoveryBanGuard). /// Паузы между авто-вступлениями (интерфейс — фейк в тестах). /// Гейт telegram-service (Search/Info/ReadForEval/Join/SetMonitor/Backfill). - /// Singleton-счётчик ошибок ключей поиска; null — локальный (на инстанс воркера). + /// Singleton-счётчик ошибок ключей поиска. public DiscoveryWorkerService( IDiscoveryStore store, DiscoveryTasksService tasks, @@ -41,7 +41,7 @@ public sealed partial class DiscoveryWorkerService DiscoveryBanGuard banGuard, IDiscoveryPacer pacer, ITelegramGateway gateway, - IDiscoverySearchErrorCounter? searchErrors = null) + IDiscoverySearchErrorCounter searchErrors) { ArgumentNullException.ThrowIfNull(store); ArgumentNullException.ThrowIfNull(tasks); @@ -51,6 +51,7 @@ public sealed partial class DiscoveryWorkerService ArgumentNullException.ThrowIfNull(banGuard); ArgumentNullException.ThrowIfNull(pacer); ArgumentNullException.ThrowIfNull(gateway); + ArgumentNullException.ThrowIfNull(searchErrors); _store = store; _tasks = tasks; _candidates = candidates; @@ -59,7 +60,7 @@ public sealed partial class DiscoveryWorkerService _banGuard = banGuard; _pacer = pacer; _gateway = gateway; - _searchErrors = searchErrors ?? new DiscoverySearchErrorCounter(); + _searchErrors = searchErrors; } /// diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs index fea2453..ec6eb0a 100644 --- a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs @@ -443,7 +443,8 @@ public sealed class DiscoveryWorkerServiceTests var pacer = Substitute.For(); var gateway = new TestDiscoveryGateway(); var worker = new DiscoveryWorkerService( - store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings.Store, ml.Client, ai), banGuard, pacer, gateway.Gateway); + store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings.Store, ml.Client, ai), banGuard, pacer, gateway.Gateway, + new DiscoverySearchErrorCounter()); return new Fixture(store, settings, gateway, pacer, worker, tasks); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs b/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs new file mode 100644 index 0000000..094310b --- /dev/null +++ b/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs @@ -0,0 +1,49 @@ +using Deal.Contracts.Integrations.Abstractions; +using Deal.Infrastructure.Integrations.Storage.Options; +using Deal.Infrastructure.Integrations.Storage.Services; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Deal.Tests.Unit.Support; + +/// +/// Тесты FileStorageFactory — выбор Local/MinIO по заполненности секции Storage:Minio. +/// +public sealed class FileStorageFactoryTests +{ + // Локальный режим: относительно пустая секция Minio → LocalFileStorage. + [Fact] + public void Create_MinioNotConfigured_ReturnsLocalFileStorage() + { + var factory = new FileStorageFactory( + new StorageOptions(), + new LocalStorageRoot("/tmp/deal-attachments"), + NullLogger.Instance); + + IFileStorage storage = factory.Create(); + + Assert.IsType(storage); + } + + // MinIO-режим: заполнены Endpoint/AccessKey/SecretKey → MinioFileStorage. + [Fact] + public void Create_MinioConfigured_ReturnsMinioFileStorage() + { + var options = new StorageOptions + { + Minio = new MinioStorageOptions + { + Endpoint = "localhost:9000", + AccessKey = "deal", + SecretKey = "deal-secret", + }, + }; + var factory = new FileStorageFactory( + options, + new LocalStorageRoot("/tmp/deal-attachments"), + NullLogger.Instance); + + IFileStorage storage = factory.Create(); + + Assert.IsType(storage); + } +} -- 2.39.5 From f788b6d39b7a4037809b78f1f879cd3e3105b2b3 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 03:41:36 +0300 Subject: [PATCH 2/8] =?UTF-8?q?=D0=97=D0=B0=D1=84=D0=B8=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D0=BE=20=D1=84=D0=B0=D0=B1=D1=80=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=B8=20=D0=B1=D0=B8=D0=BB=D0=B4=D0=B5=D1=80=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D0=BA=D0=BE=D0=B4-=D1=81=D1=82=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spec/Код-стайл-Дейл.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/spec/Код-стайл-Дейл.md b/docs/spec/Код-стайл-Дейл.md index 620a97f..b49d1b6 100644 --- a/docs/spec/Код-стайл-Дейл.md +++ b/docs/spec/Код-стайл-Дейл.md @@ -103,6 +103,25 @@ - Асинхронность: суффикс `Async`, `CancellationToken` пробрасывать до конца; `.Result` / `.Wait()` запрещены — только `await`. +### 4.1. Фабрики и билдеры + +- **Нетривиальные объекты с интерфейсом создаются только фабриками.** Реализация сервиса/адаптера, + у которого есть порт-интерфейс, не создаётся прямым `new` в прикладном коде или композиционном корне — + только внутри фабрики. Форма пары: `IXxxFactory` (порт фабрики) + `XxxFactory` (реализация), метод + `Create(...)` возвращает **интерфейс** готового объекта (`ISecretCipher`, `IFileStorage`, …). +- Фабрика сама регистрируется в DI (`AddScoped`/`AddSingleton()`) — контейнер + конструирует её без `new`; зависимости фабрики — тоже DI. +- **Билдер** (`IXxxBuilder`/`XxxBuilder`) добавляется, когда объект собирается итеративно из многих частей + или опций; фабрика делегирует сборку билдеру, а не повторяет её. +- Исключения из правила (прямой `new` допустим): + - DTO, рекорды, value-объекты, `Options`/`Settings`-снимки; + - исключения (`*Exception`) и примитивы/BCL-типы (`StringBuilder`, `NpgsqlConnection`, `MinioClient`, …); + - статические классы и хэлперы без состояния (фабрику для них не заводим); + - EF-конфигурации (`IEntityTypeConfiguration`) — это метаданные модели, а не прикладные объекты; + - обёртки ресурсов без порт-интерфейса (gRPC-соединения с `IDisposable`); + - объекты без порт-интерфейса, создаваемые контейнером (`AddScoped()`). +- Тесты могут конструировать проверяемый тип прямым `new` — это часть самого теста, а не прикладного кода. + ## 5. Комментирование кода Все комментарии — на русском языке. -- 2.39.5 From 3bcf4074b565305a4df42407e20891e5c917cb3a Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 03:54:36 +0300 Subject: [PATCH 3/8] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20ingress-?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B1=D0=B5=D0=B7=20=D0=B3?= =?UTF-8?q?=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TelegramIngressTestHost больше не мутирует процессную переменную DEAL_SERVICE_TOKEN: токен задаётся in-memory конфигурацией хоста. Три ingress-тест-класса шли параллельно и перетирали env друг другу. --- .../Support/TelegramIngressTestHost.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs index 6548585..84075ae 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/TelegramIngressTestHost.cs @@ -18,6 +18,7 @@ using Grpc.Net.Client; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -57,15 +58,17 @@ internal static class TelegramIngressTestHost Func scenario, RateLimitOptions? rateLimitOptions = null) { - string? originalToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey); - Environment.SetEnvironmentVariable(ServiceTokenEnvKey, serviceToken); - WebApplication? app = null; GrpcChannel? channel = null; try { int port = TestPort.Allocate(); WebApplicationBuilder builder = WebApplication.CreateBuilder(); + // Токен ингресса задаётся конфигурацией хоста (в приоритете над env) — без мутации процесса. + builder.Configuration.AddInMemoryCollection(new Dictionary + { + [ServiceTokenEnvKey] = serviceToken, + }); builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port, listen => listen.Protocols = HttpProtocols.Http2)); @@ -131,8 +134,6 @@ internal static class TelegramIngressTestHost await app.StopAsync(); await app.DisposeAsync(); } - - Environment.SetEnvironmentVariable(ServiceTokenEnvKey, originalToken); } } -- 2.39.5 From c7789c0b2198e6ebb16f943126aca0e02b5ed1b6 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 03:59:30 +0300 Subject: [PATCH 4/8] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=84=D0=B0=D0=B1=D1=80=D0=B8=D0=BA=20=D1=8F=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По код-стайлу §11 метод Create реализуется явно (ISecretCipher ISecretCipherFactory.Create()), интерфейс вызывается только по порту; тест фабрики типизирован интерфейсом. --- .../Integrations/Services/AiClassifierFactory.cs | 2 +- .../Integrations/Services/AiToolsFactory.cs | 2 +- .../Integrations/Storage/Services/FileStorageFactory.cs | 2 +- .../Persistence/Services/TenantLimitStoreFactory.cs | 2 +- .../Security/Services/SecretCipherFactory.cs | 2 +- .../tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs | 5 +++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs index fc0f698..6f8dce4 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs @@ -45,6 +45,6 @@ public sealed class AiClassifierFactory : IAiClassifierFactory } /// - public IAiClassifier Create() => + IAiClassifier IAiClassifierFactory.Create() => new BudgetedAiClassifier(_paidClassifier, _localClassifier, _tenantLimits, _tenantContext, _logger); } diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs index 3815c42..538c5e9 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs @@ -40,5 +40,5 @@ public sealed class AiToolsFactory : IAiToolsFactory } /// - public IAiTools Create() => new BudgetedAiTools(_paidTools, _tenantLimits, _tenantContext, _logger); + IAiTools IAiToolsFactory.Create() => new BudgetedAiTools(_paidTools, _tenantLimits, _tenantContext, _logger); } diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs index 42f73cd..14d4617 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs @@ -35,7 +35,7 @@ public sealed class FileStorageFactory : IFileStorageFactory } /// - public IFileStorage Create() => + IFileStorage IFileStorageFactory.Create() => _options.Minio.IsConfigured() ? new MinioFileStorage(_options.Minio, _minioLogger) : new LocalFileStorage(_localRoot.Path); diff --git a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs index f6a9cd3..f8f2e79 100644 --- a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs +++ b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs @@ -27,5 +27,5 @@ public sealed class TenantLimitStoreFactory : ITenantLimitStoreFactory } /// - public ITenantLimitStore Create() => new TenantLimitStore(_dbContext, _defaults); + ITenantLimitStore ITenantLimitStoreFactory.Create() => new TenantLimitStore(_dbContext, _defaults); } diff --git a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs index b6aeaad..8e92292 100644 --- a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs +++ b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs @@ -21,5 +21,5 @@ public sealed class SecretCipherFactory : ISecretCipherFactory } /// - public ISecretCipher Create() => new AesGcmSecretCipher(_keyProvider.GetKey()); + ISecretCipher ISecretCipherFactory.Create() => new AesGcmSecretCipher(_keyProvider.GetKey()); } diff --git a/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs b/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs index 094310b..3f7068a 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs @@ -1,4 +1,5 @@ using Deal.Contracts.Integrations.Abstractions; +using Deal.Infrastructure.Integrations.Storage.Abstractions; using Deal.Infrastructure.Integrations.Storage.Options; using Deal.Infrastructure.Integrations.Storage.Services; using Microsoft.Extensions.Logging.Abstractions; @@ -14,7 +15,7 @@ public sealed class FileStorageFactoryTests [Fact] public void Create_MinioNotConfigured_ReturnsLocalFileStorage() { - var factory = new FileStorageFactory( + IFileStorageFactory factory = new FileStorageFactory( new StorageOptions(), new LocalStorageRoot("/tmp/deal-attachments"), NullLogger.Instance); @@ -37,7 +38,7 @@ public sealed class FileStorageFactoryTests SecretKey = "deal-secret", }, }; - var factory = new FileStorageFactory( + IFileStorageFactory factory = new FileStorageFactory( options, new LocalStorageRoot("/tmp/deal-attachments"), NullLogger.Instance); -- 2.39.5 From 418f6efe390884da5de9d6fd3897092eeefadb59 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 04:10:21 +0300 Subject: [PATCH 5/8] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=81=D0=B0=D0=BC=D0=B0=D1=80=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=81?= =?UTF-8?q?=D1=82=D1=80=D1=83=D0=BA=D1=82=D0=BE=D1=80=D0=BE=D0=B2=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B1=D1=80=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Integrations/Services/AiClassifierFactory.cs | 8 -------- .../Integrations/Services/AiToolsFactory.cs | 7 ------- .../Integrations/Storage/Services/FileStorageFactory.cs | 6 ------ .../Persistence/Services/TenantLimitStoreFactory.cs | 5 ----- .../Security/Services/SecretCipherFactory.cs | 4 ---- 5 files changed, 30 deletions(-) diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs index 6f8dce4..ab351e9 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs @@ -17,14 +17,6 @@ public sealed class AiClassifierFactory : IAiClassifierFactory private readonly ITenantContext _tenantContext; private readonly ILogger _logger; - /// - /// Создаёт фабрику классификатора - /// - /// Платный исполнитель (gRPC-адаптер ai-service). - /// Бесплатный локальный разбор/фильтр (fallback). - /// Хранилище лимитов бюджета. - /// Контекст текущего тенанта. - /// Логгер переходов на локальный путь. public AiClassifierFactory( GrpcAiClassifier paidClassifier, LocalAiClassifier localClassifier, diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs index 538c5e9..2e922f6 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs @@ -16,13 +16,6 @@ public sealed class AiToolsFactory : IAiToolsFactory private readonly ITenantContext _tenantContext; private readonly ILogger _logger; - /// - /// Создаёт фабрику ИИ-инструментов - /// - /// Платный исполнитель (gRPC-адаптер ai-service). - /// Хранилище лимитов бюджета. - /// Контекст текущего тенанта. - /// Логгер переходов на локальный путь. public AiToolsFactory( GrpcAiTools paidTools, ITenantLimitStore tenantLimits, diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs index 14d4617..9fd60e7 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs @@ -15,12 +15,6 @@ public sealed class FileStorageFactory : IFileStorageFactory private readonly LocalStorageRoot _localRoot; private readonly ILogger _minioLogger; - /// - /// Создаёт фабрику файлового хранилища - /// - /// Настройки секции Storage. - /// Абсолютный путь корня локального режима. - /// Логгер MinIO-адаптера. public FileStorageFactory( StorageOptions options, LocalStorageRoot localRoot, diff --git a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs index f8f2e79..2ef77c3 100644 --- a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs +++ b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs @@ -13,11 +13,6 @@ public sealed class TenantLimitStoreFactory : ITenantLimitStoreFactory private readonly DealDbContext _dbContext; private readonly TokenLimitDefaults _defaults; - /// - /// Создаёт фабрику хранилища лимитов - /// - /// Системный контекст (public-схема). - /// Дефолт-параметры лениво создаваемой строки. public TenantLimitStoreFactory(DealDbContext dbContext, TokenLimitDefaults defaults) { ArgumentNullException.ThrowIfNull(dbContext); diff --git a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs index 8e92292..1eb5efb 100644 --- a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs +++ b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs @@ -10,10 +10,6 @@ public sealed class SecretCipherFactory : ISecretCipherFactory { private readonly EncryptionKeyProvider _keyProvider; - /// - /// Создаёт фабрику шифра - /// - /// Источник ключа шифрования приложения. public SecretCipherFactory(EncryptionKeyProvider keyProvider) { ArgumentNullException.ThrowIfNull(keyProvider); -- 2.39.5 From 58df018ea43484f78f0d0b601ff825a59066f280 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 04:10:21 +0300 Subject: [PATCH 6/8] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=80=D0=B5=D1=82=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D0=B0=D0=BC=D0=B0=D1=80=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B2=20=D0=BA=D0=BE=D0=B4-=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spec/Код-стайл-Дейл.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/spec/Код-стайл-Дейл.md b/docs/spec/Код-стайл-Дейл.md index b49d1b6..fc1916c 100644 --- a/docs/spec/Код-стайл-Дейл.md +++ b/docs/spec/Код-стайл-Дейл.md @@ -142,6 +142,8 @@ - **``/``** — только если смысл не очевиден из имени/типа; не переписывать сигнатуру. - **`` — только блочный.** Открывающий `` и закрывающий `` — **каждый на своей строке**; запись в одну строку (`/// текст`) **не допускается**. **[изм.]** +- **Конструкторы не документируем** — ``/`` на них не нужны: назначение очевидно из + типа и сигнатуры. В частности, не документируем конструкторы классов, реализующих интерфейс. **[изм.]** Правильно: ```csharp -- 2.39.5 From a9d26b6ffc95a3d6b8d204116fd840d27284791d Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 04:17:43 +0300 Subject: [PATCH 7/8] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20XML-?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D0=B8=20=D0=B8=D0=B7=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B9=20=D1=84=D0=B0=D0=B1?= =?UTF-8?q?=D1=80=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Integrations/Services/AiClassifierFactory.cs | 4 ---- .../Integrations/Services/AiToolsFactory.cs | 4 ---- .../Integrations/Storage/Services/FileStorageFactory.cs | 4 ---- .../Persistence/Services/TenantLimitStoreFactory.cs | 4 ---- .../Security/Services/SecretCipherFactory.cs | 4 ---- 5 files changed, 20 deletions(-) diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs index ab351e9..0f952a8 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs @@ -6,9 +6,6 @@ using Microsoft.Extensions.Logging; namespace Deal.Infrastructure.Integrations.Services; -/// -/// Фабрика декоратора бюджетного гейта классификатора -/// public sealed class AiClassifierFactory : IAiClassifierFactory { private readonly GrpcAiClassifier _paidClassifier; @@ -36,7 +33,6 @@ public sealed class AiClassifierFactory : IAiClassifierFactory _logger = logger; } - /// IAiClassifier IAiClassifierFactory.Create() => new BudgetedAiClassifier(_paidClassifier, _localClassifier, _tenantLimits, _tenantContext, _logger); } diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs index 2e922f6..0676ace 100644 --- a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs @@ -6,9 +6,6 @@ using Microsoft.Extensions.Logging; namespace Deal.Infrastructure.Integrations.Services; -/// -/// Фабрика декоратора бюджетного гейта ИИ-инструментов -/// public sealed class AiToolsFactory : IAiToolsFactory { private readonly GrpcAiTools _paidTools; @@ -32,6 +29,5 @@ public sealed class AiToolsFactory : IAiToolsFactory _logger = logger; } - /// IAiTools IAiToolsFactory.Create() => new BudgetedAiTools(_paidTools, _tenantLimits, _tenantContext, _logger); } diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs index 9fd60e7..eb15ae4 100644 --- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs +++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs @@ -6,9 +6,6 @@ using Microsoft.Extensions.Logging; namespace Deal.Infrastructure.Integrations.Storage.Services; -/// -/// Фабрика файлового хранилища: MinIO при заполненной секции, иначе локальный каталог -/// public sealed class FileStorageFactory : IFileStorageFactory { private readonly StorageOptions _options; @@ -28,7 +25,6 @@ public sealed class FileStorageFactory : IFileStorageFactory _minioLogger = minioLogger; } - /// IFileStorage IFileStorageFactory.Create() => _options.Minio.IsConfigured() ? new MinioFileStorage(_options.Minio, _minioLogger) diff --git a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs index 2ef77c3..3764624 100644 --- a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs +++ b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs @@ -5,9 +5,6 @@ using Deal.Modules.Tenants.Application.Models; namespace Deal.Infrastructure.Persistence.Services; -/// -/// Фабрика EF-адаптера хранилища лимитов с дефолт-бюджетом из конфигурации -/// public sealed class TenantLimitStoreFactory : ITenantLimitStoreFactory { private readonly DealDbContext _dbContext; @@ -21,6 +18,5 @@ public sealed class TenantLimitStoreFactory : ITenantLimitStoreFactory _defaults = defaults; } - /// ITenantLimitStore ITenantLimitStoreFactory.Create() => new TenantLimitStore(_dbContext, _defaults); } diff --git a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs index 1eb5efb..eabc786 100644 --- a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs +++ b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs @@ -3,9 +3,6 @@ using Deal.Modules.Settings.Application.Abstractions; namespace Deal.Infrastructure.Security.Services; -/// -/// Фабрика AES-256-GCM-шифра по ключу приложения -/// public sealed class SecretCipherFactory : ISecretCipherFactory { private readonly EncryptionKeyProvider _keyProvider; @@ -16,6 +13,5 @@ public sealed class SecretCipherFactory : ISecretCipherFactory _keyProvider = keyProvider; } - /// ISecretCipher ISecretCipherFactory.Create() => new AesGcmSecretCipher(_keyProvider.GetKey()); } -- 2.39.5 From 94fdfeb4e5567363b97cfbcf8753421175d93e39 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 04:17:44 +0300 Subject: [PATCH 8/8] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D0=BE=D0=B2=20=E2=80=94=20=D0=B1=D0=B5=D0=B7=20XML?= =?UTF-8?q?-=D0=B4=D0=BE=D0=BA=D0=BE=D0=B2=20(=D0=BA=D0=BE=D0=B4-=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spec/Код-стайл-Дейл.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/spec/Код-стайл-Дейл.md b/docs/spec/Код-стайл-Дейл.md index fc1916c..5296b09 100644 --- a/docs/spec/Код-стайл-Дейл.md +++ b/docs/spec/Код-стайл-Дейл.md @@ -250,9 +250,12 @@ ## 11. Интерфейсы -- **Не дублировать `` интерфейса в реализации.** Если член объявлен в интерфейсе с XML-doc, - в классе-реализации достаточно `/// ` (или вообще ничего, если doc наследуется настройкой). - Текст описания пишется **один раз** — у интерфейса. +- **В реализациях интерфейсов XML-doc не пишем вообще.** Если тип или член объявлен в интерфейсе, + класс-реализация не документируется: ни ``, ни `` (и ни `` на + конструкторе). Описание живёт **один раз** — в интерфейсе; реализации вызываются только через порт. + Под этот запрет попадает и сам класс-реализация (его `` тоже лишний — есть у интерфейса). +- **XML-doc уместен только там, где нет интерфейса:** public-типы/члены без порта (статика, константы, + extension-классы), `protected`-члены и DTO/модели. **[изм. 2026-09-13, решение владельца]** - **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11, решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели (напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные -- 2.39.5