Спецификация реализации отчёта NTR-R01

Подсистема Версия: 1.0 accepted

Карта реализации отчёта NTR-R01 в src/Ntr.Reports: раскладка папок, контракты, конвейер сборки, Excel-шаблон.

⟨/⟩ Исходник

1 Спецификация реализацииImplSpecifikaciyaRealizacii⟨/⟩

1.1 Расположение и структура папокImplRaspolozhenieIStrukturaPapok⟨/⟩

Отчёт реализуется в проекте src/Ntr.Reports/ в папке NTR-R01/. В корне папки лежит только один основной файл-оркестратор NtrR01ReportGenerator.cs; всё остальное разнесено по тематическим подпапкам.

NTR-R01/
├── NtrR01ReportGenerator.cs              ← оркестратор сборки отчёта (единственный файл в корне папки)
├── readme.md
│
├── Abstractions/                         публичные контракты модуля
│   ├── INtrR01ReportGenerator.cs
│   ├── INtrR01ReportDataProvider.cs
│   ├── INtrR01SectionBuilder.cs
│   └── NtrR01ReportGenerationResult.cs
│
├── Params/                               входные параметры отчёта
│   └── GenerateNtrR01ReportParams.cs     параметры запуска отчёта (NavId, BuildingId?,
│                                         CollectionTaskId, EnergyProfileStatusIds,
│                                         EnergyProfileConditionsStatusIds, DateOfCreation)
│
├── Model/                                модель готового отчёта (immutable records)
│   ├── NtrR01Report.cs                   корень: ReportParams + Sections
│   ├── SectionKind.cs                    enum секций данных: ElectricalEnergyConsumption /
│   │                                                         HeatEnergyConsumption /
│   │                                                         EnergyEfficiencyClass / Walls / Roofs
│   ├── SectionMetadata.cs                заголовки и имена листов Excel
│   ├── CalcKind.cs                       enum видов расчёта: Normative / Standard /
│   │                                                         Technological / Custom
│   ├── RowLevel.cs                       enum уровней строки секции:
│   │                                     Company / ExecutiveApparatus / Mes / Pmes /
│   │                                     Substation / Building
│   ├── SectionRow.cs                     узел дерева строк: RowLevel, Children,
│   │                                     путь иерархии, per-Building поля,
│   │                                     4 слота по CalcKind с per-секционными колонками
│   └── Sections/                         per-секционные колонки слотов
│       ├── ElectricalEnergyConsumptionSlot.cs
│       ├── HeatEnergyConsumptionSlot.cs
│       ├── EnergyEfficiencyClassSlot.cs
│       ├── WallsSlot.cs
│       └── RoofsSlot.cs
│
├── DataProvider/                         выборка сырых данных из NtrReadDbContext
│   ├── NtrR01ReportDataProvider.cs
│   ├── NtrR01SourceData.cs               агрегат сырых данных
│   └── Sources/                          сырые DTO с подтянутой иерархией ИА/МЭС/ПМЭС/ПС
│       ├── ConsumptionModelSource.cs
│       ├── BuildingSource.cs
│       ├── EnergyProfileSource.cs
│       └── EnergyProfileCollectionTaskSource.cs
│
├── Sections/                             по одному builder'у на каждую секцию
│   ├── ElectricalEnergyConsumptionSectionBuilder.cs
│   ├── HeatEnergyConsumptionSectionBuilder.cs
│   ├── EnergyEfficiencyClassSectionBuilder.cs
│   ├── WallsSectionBuilder.cs
│   └── RoofsSectionBuilder.cs
│
├── Hierarchy/                            построение дерева орг-структуры из выбранных зданий
│   └── OrgHierarchyTreeBuilder.cs        строит дерево по фактически присутствующим зданиям
│
└── Excel/
    ├── INtrR01ReportExcelRenderer.cs
    ├── NtrR01ReportExcelRenderer.cs
    └── Templates/
        └── ntr-r01-template.xlsx         встроенный ресурс

