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

ReportGenerationHandler — Report Generator

Обзор

ReportGenerationHandler — микросервис для асинхронной генерации отчётов через RabbitMQ/MassTransit. Принимает запросы на генерацию и возвращает готовые файлы.

Архитектура

graph LR
    WEB[FuelProcessing.Web] -->|Publish ReportData| MQ[RabbitMQ]
    MQ -->|Consume| CONSUMER[ReportConsumer]
    CONSUMER --> SERVICE[ReportService]
    SERVICE --> HISTORY[HistoryTransaction]
    SERVICE --> SELLING[SellingFuel]
    SERVICE --> BALANCE[BalanceReports]
    HISTORY --> NPOI[NPOI Excel]
    SELLING --> NPOI
    BALANCE --> NPOI
    NPOI -->|Publish| MQ
    MQ -->|Consume| WEB_ANS[ReportAnswerConsumer]
    WEB_ANS --> WEB

Компоненты

Компонент Описание
Program.cs Точка входа
Startup.cs Конфигурация DI, MassTransit, БД
ReportConsumer.cs MassTransit потребитель
ReportService.cs Оркестратор генерации
*ReportService.cs Специализированные сервисы

Сообщения

ReportData — запрос на отчёт

public class ReportData
{
    public int UserId { get; set; }
    public Report Report { get; set; }
    public ReportViewModel ReportViewModel { get; set; }
}

public class ReportViewModel
{
    public int CompanyId { get; set; }
    public int?[] DirectContractId { get; set; }
    public DateTime DateStart { get; set; }
    public DateTime DateEnd { get; set; }
    public ReportType ReportType { get; set; }
}

ReportAnswerData — готовый отчёт

public class ReportAnswerData
{
    public byte[] File { get; set; }
    public string Title { get; set; }
    public ReportItem ReportItem { get; set; }
    public Report Report { get; set; }
    public ReportViewModel ReportViewModel { get; set; }
}

Типы отчётов

Код Название Описание
HistoryTransactionReport Транзакции автопарка Детализация по картам
TransactonSellingFuelReport Продажа топлива Для перекупщиков
BalanceOverdraftAndNegativeBalances Овердрафты Проблемные балансы
BalnceAddReport Пополнение баланса История пополнений
IssuedCardsReport Выданные карты Реестр карт
TempOverdraftReport Временные овердрафты По договорам
DirectContractTermsReport Тарифы Условия договоров
ReportUsingFuelByDay Потребление Агрегированные данные
GasStationReports Заправки Детализация по АЗС
ContractTermTemplatesReport Тарифы по регионам Шаблоны условий
GasStationsFromServerVink Заправки ВИНКа Данные из Vink
TariffAtTheClient Тарифы клиентов Для менеджеров
ReportOnTheOrganization По организации Сводная информация
ReportTemporaryOverdraft Временные овердрафты Детально

Структура отчёта

Большинство отчётов содержат несколько листов:

Лист Содержимое
Данные Детальные записи
Водитель_день Агрегация по водителям/датам
Водитель_карта_день По водителям/картам/датам
Водитель_период Итоги за период
Сгруппированные транзакции Сводные данные

Генерация Excel (NPOI)

// Создание книги
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Данные");

// Заголовок
var headerRow = sheet.CreateRow(0);
headerRow.CreateCell(0).SetCellValue("Дата");
headerRow.CreateCell(1).SetCellValue("Объём");

// Форматирование чисел
var currencyStyle = workbook.CreateCellStyle();
currencyStyle.DataFormat = workbook.CreateDataFormat()
    .GetFormat("#,##0.00");

cell.CellStyle = currencyStyle;

// Сохранение
using var stream = new MemoryStream();
workbook.Write(stream);
return stream.ToArray();

Конфигурация MassTransit

services.AddMassTransit(mt =>
{
    mt.AddConsumers(typeof(ReportConsumer).Assembly);
    mt.AddConsumer<ReportConsumer>(cc =>
    {
        cc.UseConcurrencyLimit(5); // До 5 отчётов одновременно
    });

    mt.UsingRabbitMq((context, mq) =>
    {
        mq.Host(rabbitMqOptions.Host, port, "/", h =>
        {
            h.Username(rabbitMqOptions.Username);
            h.Password(rabbitMqOptions.Password);
            h.ContinuationTimeout(TimeSpan.FromMinutes(180)); // 3 часа
        });
        mq.ConfigureJsonSerializerOptions(settings =>
        {
            settings.ReferenceHandler = ReferenceHandler.IgnoreCycles;
        });
        mq.ConfigureEndpoints(context);
    });
});

