reportEngine:dcp报告支持

This commit is contained in:
zhugongping 2026-10-09 19:53:09 +08:00
parent a2f9285d29
commit 65da2d1cca
8 changed files with 201 additions and 16 deletions

View File

@ -570,17 +570,33 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
BigDecimal averageRate = rateCalculator.ratePerDay(stats, durationSeconds); BigDecimal averageRate = rateCalculator.ratePerDay(stats, durationSeconds);
BigDecimal maxSegmentRate = rateCalculator.maxSegmentRatePerDay(data); 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() return MetricPointData.builder()
.identifier(stats.getIdentifier()) .identifier(stats.getIdentifier())
.modelName(stats.getModelName()) .modelName(stats.getModelName())
.initialValue(stats.getInitial()) .initialValue(stats.getInitial())
.initialUnit(resolvedUnit) .initialUnit(resolvedUnit)
.initialTime(stats.getInitialTime()) .initialTime(stats.getInitialTime())
.previousValue(previous)
.previousUnit(resolvedUnit)
.currentValue(stats.getCurrent()) .currentValue(stats.getCurrent())
.currentUnit(resolvedUnit) .currentUnit(resolvedUnit)
.currentTime(stats.getCurrentTime()) .currentTime(stats.getCurrentTime())
.changeValue(stats.getDelta()) .changeValue(stats.getDelta())
.changeUnit(resolvedUnit) .changeUnit(resolvedUnit)
.periodChangeValue(periodChange)
.periodChangeUnit(resolvedUnit)
.cumulativeValue(cumulative)
.cumulativeUnit(resolvedUnit)
.changeRate(rate) .changeRate(rate)
.averageRate(averageRate) .averageRate(averageRate)
.maxSegmentRate(maxSegmentRate) .maxSegmentRate(maxSegmentRate)

View File

