|
|
@@ -0,0 +1,332 @@
|
|
|
+package com.zksy.cockpit.type;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.zksy.base.domain.EquipmentBase;
|
|
|
+import com.zksy.base.domain.EquipmentType;
|
|
|
+import com.zksy.base.gas.domain.GasMonitorData;
|
|
|
+import com.zksy.base.gas.service.GasMonitorDataService;
|
|
|
+import com.zksy.base.service.EquipmentBaseService;
|
|
|
+import com.zksy.base.service.EquipmentTypeService;
|
|
|
+import com.zksy.base.water.domain.WaterMonitorData;
|
|
|
+import com.zksy.base.water.service.WaterMonitorDataService;
|
|
|
+import com.zksy.cockpit.foundation.CockpitTypeIndustry;
|
|
|
+import org.springframework.context.annotation.Primary;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.OffsetDateTime;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.function.Function;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 类型指标真实数据适配器(BT-5)。
|
|
|
+ *
|
|
|
+ * <p>指标采样来源按设备行业分流:
|
|
|
+ * <ul>
|
|
|
+ * <li>燃气({@code GAS}):{@code gas_monitor_data},现网惯例以
|
|
|
+ * {@code mac_address} 存设备编码(见 {@code GasDashboardServiceImpl}),
|
|
|
+ * 指标为气体浓度/%LEL、电池电量/%、信号强度/dBm;报警位取自帧内 {@code alarm_info};</li>
|
|
|
+ * <li>供水/排水({@code WATER_SUPPLY}、{@code DRAINAGE}):{@code water_monitor_data},
|
|
|
+ * {@code device_code} 关联设备编码,指标为水位、电源电压、信号质量;
|
|
|
+ * 水位/电压为字符串字段,可解析成数值才上报,解析失败按缺失处理;</li>
|
|
|
+ * <li>其他行业或类型不可推导:不臆造数据源,返回空指标(服务层按 EMPTY 处理)。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>不臆造</b>:水位帧无报警位字段 → 状态为 {@code UNKNOWN};气体帧有报警位 → {@code ALARM}/{@code NORMAL};
|
|
|
+ * 无报告时间回退入库时间;不补造趋势与阈值判定。
|
|
|
+ */
|
|
|
+@Component
|
|
|
+@Primary
|
|
|
+public class BusinessCockpitTypeMetricsAdapter implements CockpitTypeMetricsAdapter {
|
|
|
+
|
|
|
+ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
|
|
+ private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
|
|
+
|
|
|
+ private final EquipmentBaseService equipmentBaseService;
|
|
|
+ private final EquipmentTypeService equipmentTypeService;
|
|
|
+ private final GasMonitorDataService gasMonitorDataService;
|
|
|
+ private final WaterMonitorDataService waterMonitorDataService;
|
|
|
+
|
|
|
+ public BusinessCockpitTypeMetricsAdapter(
|
|
|
+ EquipmentBaseService equipmentBaseService,
|
|
|
+ EquipmentTypeService equipmentTypeService,
|
|
|
+ GasMonitorDataService gasMonitorDataService,
|
|
|
+ WaterMonitorDataService waterMonitorDataService) {
|
|
|
+ this.equipmentBaseService = equipmentBaseService;
|
|
|
+ this.equipmentTypeService = equipmentTypeService;
|
|
|
+ this.gasMonitorDataService = gasMonitorDataService;
|
|
|
+ this.waterMonitorDataService = waterMonitorDataService;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CockpitTypeMetricsSnapshot loadCurrent(String deviceCode) {
|
|
|
+ EquipmentBase device = findDevice(deviceCode);
|
|
|
+ if (device == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String code = device.getEquipmentCode().trim();
|
|
|
+ Map<Integer, EquipmentType> types = equipmentTypes();
|
|
|
+ String typeCode = typeCodeOf(device, types);
|
|
|
+ String typeName = typeNameOf(device, types);
|
|
|
+ String industry = industryOf(device, types);
|
|
|
+ if ("GAS".equals(industry)) {
|
|
|
+ return gasCurrent(device, code, typeCode, typeName);
|
|
|
+ }
|
|
|
+ if ("WATER_SUPPLY".equals(industry) || "DRAINAGE".equals(industry)) {
|
|
|
+ return waterCurrent(device, code, typeCode, typeName);
|
|
|
+ }
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, Collections.emptyList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CockpitTypeMetricsSnapshot loadHistory(
|
|
|
+ String deviceCode,
|
|
|
+ OffsetDateTime startTime,
|
|
|
+ OffsetDateTime endTime,
|
|
|
+ int page,
|
|
|
+ int pageSize) {
|
|
|
+ EquipmentBase device = findDevice(deviceCode);
|
|
|
+ if (device == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String code = device.getEquipmentCode().trim();
|
|
|
+ Map<Integer, EquipmentType> types = equipmentTypes();
|
|
|
+ String typeCode = typeCodeOf(device, types);
|
|
|
+ String typeName = typeNameOf(device, types);
|
|
|
+ String industry = industryOf(device, types);
|
|
|
+ if ("GAS".equals(industry)) {
|
|
|
+ return gasHistory(device, code, typeCode, typeName, startTime, endTime, page, pageSize);
|
|
|
+ }
|
|
|
+ if ("WATER_SUPPLY".equals(industry) || "DRAINAGE".equals(industry)) {
|
|
|
+ return waterHistory(device, code, typeCode, typeName, startTime, endTime, page, pageSize);
|
|
|
+ }
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, Collections.emptyList(), 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------- 燃气 ----------------
|
|
|
+
|
|
|
+ private CockpitTypeMetricsSnapshot gasCurrent(
|
|
|
+ EquipmentBase device, String code, String typeCode, String typeName) {
|
|
|
+ GasMonitorData latest = latestGas(code);
|
|
|
+ if (latest == null) {
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, Collections.emptyList());
|
|
|
+ }
|
|
|
+ String sampledAt = offsetTime(sampledAt(latest.getReportTime(), latest.getCreateTime()));
|
|
|
+ String status = gasStatus(latest);
|
|
|
+ List<CockpitMetricSnapshot> metrics = new ArrayList<>();
|
|
|
+ addMetric(metrics, "GAS_CONCENTRATION", "气体浓度", latest.getGasConcentration(), "%LEL", status, sampledAt);
|
|
|
+ addMetric(metrics, "GAS_BATTERY", "电池电量", latest.getBattery(), "%", status, sampledAt);
|
|
|
+ addMetric(metrics, "GAS_SIGNAL_STRENGTH", "信号强度",
|
|
|
+ latest.getSignalStrength() == null
|
|
|
+ ? null : BigDecimal.valueOf(latest.getSignalStrength().doubleValue()),
|
|
|
+ "dBm", status, sampledAt);
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, metrics);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitTypeMetricsSnapshot gasHistory(
|
|
|
+ EquipmentBase device, String code, String typeCode, String typeName,
|
|
|
+ OffsetDateTime startTime, OffsetDateTime endTime, int page, int pageSize) {
|
|
|
+ IPage<GasMonitorData> result = gasMonitorDataService.page(
|
|
|
+ new Page<>(page, pageSize),
|
|
|
+ new LambdaQueryWrapper<GasMonitorData>()
|
|
|
+ .eq(GasMonitorData::getMacAddress, code)
|
|
|
+ .between(GasMonitorData::getCreateTime, startTime.toLocalDateTime(), endTime.toLocalDateTime())
|
|
|
+ .orderByAsc(GasMonitorData::getCreateTime));
|
|
|
+ List<GasMonitorData> rows = result == null || result.getRecords() == null
|
|
|
+ ? Collections.emptyList() : result.getRecords();
|
|
|
+ int total = result == null ? 0 : (int) result.getTotal();
|
|
|
+ if (rows.isEmpty()) {
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, Collections.emptyList(), total);
|
|
|
+ }
|
|
|
+ List<CockpitMetricSnapshot> metrics = new ArrayList<>();
|
|
|
+ metrics.add(buildGasPoints("GAS_CONCENTRATION", "气体浓度", "%LEL", rows, GasMonitorData::getGasConcentration));
|
|
|
+ metrics.add(buildGasPoints("GAS_BATTERY", "电池电量", "%", rows, GasMonitorData::getBattery));
|
|
|
+ metrics.add(buildGasPoints("GAS_SIGNAL_STRENGTH", "信号强度", "dBm", rows, data -> data.getSignalStrength() == null
|
|
|
+ ? null : BigDecimal.valueOf(data.getSignalStrength().doubleValue())));
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, metrics, total);
|
|
|
+ }
|
|
|
+
|
|
|
+ private GasMonitorData latestGas(String code) {
|
|
|
+ List<GasMonitorData> rows = safeList(gasMonitorDataService.list(
|
|
|
+ new LambdaQueryWrapper<GasMonitorData>()
|
|
|
+ .eq(GasMonitorData::getMacAddress, code)
|
|
|
+ .orderByDesc(GasMonitorData::getCreateTime)
|
|
|
+ .last("limit 1")));
|
|
|
+ return rows.isEmpty() ? null : rows.get(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitMetricSnapshot buildGasPoints(
|
|
|
+ String metricCode, String metricName, String unit,
|
|
|
+ List<GasMonitorData> rows, Function<GasMonitorData, BigDecimal> extractor) {
|
|
|
+ List<CockpitMetricSnapshot.PointSnapshot> points = new ArrayList<>();
|
|
|
+ BigDecimal latestValue = null;
|
|
|
+ String latestAt = null;
|
|
|
+ String latestStatus = "UNKNOWN";
|
|
|
+ for (GasMonitorData row : rows) {
|
|
|
+ BigDecimal value = extractor.apply(row);
|
|
|
+ String status = gasStatus(row);
|
|
|
+ String at = offsetTime(sampledAt(row.getReportTime(), row.getCreateTime()));
|
|
|
+ points.add(new CockpitMetricSnapshot.PointSnapshot(value, status, at));
|
|
|
+ latestValue = value;
|
|
|
+ latestAt = at;
|
|
|
+ latestStatus = status;
|
|
|
+ }
|
|
|
+ return new CockpitMetricSnapshot(metricCode, metricName, latestValue, unit, latestStatus, latestAt, points);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String gasStatus(GasMonitorData data) {
|
|
|
+ if (data == null || data.getAlarmInfo() == null) {
|
|
|
+ return "UNKNOWN";
|
|
|
+ }
|
|
|
+ boolean alarm = data.parseAlarmBits().values().stream().anyMatch(Boolean::booleanValue);
|
|
|
+ return alarm ? "ALARM" : "NORMAL";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------- 供水/排水 ----------------
|
|
|
+
|
|
|
+ private CockpitTypeMetricsSnapshot waterCurrent(
|
|
|
+ EquipmentBase device, String code, String typeCode, String typeName) {
|
|
|
+ WaterMonitorData latest = latestWater(code);
|
|
|
+ if (latest == null) {
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, Collections.emptyList());
|
|
|
+ }
|
|
|
+ String sampledAt = offsetTime(sampledAt(latest.getTimestampSince(), latest.getCreateTime()));
|
|
|
+ List<CockpitMetricSnapshot> metrics = new ArrayList<>();
|
|
|
+ addMetric(metrics, "WATER_LEVEL", "水位", decimal(latest.getWaterLevel()), null, "UNKNOWN", sampledAt);
|
|
|
+ addMetric(metrics, "WATER_POWER_VOLTAGE", "电源电压", decimal(latest.getPowerVoltage()), "V", "UNKNOWN", sampledAt);
|
|
|
+ addMetric(metrics, "WATER_SIGNAL_QUALITY", "信号质量", decimal(latest.getSignalQuality()), null, "UNKNOWN", sampledAt);
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, metrics);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitTypeMetricsSnapshot waterHistory(
|
|
|
+ EquipmentBase device, String code, String typeCode, String typeName,
|
|
|
+ OffsetDateTime startTime, OffsetDateTime endTime, int page, int pageSize) {
|
|
|
+ IPage<WaterMonitorData> result = waterMonitorDataService.page(
|
|
|
+ new Page<>(page, pageSize),
|
|
|
+ new LambdaQueryWrapper<WaterMonitorData>()
|
|
|
+ .eq(WaterMonitorData::getDeviceCode, code)
|
|
|
+ .between(WaterMonitorData::getCreateTime, startTime.toLocalDateTime(), endTime.toLocalDateTime())
|
|
|
+ .orderByAsc(WaterMonitorData::getCreateTime));
|
|
|
+ List<WaterMonitorData> rows = result == null || result.getRecords() == null
|
|
|
+ ? Collections.emptyList() : result.getRecords();
|
|
|
+ int total = result == null ? 0 : (int) result.getTotal();
|
|
|
+ if (rows.isEmpty()) {
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, Collections.emptyList(), total);
|
|
|
+ }
|
|
|
+ List<CockpitMetricSnapshot> metrics = new ArrayList<>();
|
|
|
+ metrics.add(buildWaterPoints("WATER_LEVEL", "水位", null, rows, WaterMonitorData::getWaterLevel));
|
|
|
+ metrics.add(buildWaterPoints("WATER_POWER_VOLTAGE", "电源电压", "V", rows, WaterMonitorData::getPowerVoltage));
|
|
|
+ metrics.add(buildWaterPoints("WATER_SIGNAL_QUALITY", "信号质量", null, rows, WaterMonitorData::getSignalQuality));
|
|
|
+ return new CockpitTypeMetricsSnapshot(code, typeCode, typeName, metrics, total);
|
|
|
+ }
|
|
|
+
|
|
|
+ private WaterMonitorData latestWater(String code) {
|
|
|
+ List<WaterMonitorData> rows = safeList(waterMonitorDataService.list(
|
|
|
+ new LambdaQueryWrapper<WaterMonitorData>()
|
|
|
+ .eq(WaterMonitorData::getDeviceCode, code)
|
|
|
+ .orderByDesc(WaterMonitorData::getCreateTime)
|
|
|
+ .last("limit 1")));
|
|
|
+ return rows.isEmpty() ? null : rows.get(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitMetricSnapshot buildWaterPoints(
|
|
|
+ String metricCode, String metricName, String unit,
|
|
|
+ List<WaterMonitorData> rows, Function<WaterMonitorData, String> extractor) {
|
|
|
+ List<CockpitMetricSnapshot.PointSnapshot> points = new ArrayList<>();
|
|
|
+ BigDecimal latestValue = null;
|
|
|
+ String latestAt = null;
|
|
|
+ for (WaterMonitorData row : rows) {
|
|
|
+ BigDecimal value = decimal(extractor.apply(row));
|
|
|
+ String at = offsetTime(sampledAt(row.getTimestampSince(), row.getCreateTime()));
|
|
|
+ points.add(new CockpitMetricSnapshot.PointSnapshot(value, "UNKNOWN", at));
|
|
|
+ latestValue = value;
|
|
|
+ latestAt = at;
|
|
|
+ }
|
|
|
+ return new CockpitMetricSnapshot(metricCode, metricName, latestValue, unit, "UNKNOWN", latestAt, points);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------- 公共 ----------------
|
|
|
+
|
|
|
+ private EquipmentBase findDevice(String deviceCode) {
|
|
|
+ if (isBlank(deviceCode)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<EquipmentBase> devices = safeList(equipmentBaseService.list(
|
|
|
+ new LambdaQueryWrapper<EquipmentBase>()
|
|
|
+ .eq(EquipmentBase::getEquipmentCode, deviceCode.trim())
|
|
|
+ .last("limit 1")));
|
|
|
+ return devices.isEmpty() ? null : devices.get(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Integer, EquipmentType> equipmentTypes() {
|
|
|
+ Map<Integer, EquipmentType> result = new HashMap<>();
|
|
|
+ for (EquipmentType type : safeList(equipmentTypeService.list())) {
|
|
|
+ if (type != null && type.getId() != null) {
|
|
|
+ result.put(type.getId(), type);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String typeCodeOf(EquipmentBase device, Map<Integer, EquipmentType> types) {
|
|
|
+ EquipmentType type = device == null || device.getEquipmentTypeId() == null
|
|
|
+ ? null : types.get(device.getEquipmentTypeId());
|
|
|
+ return type == null || type.getId() == null ? null : String.valueOf(type.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String typeNameOf(EquipmentBase device, Map<Integer, EquipmentType> types) {
|
|
|
+ EquipmentType type = device == null || device.getEquipmentTypeId() == null
|
|
|
+ ? null : types.get(device.getEquipmentTypeId());
|
|
|
+ return type == null ? null : type.getTypeName();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String industryOf(EquipmentBase device, Map<Integer, EquipmentType> types) {
|
|
|
+ EquipmentType type = device == null || device.getEquipmentTypeId() == null
|
|
|
+ ? null : types.get(device.getEquipmentTypeId());
|
|
|
+ return CockpitTypeIndustry.industryOf(type, types);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static LocalDateTime sampledAt(LocalDateTime primary, LocalDateTime fallback) {
|
|
|
+ return primary != null ? primary : fallback;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String offsetTime(LocalDateTime value) {
|
|
|
+ return value == null ? null : TIME_FORMATTER.format(value.atZone(BUSINESS_ZONE).toOffsetDateTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void addMetric(
|
|
|
+ List<CockpitMetricSnapshot> metrics, String code, String name,
|
|
|
+ BigDecimal value, String unit, String status, String sampledAt) {
|
|
|
+ metrics.add(new CockpitMetricSnapshot(code, name, value, unit, status, sampledAt, null));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal decimal(String value) {
|
|
|
+ if (value == null || value.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return new BigDecimal(value.trim());
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static <T> List<T> safeList(List<T> value) {
|
|
|
+ return value == null ? Collections.emptyList() : value.stream().filter(Objects::nonNull).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isBlank(String value) {
|
|
|
+ return value == null || value.trim().isEmpty();
|
|
|
+ }
|
|
|
+}
|