From 65da2d1ccad2938f1e02a0d4c963433a2d99c277 Mon Sep 17 00:00:00 2001 From: zhugongping Date: Fri, 9 Oct 2026 19:53:09 +0800 Subject: [PATCH] =?UTF-8?q?reportEngine=EF=BC=9Adcp=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analyzer/AbstractDeviceAnalyzer.java | 16 ++++++ .../fastbee/report/core/ReportContext.java | 10 ++++ .../report/core/ReportContextBuilder.java | 6 ++ .../report/data/ReportDataProvider.java | 55 ++++++++++++++++++ .../fastbee/report/model/MetricPointData.java | 40 ++++++++++++- .../fastbee/report/renderer/WordRenderer.java | 56 +++++++++++++++---- .../WordRendererDcpReportTemplateTest.java | 26 +++++++++ yack-web/src/views/iot/scene/script.vue | 8 +-- 8 files changed, 201 insertions(+), 16 deletions(-) diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java index fa23ee3d..06da3d09 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java @@ -570,17 +570,33 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer { BigDecimal averageRate = rateCalculator.ratePerDay(stats, durationSeconds); BigDecimal maxSegmentRate = rateCalculator.maxSegmentRatePerDay(data); + // 上期测值:业主版式(DCP 三节)的「上期测值 / 本期变化量」列,从 context 回查 + BigDecimal previous = context.getPreviousValues() == null + ? null : context.getPreviousValues().get(stats.getIdentifier()); + // 本期变化量 = 当前 - 上期(上期缺失时为 null,渲染层显示「-」) + BigDecimal periodChange = (previous == null || stats.getCurrent() == null) + ? null : stats.getCurrent().subtract(previous); + // 累计变化量 = 当前 - 初始(数值同 changeValue,业主版式独立成列) + BigDecimal cumulative = (stats.getCurrent() == null || stats.getInitial() == null) + ? null : stats.getCurrent().subtract(stats.getInitial()); + return MetricPointData.builder() .identifier(stats.getIdentifier()) .modelName(stats.getModelName()) .initialValue(stats.getInitial()) .initialUnit(resolvedUnit) .initialTime(stats.getInitialTime()) + .previousValue(previous) + .previousUnit(resolvedUnit) .currentValue(stats.getCurrent()) .currentUnit(resolvedUnit) .currentTime(stats.getCurrentTime()) .changeValue(stats.getDelta()) .changeUnit(resolvedUnit) + .periodChangeValue(periodChange) + .periodChangeUnit(resolvedUnit) + .cumulativeValue(cumulative) + .cumulativeUnit(resolvedUnit) .changeRate(rate) .averageRate(averageRate) .maxSegmentRate(maxSegmentRate) diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java index e9c959ec..5cbf4533 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContext.java @@ -10,6 +10,7 @@ import com.fastbee.report.period.TimeWindow; import com.fastbee.report.threshold.ThingsThreshold; import lombok.Data; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,6 +40,15 @@ public class ReportContext { /** 时序数据 */ private List metricDataList; + /** + * 各物模型的「上期测值」(identifier → 窗口开始时间之前最近一条读数) + * + *

业主版式(DCP 三节)测点表的「上期测值 / 本期变化量」列依赖它,由 + * {@code ReportDataProvider#loadPreviousValues} 回查装配。为 null 表示未加载 + * (如完整版 V2 不需要,或该设备无历史上报)。

+ */ + private Map previousValues; + /** * 基准点设备的时序数据(device.baselineDeviceId 非空时加载) * diff --git a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java index 540c3853..189d3fc1 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java +++ b/fastbee-report/src/main/java/com/fastbee/report/core/ReportContextBuilder.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.math.BigDecimal; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -100,6 +101,10 @@ public class ReportContextBuilder { List metricDataList = dataProvider.loadHistory(device, models, window); + // 上期测值(业主版式 DCP 三节测点表的「上期测值 / 本期变化量」列): + // 单独回查窗口开始时间之前的最近读数,与窗口内时序分开 + Map previousValues = dataProvider.loadPreviousValues(device, models, window); + // 基准点设备数据:相对测量类设备(静力水准)需按时间对齐做差值换算 // deviceId 全局唯一,因此基准点可以属于另一个网关(跨网关参考点) List baselineMetricDataList = null; @@ -141,6 +146,7 @@ public class ReportContextBuilder { context.setThingsModels(models); context.setMetricDataList(metricDataList); context.setBaselineMetricDataList(baselineMetricDataList); + context.setPreviousValues(previousValues); context.setThresholds(thresholds); context.setExplicitPrimaryIdentifiers(explicitPrimaryIdentifiers); context.setReportNo(reportNo); diff --git a/fastbee-report/src/main/java/com/fastbee/report/data/ReportDataProvider.java b/fastbee-report/src/main/java/com/fastbee/report/data/ReportDataProvider.java index af8c510b..5271418f 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/data/ReportDataProvider.java +++ b/fastbee-report/src/main/java/com/fastbee/report/data/ReportDataProvider.java @@ -81,6 +81,61 @@ public class ReportDataProvider { return result; } + /** + * 加载各物模型的「上期测值」:统计窗口开始时间之前最近一条读数 + * + *

业主版式(DCP 三节)测点表需要「上期测值」「本期变化量(相对上期)」两列, + * 而 {@link #loadHistory} 只查窗口内数据,上期值必须往回单独取一次。 + * 语义:{@code 上期测值} = 上一个统计周期末(即本窗口开始前)最近一次上报值。

+ * + *

往回查 90 天足够覆盖上期值场景(报告周期最长月报);设备停测超过 90 天则取不到, + * 对应测点该列留空,不视为错误。

+ * + *

{@code listHistory} 返回 {@code order by create_time desc},故按 identifier + * {@code putIfAbsent} 即天然取到每个物模型最近一条,无需再排序。

+ * + * @param device 设备 + * @param models 监测物模型 + * @param window 统计窗口(上期值取 startTime 之前的最近一条) + * @return identifier → 上期测值(无上期数据的物模型不出现在结果中) + */ + public Map loadPreviousValues(Device device, List models, TimeWindow window) { + Map result = new LinkedHashMap<>(); + if (window == null || window.getStartTime() == null) { + return result; + } + List identifiers = models.stream() + .filter(m -> Objects.equals(m.getType(), 1)) + .map(ThingsModel::getIdentifier) + .filter(StringUtils::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + if (identifiers.isEmpty()) { + return result; + } + + DeviceLog query = new DeviceLog(); + query.setSerialNumber(device.getSerialNumber() == null ? null : device.getSerialNumber().toUpperCase()); + query.setIdentityList(identifiers); + // 上期值严格早于 startTime:endTime 取 startTime 前一秒,避免把窗口首条数据当成上期值 + query.setBeginTime(format(window.getStartTime().minusDays(90))); + query.setEndTime(format(window.getStartTime().minusSeconds(1))); + + List historyList = deviceLogService.listHistory(query); + for (HistoryModel history : historyList) { + if (history.getIdentity() == null || history.getValue() == null) { + continue; + } + BigDecimal value = parseNumeric(history.getValue()); + if (value == null) { + continue; + } + // desc 排序,首个即最近一条 + result.putIfAbsent(history.getIdentity(), value); + } + return result; + } + private BigDecimal parseNumeric(String value) { if (StringUtils.isEmpty(value)) { return null; diff --git a/fastbee-report/src/main/java/com/fastbee/report/model/MetricPointData.java b/fastbee-report/src/main/java/com/fastbee/report/model/MetricPointData.java index d40f819b..9b8a9aca 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/model/MetricPointData.java +++ b/fastbee-report/src/main/java/com/fastbee/report/model/MetricPointData.java @@ -50,6 +50,18 @@ public class MetricPointData { /** 初始观测时间(初始值对应数据的上报时间) */ private Date initialTime; + /** + * 上期测值(上一个统计周期末的读数,窗口开始时间之前最近一条) + * + *

业主版式(DCP 三节)测点表专列。由 {@code ReportDataProvider#loadPreviousValues} + * 单独回查,与窗口内 {@link #initialValue}(窗口首条)语义不同:initial 是「本窗口最早」, + * 上期是「本窗口之前最近」。

+ */ + private BigDecimal previousValue; + + /** 上期测值单位 */ + private String previousUnit; + /** 当前值 */ private BigDecimal currentValue; @@ -59,12 +71,38 @@ public class MetricPointData { /** 本次观测时间 */ private Date currentTime; - /** 变化量(当前 - 初始) */ + /** + * 变化量(当前 - 初始,即「累计变化量」语义) + * + *

注意:完整版 V2 报告沿用此口径(当前相对初始)。业主版式的「本期变化量」是 + * 「当前 - 上期」,落在 {@link #periodChangeValue},两者不可混用。

+ */ private BigDecimal changeValue; /** 变化量单位 */ private String changeUnit; + /** + * 本期变化量(当前 - 上期测值),业主版式专列 + * + *

上期测值缺失时本字段为 null,渲染层该列显示「-」。

+ */ + private BigDecimal periodChangeValue; + + /** 本期变化量单位 */ + private String periodChangeUnit; + + /** + * 累计变化量(当前 - 初始),业主版式专列 + * + *

数值上等于 {@link #changeValue},但作为业主版式的独立语义列呈现, + * 与「本期变化量」并列,便于一眼看出「本周期变了多少」与「从初始累计变了多少」。

+ */ + private BigDecimal cumulativeValue; + + /** 累计变化量单位 */ + private String cumulativeUnit; + /** * 变化速率(主口径):一元线性回归斜率,单位/天 * diff --git a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java index a90f9df9..5c1d88e1 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java @@ -576,13 +576,24 @@ public class WordRenderer implements ReportRenderer { header.add("断面"); } header.add("测点编号"); - header.add("测点名称"); + if (!primaryOnly) { + // 完整版保留「测点名称」列;精简版(DCP 业主格式)无此列 + header.add("测点名称"); + } for (String identifier : orderedIdentifiers) { MetricPointData sample = findSample(points, identifier); String name = sample == null ? identifier : nvl(sample.getModelName()); String unit = sample == null ? "" : nvl(sample.getCurrentUnit()); boolean primary = primarySet.contains(identifier); - if (primary) { + if (primary && primaryOnly) { + // 精简版(DCP 业主格式)主指标列:初始值 / 上期测值 / 本期测值 / 本期变化量 / 累计变化量 / 变化速率 + header.add(name + "·初始值" + unitSuffix(unit)); + header.add(name + "·上期测值" + unitSuffix(unit)); + header.add(name + "·本期测值" + unitSuffix(unit)); + header.add(name + "·本期变化量" + unitSuffix(unit)); + header.add(name + "·累计变化量" + unitSuffix(unit)); + header.add(name + "·变化速率" + unitSuffix(sample == null ? "" : nvl(sample.getRateUnit()))); + } else if (primary) { header.add(name + "·初始值" + unitSuffix(unit)); header.add(name + "·本次值" + unitSuffix(unit)); header.add(name + "·变化量" + unitSuffix(unit)); @@ -592,8 +603,14 @@ public class WordRenderer implements ReportRenderer { header.add(name + "·本次值(变化量)" + unitSuffix(unit)); } } - header.add("初始观测时间"); - header.add("监测结论"); + if (primaryOnly) { + // 精简版(DCP 业主格式)尾部列序:监测结论在前,初始观测时间在后 + header.add("监测结论"); + header.add("初始观测时间"); + } else { + header.add("初始观测时间"); + header.add("监测结论"); + } Tables.TableBuilder builder = Tables.ofA4Width().center(); // 表头字号小于数据行,减少长表头(如"设备电压·本次值(变化量)(V)")的折行行数 @@ -613,20 +630,32 @@ public class WordRenderer implements ReportRenderer { row.add(nvl(first.getSectionName())); } row.add(nvl(first.getPointNo())); - row.add(nvl(first.getPointName())); + if (!primaryOnly) { + row.add(nvl(first.getPointName())); + } for (String identifier : orderedIdentifiers) { MetricPointData point = byIdentifier.get(identifier); if (point == null) { row.add("-"); if (primarySet.contains(identifier)) { - row.add("-"); - row.add("-"); - row.add("-"); + // 主指标:精简版 6 列、完整版 4 列,补足占位 + int extraCols = primaryOnly ? 5 : 3; + for (int i = 0; i < extraCols; i++) { + row.add("-"); + } } continue; } - if (primarySet.contains(identifier)) { + if (primarySet.contains(identifier) && primaryOnly) { + // 精简版(DCP 业主格式):初始/上期/本期/本期变化/累计变化/速率 + row.add(strForTable(point.getInitialValue(), category)); + row.add(strForTable(point.getPreviousValue(), category)); + row.add(strForTable(point.getCurrentValue(), category)); + row.add(strForTable(point.getPeriodChangeValue(), category)); + row.add(strForTable(point.getCumulativeValue(), category)); + row.add(strForTable(point.getChangeRate(), category)); + } else if (primarySet.contains(identifier)) { row.add(strForTable(point.getInitialValue(), category)); row.add(strForTable(point.getCurrentValue(), category)); row.add(strForTable(point.getChangeValue(), category)); @@ -636,8 +665,13 @@ public class WordRenderer implements ReportRenderer { row.add(mergedValue(point.getCurrentValue(), point.getChangeValue(), category)); } } - row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier))); - row.add(worstStatusText(rowPoints, primaryIdentifier)); + if (primaryOnly) { + row.add(worstStatusText(rowPoints, primaryIdentifier)); + row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier))); + } else { + row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier))); + row.add(worstStatusText(rowPoints, primaryIdentifier)); + } builder.addRow(Rows.of(row.toArray(new String[0])).center().textFontSize(9).create()); } return builder.create(); diff --git a/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java b/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java index 735b1404..a47e696c 100644 --- a/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java +++ b/fastbee-report/src/test/java/com/fastbee/report/renderer/WordRendererDcpReportTemplateTest.java @@ -202,6 +202,26 @@ public class WordRendererDcpReportTemplateTest { assertTrue(!xml.contains("结论内容"), "DCP 版式不应有结论表格"); } + /** DCP 版式(业主格式):主指标按「初始/上期/本期/本期变化/累计变化/速率」六列,无「测点名称」 */ + @Test + public void testDcpReportOwnerColumns() throws Exception { + ReportModel model = buildModel(Arrays.asList("crackWidth")); + byte[] docx = newRenderer().render(model); + String xml = readDocumentXml(docx); + + // 业主六列齐全 + assertTrue(xml.contains("裂缝宽度·初始值(mm)"), "应有初始值列"); + assertTrue(xml.contains("裂缝宽度·上期测值(mm)"), "应有上期测值列"); + assertTrue(xml.contains("裂缝宽度·本期测值(mm)"), "应有本期测值列"); + assertTrue(xml.contains("裂缝宽度·本期变化量(mm)"), "应有本期变化量列"); + assertTrue(xml.contains("裂缝宽度·累计变化量(mm)"), "应有累计变化量列"); + assertTrue(xml.contains("裂缝宽度·变化速率(mm/d)"), "应有变化速率列"); + // 业主格式无「测点名称」列,且旧版「·本次值」「·变化量」列名不再出现 + assertTrue(!xml.contains("测点名称"), "业主格式不应有测点名称列"); + assertTrue(!xml.contains("裂缝宽度·本次值"), "业主格式不再用「本次值」列名"); + assertTrue(!xml.contains("裂缝宽度·变化量"), "业主格式不再用「变化量」列名(改为本期变化量/累计变化量)"); + } + /** 从 docx 字节中读出 document.xml 文本 */ private String readDocumentXml(byte[] docx) throws Exception { try (java.util.zip.ZipInputStream zin = new java.util.zip.ZipInputStream( @@ -357,10 +377,16 @@ public class WordRendererDcpReportTemplateTest { .pointName("裂缝计_1") .initialValue(new BigDecimal("0.350")) .initialUnit("mm") + .previousValue(new BigDecimal("0.352")) + .previousUnit("mm") .currentValue(new BigDecimal("0.382")) .currentUnit("mm") .changeValue(new BigDecimal("0.032")) .changeUnit("mm") + .periodChangeValue(new BigDecimal("0.030")) + .periodChangeUnit("mm") + .cumulativeValue(new BigDecimal("0.032")) + .cumulativeUnit("mm") .changeRate(new BigDecimal("0.0008")) .rateUnit("mm/d") .maxValue(new BigDecimal("0.405")) diff --git a/yack-web/src/views/iot/scene/script.vue b/yack-web/src/views/iot/scene/script.vue index fef50256..e61fe0cf 100644 --- a/yack-web/src/views/iot/scene/script.vue +++ b/yack-web/src/views/iot/scene/script.vue @@ -337,13 +337,13 @@ export default { // 2. 数据转换(自己处理) msgContext.logger.info("数据转换处理") - String NewTopic = topic; - String NewPayload = payload; + String newTopic = topic; + String newPayload = payload; // 3. 返回新的数据(必要) - msgContext.setTopic(NewTopic); - msgContext.setPayload(NewPayload);`, + msgContext.setTopic(newTopic); + msgContext.setPayload(newPayload);`, }; this.resetForm('form'); },