Тема оформления
Регламент добавления новых модулей
1. Концепция модульного расширения платформы
Платформа «UrFU HeatBalance» спроектирована в строгом соответствии с принципом открытости/закрытости (Open-Closed Principle) архитектуры чистых микросервисов: ядро системы открыто для расширения новыми типами теплотехнических агрегатов без необходимости модификации кода существующих расчётных модулей.
Каждый теплотехнический агрегат (камерная, конвейерная, электрическая печь, сушильный барабан или проектируемый новый агрегат — например, туннельная сушилка TunnelKiln или шахтная печь ShaftKiln) функционирует как автономный микросервис со своим независимым жизненным циклом.
2. Пошаговая инструкция добавления нового агрегата
В качестве практического примера ниже рассмотрен регламент интеграции туннельной сушильной печи (TunnelKiln).
2.1. Шаг 1: Разработка математического ядра (HeatBalance.Math)
- В проекте
HeatBalance.Mathсоздайте модель входных данныхTunnelKilnInput.cs:
csharp
namespace HeatBalance.Math;
public sealed class TunnelKilnInput
{
public double WetMaterialMassFlowKgPerHour { get; init; }
public double MoistureInMassFraction { get; init; }
public double MoistureOutMassFraction { get; init; }
public double MaterialTempInC { get; init; }
public double MaterialTempOutC { get; init; }
public double FuelLowerHeatingValueKJPerKg { get; init; }
public double FuelMassFlowKgPerHour { get; init; }
public double TunnelLengthMeters { get; init; }
public double CarTrolleyMassKg { get; init; }
public double HeatLossFraction { get; init; } = 0.12;
}- В класс
HeatBalanceCalculations.csдобавьте перегрузку статического методаCalculate:
csharp
public static HeatBalanceResult Calculate(TunnelKilnInput input)
{
ArgumentNullException.ThrowIfNull(input);
if (input.MoistureOutMassFraction >= input.MoistureInMassFraction)
{
throw new ArgumentException("Конечная влажность должна быть строго меньше начальной влажности материала.");
}
// Расчёт массы испарённой влаги
double dryMassFlow = input.WetMaterialMassFlowKgPerHour * (1.0 - input.MoistureInMassFraction);
double evaporatedMoistureKgPerHour = input.WetMaterialMassFlowKgPerHour - (dryMassFlow / (1.0 - input.MoistureOutMassFraction));
// Подведённая теплота сгорания топлива
double heatInput = input.FuelMassFlowKgPerHour * input.FuelLowerHeatingValueKJPerKg;
// Полезно затраченная теплота на испарение и нагрев материала
double usefulHeat = evaporatedMoistureKgPerHour * 2490.0 + dryMassFlow * 1.25 * (input.MaterialTempOutC - input.MaterialTempInC);
// Потери тепла через ограждающие конструкции туннеля и с вагонетками
double losses = heatInput * input.HeatLossFraction + input.CarTrolleyMassKg * 0.50 * (input.MaterialTempOutC - 20.0);
// Невязка теплового баланса
double unaccounted = heatInput - usefulHeat - losses;
double efficiency = usefulHeat / heatInput;
return new HeatBalanceResult
{
UnitType = DryerUnitType.TunnelKiln,
TotalInputKJPerHour = heatInput,
UsefulHeatKJPerHour = usefulHeat,
LossesKJPerHour = losses,
UnaccountedKJPerHour = unaccounted,
Efficiency = efficiency
};
}2.2. Шаг 2: Создание модульных тестов (HeatBalance.Math.Tests)
В файле UnitTest1.cs создайте обязательный юнит-тест с проверкой сходимости баланса:
csharp
[Fact]
public void TunnelKiln_SampleCase_IsStable()
{
var input = new TunnelKilnInput
{
WetMaterialMassFlowKgPerHour = 2000,
MoistureInMassFraction = 0.25,
MoistureOutMassFraction = 0.05,
MaterialTempInC = 15,
MaterialTempOutC = 95,
FuelLowerHeatingValueKJPerKg = 41800,
FuelMassFlowKgPerHour = 45,
TunnelLengthMeters = 60,
CarTrolleyMassKg = 800,
HeatLossFraction = 0.12
};
var result = HeatBalanceCalculations.Calculate(input);
Assert.Equal(DryerUnitType.TunnelKiln, result.UnitType);
Assert.True(result.TotalInputKJPerHour > 0);
Assert.True(result.UsefulHeatKJPerHour > 0);
Assert.True(result.LossesKJPerHour > 0);
Assert.True(result.Efficiency > 0.0 && result.Efficiency < 1.0);
Assert.True(result.UnaccountedKJPerHour >= 0.0);
}Убедитесь в успешном прохождении тестов через консоль:
bash
dotnet test --filter "FullyQualifiedName~TunnelKiln"2.3. Шаг 3: Обновление контрактов системы (HeatBalance.Contracts)
- Откройте файл
src/Shared/HeatBalance.Contracts/CalculationType.csи дополните перечисление:
csharp
public enum CalculationType
{
ConveyorKiln = 1,
ChamberKiln = 2,
ElectricKiln = 3,
DrumDryer = 4,
TunnelKiln = 5 // Новый тип агрегата
}
public static class CalculationTypeExtensions
{
public static string ToRoutePrefix(this CalculationType type) => type switch
{
CalculationType.ConveyorKiln => "conveyor",
CalculationType.ChamberKiln => "chamber",
CalculationType.ElectricKiln => "electric",
CalculationType.DrumDryer => "drum",
CalculationType.TunnelKiln => "tunnel",
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
public static string ToDisplayNameRu(this CalculationType type) => type switch
{
CalculationType.ConveyorKiln => "Конвейерная сушильная печь",
CalculationType.ChamberKiln => "Камерная сушильная печь",
CalculationType.ElectricKiln => "Электрическая сушильная печь",
CalculationType.DrumDryer => "Сушильный барабан",
CalculationType.TunnelKiln => "Туннельная сушильная печь",
_ => type.ToString()
};
}- В
AuthConstants.csзарегистрируйте политику авторизацииAuthPolicies.TunnelCalculate = "tunnel:calculate".
2.4. Шаг 4: Разработка микросервиса (HeatBalance.TunnelKilnService)
- Создайте проект Web API в каталоге
src/Services/HeatBalance.TunnelKilnService:
bash
cd src/Services
dotnet new webapi -n HeatBalance.TunnelKilnService- Подключите зависимости в
.csproj:
xml
<ItemGroup>
<ProjectReference Include="..\..\Shared\HeatBalance.Contracts\HeatBalance.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\HeatBalance.ServiceDefaults\HeatBalance.ServiceDefaults.csproj" />
<ProjectReference Include="..\..\HeatBalance.Math\HeatBalance.Math.csproj" />
</ItemGroup>- Настройте файл
Program.cs:
csharp
using HeatBalance.Contracts;
using HeatBalance.ServiceDefaults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHeatBalanceApiDefaults();
builder.Services.AddHeatBalanceJwtAuth(builder.Configuration);
var app = builder.Build();
app.UseHeatBalanceApiDefaults();
app.MapGet("/", () => Results.Ok(new { service = "TunnelKiln", policy = "tunnel:calculate" }));
app.Run();- Создайте контроллер
Controllers/CalculateController.cs:
csharp
using HeatBalance.Math;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HeatBalance.TunnelKilnService.Controllers;
[ApiController]
[Route("api/v1")]
[AllowAnonymous]
public sealed class CalculateController : ControllerBase
{
[HttpPost("calculate")]
public ActionResult<HeatBalanceResult> Calculate([FromBody] TunnelKilnInput input)
{
try
{
return HeatBalanceCalculations.Calculate(input);
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
}- Создайте оптимизированный
Dockerfileдля сервиса:
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /app
COPY . .
RUN dotnet restore src/Services/HeatBalance.TunnelKilnService/HeatBalance.TunnelKilnService.csproj
RUN dotnet publish src/Services/HeatBalance.TunnelKilnService/HeatBalance.TunnelKilnService.csproj -c Release -o /out --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime
WORKDIR /app
COPY --from=build /out .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "HeatBalance.TunnelKilnService.dll"]2.5. Шаг 5: Маршрутизация в YARP Gateway
В файле src/Gateway/HeatBalance.Gateway/appsettings.json зарегистрируйте новый маршрут и кластер:
json
{
"ReverseProxy": {
"Routes": {
"tunnel": {
"ClusterId": "tunnel",
"Match": { "Path": "/api/tunnel/{**catch-all}" },
"Transforms": [ { "PathPattern": "/api/v1/{**catch-all}" } ]
}
},
"Clusters": {
"tunnel": {
"Destinations": {
"d1": { "Address": "http://tunnel:8080/" }
}
}
}
}
}В dev-режиме локального запуска адрес назначения меняется на
http://127.0.0.1:5105/.
2.6. Шаг 6: Интеграция в оркестрацию и CI/CD
- Docker Compose (
docker-compose.yml):
yaml
tunnel:
image: heatbalance-tunnel:latest
container_name: heatbalance-tunnel
restart: unless-stopped
networks:
- heatbalance_network
environment:
- ASPNETCORE_ENVIRONMENT=Production- Конвейер сборки (
Jenkinsfile): В стадиюDocker Build Servicesдобавьте команду:
groovy
sh "docker build -t heatbalance-tunnel:${DOCKER_TAG} -t heatbalance-tunnel:latest -f src/Services/HeatBalance.TunnelKilnService/Dockerfile ."- Скрипт локального запуска (
run-microservices.ps1): Добавьте сервис в массив$services:
powershell
@{ Name = "TunnelKiln"; Project = "src\Services\HeatBalance.TunnelKilnService\HeatBalance.TunnelKilnService.csproj"; Port = 5105 }2.7. Шаг 7: Разработка клиентского интерфейса (React SPA)
- В каталоге
frontend/src/pages/создайте компонент формы вводаTunnelKilnPage.jsxс компонентами Ant Design (Form,InputNumber,Card,Button,Table). - В файле
frontend/src/App.jsxдобавьте роут:
jsx
<Route path="/calculate/tunnel" element={<TunnelKilnPage />} />- Реализуйте отправку расчётного запроса на шлюз:
javascript
const response = await axios.post('/api/tunnel/calculate', payload);
setCalculationResult(response.data);3. Архитектурный чек-лист готовности модуля (Definition of Done)
Перед слиянием рабочей ветки с новым модулем в основную ветку main убедитесь в выполнении всех 10 обязательных критериев:
| № | Критерий готовности | Метод верификации |
|---|---|---|
| 1 | Математическая модель полностью изолирована в HeatBalance.Math. | Отсутствие внешних платформенных зависимостей в проекте библиотеки. |
| 2 | Сходимость теплового баланса покрыта модульными тестами xUnit. | dotnet test HeatBalance.Math.Tests выполняется без ошибок. |
| 3 | Перечисление CalculationType обновлено в контрактах. | Наличие строковых маппингов в CalculationTypeExtensions. |
| 4 | Контроллер микросервиса обрабатывает физические исключения и возвращает HTTP 400. | Проверка негативных сценариев через Swagger UI. |
| 5 | Маршрут зарегистрирован в конфигурации YARP API Gateway. | Успешный сквозной вызов POST /api/<type>/calculate. |
| 6 | Сборка Dockerfile протестирована локально. | docker build выполняется без предупреждений, образ протегирован. |
| 7 | Контейнер добавлен в docker-compose.yml и подключён к heatbalance_network. | docker compose config валидирует корректность синтаксиса YAML. |
| 8 | Сборка нового сервиса включена в декларативный конвейер Jenkinsfile. | Успешная тестовая сборка на сервере CI/CD. |
| 9 | Экранная форма React содержит валидацию числовых диапазонов. | Проверка ввода отрицательных чисел и граничных условий в браузере. |
| 10 | Экспорт печатных протоколов в QuestPDF поддерживает новый тип агрегата. | Успешная генерация файла .pdf со статьями нового теплового баланса. |