RabbitMQ настройки

Параметр Значение Описание
ContinuationTimeout 180 минут Максимальное время ожидания ответа от RabbitMQ
ReferenceHandler IgnoreCycles Игнорировать циклические ссылки при сериализации JSON

DbContext изоляция

Каждое сообщение обрабатывается в отдельном scope для предотвращения конфликтов DbContext:

public async Task Consume(ConsumeContext<ReportData> context)
{
    // Создаём scope для каждого сообщения
    using var scope = _serviceProvider.CreateScope();
    var scopedProvider = scope.ServiceProvider;

    // Создаём новый DataContext через фабрику
    var contextFactory = scopedProvider.GetRequiredService<IDbContextFactory<DataContext>>();
    await using var dataContext = contextFactory.CreateDbContext();

    // ... обработка отчёта
}

Зачем это нужно: - Изоляция изменений между concurrent запросами - Предотвращение EntityState conflicts - Правильное управление lifetime scoped сервисов

Обработка ошибок в ReportConsumer

При ошибке генерации отчёта выполняются следующие действия:

catch (Exception e)
{
    // 1. Находим сущность Report через reflection
    var reportType = obj.Report.GetType();
    var report = await dataContext.FindAsync(reportType, obj.Report.Id);

    // 2. Устанавливаем статус ошибки (ErrorReport = 3)
    if (report != null)
    {
        var reportStatusProperty = reportType.GetProperty("ReportStatus");
        if (reportStatusProperty != null)
        {
            reportStatusProperty.SetValue(report, 3);
            await dataContext.SaveChangesAsync();
        }
    }

    // 3. Логируем ошибку
    _logUserActionService.SimpleLogData("ReportService.Consume.GetRerport.Error", ...);

    // 4. Re-throw для MassTransit retry механизма
    throw;
}

Стратегия retry

Шаг Действие
1 Установить ReportStatus = 3 (ошибка) в БД
2 Записать error log
3 Пробросить исключение (throw)
4 MassTransit автоматически retry (настраивается в политике retry)

Важно: Exception re-throw необходим для активации MassTransit retry механизма. Без этого сообщение будет помечено как успешно обработано.

Логирование через LogUserActionService

Все операции генерации отчётов логируются через LogUserActionService:

// Устанавливаем флаг, что это отчёт
_logUserActionService.isReport = true;

// Лог начала обработки
_logUserActionService.SimpleLogData("ReportService.Consume.GetRerport",
    $"Начало формирования отчета. ReportType: {type}; " +
    $"Start: {dateStart}; End: {dateEnd}; CompanyId: {companyId}");

// Лог завершения
_logUserActionService.SimpleLogData("ReportService.Consume.GetRerport",
    logForReport.Replace("Начало", "Завершение"));

// Лог ошибки
_logUserActionService.SimpleLogData("ReportService.Consume.GetRerport.Error",
    logForReport.Replace("Начало", "Ошибка") + $". {e.Message}");
Параметр Значение Описание
isReport true Флаг для идентификации логов отчётов
SimpleLogData метод Логирование с текстовым сообщением

Управление статусами

Статус Код Описание
Новая 0 Отчёт создан, ожидает обработки
В обработке 1 Отчёт генерируется
Готов 2 Файл успешно создан
Ошибка 3 Ошибка при генерации

При ошибке статус = 3, исключение пробрасывается для retry MassTransit.

Хранение файлов

Сгенерированные файлы сохраняются:

wwwroot/TempFiles/Reports/{CompanyId}/{Guid}.xlsx

Файлы старше 2 дней удаляются автоматически (CleanOldFiles()).

Endpoints

Endpoint Метод Описание
/health GET Health check
/ GET Главная страница

Зависимости

Пакет Назначение
MassTransit Message bus
MassTransit.RabbitMQ Транспорт RabbitMQ
NPOI Генерация Excel
AutoMapper Маппинг сущностей
EF Core Доступ к БД
Serilog Логирование

Конкурентность

Ограничение: до 5 отчётов одновременно.

cc.UseConcurrencyLimit(5);

Troubleshooting

Отчёт не генерируется

Проверьте: 1. Работает ли RabbitMQ 2. Запущен ли ReportGenerationHandler 3. Нет ли ошибок в логах

Файл не скачивается

Проверьте: 1. Права на директорию wwwroot/TempFiles/Reports/ 2. Существует ли файл 3. Не истёк ли срок действия (2 дня)

Связанная документация