1.2 Архитектурные решенияImplArhitekturnyeResheniya⟨/⟩

1.2.1 Объект отчёта `NtrR01Report`ImplObektOtchetaNtrr01report⟨/⟩

Отчёт содержит секции и параметры. Все модели — record с init-свойствами.

NtrR01Report:
  ReportParams              : GenerateNtrR01ReportParams
  ElectricalEnergyConsumption: Section<ElectricalEnergyConsumptionSlot>
  HeatEnergyConsumption     : Section<HeatEnergyConsumptionSlot>
  EnergyEfficiencyClass     : Section<EnergyEfficiencyClassSlot>
  Walls                     : Section<WallsSlot>
  Roofs                     : Section<RoofsSlot>          // ВОК

Section<TSlot>:
  Header                    : string           // из SectionMetadata
  TabName                   : string           // имя листа в Excel
  Roots                     : List<SectionRow<TSlot>>     // корни дерева строк секции

Cекций данных — пять (SectionKind). Шестой пункт спецификации, «Параметры отчёта», самостоятельной секцией не является — его значения хранятся прямо в NtrR01Report.ReportParams и при рендере выводятся на отдельный лист Excel.

1.2.2 Параметры отчётаImplParametryOtcheta⟨/⟩

GenerateNtrR01ReportParams:
  DateOfCreation                    : DateTime
  NavId                             : Guid              // обязателен; орг-объект (Компания/ИА/МЭС/ПМЭС/ПС)
  BuildingId                        : Guid?             // опционален; если задан — отчёт по одному Зданию
  CollectionTaskId                  : Guid              // выбранное Задание на сбор энергопрофилей
  EnergyProfileStatusIds            : IReadOnlyList<Guid>   // соответствуют EpEnergyProfileStatus.Id; пусто = «все»
  EnergyProfileConditionsStatusIds  : IReadOnlyList<Guid>   // соответствуют EpModelConsistencyStatus.Id; пусто = «все»

В блоке навигации пользователь выбирает узел в едином иерархическом дереве, в котором присутствуют и орг-объекты, и здания. В отчёт прилетает пара (NavId, BuildingId?):

  • BuildingId задан → отчёт по выбранному Зданию (его Энергопрофиль в выбранном Задании). NavId при этом содержит идентификатор орг-объекта, к которому привязано Здание (используется для построения цепочки иерархии в строках отчёта).
  • BuildingId не задан → отчёт по всем Зданиям, находящимся в узле NavId или ниже.

Задание на сбор энергопрофилей (CollectionTaskId) обязательно. По умолчанию UI подставляет последнее созданное задание.

1.2.3 Обёртка результатаImplObertkaRezultata⟨/⟩

NtrR01ReportGenerationResult:
  Report    : NtrR01Report?
  Warnings  : IReadOnlyList<string>    // мягкие проблемы (расхождения per-Building полей и т.п.)
  Errors    : IReadOnlyList<string>    // блокирующие ошибки
  IsSuccess : bool                     // Report != null && Errors.Count == 0

1.2.4 Конвейер сборкиImplKonveyerSborki⟨/⟩

INtrR01ReportDataProvider одним проходом тянет сырьё из NtrReadDbContext с join'ами на Navs/NavIa/NavMeses/NavPmeses/NavSubstations/RegBuildings и справочники → OrgHierarchyTreeBuilder строит дерево орг-структуры по фактически присутствующим в выборке зданиям → набор INtrR01SectionBuilder (по одному на секцию) раскладывает данные по узлам дерева и per-CalcKind слотам → NtrR01ReportGenerator собирает корневой NtrR01Report → INtrR01ReportExcelRenderer обходит дерево preorder'ом, выводит строки в xlsx поверх шаблона и попутно формирует Excel-формулы агрегации.

DI. Один INtrR01ReportGenerator / INtrR01ReportDataProvider / INtrR01ReportExcelRenderer + N INtrR01SectionBuilder через IEnumerable<>. Регистрация — в NtrReportsDiExtension.

1.2.5 Состав секцийImplSostavSekciy⟨/⟩

