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 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)

View File

@ -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<MetricData> metricDataList;
/**
* 各物模型的「上期测值」(identifier → 窗口开始时间之前最近一条读数)
*
* <p>业主版式(DCP 三节)测点表的「上期测值 / 本期变化量」列依赖它,由
* {@code ReportDataProvider#loadPreviousValues} 回查装配。为 null 表示未加载
* (如完整版 V2 不需要,或该设备无历史上报)。</p>
*/
private Map<String, BigDecimal> previousValues;
/**
* 基准点设备的时序数据(device.baselineDeviceId 非空时加载)
*

View File

@ -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<MetricData> metricDataList = dataProvider.loadHistory(device, models, window);
// 上期测值(业主版式 DCP 三节测点表的「上期测值 / 本期变化量」列):
// 单独回查窗口开始时间之前的最近读数,与窗口内时序分开
Map<String, BigDecimal> previousValues = dataProvider.loadPreviousValues(device, models, window);
// 基准点设备数据:相对测量类设备(静力水准)需按时间对齐做差值换算
// deviceId 全局唯一,因此基准点可以属于另一个网关(跨网关参考点)
List<MetricData> 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);

View File

@ -81,6 +81,61 @@ public class ReportDataProvider {
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) {
if (StringUtils.isEmpty(value)) {
return null;

View File

@ -50,6 +50,18 @@ public class MetricPointData {
/** 初始观测时间(初始值对应数据的上报时间) */
private Date initialTime;
/**
* 上期测值(上一个统计周期末的读数,窗口开始时间之前最近一条)
*
* <p>业主版式(DCP 三节)测点表专列。由 {@code ReportDataProvider#loadPreviousValues}
* 单独回查,与窗口内 {@link #initialValue}(窗口首条)语义不同:initial 是「本窗口最早」,
* 上期是「本窗口之前最近」。</p>
*/
private BigDecimal previousValue;
/** 上期测值单位 */
private String previousUnit;
/** 当前值 */
private BigDecimal currentValue;
@ -59,12 +71,38 @@ public class MetricPointData {
/** 本次观测时间 */
private Date currentTime;
/** 变化量(当前 - 初始) */
/**
* 变化量(当前 - 初始,即「累计变化量」语义)
*
* <p>注意:完整版 V2 报告沿用此口径(当前相对初始)。业主版式的「本期变化量」是
* 「当前 - 上期」,落在 {@link #periodChangeValue},两者不可混用。</p>
*/
private BigDecimal changeValue;
/** 变化量单位 */
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("测点名称");
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();

View File

@ -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"))

View File

@ -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');
},