@ -10,6 +10,7 @@ import com.fastbee.report.period.TimeWindow;
import com.fastbee.report.threshold.ThingsThreshold; import com.fastbee.report.threshold.ThingsThreshold;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -39,6 +40,15 @@ public class ReportContext {
/** 时序数据 */ /** 时序数据 */
private List<MetricData> metricDataList; private List<MetricData> metricDataList;
/**
* 各物模型的「上期测值」(identifier → 窗口开始时间之前最近一条读数)
*
* <p>业主版式(DCP 三节)测点表的「上期测值 / 本期变化量」列依赖它,由
* {@code ReportDataProvider#loadPreviousValues} 回查装配。为 null 表示未加载
* (如完整版 V2 不需要,或该设备无历史上报)。</p>
*/
private Map<String, BigDecimal> previousValues;
/** /**
* 基准点设备的时序数据(device.baselineDeviceId 非空时加载) * 基准点设备的时序数据(device.baselineDeviceId 非空时加载)
* *

View File

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -100,6 +101,10 @@ public class ReportContextBuilder {
List<MetricData> metricDataList = dataProvider.loadHistory(device, models, window); List<MetricData> metricDataList = dataProvider.loadHistory(device, models, window);
// 上期测值(业主版式 DCP 三节测点表的「上期测值 / 本期变化量」列):
// 单独回查窗口开始时间之前的最近读数,与窗口内时序分开
Map<String, BigDecimal> previousValues = dataProvider.loadPreviousValues(device, models, window);
// 基准点设备数据:相对测量类设备(静力水准)需按时间对齐做差值换算 // 基准点设备数据:相对测量类设备(静力水准)需按时间对齐做差值换算
// deviceId 全局唯一,因此基准点可以属于另一个网关(跨网关参考点) // deviceId 全局唯一,因此基准点可以属于另一个网关(跨网关参考点)
List<MetricData> baselineMetricDataList = null; List<MetricData> baselineMetricDataList = null;
@ -141,6 +146,7 @@ public class ReportContextBuilder {
context.setThingsModels(models); context.setThingsModels(models);
context.setMetricDataList(metricDataList); context.setMetricDataList(metricDataList);
context.setBaselineMetricDataList(baselineMetricDataList); context.setBaselineMetricDataList(baselineMetricDataList);
context.setPreviousValues(previousValues);
context.setThresholds(thresholds); context.setThresholds(thresholds);
context.setExplicitPrimaryIdentifiers(explicitPrimaryIdentifiers); context.setExplicitPrimaryIdentifiers(explicitPrimaryIdentifiers);
context.setReportNo(reportNo); context.setReportNo(reportNo);

View File

@ -81,6 +81,61 @@ public class ReportDataProvider {
return result; return result;
} }
/**
* 加载各物模型的「上期测值」:统计窗口开始时间之前最近一条读数
*
* <p>业主版式(DCP 三节)测点表需要「上期测值」「本期变化量(相对上期)」两列,
* 而 {@link #loadHistory} 只查窗口内数据,上期值必须往回单独取一次。
* 语义:{@code 上期测值} = 上一个统计周期末(即本窗口开始前)最近一次上报值。</p>
*
* <p>往回查 90 天足够覆盖上期值场景(报告周期最长月报);设备停测超过 90 天则取不到,
* 对应测点该列留空,不视为错误。</p>
*
* <p>{@code listHistory} 返回 {@code order by create_time desc},故按 identifier
* {@code putIfAbsent} 即天然取到每个物模型最近一条,无需再排序。</p>
*
* @param device 设备
* @param models 监测物模型
* @param window 统计窗口(上期值取 startTime 之前的最近一条)
* @return identifier → 上期测值(无上期数据的物模型不出现在结果中)
*/
public Map<String, BigDecimal> loadPreviousValues(Device device, List<ThingsModel> models, TimeWindow window) {
Map<String, BigDecimal> result = new LinkedHashMap<>();
if (window == null || window.getStartTime() == null) {
return result;
}
List<String> 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<HistoryModel> 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) { private BigDecimal parseNumeric(String value) {
if (StringUtils.isEmpty(value)) { if (StringUtils.isEmpty(value)) {
return null; return null;

View File

@ -50,6 +50,18 @@ public class MetricPointData {
/** 初始观测时间(初始值对应数据的上报时间) */ /** 初始观测时间(初始值对应数据的上报时间) */
private Date initialTime; private Date initialTime;
/**
* 上期测值(上一个统计周期末的读数,窗口开始时间之前最近一条)
*
* <p>业主版式(DCP 三节)测点表专列。由 {@code ReportDataProvider#loadPreviousValues}
* 单独回查,与窗口内 {@link #initialValue}(窗口首条)语义不同:initial 是「本窗口最早」,
* 上期是「本窗口之前最近」。</p>
*/
private BigDecimal previousValue;
/** 上期测值单位 */
private String previousUnit;
/** 当前值 */ /** 当前值 */
private BigDecimal currentValue; private BigDecimal currentValue;
@ -59,12 +71,38 @@ public class MetricPointData {
/** 本次观测时间 */ /** 本次观测时间 */
private Date currentTime; private Date currentTime;
/** 变化量(当前 - 初始) */ /**
* 变化量(当前 - 初始,即「累计变化量」语义)
*
* <p>注意:完整版 V2 报告沿用此口径(当前相对初始)。业主版式的「本期变化量」是
* 「当前 - 上期」,落在 {@link #periodChangeValue},两者不可混用。</p>
*/
private BigDecimal changeValue; private BigDecimal changeValue;
/** 变化量单位 */ /** 变化量单位 */
private String changeUnit; private String changeUnit;
/**
* 本期变化量(当前 - 上期测值),业主版式专列
*
* <p>上期测值缺失时本字段为 null,渲染层该列显示「-」。</p>
*/
private BigDecimal periodChangeValue;
/** 本期变化量单位 */
private String periodChangeUnit;
/**
* 累计变化量(当前 - 初始),业主版式专列
*
* <p>数值上等于 {@link #changeValue},但作为业主版式的独立语义列呈现,
* 与「本期变化量」并列,便于一眼看出「本周期变了多少」与「从初始累计变了多少」。</p>
*/
private BigDecimal cumulativeValue;
/** 累计变化量单位 */
private String cumulativeUnit;
/** /**
* 变化速率(主口径):一元线性回归斜率,单位/天 * 变化速率(主口径):一元线性回归斜率,单位/天
* *

View File

@ -576,13 +576,24 @@ public class WordRenderer implements ReportRenderer {
header.add("断面"); header.add("断面");
} }
header.add("测点编号"); header.add("测点编号");
if (!primaryOnly) {
// 完整版保留「测点名称」列;精简版(DCP 业主格式)无此列
header.add("测点名称"); header.add("测点名称");
}
for (String identifier : orderedIdentifiers) { for (String identifier : orderedIdentifiers) {
MetricPointData sample = findSample(points, identifier); MetricPointData sample = findSample(points, identifier);
String name = sample == null ? identifier : nvl(sample.getModelName()); String name = sample == null ? identifier : nvl(sample.getModelName());
String unit = sample == null ? "" : nvl(sample.getCurrentUnit()); String unit = sample == null ? "" : nvl(sample.getCurrentUnit());
boolean primary = primarySet.contains(identifier); 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)); 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(name + "·本次值(变化量)" + unitSuffix(unit));
} }
} }
if (primaryOnly) {
// 精简版(DCP 业主格式)尾部列序:监测结论在前,初始观测时间在后
header.add("监测结论");
header.add("初始观测时间");
} else {
header.add("初始观测时间"); header.add("初始观测时间");
header.add("监测结论"); header.add("监测结论");
}
Tables.TableBuilder builder = Tables.ofA4Width().center(); Tables.TableBuilder builder = Tables.ofA4Width().center();
// 表头字号小于数据行,减少长表头(如"设备电压·本次值(变化量)(V)")的折行行数 // 表头字号小于数据行,减少长表头(如"设备电压·本次值(变化量)(V)")的折行行数
@ -613,20 +630,32 @@ public class WordRenderer implements ReportRenderer {
row.add(nvl(first.getSectionName())); row.add(nvl(first.getSectionName()));
} }
row.add(nvl(first.getPointNo())); row.add(nvl(first.getPointNo()));
if (!primaryOnly) {
row.add(nvl(first.getPointName())); row.add(nvl(first.getPointName()));
}
for (String identifier : orderedIdentifiers) { for (String identifier : orderedIdentifiers) {
MetricPointData point = byIdentifier.get(identifier); MetricPointData point = byIdentifier.get(identifier);
if (point == null) { if (point == null) {
row.add("-"); row.add("-");
if (primarySet.contains(identifier)) { if (primarySet.contains(identifier)) {
// 主指标:精简版 6 列、完整版 4 列,补足占位
int extraCols = primaryOnly ? 5 : 3;
for (int i = 0; i < extraCols; i++) {
row.add("-"); row.add("-");
row.add("-"); }
row.add("-");
} }
continue; 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.getInitialValue(), category));
row.add(strForTable(point.getCurrentValue(), category)); row.add(strForTable(point.getCurrentValue(), category));
row.add(strForTable(point.getChangeValue(), 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(mergedValue(point.getCurrentValue(), point.getChangeValue(), category));
} }
} }
if (primaryOnly) {
row.add(worstStatusText(rowPoints, primaryIdentifier));
row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier)));
} else {
row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier))); row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier)));
row.add(worstStatusText(rowPoints, primaryIdentifier)); row.add(worstStatusText(rowPoints, primaryIdentifier));
}
builder.addRow(Rows.of(row.toArray(new String[0])).center().textFontSize(9).create()); builder.addRow(Rows.of(row.toArray(new String[0])).center().textFontSize(9).create());
} }
return builder.create(); return builder.create();

View File

@ -202,6 +202,26 @@ public class WordRendererDcpReportTemplateTest {
assertTrue(!xml.contains("结论内容"), "DCP 版式不应有结论表格"); 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 文本 */ /** 从 docx 字节中读出 document.xml 文本 */
private String readDocumentXml(byte[] docx) throws Exception { private String readDocumentXml(byte[] docx) throws Exception {
try (java.util.zip.ZipInputStream zin = new java.util.zip.ZipInputStream( try (java.util.zip.ZipInputStream zin = new java.util.zip.ZipInputStream(
@ -357,10 +377,16 @@ public class WordRendererDcpReportTemplateTest {
.pointName("裂缝计_1") .pointName("裂缝计_1")
.initialValue(new BigDecimal("0.350")) .initialValue(new BigDecimal("0.350"))
.initialUnit("mm") .initialUnit("mm")
.previousValue(new BigDecimal("0.352"))
.previousUnit("mm")
.currentValue(new BigDecimal("0.382")) .currentValue(new BigDecimal("0.382"))
.currentUnit("mm") .currentUnit("mm")
.changeValue(new BigDecimal("0.032")) .changeValue(new BigDecimal("0.032"))
.changeUnit("mm") .changeUnit("mm")
.periodChangeValue(new BigDecimal("0.030"))
.periodChangeUnit("mm")
.cumulativeValue(new BigDecimal("0.032"))
.cumulativeUnit("mm")
.changeRate(new BigDecimal("0.0008")) .changeRate(new BigDecimal("0.0008"))
.rateUnit("mm/d") .rateUnit("mm/d")
.maxValue(new BigDecimal("0.405")) .maxValue(new BigDecimal("0.405"))

View File

@ -337,13 +337,13 @@ export default {
// 2. 数据转换(自己处理) // 2. 数据转换(自己处理)
msgContext.logger.info("数据转换处理") msgContext.logger.info("数据转换处理")
String NewTopic = topic; String newTopic = topic;
String NewPayload = payload; String newPayload = payload;
// 3. 返回新的数据(必要) // 3. 返回新的数据(必要)
msgContext.setTopic(NewTopic); msgContext.setTopic(newTopic);
msgContext.setPayload(NewPayload);`, msgContext.setPayload(newPayload);`,
}; };
this.resetForm('form'); this.resetForm('form');
}, },