Перейти к содержанию

7. Генерация отчётов

Отчёт помогает выгрузить данные для анализа в Excel, LibreOffice Calc или BI-систему. В практике используется CSV, потому что это простой текстовый формат.

В производственных системах данные часто нужны не только внутри API. Руководитель может попросить таблицу за неделю, аналитик — выгрузку ошибок, преподаватель — доказательство того, что система действительно накопила события. CSV закрывает эти сценарии без сложных библиотек.

CSV — это текст, где каждая строка является строкой таблицы, а значения разделяются запятыми. Его можно открыть в табличном редакторе. Но у формата есть нюанс: если внутри значения есть запятая, кавычки или перенос строки, значение нужно экранировать.

DTO строки отчёта

DTO отчёта лучше отделить от сущности ActivityEvent. В отчёте может быть имя пользователя, человекочитаемая роль или вычисляемое поле, которых нет напрямую в таблице события.

Пример ниже задаёт форму одной строки CSV-файла. Это не таблица базы данных, а удобное представление данных для выгрузки и чтения человеком.

public record ActivityEventReportRow(
    int Id,
    string UserName,
    string Type,
    string Source,
    string Severity,
    DateTime CreatedAt,
    string Description);

Генератор CSV

Генератор получает уже подготовленные строки и не обращается к базе. Это хорошее разделение ответственности: сервис отчётов выбирает данные, генератор только превращает их в файл.

public class CsvReportGenerator
{
    public byte[] GenerateEventsReport(IEnumerable<ActivityEventReportRow> rows)
    {
        var builder = new StringBuilder();
        builder.AppendLine("Id,UserName,Type,Source,Severity,CreatedAt,Description");

        foreach (var row in rows)
        {
            builder.Append(row.Id).Append(',');
            builder.Append(Escape(row.UserName)).Append(',');
            builder.Append(Escape(row.Type)).Append(',');
            builder.Append(Escape(row.Source)).Append(',');
            builder.Append(Escape(row.Severity)).Append(',');
            builder.Append(row.CreatedAt.ToString("O")).Append(',');
            builder.AppendLine(Escape(row.Description));
        }

        return Encoding.UTF8.GetBytes(builder.ToString());
    }

    private static string Escape(string value)
    {
        if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
            return $"\"{value.Replace("\"", "\"\"")}\"";

        return value;
    }
}

Контроллер отчёта

Контроллер возвращает не JSON, а файл. Метод File(...) задаёт содержимое, MIME-тип и имя файла. Браузер или HTTP-клиент сможет скачать результат как activity-events.csv.

[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
    private readonly ReportsService _reportsService;

    public ReportsController(ReportsService reportsService)
    {
        _reportsService = reportsService;
    }

    [Authorize(Roles = "Admin,Analyst")]
    [HttpGet("events.csv")]
    public async Task<IActionResult> EventsCsv(DateTime? from, DateTime? to, CancellationToken ct)
    {
        var bytes = await _reportsService.BuildEventsCsvAsync(from, to, ct);
        return File(bytes, "text/csv; charset=utf-8", "activity-events.csv");
    }
}

TXT-отчёт

TXT удобен для краткой сводки.

Такой формат подходит, когда нужен не подробный список строк, а короткое текстовое резюме для отчёта, письма или проверки результата практики.

public string GenerateSummaryText(AnalyticsSummaryDto summary)
{
    return $"""
    Отчёт по активности
    Всего событий: {summary.TotalEvents}
    Уникальных пользователей: {summary.UniqueUsers}
    Ошибок: {summary.Errors}
    Дата формирования: {DateTime.UtcNow:O}
    """;
}

CSV применяется для таблиц, TXT — для текстовой сводки.

Как проверить отчёт

  1. Создайте несколько событий или запустите генератор.
  2. Войдите под ролью Admin или Analyst.
  3. Выполните GET /api/reports/events.csv.
  4. Откройте файл в табличном редакторе.
  5. Проверьте, что строки соответствуют событиям в базе.