Cекций данных — пять, по одной на каждый SectionKind:

SectionKind Заголовок (краткое наименование) Колонки слота (per-CalcKind)
ElectricalEnergyConsumption Расход ЭЭ Общее потребление ЭЭ зданием, кВт·ч
HeatEnergyConsumption Расход ТЭ Общее потребление ТЭ зданием, Гкал
EnergyEfficiencyClass Класс Удельная характеристика расхода ТЭ (расч.) Вт/(м³·℃); Удельная характеристика расхода ТЭ (норм.) Вт/(м³·℃); Отклонение расчётной от нормативной, %; Класс энергоэффективности
Walls Стены По 3 колонки (материал / теплопроводность Вт/(м·К) / толщина мм) для несущего слоя, основного утеплителя и дополнительного утеплителя — 9 колонок
Roofs ВОК По 3 колонки для несущего слоя верхнего перекрытия, утеплителя верхнего перекрытия и утеплителя перекрытия крыши — 9 колонок

1.2.6 Структура строки секцииImplStrukturaStrokiSekcii⟨/⟩

Одна листовая строка секции = одно Здание. Каждая листовая строка содержит четыре набора per-секционных колонок (слотов) — по одному на CalcKind: «Характеристики нормативной модели», «Характеристики эталонной модели», «Характеристики технологической модели», «Характеристики произвольной модели». Каждый слот заполняется значениями соответствующей Модели потребления из Энергопрофиля Здания. Если модели для какого-то вида расчёта в Энергопрофиле здания нет — соответствующий слот пуст.

Кроме листовых, в секции присутствуют строки-агрегаты уровней Компания / ИА / МЭС / ПМЭС / Подстанция (см. иерархию ниже).

Строки секции образуют дерево: узлы агрегатов содержат дочерние узлы; листья — здания. Рендер обходит дерево preorder'ом.

SectionRow<TSlot>  (узел дерева, общая часть для всех секций):
  RowLevel          : Company | ExecutiveApparatus | Mes | Pmes | Substation | Building
  Children          : List<SectionRow<TSlot>>           // у Building пуст
  Hierarchy         : имена Компании / ИА / МЭС / ПМЭС / Подстанции / Здания
                      (повторяются на потомках строки)
  PerBuildingFields : Адрес, Климатический район, Функциональное назначение,
                      Тип системы отопления, Объём отапливаемый м³, Этажность,
                      Статус энергопрофиля, Статус выполнения условий расчётов
                      // заполняются ТОЛЬКО на строках уровня Building
  Slots             : Dictionary<CalcKind, TSlot?>      // 4 ключа
                      // на Building заполняются из соответствующей модели Энергопрофиля
                      // на агрегатах остаются null; суммируемые колонки рендер
                      //   заполняет формулой SUM (см. «Агрегация по орг-иерархии»)

1.2.7 Иерархия и состав строк секцииImplIerarhiyaISostavStrokSekcii⟨/⟩

RowLevel: Company → ExecutiveApparatus (ИА) → Mes → Pmes → Substation → Building. Состав прямых детей по уровням, правила образования и наполнения дерева — см. функциональную часть выше (раздел «Состав агрегатных записей по уровням»).

Здание принадлежит ровно одному орг-объекту (ИА / МЭС / ПМЭС / Подстанция) — это гарантировано доменом. Дерево строится только по фактически присутствующим в выборке зданиям; пустых веток не создаётся. Порядок строк — preorder обхода дерева (агрегат, ниже его потомки).

1.2.8 Источники значений в листовой строкеImplIstochnikiZnacheniyVListovoyStroke⟨/⟩

Общая часть листовой строки (имена орг-иерархии и per-Building поля, включая статус энергопрофиля и статус выполнения условий расчётов) — состав колонок и источники см. в функциональной части выше (таблица «Общие для всех секций параметры записей секции»). Значения, источник которых — Модель потребления (Функциональное назначение, Тип системы отопления, Объём отапливаемый, Этажность), берутся из Нормативной модели Энергопрофиля Здания; при расхождении этих полей между 4 моделями фиксируется предупреждение в NtrR01ReportGenerationResult.Warnings.

