using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
namespace Deal.Modules.Tenants.Application.Services;
///
/// Прикладной сервис операторской аналитики
///
public sealed class AnalyticsService(
ITenantRepository tenants,
AuditService audit,
TokenUsageEventService tokenUsage)
{
///
/// Размер страницы ленты действий по умолчанию
///
public const int DefaultActivityLimit = AuditService.DefaultQueryLimit;
///
/// Верхняя граница размера страницы ленты действий
///
public const int MaxActivityLimit = AuditService.MaxQueryLimit;
///
/// Сводка: тенанты
///
/// Начало периода (включительно; null — без границы).
/// Конец периода (включительно; null — без границы).
/// Сводка аналитики.
public async Task OverviewAsync(
DateTimeOffset? from,
DateTimeOffset? to,
CancellationToken ct)
{
IReadOnlyList allTenants = await tenants.ListAsync(ct);
int active = allTenants.Count(tenant => tenant.Status == TenantStatuses.Active);
// Токены за период — суммируем по тенантам (агрегат по тенантам: ключ не важен, берём суммы).
IReadOnlyList perTenant = await tokenUsage.AggregateAsync(
new TokenUsageEventQueryDto(null, null, null, null, from, to, TokenUsageGroupBys.Tenant), ct);
long promptTokens = perTenant.Sum(row => row.PromptTokens);
long completionTokens = perTenant.Sum(row => row.CompletionTokens);
long totalTokens = perTenant.Sum(row => row.TotalTokens);
long tokenEvents = perTenant.Sum(row => row.EventCount);
int events = await audit.CountAsync(
new AuditQueryDto(null, null, null, from, to, AuditService.DefaultQueryLimit), ct);
int logins = await CountEventTypesAsync(
from, to, [AuditEvents.TenantLoginOk, AuditEvents.OperatorLoginOk], ct);
int logouts = await CountEventTypesAsync(
from, to, [AuditEvents.TenantLogout, AuditEvents.OperatorLogout], ct);
int failedLogins = await CountEventTypesAsync(
from, to, [AuditEvents.TenantLoginFailed, AuditEvents.OperatorLoginFailed], ct);
return new AnalyticsOverviewDto(
TenantsTotal: allTenants.Count,
TenantsActive: active,
PromptTokens: promptTokens,
CompletionTokens: completionTokens,
TotalTokens: totalTokens,
TokenEvents: tokenEvents,
Events: events,
Logins: logins,
Logouts: logouts,
FailedLogins: failedLogins,
From: from,
To: to);
}
///
/// Агрегаты расхода токенов по группировке и фильтрам
///
/// Группировка day|tenant|provider|model (валидирует HTTP-слой).
/// Тенант (равенство; null — все тенанты).
/// Начало периода (включительно; null — без границы).
/// Конец периода (включительно; null — без границы).
/// Строки агрегатов и итог.
public async Task TokensAsync(
string groupBy,
Guid? tenantId,
DateTimeOffset? from,
DateTimeOffset? to,
CancellationToken ct)
{
IReadOnlyList items = await tokenUsage.AggregateAsync(
new TokenUsageEventQueryDto(tenantId, null, null, null, from, to, groupBy), ct);
var total = new TokenUsageAggregateDto(
Key: "total",
PromptTokens: items.Sum(row => row.PromptTokens),
CompletionTokens: items.Sum(row => row.CompletionTokens),
TotalTokens: items.Sum(row => row.TotalTokens),
EventCount: items.Sum(row => row.EventCount));
return new AnalyticsTokensDto(groupBy, from, to, items, total);
}
///
/// Лента действий
///
/// Тип события (равенство; null — без фильтра).
/// Тип актора operator|tenant|system (равенство; null — без фильтра).
/// Идентификатор актора (равенство; null — без фильтра).
/// Тенант (равенство; null — без фильтра).
/// Нижняя граница At (включительно; null — без границы).
/// Верхняя граница At (включительно; null — без границы).
/// Размер страницы (дефолт 100, кламп 1..500).
/// Смещение страницы (≥0).
/// Страница записей и полное число по фильтру.
public async Task ActivityAsync(
string? eventType,
string? actorType,
Guid? actorId,
Guid? tenantId,
DateTimeOffset? from,
DateTimeOffset? to,
int? limit,
int? offset,
CancellationToken ct)
{
int normalizedLimit = NormalizeActivityLimit(limit);
int normalizedOffset = Math.Max(0, offset ?? 0);
var filter = new AuditQueryDto(
eventType, actorType, tenantId, from, to, normalizedLimit, actorId, normalizedOffset);
IReadOnlyList items = await audit.QueryAsync(filter, ct);
int total = await audit.CountAsync(filter, ct);
return new AnalyticsActivityDto(items, total, normalizedLimit, normalizedOffset);
}
///
/// Нормализует limit ленты действий
///
/// Запрошенный размер (null — не задан).
/// Значение для фильтра.
public static int NormalizeActivityLimit(int? limit) =>
limit is null
? DefaultActivityLimit
: Math.Max(1, Math.Min(MaxActivityLimit, limit.Value));
// Суммарное число записей аудита по списку типов событий за период.
// from: Нижняя граница At (включительно; null — без границы).
// to: Верхняя граница At (включительно; null — без границы).
// eventTypes: Типы событий (каждый считается отдельным равенством).
// ct: Токен отмены.
// Возвращает: Сумма по типам событий.
private async Task CountEventTypesAsync(
DateTimeOffset? from,
DateTimeOffset? to,
string[] eventTypes,
CancellationToken ct)
{
int total = 0;
foreach (string eventType in eventTypes)
{
total += await audit.CountAsync(
new AuditQueryDto(eventType, null, null, from, to, AuditService.DefaultQueryLimit), ct);
}
return total;
}
}