reportEngine:dcp报告支持
This commit is contained in:
parent
a2f9285d29
commit
65da2d1cca
@ -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)
|
||||
|
||||
@ -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 非空时加载)
|
||||
*
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 变化速率(主口径):一元线性回归斜率,单位/天
|
||||
*
|
||||
|
||||
@ -576,13 +576,24 @@ public class WordRenderer implements ReportRenderer {
|
||||
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));
|
||||
}
|
||||
}
|
||||
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()));
|
||||
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)) {
|
||||
// 主指标:精简版 6 列、完整版 4 列,补足占位
|
||||
int extraCols = primaryOnly ? 5 : 3;
|
||||
for (int i = 0; i < extraCols; i++) {
|
||||
row.add("-");
|
||||
row.add("-");
|
||||
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));
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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');
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user