Разбить Infrastructure и корень Deal.Api по назначению
Integrations -> Abstractions/Exceptions/Extensions/Models/Options/ Services (включая Storage); Persistence-конфигурации -> Configurations; корень Deal.Api (оркестратор/планировщики/DTO) -> Services/Dtos. namespace приведён к путям, using добавлены/дедуплицированы, FQN обновлены.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using Deal.Grpc.Telegram;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Deal.Infrastructure.Integrations.Abstractions;
|
||||
using Deal.Infrastructure.Integrations.Exceptions;
|
||||
using Deal.Infrastructure.Integrations.Extensions;
|
||||
using Deal.Infrastructure.Integrations.Options;
|
||||
using Deal.Infrastructure.Integrations.Services;
|
||||
|
||||
namespace Deal.Infrastructure.Integrations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Транспорт gRPC-клиента telegram-service: общий канал + обязательные metadata (Ruling 1, план Task 14).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Singleton (канал живёт долго и переиспользуется всеми вызовами): endpoint из
|
||||
/// <see cref="TelegramServiceOptions"/>, service-token — из env <c>DEAL_SERVICE_TOKEN</c> (Ruling 13: секреты
|
||||
/// только в env). Dev-транспорт без TLS (Ruling 2); mTLS (Ruling 6, Task 13): при включённом флаге канал
|
||||
/// подписывает запрос клиентским сертификатом и проверяет CA сервера (сертификаты передаются
|
||||
/// <see cref="MtlsCertificates"/>). Пустой endpoint либо пустой токен при создании — ошибка конфигурации (fail-closed:
|
||||
/// без токена сервис отвергнет каждый вызов UNAUTHENTICATED, Ruling 1). Автоповторы Grpc.Net.Client отключены
|
||||
/// (MaxRetryAttempts=0): стратегию повторов реализует вызывающий (фоновые циклы Api пробуют в следующем тике).
|
||||
/// </remarks>
|
||||
public sealed class TelegramGrpcConnection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Env-ключ ожидаемого service-token (зеркало ServiceTokenInterceptor сервисов, Ruling 1).
|
||||
/// </summary>
|
||||
public const string ServiceTokenEnvKey = "DEAL_SERVICE_TOKEN";
|
||||
|
||||
/// <summary>
|
||||
/// Ключ gRPC-metadata с tenant-id (зеркало TelegramServiceImpl, Ruling 1).
|
||||
/// </summary>
|
||||
public const string TenantIdMetadataKey = "tenant-id";
|
||||
|
||||
/// <summary>
|
||||
/// Ключ gRPC-metadata с service-token (зеркало TelegramServiceImpl, Ruling 1).
|
||||
/// </summary>
|
||||
public const string ServiceTokenMetadataKey = "service-token";
|
||||
|
||||
private readonly GrpcChannel _channel;
|
||||
private readonly string _serviceToken;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт транспорт telegram-service по конфигурации и env-токену (валидация fail-closed).
|
||||
/// </summary>
|
||||
/// <param name="options">Конфигурация секции <c>Services:Telegram</c> (endpoint).</param>
|
||||
/// <param name="mtlsCertificates">Сертификаты mTLS (Ruling 6): null — plaintext-канал (dev, флаг выключен).</param>
|
||||
/// <exception cref="InvalidOperationException">Пустой endpoint или пустой DEAL_SERVICE_TOKEN.</exception>
|
||||
public TelegramGrpcConnection(TelegramServiceOptions options, MtlsCertificates? mtlsCertificates = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
if (string.IsNullOrWhiteSpace(options.Endpoint))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"TelegramGrpcConnection: не задан endpoint telegram-service (секция \"{TelegramServiceOptions.SectionName}:Endpoint\").");
|
||||
}
|
||||
|
||||
_serviceToken = Environment.GetEnvironmentVariable(ServiceTokenEnvKey) ?? string.Empty;
|
||||
if (_serviceToken.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"TelegramGrpcConnection: не задан env {ServiceTokenEnvKey} — telegram-service отвергнет вызовы (fail-closed, Ruling 1).");
|
||||
}
|
||||
|
||||
GrpcChannelOptions channelOptions = new() { MaxRetryAttempts = 0 };
|
||||
if (mtlsCertificates is not null)
|
||||
{
|
||||
channelOptions.HttpHandler = mtlsCertificates.CreateClientHttpHandler();
|
||||
}
|
||||
|
||||
_channel = GrpcChannel.ForAddress(options.Endpoint, channelOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт клиент RPC TelegramService поверх общего канала (клиент — лёгкий, на каждый вызов).
|
||||
/// </summary>
|
||||
/// <returns>Клиент сервиса Telegram (команды ядра наружу).</returns>
|
||||
public TelegramService.TelegramServiceClient CreateClient() => new(_channel);
|
||||
|
||||
/// <summary>
|
||||
/// Собирает обязательные metadata вызова: tenant-id + service-token (Ruling 1).
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Id тенанта (строка, формат N — как в сессиях telegram-service).</param>
|
||||
/// <returns>Metadata для CallOptions вызова.</returns>
|
||||
public Metadata CreateMetadata(string tenantId)
|
||||
{
|
||||
var metadata = new Metadata
|
||||
{
|
||||
{ TenantIdMetadataKey, tenantId },
|
||||
{ ServiceTokenMetadataKey, _serviceToken },
|
||||
};
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => _channel.Dispose();
|
||||
}
|
||||
Reference in New Issue
Block a user