Reporting и Export — Практическое Руководство
Назначение
Документ описывает архитектуру, форматы, поля, фильтры и правила реализации отчётов и выгрузок для администраторов, партнёров и регуляторных задач. Охватывает: Excel (ClosedXML), CSV, JSON; роли и permissions; privacy правила; audit.
Форматы Выгрузки
| Формат | Использование | Библиотека |
|---|---|---|
.xlsx | Финансовые отчёты, аналитика, admin выгрузки | ClosedXML |
.csv | Большие dataframes, machine-readable выгрузки | CsvHelper |
.json | API-style regulatory export, machine integration | System.Text.Json |
Архитектура Export Pipeline
Выгрузка не генерируется синхронно в HTTP handler. Используется async job:
POST /api/admin/reports/exports
→ CreateReportExportCommand
→ Validate (format, filters, date range, row limit)
→ Authorize (permission, privileged session если PII)
→ Rate limit check
→ Create ReportExportJob record
→ Audit (actor, filters hash, format, report type, reason)
→ Idempotency record
→ Commit
→ Background Job получает ReportExportJobId
→ Fetch data (ClickHouse / PostgreSQL / Elasticsearch)
→ Apply privacy suppression
→ Generate file (ClosedXML / CsvHelper / JsonSerializer)
→ Upload to Object Storage (private, with manifest)
→ Update ReportExportJob status = Completed
→ Notify actor (optional)
GET /api/admin/reports/exports/{jobId}
→ Get job status и signed URL (short-lived)
Типы Отчётов
Отчёты Для Администратора
| Тип Отчёта | Описание | Форматы | Разрешение |
|---|---|---|---|
platform-financial-summary | Общий P&L: GGR, NGR, deposits, withdrawals по дням/неделям/месяцам | xlsx, csv | analytics.reports.financial |
user-transactions | Транзакции конкретного игрока: ставки, депозиты, выводы, бонусы | xlsx, csv, json | analytics.reports.export-user + MFA |
user-activity-summary | KPI игрока: sessions, bets, GGR, incentivees, risk score | xlsx, csv | analytics.reports.export-user |
withdrawal-queue | Список выводов в обработке с деталями | xlsx, csv | accounting.withdrawal.export |
deposit-reconciliation | Сверка депозитов с провайдером | xlsx, csv, json | accounting.reconciliation.export |
partner-commission-summary | Комиссии партнёров за период | xlsx, csv | analytics.reports.partner-commission |
kyc-verification-queue | Список KYC на верификации | xlsx, csv | kyc.reports.export |
risk-cases | Fraud/AML cases за период | xlsx, csv | anti-fraud.reports.export |
incentive-usage | Выданные и отыгранные бонусы | xlsx, csv | incentive.reports.export |
user-safety-events | Лимиты, самоисключения, cooling-off | xlsx, csv | user-safety.reports.export + MFA |
regulatory-aml-sar | SAR-ready отчёт для регулятора | xlsx, json | compliance.reports.regulatory + MFA |
game-session-log | Детальный лог игровых сессий | csv, json | analytics.reports.game-sessions |
Отчёты Для Партнёра
| Тип Отчёта | Описание | Форматы | Разрешение |
|---|---|---|---|
partner-dashboard-summary | KPI: referred users, GGR, commission accrued | xlsx, csv | partner.reports.read |
partner-commissions | Детальный список начисленных комиссий | xlsx, csv | partner.reports.commissions |
partner-referrals | Список привлечённых игроков (без raw PII) | xlsx, csv | partner.reports.referrals |
partner-promo-codes | Статистика промокодов | xlsx, csv | partner.reports.promo |
partner-payouts | История выплат партнёру | xlsx, csv | partner.reports.payouts |
Поля Отчётов — Детали
platform-financial-summary Fields
| Поле | Тип | Описание |
|---|---|---|
Period | date | Дата (день/неделя/месяц в зависимости от granularity) |
TenantId | guid | Tenant (только для global admin) |
BrandId | guid | Brand |
TotalDepositsAmount | decimal | Сумма депозитов |
TotalWithdrawalsAmount | decimal | Сумма выводов |
TotalBetsAmount | decimal | Общая сумма ставок |
TotalWinsAmount | decimal | Общая сумма выигрышей |
GrossGamingRevenue | decimal | GGR = Bets − Wins |
IncentiveCostAmount | decimal | Стоимость бонусов |
NetGamingRevenue | decimal | NGR = GGR − Incentive Cost |
ActiveUsersCount | int | Уникальные игравшие игроки |
NewUsersCount | int | Новые регистрации |
DepositsCount | int | Количество успешных депозитов |
WithdrawalsCount | int | Количество выводов |
CurrencyId | guid | Валюта отчёта |
user-transactions Fields
| Поле | Тип | PII | Описание |
|---|---|---|---|
TransactionId | guid | нет | Id транзакции |
TransactionType | enum | нет | Deposit, Withdrawal, Bet, Win, IncentiveGrant, IncentiveRedemption, Cashback |
Status | enum | нет | Completed, Pending, Failed, Cancelled |
Amount | decimal | нет | Сумма |
CurrencyId | guid | нет | Валюта |
BalanceAfter | decimal | нет | Баланс после транзакции |
OccurredAt | datetime | нет | Время транзакции |
ProviderReference | string | нет | Ссылка провайдера (для payment) |
UserId | guid | PII | Id игрока |
UserEmail | string | PII-high | Email (только с export-user + MFA + reason) |
GameId | guid | нет | Id игры (для bet/win) |
GameName | string | нет | Название игры |
IncentiveId | guid | нет | Id бонуса (если связан) |
partner-commissions Fields
| Поле | Тип | Описание |
|---|---|---|
CommissionId | guid | Id начисления |
Period | date | Период начисления |
CommissionType | enum | RevShare, CPA, Hybrid |
ReferredUsersCount | int | Игроков за период (без raw ids) |
BaseAmount | decimal | База для расчёта |
CommissionRate | decimal | Ставка (%) |
CommissionAmount | decimal | Начислено |
CurrencyId | guid | Валюта |
Status | enum | Accrued, Approved, Paid |
CalculatedAt | datetime | Время расчёта |
Фильтры
Общие Фильтры (для большинства отчётов)
| Фильтр | Тип | Валидация |
|---|---|---|
DateFrom | DateTimeOffset | Обязателен; не старше retention window |
DateTo | DateTimeOffset | Обязателен; >= DateFrom; не в будущем |
Granularity | enum | Day, Week, Month — для summary отчётов |
CurrencyId | guid? | Фильтр по валюте |
BrandIds | guid[]? | Фильтр по brand (admin only) |
TenantId | guid? | Только global admin с cross-tenant permission |
Фильтры Для User Reports
| Фильтр | Тип | Требуемое Разрешение |
|---|---|---|
UserId | guid | analytics.reports.export-user + MFA |
TransactionTypes | enum[]? | Нет доп. разрешения |
Statuses | enum[]? | Нет доп. разрешения |
MinAmount | decimal? | Нет доп. разрешения |
MaxAmount | decimal? | Нет доп. разрешения |
Фильтры Для Partner Reports
| Фильтр | Тип | Описание |
|---|---|---|
PartnerId | guid | Автоматически из actor context (партнёр видит только своё) |
CommissionTypes | enum[]? | RevShare, CPA, Hybrid |
Statuses | enum[]? | Accrued, Approved, Paid |
Request И Result Models
// Pin.Analytics.Contracts/Reports/
// Базовый request для всех export типов
public abstract record BaseReportExportRequest
{
public required ExportFormat Format { get; init; }
public required DateTimeOffset DateFrom { get; init; }
public required DateTimeOffset DateTo { get; init; }
public string? ReasonCode { get; init; } // обязателен для PII-adjacent
public int? RowLimit { get; init; } // max 500_000; default по policy
}
public enum ExportFormat { Xlsx, Csv, Json }
// Конкретный request
public sealed record CreatePlatformFinancialSummaryExportRequest
: BaseReportExportRequest
{
public Granularity Granularity { get; init; } = Granularity.Day;
public Guid[]? BrandIds { get; init; }
public Guid? CurrencyId { get; init; }
}
public sealed record CreateUserTransactionsExportRequest
: BaseReportExportRequest
{
public required Guid UserId { get; init; }
public TransactionType[]? TransactionTypes { get; init; }
public TransactionStatus[]? Statuses { get; init; }
public decimal? MinAmount { get; init; }
public decimal? MaxAmount { get; init; }
public required string MfaToken { get; init; } // обязателен
}
// Job result
public sealed class ReportExportJobView
{
public Guid JobId { get; init; }
public ExportJobStatus Status { get; init; }
public string ReportType { get; init; } = default!;
public ExportFormat Format { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? CompletedAt { get; init; }
public int? RowCount { get; init; }
public long? FileSizeBytes { get; init; }
public string? DownloadUrl { get; init; } // signed URL, short-lived
public DateTimeOffset? UrlExpiresAt { get; init; }
public string? FailureReason { get; init; }
}
ClosedXML Excel Generator
// Pin.Analytics.Infrastructure/Export/Xlsx/
public sealed class XlsxReportGenerator<TRow>
{
private readonly IReportColumnDefinition<TRow> _columns;
public Stream GenerateStream(IEnumerable<TRow> rows, ExportMetadata metadata)
{
using var workbook = new XLWorkbook();
var sheet = workbook.AddWorksheet("Report");
// 1. Metadata sheet
AddMetadataSheet(workbook, metadata);
// 2. Headers row
WriteHeaders(sheet);
// 3. Data rows
var rowIndex = 2;
foreach (var row in rows)
{
WriteDataRow(sheet, row, rowIndex++);
}
// 4. Auto-fit columns
sheet.Columns().AdjustToContents();
// 5. Table style
var range = sheet.Range(1, 1, rowIndex - 1, _columns.Count);
range.CreateTable().Theme = XLTableTheme.TableStyleLight1;
var stream = new MemoryStream();
workbook.SaveAs(stream);
stream.Position = 0;
return stream;
}
private void WriteHeaders(IXLWorksheet sheet)
{
for (int i = 0; i < _columns.Count; i++)
{
var cell = sheet.Cell(1, i + 1);
cell.Value = _columns[i].Header;
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#2E6DA4");
cell.Style.Font.FontColor = XLColor.White;
}
}
private void WriteDataRow(IXLWorksheet sheet, TRow row, int rowIndex)
{
for (int i = 0; i < _columns.Count; i++)
{
var cell = sheet.Cell(rowIndex, i + 1);
var column = _columns[i];
var value = column.ValueSelector(row);
cell.Value = value switch
{
decimal d => d, // числа без кавычек
int n => n,
DateTimeOffset dt => dt.ToString("yyyy-MM-dd HH:mm:ss UTC"),
null => "",
_ => value.ToString()
};
if (column.Format is not null)
cell.Style.NumberFormat.Format = column.Format; // например "#,##0.00"
}
}
private void AddMetadataSheet(XLWorkbook workbook, ExportMetadata metadata)
{
var meta = workbook.AddWorksheet("Metadata");
meta.Cell("A1").Value = "Report Type";
meta.Cell("B1").Value = metadata.ReportType;
meta.Cell("A2").Value = "Generated At";
meta.Cell("B2").Value = metadata.GeneratedAt.ToString("o");
meta.Cell("A3").Value = "Generated By";
meta.Cell("B3").Value = metadata.ActorType; // не raw actor id
meta.Cell("A4").Value = "Date From";
meta.Cell("B4").Value = metadata.DateFrom.ToString("yyyy-MM-dd");
meta.Cell("A5").Value = "Date To";
meta.Cell("B5").Value = metadata.DateTo.ToString("yyyy-MM-dd");
meta.Cell("A6").Value = "Row Count";
meta.Cell("B6").Value = metadata.RowCount;
meta.Cell("A7").Value = "Export ID";
meta.Cell("B7").Value = metadata.ExportId.ToString();
}
}
// Column definition
public sealed class ReportColumnDefinition<TRow>
{
public string Header { get; init; } = default!;
public Func<TRow, object?> ValueSelector { get; init; } = default!;
public string? Format { get; init; } // Excel format string
public bool IsPii { get; init; } // для privacy suppression
}
CSV Generator
// Pin.Analytics.Infrastructure/Export/Csv/
public sealed class CsvReportGenerator<TRow>
{
public Stream GenerateStream(IEnumerable<TRow> rows, Type rowType)
{
var stream = new MemoryStream();
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
using var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture)
{
Delimiter = ",",
HasHeaderRecord = true,
// Всегда quote strings с запятой/кавычками
});
csv.WriteRecords(rows);
writer.Flush();
stream.Position = 0;
return stream;
}
}
Privacy Suppression
Для соблюдения конфиденциальности:
- Partner не видит raw PII игроков (email, phone, full name) — только anonymized id и агрегаты.
- Small bucket suppression: если группа содержит < 5 игроков — скрываем детали.
- Поля
UserEmail,UserPhone,UserFullNameдоступны только admin сexport-user+ MFA + ReasonCode.
public sealed class PrivacySuppressor<TRow>
{
private readonly IReadOnlyList<string> _piiFields;
private readonly ActorPermissions _permissions;
public IEnumerable<TRow> Apply(IEnumerable<TRow> rows)
{
if (_permissions.HasPermission("analytics.reports.export-pii"))
return rows; // полный доступ
// Suppress PII fields
return rows.Select(row =>
{
foreach (var field in _piiFields)
SetField(row, field, "***");
return row;
});
}
}
Limits и Rate Limits
| Параметр | Admin | Partner |
|---|---|---|
| Max row limit | 1 000 000 | 100 000 |
| Max date range | 365 дней | 90 дней |
| Rate limit exports | 10/час | 5/час |
| Concurrent exports | 3 | 1 |
| Max file size | 500 MB | 100 MB |
| Signed URL TTL | 2 часа | 1 час |
| File retention | 7 дней | 3 дня |
Audit Export
Каждое создание выгрузки пишет audit record:
public sealed class ExportAuditRecord
{
public Guid AuditId { get; init; }
public string Action { get; init; } = "analytics.report.export-created";
public ActorSnapshot Actor { get; init; } = default!;
public TenantId? TenantId { get; init; }
public BrandId? BrandId { get; init; }
public string ReportType { get; init; } = default!;
public ExportFormat Format { get; init; }
public string FiltersHash { get; init; } = default!; // hash фильтров, не raw values
public int? RowLimit { get; init; }
public string? ReasonCode { get; init; }
public bool ContainsPii { get; init; }
public string LegalBasis { get; init; } = default!; // "legitimate-interest", "regulatory"
public DateTimeOffset CreatedAt { get; init; }
}
ReportExportJob Entity
public sealed class ReportExportJob : AggregateRoot<ReportExportJobId>,
ITenantScoped, IHasUpdatedAt
{
public TenantId TenantId { get; private set; }
public BrandId? BrandId { get; private set; }
public ActorSnapshot CreatedByActor { get; private set; }
public string ReportType { get; private set; } = default!;
public ExportFormat Format { get; private set; }
public ExportJobStatus Status { get; private set; }
public string FiltersHash { get; private set; } = default!;
public DateTimeOffset? CompletedAt { get; private set; }
public int? RowCount { get; private set; }
public long? FileSizeBytes { get; private set; }
public string? ObjectStorageKey { get; private set; } // private key, не signed URL
public string? FailureReason { get; private set; }
public DateTimeOffset ExpiresAt { get; private set; }
public DateTimeOffset UpdatedAt { get; private set; }
public Result MarkCompleted(int rowCount, long fileSizeBytes, string objectKey)
{
if (Status != ExportJobStatus.Running)
return Result.Fail(ExportErrors.InvalidTransition(Status, ExportJobStatus.Completed));
Status = ExportJobStatus.Completed;
RowCount = rowCount;
FileSizeBytes = fileSizeBytes;
ObjectStorageKey = objectKey;
CompletedAt = DateTimeOffset.UtcNow;
UpdatedAt = DateTimeOffset.UtcNow;
RaiseDomainEvent(new ExportJobCompletedEvent(Id, CreatedByActor, rowCount));
return Result.Ok();
}
}
Ссылки
- Export And Data Access Registry
- Analytics Service — Admin Reports API
- Analytics Service — Partner Reports API
- PII Classification Matrix
- Audit Trail Standard
- Backend Testing Rules — Export Tests
Обратные ссылки
Автоссылки: где используется этот документ.
- adr (1): ADR-0041: Reporting И Export Architecture
- index.md (1): Backend SDK — Руководство Разработчика
- backend-shared-standards-index.md (1): Backend Shared Standards Index