reportEngine:导出报告支持指定物模型
This commit is contained in:
parent
426cdb2567
commit
7039f43964
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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());
|
Set<String> wanted = identifiers.stream()
|
||||||
if (subProduct == null) {
|
.filter(Objects::nonNull)
|
||||||
continue;
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (wanted.isEmpty()) {
|
||||||
|
return models;
|
||||||
}
|
}
|
||||||
MonitorCategory cat = MonitorCategory.resolve(subProduct.getMonitorCategoryCode());
|
List<ThingsModel> filtered = models.stream()
|
||||||
if (cat != MonitorCategory.UNKNOWN) {
|
.filter(m -> m.getIdentifier() != null && wanted.contains(m.getIdentifier()))
|
||||||
return subDevice;
|
.collect(Collectors.toList());
|
||||||
|
if (filtered.isEmpty()) {
|
||||||
|
log.warn("所选物模型在设备监测物模型中没有匹配项,该测点将按无数据跳过 deviceId={} identifiers={}",
|
||||||
|
deviceId, wanted);
|
||||||
}
|
}
|
||||||
}
|
return filtered;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user