This commit is contained in:
GukSang.Jin
2026-05-07 16:36:24 +08:00
parent 246e16d45d
commit 32f56a8710
2 changed files with 91 additions and 27 deletions

View File

@@ -114,7 +114,7 @@ public sealed class ExperimentDataService : IExperimentDataService
{
if (double.IsFinite(snapshot.HeatReleaseRate))
{
_accumulatedTotalHeatRelease += snapshot.HeatReleaseRate * deltaSeconds;
_accumulatedTotalHeatRelease += snapshot.HeatReleaseRate * deltaSeconds / 1000;
}
if (double.IsFinite(snapshot.SmokeProduction))

View File

@@ -44,12 +44,14 @@ public sealed class NpoiReportExportService : IReportExportService
public void Export(string outputPath, ReportInput input, IReadOnlyList<RealtimeDataRecord> records)
{
var exportRecords = records.Where(IsValidExperimentRecord).ToList();
var exportRecords = PrepareExportRecords(records);
if (exportRecords.Count == 0)
{
throw new InvalidOperationException("请先点击“测试开始”,产生有效实验数据后再导出报表。");
}
ValidateExportRecords(exportRecords);
var templatePath = FindTemplatePath();
using var templateStream = File.OpenRead(templatePath);
var workbook = new HSSFWorkbook(templateStream);
@@ -57,11 +59,21 @@ public sealed class NpoiReportExportService : IReportExportService
FillReportSheet(workbook.GetSheet("Result_ISO"), "Result_ISO", input, exportRecords);
FillReportSheet(workbook.GetSheet("Result_FTP"), "Result_FTP", input, exportRecords);
FillDataSheet(workbook.GetSheet("Data") ?? workbook.CreateSheet("Data"), exportRecords);
ValidateWorkbookDataSheet(workbook, exportRecords.Count);
using var outputStream = File.Create(outputPath);
workbook.Write(outputStream);
}
private static List<RealtimeDataRecord> PrepareExportRecords(IReadOnlyList<RealtimeDataRecord> records)
{
return records
.Where(IsValidExperimentRecord)
.OrderBy(record => record.TestSeconds)
.ThenBy(record => record.Timestamp)
.ToList();
}
private static void FillReportSheet(
ISheet? sheet,
string sheetName,
@@ -147,13 +159,12 @@ public sealed class NpoiReportExportService : IReportExportService
headerRow.CreateCell(i).SetCellValue(DataHeaders[i]);
}
var figra = CalculatePeakGrowthIndex(records, record => record.HeatReleaseRate, 1000);
var smogra = CalculatePeakGrowthIndex(records, record => record.SmokeProduction, 1);
for (var i = 0; i < records.Count; i++)
{
var row = sheet.CreateRow(i + 1);
var record = records[i];
var figra = CalculateGrowthIndex(record.TestSeconds, record.HeatReleaseRate, 1000);
var smogra = CalculateGrowthIndex(record.TestSeconds, record.SmokeProduction, 1);
// Keep columns A-G fixed; the Excel template charts reference these positions.
SetNumeric(row, 0, record.TestSeconds >= 0 ? record.TestSeconds : double.NaN);
SetNumeric(row, 1, record.HeatReleaseRate);
@@ -191,32 +202,85 @@ public sealed class NpoiReportExportService : IReportExportService
}
}
private static double CalculatePeakGrowthIndex(
IReadOnlyList<RealtimeDataRecord> records,
Func<RealtimeDataRecord, double> selector,
double multiplier)
private static double CalculateGrowthIndex(int testSeconds, double value, double multiplier)
{
double? peak = null;
var peakSeconds = 0;
foreach (var record in records)
if (testSeconds <= 0 || !double.IsFinite(value) || value < 0)
{
var value = selector(record);
if (record.TestSeconds <= 0 || !double.IsFinite(value) || value < 0)
{
continue;
}
if (!peak.HasValue || value > peak.Value)
{
peak = value;
peakSeconds = record.TestSeconds;
}
return double.NaN;
}
return peak.HasValue && peakSeconds > 0
? peak.Value * multiplier / peakSeconds
: double.NaN;
return value * multiplier / testSeconds;
}
private static void ValidateExportRecords(IReadOnlyList<RealtimeDataRecord> records)
{
var previousSeconds = -1;
foreach (var record in records)
{
if (record.TestSeconds < 0)
{
throw new InvalidOperationException("导出数据时间列包含负数,无法生成准确曲线。");
}
if (record.TestSeconds < previousSeconds)
{
throw new InvalidOperationException("导出数据时间列存在倒退,无法生成准确曲线。");
}
previousSeconds = record.TestSeconds;
}
RequireAnyFiniteCurveValue(records, "热释放", record => record.HeatReleaseRate);
RequireAnyFiniteCurveValue(records, "产烟率", record => record.SmokeProduction);
RequireAnyFiniteCurveValue(records, "总热释放", record => record.TotalHeatRelease);
RequireAnyFiniteCurveValue(records, "总产烟量", record => record.TotalSmoke);
}
private static void RequireAnyFiniteCurveValue(
IReadOnlyList<RealtimeDataRecord> records,
string label,
Func<RealtimeDataRecord, double> selector)
{
if (!records.Any(record => double.IsFinite(selector(record))))
{
throw new InvalidOperationException($"导出数据缺少有效曲线列:{label}。请确认实验数据采集正常。");
}
}
private static void ValidateWorkbookDataSheet(IWorkbook workbook, int expectedDataRows)
{
var sheet = workbook.GetSheet("Data")
?? throw new InvalidOperationException("导出报表缺少 Data 工作表,曲线无法显示。");
if (sheet.LastRowNum < expectedDataRows)
{
throw new InvalidOperationException("导出报表 Data 工作表行数不足,曲线数据不完整。");
}
for (var columnIndex = 0; columnIndex <= 6; columnIndex++)
{
var header = sheet.GetRow(0)?.GetCell(columnIndex);
if (CellText(header) != DataHeaders[columnIndex])
{
throw new InvalidOperationException($"导出报表 Data 工作表缺少曲线列:{DataHeaders[columnIndex]}。");
}
var hasNumericValue = false;
for (var rowIndex = 1; rowIndex <= expectedDataRows; rowIndex++)
{
var cell = sheet.GetRow(rowIndex)?.GetCell(columnIndex);
if (cell?.CellType == CellType.Numeric && double.IsFinite(cell.NumericCellValue))
{
hasNumericValue = true;
break;
}
}
if (!hasNumericValue)
{
throw new InvalidOperationException($"导出报表 Data 工作表曲线列没有有效数值:{DataHeaders[columnIndex]}。");
}
}
}
private static bool SetValueBesideLabel(ISheet sheet, string label, string value)