Codemod scripts/make_explicit.py: 161 член в 30 прод-файлах конвертирован в вид "Тип IFoo.Член" (частичные классы и многострочные сигнатуры учтены; Card и ICard-семейство — DTO, оставлены implicit). Потребители, дёргавшие классы напрямую, перетипизированы на интерфейсы: 8 мест в проде (самовызовы через ((ISessionClient)this), снят дефолт параметра в явной реализации) и 17 тестовых файлов (поля, tuple-деконструкции, var/target-typed new). Build 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
107 lines
5.0 KiB
C#
107 lines
5.0 KiB
C#
using Deal.Contracts.Integrations.Abstractions;
|
|
using Deal.Contracts.Integrations.Models;
|
|
using Deal.Infrastructure.Integrations.Exceptions;
|
|
using Deal.Modules.Tenants.Application.Abstractions;
|
|
using Deal.Modules.Tenants.Application.Models;
|
|
using Deal.SharedKernel.Tenants.Abstractions;
|
|
using Deal.SharedKernel.Tenants.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Deal.Infrastructure.Integrations.Services;
|
|
|
|
/// <summary>
|
|
/// Декоратор бюджетного гейта порта <see cref="IAiClassifier"/>
|
|
/// </summary>
|
|
public sealed class BudgetedAiClassifier : IAiClassifier
|
|
{
|
|
// Текст ошибки вызова вне tenant-контекста (гейт читает лимиты по тенанту).
|
|
private const string NoTenantContextText =
|
|
"BudgetedAiClassifier запрошен вне tenant-контекста (ITenantContext.TenantId == null/не-Guid).";
|
|
|
|
// Причина запрета в логе (общий текст для исчерпания и приостановки).
|
|
private const string GateDeniedLogText = "бюджет исчерпан или тенант приостановлен";
|
|
|
|
private readonly IAiClassifier _paidClassifier;
|
|
private readonly LocalAiClassifier _localClassifier;
|
|
private readonly ITenantLimitStore _tenantLimits;
|
|
private readonly ITenantContext _tenantContext;
|
|
private readonly ILogger<BudgetedAiClassifier> _logger;
|
|
|
|
/// <summary>
|
|
/// Создаёт декоратор бюджетного гейта классификатора.
|
|
/// </summary>
|
|
/// <param name="paidClassifier">Платный исполнитель (gRPC-адаптер ai-service; вызывается только при Allowed).</param>
|
|
/// <param name="localClassifier">Бесплатный локальный разбор/фильтр (fallback при запрете гейта).</param>
|
|
/// <param name="tenantLimits">Хранилище лимитов бюджета (public.tenant_limits; источник гейта).</param>
|
|
/// <param name="tenantContext">Контекст текущего тенанта (AsyncLocal; tenantId запроса).</param>
|
|
/// <param name="logger">Логгер переходов на локальный путь.</param>
|
|
public BudgetedAiClassifier(
|
|
IAiClassifier paidClassifier,
|
|
LocalAiClassifier localClassifier,
|
|
ITenantLimitStore tenantLimits,
|
|
ITenantContext tenantContext,
|
|
ILogger<BudgetedAiClassifier> 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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
async Task<AiFilterResultDto> IAiClassifier.FilterAsync(string text, CancellationToken ct)
|
|
{
|
|
if (await IsPaidAllowedAsync(ct))
|
|
{
|
|
return await _paidClassifier.FilterAsync(text, ct);
|
|
}
|
|
|
|
_logger.LogDebug(
|
|
"ИИ-фильтр: {Reason} — Local-пропуск (тенант {TenantId})", GateDeniedLogText, TenantIdForLog());
|
|
return await _localClassifier.FilterAsync(text, ct);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
async Task<AiParsedCardDto> IAiClassifier.ClassifyAsync(string text, CancellationToken ct)
|
|
{
|
|
if (await IsPaidAllowedAsync(ct))
|
|
{
|
|
return await _paidClassifier.ClassifyAsync(text, ct);
|
|
}
|
|
|
|
_logger.LogDebug(
|
|
"ИИ-классификация: {Reason} — Local-разбор (тенант {TenantId})", GateDeniedLogText, TenantIdForLog());
|
|
return await _localClassifier.ClassifyAsync(text, ct);
|
|
}
|
|
|
|
private async Task<bool> IsPaidAllowedAsync(CancellationToken ct)
|
|
{
|
|
BudgetStateDto state = await _tenantLimits.GetStateAsync(RequireTenantId(), ct);
|
|
return state.Allowed;
|
|
}
|
|
|
|
// Текущий тенант scope как Guid строки public.tenants (гейт читает лимиты по тенанту).
|
|
// Возвращает: Идентификатор тенанта (Guid).
|
|
// Исключение InvalidOperationException: Вызов вне tenant-контекста или не-Guid формат id.
|
|
private Guid RequireTenantId()
|
|
{
|
|
TenantId? tenantId = _tenantContext.TenantId;
|
|
if (tenantId is null || !Guid.TryParse(tenantId.Value.Value, out Guid id))
|
|
{
|
|
throw new InvalidOperationException(NoTenantContextText);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
// Id тенанта для лога («-» вне контекста — недостижимо после RequireTenantId).
|
|
// Возвращает: Строка id тенанта.
|
|
private string TenantIdForLog() => _tenantContext.TenantId?.Value ?? "-";
|
|
}
|