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)