reportEngine:导出报告支持指定物模型

This commit is contained in:
zhugongping 2026-09-23 13:46:28 +08:00
parent 426cdb2567
commit 7039f43964
4 changed files with 63 additions and 30 deletions

View File

@ -11,6 +11,8 @@ import java.util.List;
* 报告生成请求 * 报告生成请求
* *
* <p>deviceId 必填;startTime/endTime 与 period 二选一(都传时以自定义时间为准)。</p> * <p>deviceId 必填;startTime/endTime 与 period 二选一(都传时以自定义时间为准)。</p>
* <p>identifiers 选填:指定后只分析所选物模型(物理量),为空表示分析产品下全部监测物模型;
* 多测点(deviceIds / groupId / 网关展开)时该列表对每个测点生效。</p>
* *
* @author FastBee * @author FastBee
* @date 2026-08-16 * @date 2026-08-16
@ -30,6 +32,9 @@ public class ReportRequest implements Serializable {
@ApiModelProperty("设备分组ID(按 iot_group 导出,报告表格展示断面列,与 deviceId/deviceIds 三选一)") @ApiModelProperty("设备分组ID(按 iot_group 导出,报告表格展示断面列,与 deviceId/deviceIds 三选一)")
private Long groupId; private Long groupId;
@ApiModelProperty("物模型标识符列表(选填;为空表示分析产品下全部监测物模型)")
private List<String> identifiers;
@ApiModelProperty("统计开始时间 yyyy-MM-dd HH:mm:ss(与 period 二选一)") @ApiModelProperty("统计开始时间 yyyy-MM-dd HH:mm:ss(与 period 二选一)")
private String startTime; private String startTime;

View File

@ -6,7 +6,6 @@ import com.fastbee.iot.domain.Product;
import com.fastbee.iot.domain.ThingsModel; import com.fastbee.iot.domain.ThingsModel;
import com.fastbee.common.utils.SecurityUtils; import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.core.domain.model.LoginUser; 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.IDeviceService;
import com.fastbee.iot.service.IProductService; import com.fastbee.iot.service.IProductService;
import com.fastbee.iot.service.IThingsModelService; 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.period.TimeWindow;
import com.fastbee.report.threshold.ThingsThreshold; import com.fastbee.report.threshold.ThingsThreshold;
import com.fastbee.report.threshold.ThingsThresholdProvider; import com.fastbee.report.threshold.ThingsThresholdProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -24,16 +24,20 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 报告上下文构建器:设备/产品/物模型/时序数据/阈值一次性装配 * 报告上下文构建器:设备/产品/物模型/时序数据/阈值一次性装配
* *
* <p>网关设备特殊处理:使用网关下第一个有监测分类的子设备构建上下文。</p> * <p>调用方按测点(iot_device)逐个调用:物模型、时序数据、阈值都以设备维度装配,
* 分析器按设备分析,再由 {@link MultiDeviceAnalysisMerger} 归并。
* 网关设备在 {@code ReportTargetResolver} 阶段就已展开为子设备,这里不感知网关概念。</p>
* *
* @author FastBee * @author FastBee
* @date 2026-08-16 * @date 2026-08-16
*/ */
@Slf4j
@Component @Component
public class ReportContextBuilder { public class ReportContextBuilder {
@ -55,9 +59,6 @@ public class ReportContextBuilder {
@Resource @Resource
private ThingsThresholdProvider thingsThresholdProvider; private ThingsThresholdProvider thingsThresholdProvider;
@Resource
private SubGatewayMapper subGatewayMapper;
/** /**
* 构建报告上下文(按请求中的设备) * 构建报告上下文(按请求中的设备)
*/ */
@ -90,6 +91,8 @@ public class ReportContextBuilder {
modelQuery.setType(1); modelQuery.setType(1);
modelQuery.setIsMonitor(1); modelQuery.setIsMonitor(1);
List<ThingsModel> models = thingsModelService.selectThingsModelList(modelQuery); List<ThingsModel> models = thingsModelService.selectThingsModelList(modelQuery);
// 前端勾选的物模型:只分析所选物理量;未勾选(空)表示分析产品下全部监测物模型
models = filterByIdentifiers(models, request.getIdentifiers(), deviceId);
List<MetricData> metricDataList = dataProvider.loadHistory(device, models, window); List<MetricData> metricDataList = dataProvider.loadHistory(device, models, window);
@ -149,30 +152,37 @@ public class ReportContextBuilder {
} }
/** /**
* 解析网关下第一个有监测分类的子设备 * 按请求指定的物模型标识符过滤监测物模型
* *
* @param gatewayDeviceId 网关设备ID * <p>语义:<b>未指定(空列表)= 全量分析</b>,与「未勾选子设备即全部子设备」保持一致的默认口径;
* @return 第一个有监测分类的子设备,如果没有则返回null * 指定后只分析所选物理量,避免报告被无关变量(如设备电压、轴角度)稀释。</p>
*
* <p>一个都匹配不上时返回空列表而非抛异常:多测点(网关子设备 / 分组 / 多选)场景下
* 允许个别测点未配置该物模型,交由 {@code ReportAsyncExecutor} 按「该测点无有效数据」跳过,
* 避免一台子设备漏配就把整份报告打失败。</p>
*/ */
private Device resolveFirstMonitorSubDevice(Long gatewayDeviceId) { private List<ThingsModel> filterByIdentifiers(List<ThingsModel> models, List<String> identifiers, Long deviceId) {
List<Long> subDeviceIds = subGatewayMapper.selectSubDeviceIdsByGwDeviceId(gatewayDeviceId); if (models == null || models.isEmpty()) {
if (subDeviceIds == null || subDeviceIds.isEmpty()) { return models;
return null;
} }
for (Long subDeviceId : subDeviceIds) { if (identifiers == null || identifiers.isEmpty()) {
Device subDevice = deviceService.selectDeviceByDeviceId(subDeviceId); return models;
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;
}
} }
return null; Set<String> wanted = identifiers.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
if (wanted.isEmpty()) {
return models;
}
List<ThingsModel> 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;
} }
} }

View File

@ -225,10 +225,16 @@ public class ReportAsyncExecutor {
} }
if (analyses.isEmpty()) { if (analyses.isEmpty()) {
log.warn("报告生成中止:全部测点在时间范围内无有效监测数据 reportNo={} 测点={}", log.warn("报告生成中止:全部测点在时间范围内无有效监测数据 reportNo={} 测点={} 指定物模型={}",
reportNo, String.join("、", skippedPoints)); reportNo, String.join("、", skippedPoints), request.getIdentifiers());
reportRecordService.markFailed(record, "所选时间范围内无有效监测数据,无法生成报告(测点:" boolean hasIdentifiers = request.getIdentifiers() != null && !request.getIdentifiers().isEmpty();
+ String.join("、", skippedPoints) + ")"); String reason = hasIdentifiers
? "所选物模型在统计期内无有效监测数据,无法生成报告(测点:"
+ String.join("、", skippedPoints)
+ ");请确认所选物模型在产品上已开启监测,且设备在该时段有上报记录"
: "所选时间范围内无有效监测数据,无法生成报告(测点:"
+ String.join("、", skippedPoints) + ")";
reportRecordService.markFailed(record, reason);
return; return;
} }

View File

@ -488,10 +488,22 @@ export default {
if (Array.isArray(subIds) && subIds.length > 0) { if (Array.isArray(subIds) && subIds.length > 0) {
payload.deviceIds = subIds; payload.deviceIds = subIds;
} else { } else {
// 未勾选子设备 = 全量子设备:传网关 deviceId,由后端展开其下同产品子设备
payload.deviceId = this.device.deviceId; payload.deviceId = this.device.deviceId;
} }
// 物模型:只传「子设备变量」(计算量是网关纯计算,不参与多测点物模型分析);
// 未勾选时不传该字段,后端按产品全部监测物模型分析
const subIdentifiers = this.gwSelectedSubIdentifiers || [];
if (subIdentifiers.length > 0) {
payload.identifiers = [...subIdentifiers];
}
} else { } else {
payload.deviceId = reportDeviceId; payload.deviceId = reportDeviceId;
// 物模型:未勾选时不传该字段,后端按全部监测物模型分析
const picked = (this.queryParams.identifiers || []).filter((id) => id !== '__selectAll__');
if (picked.length > 0) {
payload.identifiers = picked;
}
} }
this.reportLoading = true; this.reportLoading = true;
generateDeviceReport(payload) generateDeviceReport(payload)