diff --git a/fastbee-report/src/main/java/com/fastbee/report/api/ReportRequest.java b/fastbee-report/src/main/java/com/fastbee/report/api/ReportRequest.java index ddc17c98..64543de9 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/api/ReportRequest.java +++ b/fastbee-report/src/main/java/com/fastbee/report/api/ReportRequest.java @@ -11,6 +11,8 @@ import java.util.List; * 报告生成请求 * *

deviceId 必填;startTime/endTime 与 period 二选一(都传时以自定义时间为准)。

+ *

identifiers 选填:指定后只分析所选物模型(物理量),为空表示分析产品下全部监测物模型; + * 多测点(deviceIds / groupId / 网关展开)时该列表对每个测点生效。

* * @author FastBee * @date 2026-08-16 @@ -30,6 +32,9 @@ public class ReportRequest implements Serializable { @ApiModelProperty("设备分组ID(按 iot_group 导出,报告表格展示断面列,与 deviceId/deviceIds 三选一)") private Long groupId; + @ApiModelProperty("物模型标识符列表(选填;为空表示分析产品下全部监测物模型)") + private List identifiers; + @ApiModelProperty("统计开始时间 yyyy-MM-dd HH:mm:ss(与 period 二选一)") private String startTime; 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 df2c9881..26fc3857 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 @@ -6,7 +6,6 @@ import com.fastbee.iot.domain.Product; import com.fastbee.iot.domain.ThingsModel; import com.fastbee.common.utils.SecurityUtils; import com.fastbee.common.core.domain.model.LoginUser; -import com.fastbee.iot.mapper.SubGatewayMapper; import com.fastbee.iot.service.IDeviceService; import com.fastbee.iot.service.IProductService; import com.fastbee.iot.service.IThingsModelService; @@ -17,6 +16,7 @@ import com.fastbee.report.exception.ReportException; import com.fastbee.report.period.TimeWindow; import com.fastbee.report.threshold.ThingsThreshold; import com.fastbee.report.threshold.ThingsThresholdProvider; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.Resource; @@ -24,16 +24,20 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; /** * 报告上下文构建器:设备/产品/物模型/时序数据/阈值一次性装配 * - *

网关设备特殊处理:使用网关下第一个有监测分类的子设备构建上下文。

+ *

调用方按测点(iot_device)逐个调用:物模型、时序数据、阈值都以设备维度装配, + * 分析器按设备分析,再由 {@link MultiDeviceAnalysisMerger} 归并。 + * 网关设备在 {@code ReportTargetResolver} 阶段就已展开为子设备,这里不感知网关概念。

* * @author FastBee * @date 2026-08-16 */ +@Slf4j @Component public class ReportContextBuilder { @@ -55,9 +59,6 @@ public class ReportContextBuilder { @Resource private ThingsThresholdProvider thingsThresholdProvider; - @Resource - private SubGatewayMapper subGatewayMapper; - /** * 构建报告上下文(按请求中的设备) */ @@ -90,6 +91,8 @@ public class ReportContextBuilder { modelQuery.setType(1); modelQuery.setIsMonitor(1); List models = thingsModelService.selectThingsModelList(modelQuery); + // 前端勾选的物模型:只分析所选物理量;未勾选(空)表示分析产品下全部监测物模型 + models = filterByIdentifiers(models, request.getIdentifiers(), deviceId); List metricDataList = dataProvider.loadHistory(device, models, window); @@ -149,30 +152,37 @@ public class ReportContextBuilder { } /** - * 解析网关下第一个有监测分类的子设备 + * 按请求指定的物模型标识符过滤监测物模型 * - * @param gatewayDeviceId 网关设备ID - * @return 第一个有监测分类的子设备,如果没有则返回null + *

语义:未指定(空列表)= 全量分析,与「未勾选子设备即全部子设备」保持一致的默认口径; + * 指定后只分析所选物理量,避免报告被无关变量(如设备电压、轴角度)稀释。

+ * + *

一个都匹配不上时返回空列表而非抛异常:多测点(网关子设备 / 分组 / 多选)场景下 + * 允许个别测点未配置该物模型,交由 {@code ReportAsyncExecutor} 按「该测点无有效数据」跳过, + * 避免一台子设备漏配就把整份报告打失败。

*/ - private Device resolveFirstMonitorSubDevice(Long gatewayDeviceId) { - List subDeviceIds = subGatewayMapper.selectSubDeviceIdsByGwDeviceId(gatewayDeviceId); - if (subDeviceIds == null || subDeviceIds.isEmpty()) { - return null; + private List filterByIdentifiers(List models, List identifiers, Long deviceId) { + if (models == null || models.isEmpty()) { + return models; } - for (Long subDeviceId : subDeviceIds) { - Device subDevice = deviceService.selectDeviceByDeviceId(subDeviceId); - if (subDevice == null || subDevice.getProductId() == null) { - continue; - } - Product subProduct = productService.selectProductByProductId(subDevice.getProductId()); - if (subProduct == null) { - continue; - } - MonitorCategory cat = MonitorCategory.resolve(subProduct.getMonitorCategoryCode()); - if (cat != MonitorCategory.UNKNOWN) { - return subDevice; - } + if (identifiers == null || identifiers.isEmpty()) { + return models; } - return null; + Set wanted = identifiers.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet()); + if (wanted.isEmpty()) { + return models; + } + List filtered = models.stream() + .filter(m -> m.getIdentifier() != null && wanted.contains(m.getIdentifier())) + .collect(Collectors.toList()); + if (filtered.isEmpty()) { + log.warn("所选物模型在设备监测物模型中没有匹配项,该测点将按无数据跳过 deviceId={} identifiers={}", + deviceId, wanted); + } + return filtered; } } diff --git a/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java b/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java index 3f234cd8..b9124add 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java +++ b/fastbee-report/src/main/java/com/fastbee/report/executor/ReportAsyncExecutor.java @@ -225,10 +225,16 @@ public class ReportAsyncExecutor { } if (analyses.isEmpty()) { - log.warn("报告生成中止:全部测点在时间范围内无有效监测数据 reportNo={} 测点={}", - reportNo, String.join("、", skippedPoints)); - reportRecordService.markFailed(record, "所选时间范围内无有效监测数据,无法生成报告(测点:" - + String.join("、", skippedPoints) + ")"); + log.warn("报告生成中止:全部测点在时间范围内无有效监测数据 reportNo={} 测点={} 指定物模型={}", + reportNo, String.join("、", skippedPoints), request.getIdentifiers()); + boolean hasIdentifiers = request.getIdentifiers() != null && !request.getIdentifiers().isEmpty(); + String reason = hasIdentifiers + ? "所选物模型在统计期内无有效监测数据,无法生成报告(测点:" + + String.join("、", skippedPoints) + + ");请确认所选物模型在产品上已开启监测,且设备在该时段有上报记录" + : "所选时间范围内无有效监测数据,无法生成报告(测点:" + + String.join("、", skippedPoints) + ")"; + reportRecordService.markFailed(record, reason); return; } diff --git a/yack-web/src/views/dataCenter/components/DeviceHistoryTablePanel.vue b/yack-web/src/views/dataCenter/components/DeviceHistoryTablePanel.vue index 0ffff2cf..6dc06fb0 100644 --- a/yack-web/src/views/dataCenter/components/DeviceHistoryTablePanel.vue +++ b/yack-web/src/views/dataCenter/components/DeviceHistoryTablePanel.vue @@ -488,10 +488,22 @@ export default { if (Array.isArray(subIds) && subIds.length > 0) { payload.deviceIds = subIds; } else { + // 未勾选子设备 = 全量子设备:传网关 deviceId,由后端展开其下同产品子设备 payload.deviceId = this.device.deviceId; } + // 物模型:只传「子设备变量」(计算量是网关纯计算,不参与多测点物模型分析); + // 未勾选时不传该字段,后端按产品全部监测物模型分析 + const subIdentifiers = this.gwSelectedSubIdentifiers || []; + if (subIdentifiers.length > 0) { + payload.identifiers = [...subIdentifiers]; + } } else { payload.deviceId = reportDeviceId; + // 物模型:未勾选时不传该字段,后端按全部监测物模型分析 + const picked = (this.queryParams.identifiers || []).filter((id) => id !== '__selectAll__'); + if (picked.length > 0) { + payload.identifiers = picked; + } } this.reportLoading = true; generateDeviceReport(payload)