Per-CalcKind слоты по секциям (маппинг на свойства доменных сущностей для реализации)

В строке здания на каждый CalcKind заполняется свой слот, значения берутся из соответствующей Модели потребления:

  • Расход ЭЭ — ConsumptionModel.TotalElectricalEnergyConsumptionKwh.
  • Расход ТЭ — ConsumptionModel.TotalHeatEnergyConsumptionGcal.
  • Класс — ConsumptionModel.CalculatedHeatLossCoeff, ConsumptionModel.NormativeHeatLossCoeff, ConsumptionModel.DeviationPercent, ConsumptionModel → RefEnergyEfficiencyClass.Name.
  • Стены — ConsumptionModel → WallsModel:
    • Несущий слой: → RefMaterial.Name, → RefMaterial.ThermalConductivity, LoadBearingThicknessMm.
    • Основной утеплитель: → RefInsulationMaterial.Name, → RefInsulationMaterial.ThermalConductivity, MainInsulationThicknessMm.
    • Дополнительный утеплитель: аналогично, поля AdditionalInsulation*.
  • ВОК — ConsumptionModel → RoofModel:
    • Верхнее перекрытие, несущий слой: → RefMaterial.Name, → RefMaterial.ThermalConductivity, UpperFloorLoadBearingThicknessMm.
    • Верхнее перекрытие, утеплитель: → RefInsulationMaterial.Name, → RefInsulationMaterial.ThermalConductivity, UpperFloorInsulationThicknessMm.
    • Перекрытие крыши, утеплитель: аналогично, поля RoofInsulation*.

Конкретные имена свойств уточняются по Ntr.Domain.Modules.Consumption.Entities при реализации.

1.2.9 Агрегация по орг-иерархииImplAgregaciyaPoOrgIerarhii⟨/⟩

В дополнение к листовым строкам в каждой секции присутствуют строки-агрегаты по орг-иерархии (см. таблицу выше). На этих строках заполняется только имя соответствующего уровня иерархии; per-Building поля и большинство per-секционных колонок остаются пустыми.

Агрегируются (суммой по листовым потомкам, отдельно по каждому слоту CalcKind) только показатели потребления:

  • секция «Расход ЭЭ» — «Общее потребление ЭЭ зданием, кВт·ч»;
  • секция «Расход ТЭ» — «Общее потребление ТЭ зданием, Гкал».

Остальные показатели (удельные характеристики, отклонение, класс энергоэффективности, материалы / теплопроводности / толщины стен и ВОК) по орг-иерархии не агрегируются — ячейки остаются пустыми.

Расчёт агрегатов выполняется Excel-формулами в самом файле отчёта. В NtrR01Report суммируемые поля на агрегатных строках имеют значение null; INtrR01ReportExcelRenderer при выводе строки-агрегата подставляет в эти ячейки формулу SUM(...), диапазон которой собирается из Excel-номеров строк листовых потомков (рендер ведёт SectionRow → ExcelRowNumber при preorder-обходе дерева). Это даёт пользователю прозрачность (формула видна в ячейке) и снимает с генератора арифметику.

1.2.10 Выборка моделей потребленияImplVyborkaModeleyPotrebleniya⟨/⟩

  • Задание на сбор энергопрофилей создаётся на конкретный отчётный период. В рамках одного Задания на каждое Здание создаётся один Энергопрофиль; Энергопрофиль ссылается на 4 Модели потребления — по одной на каждый CalcKind (Нормативный/Эталонный/Технологический/Произвольный; «Базовый» в Энергопрофиль не входит).
  • У одного Здания могут существовать Энергопрофили в разных Заданиях — за разные отчётные периоды, в разных Заданиях. Выбор CollectionTaskId в параметрах отчёта однозначно выбирает один Энергопрофиль на Здание из этого Задания.
  • Определение слота модели. Принадлежность Модели потребления к слоту CalcKind определяется по тому, какой именно ссылкой Энергопрофиля (NormativeModelId / StandardModelId / TechnologicalModelId / CustomModelId) она указана. Поле ConsumptionModel.CalcKindId для отчёта не используется — слот задаётся именно ссылкой из Энергопрофиля.
  • Сужение по BuildingId. Если GenerateNtrR01ReportParams.BuildingId задан — выборка сужается до Энергопрофиля этого Здания в выбранном Задании. Иначе берутся все Здания под узлом NavId и их Энергопрофили в выбранном Задании.
  • Фильтр EnergyProfileStatusIds («Включать энергопрофили в статусах»): если список не пуст — фильтруем EpEnergyProfile.StatusId по этим Id напрямую; если пуст — фильтр не применяется (включаются все статусы).
  • Фильтр EnergyProfileConditionsStatusIds («Включать энергопрофили со статусом выполнения условий расчётов»): если список не пуст — фильтруем EpEnergyProfile.ModelConsistencyStatusId по этим Id напрямую; если пуст — фильтр не применяется.
  • Неизвестные Id (не соответствующие ни одной известной доменной константе) валидацией не отсекаются — они просто не пройдут как фильтр по БД (ни одного профиля с таким Id не найдётся).
  • Порядок слотов в шапке таблиц секций — по справочнику CalcKind: Нормативный → Эталонный → Технологический → Произвольный.

1.2.11 Валидация параметровImplValidaciyaParametrov⟨/⟩

  • NavId обязателен (Guid.Empty недопустим).
  • CollectionTaskId обязателен.
  • BuildingId опционален; если задан — проверяется, что Здание принадлежит узлу NavId или его потомкам.
  • При нарушении — NtrR01ReportGenerationResult.Errors заполняется, Report = null, IsSuccess = false.

1.2.12 Пустая выборкаImplPustayaVyborka⟨/⟩

Если по выбранному Заданию и фильтрам не нашлось ни одного Энергопрофиля:

  • NtrR01Report собирается с пустыми секциями (Roots = []) и заполненным ReportParams;
  • в Warnings добавляется сообщение «По выбранному заданию и фильтрам не найдено энергопрофилей»;
  • IsSuccess = true (это не ошибка).

1.2.13 Excel-шаблонImplExcelShablon⟨/⟩

Templates/ntr-r01-template.xlsx — встроенный ресурс. 6 листов: 5 листов секций (имена = Section.TabName из SectionMetadata) + лист «Параметры отчёта». На каждом листе данных — sheet-scoped именованная ячейка DataAnchor, с которой начинается вывод строк. Форматы чисел и дат заданы прямо в шаблоне на колонках (в каждой колонке однотипные значения — формат привязан к колонке шаблона). На листе «Параметры отчёта» — именованные ячейки Param_DateOfCreation, Param_NavObject, Param_BuildingObject, Param_CollectionTask, Param_StatusFilter, Param_ConditionsStatusFilter. Рендер обходит дерево SectionRow preorder'ом, по ходу записи запоминает Excel-номер строки каждого узла и для агрегатов формирует Excel-формулы SUM по Excel-номерам своих листовых потомков.

1.2.14 Read-контекстImplReadKontekst⟨/⟩

Используется существующий NtrReadDbContext (src/Infrastructure.Read/). Основные DbSet:

  • CmConsumptionModels, CmOperatingConditions, CmShapeAndDimensions, CmWallsModels, CmRoofModels, CmCalcKinds, CmCalcStatuses;
  • EpEnergyProfiles, EpEnergyProfileCollectionTasks, EpEnergyProfileStatuses, EpEnergyProfileCollectionTaskStatuses;
  • RegBuildings;
  • Navs, NavCompanies, NavIa, NavMeses, NavPmeses, NavSubstations;
  • справочники: RefClimateRegion, RefBuildingPurpose, RefHeatingType, RefMaterial, RefInsulationMaterial, RefEnergyEfficiencyClass.