|
|
@@ -3,6 +3,7 @@ package com.usky.cdi.service.util;
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.usky.cdi.domain.DmpDevice;
|
|
|
import com.usky.cdi.mapper.DmpDeviceMapper;
|
|
|
import com.usky.cdi.service.config.DeviceFieldConfig;
|
|
|
@@ -40,9 +41,14 @@ public class DeviceDataQuery {
|
|
|
@Autowired
|
|
|
private DeviceFieldConfig deviceFieldConfig;
|
|
|
|
|
|
+ private static final Set<Integer> ALLOWED_DEVICE_TYPES = new HashSet<>(Arrays.asList(
|
|
|
+ 711, 710, 709, 708, 707
|
|
|
+ ));
|
|
|
+
|
|
|
// 定义各参数的格式化器(整数位,小数位)
|
|
|
private static final DecimalFormat FORMAT_2_2 = new DecimalFormat("00.00"); // 2位整数+2位小数
|
|
|
private static final DecimalFormat FORMAT_0_3 = new DecimalFormat("0.000"); // 0位整数+3位小数
|
|
|
+ private static final DecimalFormat FORMAT_1_2 = new DecimalFormat("0.00"); // 1位整数+2位小数
|
|
|
private static final DecimalFormat FORMAT_3_2 = new DecimalFormat("000.00"); // 3位整数+2位小数
|
|
|
private static final DecimalFormat FORMAT_4_2 = new DecimalFormat("0000.00"); // 4位整数+2位小数
|
|
|
|
|
|
@@ -79,6 +85,8 @@ public class DeviceDataQuery {
|
|
|
private static final double FLOATING_RANGE_MAX = 0.5;
|
|
|
private static final double FLOATING_RANGE_MIN1 = 0.0;
|
|
|
private static final double FLOATING_RANGE_MAX1 = 1.0;
|
|
|
+ private static final double SEWAGE_LEVEL_MIN = 0.0;
|
|
|
+ private static final double SEWAGE_LEVEL_MAX = 0.5;
|
|
|
|
|
|
private static int callCount = 0;
|
|
|
|
|
|
@@ -107,11 +115,52 @@ public class DeviceDataQuery {
|
|
|
log.warn("接口返回数据:{}", response);
|
|
|
|
|
|
List<JSONObject> resultList = parseResponseData(response, transferVO.getDeviceType(), transferVO.getDevices());
|
|
|
+ boolean useXiangyuMock = false;
|
|
|
+ List<JSONObject> xiangyuDataList = new ArrayList<>();
|
|
|
+
|
|
|
+ // 当租户数据为空时获取桃浦象屿相同设备数据进行模拟(修复版:不修改原始设备列表)
|
|
|
+ if (resultList.isEmpty() && ALLOWED_DEVICE_TYPES.contains(transferVO.getDeviceType())) {
|
|
|
+ LambdaQueryWrapper<DmpDevice> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(DmpDevice::getTenantId, 1205)
|
|
|
+ .eq(DmpDevice::getDeviceType, transferVO.getDeviceType())
|
|
|
+ .orderByDesc(DmpDevice::getId)
|
|
|
+ .last("LIMIT 1");
|
|
|
+ DmpDevice xiangyuDevice = dmpDeviceMapper.selectOne(queryWrapper);
|
|
|
+
|
|
|
+ if (xiangyuDevice != null) {
|
|
|
+ JSONObject xiangyuReq = new JSONObject();
|
|
|
+ xiangyuReq.put("deviceuuid", Collections.singletonList(xiangyuDevice.getDeviceUuid()));
|
|
|
+ String xiangyuResp = HttpClientUtils.doPostJson(baseUrl, xiangyuReq.toJSONString());
|
|
|
+
|
|
|
+ JSONObject responseObj = JSON.parseObject(xiangyuResp);
|
|
|
+ if (responseObj.getJSONArray("data") != null && !responseObj.getJSONArray("data").isEmpty()) {
|
|
|
+ JSONArray dataArray = responseObj.getJSONArray("data");
|
|
|
+ JSONObject deviceData = dataArray.getJSONObject(0);
|
|
|
+ JSONObject metrics = deviceData.getJSONObject("metrics");
|
|
|
+ Object realtime = metrics.get("realtime");
|
|
|
+ System.out.println("接口返回数据时间戳对象:" + realtime);
|
|
|
+ Long timestamp = realtime instanceof Long ? (Long) realtime : null;
|
|
|
+ System.out.println("接口返回数据时间戳:" + timestamp);
|
|
|
+
|
|
|
+ if (timestamp != null && timestamp >= (System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L)) {
|
|
|
+ // 修复:不修改原始设备列表,直接生成模拟模板
|
|
|
+ useXiangyuMock = true;
|
|
|
+ xiangyuDataList = parseResponseData(xiangyuResp, transferVO.getDeviceType(),
|
|
|
+ Collections.singletonList(xiangyuDevice));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 若返回数据为空且开启模拟模式,则生成模拟数据
|
|
|
if (resultList.isEmpty() && simulation) {
|
|
|
log.info("接口返回数据为空,生成模拟数据,设备类型:{}", transferVO.getDeviceType());
|
|
|
- resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
|
|
|
+ if (useXiangyuMock && !xiangyuDataList.isEmpty()) {
|
|
|
+ // 使用象屿数据作为模板生成全量模拟
|
|
|
+ resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), xiangyuDataList.get(0));
|
|
|
+ } else {
|
|
|
+ resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
|
|
|
+ }
|
|
|
} else if (resultList.size() < transferVO.getDevices().size()) {
|
|
|
log.warn("接口返回数据数量与请求数量不一致,设备类型:{}", transferVO.getDeviceType());
|
|
|
|
|
|
@@ -136,22 +185,22 @@ public class DeviceDataQuery {
|
|
|
List<DmpDevice> missingDevices = missingDeviceIds.stream()
|
|
|
.map(requestDeviceMap::get)
|
|
|
.collect(Collectors.toList());
|
|
|
- List<JSONObject> missingSimulationData = generateSimulationData(transferVO.getDeviceType(), missingDevices, resultList.get(0));
|
|
|
- // 将模拟数据与返回数据结合
|
|
|
- resultList.addAll(missingSimulationData);
|
|
|
|
|
|
- // 校验结合后的数据是否与请求的device_id一一对应
|
|
|
- Set<String> combinedDeviceIds = resultList.stream()
|
|
|
- .map(data -> data.getString("device_id"))
|
|
|
- .filter(Objects::nonNull)
|
|
|
- .collect(Collectors.toSet());
|
|
|
+ JSONObject templateData = resultList.isEmpty() ?
|
|
|
+ (xiangyuDataList.isEmpty() ? null : xiangyuDataList.get(0))
|
|
|
+ : resultList.get(0);
|
|
|
|
|
|
- if (combinedDeviceIds.size() != transferVO.getDevices().size()) {
|
|
|
- log.warn("数据整合后仍存在缺失,请求设备数量:{},返回设备数量:{}",
|
|
|
- transferVO.getDevices().size(), combinedDeviceIds.size());
|
|
|
- } else {
|
|
|
- log.debug("数据整合完成,设备数量与请求一致:{}", combinedDeviceIds.size());
|
|
|
- }
|
|
|
+ List<JSONObject> missingSimulationData = generateSimulationData(transferVO.getDeviceType(), missingDevices, templateData);
|
|
|
+ // 修复:不再清空原有数据
|
|
|
+ resultList.addAll(missingSimulationData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 最终校验
|
|
|
+ if (resultList.size() != transferVO.getDevices().size()) {
|
|
|
+ log.warn("数据整合后仍存在缺失,请求设备数量:{},返回设备数量:{}",
|
|
|
+ transferVO.getDevices().size(), resultList.size());
|
|
|
+ } else {
|
|
|
+ log.debug("数据整合完成,设备数量与请求一致:{}", resultList.size());
|
|
|
}
|
|
|
|
|
|
return resultList;
|
|
|
@@ -231,8 +280,15 @@ public class DeviceDataQuery {
|
|
|
}
|
|
|
|
|
|
// 添加设备标识信息
|
|
|
- targetData.put("deviceuuid", deviceUuid);
|
|
|
String deviceId = deviceUuidToIdMap.get(deviceUuid);
|
|
|
+
|
|
|
+ // 如果找不到,直接丢弃这条数据,不放入结果
|
|
|
+ if (deviceId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 能走到这里,说明一定有 deviceId
|
|
|
+ targetData.put("deviceuuid", deviceUuid);
|
|
|
targetData.put("device_id", deviceId);
|
|
|
|
|
|
if (hasValidData) {
|
|
|
@@ -287,6 +343,7 @@ public class DeviceDataQuery {
|
|
|
private List<JSONObject> generateSimulationData(Integer deviceType, List<DmpDevice> devices, JSONObject standard) {
|
|
|
List<JSONObject> simulationList = new ArrayList<>();
|
|
|
long currentTime = System.currentTimeMillis();
|
|
|
+ // System.out.println("深拷贝当前时间戳:" + currentTime);
|
|
|
|
|
|
// 获取天气数据(仅在需要时)
|
|
|
Map<String, Double> weatherData = null;
|
|
|
@@ -298,18 +355,25 @@ public class DeviceDataQuery {
|
|
|
JSONObject simData;
|
|
|
|
|
|
if (standard != null) {
|
|
|
- // ✅ 使用标准模板进行模拟(深拷贝)
|
|
|
- simData = JSONObject.parseObject(standard.toJSONString());
|
|
|
+ simData = new JSONObject();
|
|
|
+ simData.putAll(standard);
|
|
|
+
|
|
|
+ // 【关键】先移除,确保 FastJSON 内部缓存失效
|
|
|
+ simData.remove("realtime");
|
|
|
+ simData.remove("device_id");
|
|
|
+ simData.remove("device_uuid");
|
|
|
+
|
|
|
+ // 再重新 put,强制类型正确
|
|
|
simData.put("realtime", currentTime);
|
|
|
simData.put("device_id", device.getDeviceId());
|
|
|
- // 可选:替换 device_uuid(如果模板中有)
|
|
|
- if (device.getDeviceUuid() != null && simData.containsKey("device_uuid")) {
|
|
|
+ // System.out.println("深拷贝时间戳1:" + simData.getLong("realtime"));
|
|
|
+
|
|
|
+ if (device.getDeviceUuid() != null && standard.containsKey("device_uuid")) {
|
|
|
simData.put("device_uuid", device.getDeviceUuid());
|
|
|
}
|
|
|
|
|
|
- // 对数值字段添加微小扰动(±2% 或固定范围)
|
|
|
addNoiseToNumericFields(simData, deviceType);
|
|
|
-
|
|
|
+ // System.out.println("深拷贝时间戳2:" + simData.getLong("realtime"));
|
|
|
} else {
|
|
|
// ❌ 无标准模板,走原始随机逻辑
|
|
|
simData = new JSONObject();
|
|
|
@@ -319,17 +383,24 @@ public class DeviceDataQuery {
|
|
|
switch (deviceType) {
|
|
|
case 707:
|
|
|
double temp707 = 0.0;
|
|
|
- if (weatherData != null && !weatherData.isEmpty()) {
|
|
|
- double floating = ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN, FLOATING_RANGE_MAX);
|
|
|
- temp707 = weatherData.get("temperature") + 0.5 + floating;
|
|
|
+ if (!weatherData.isEmpty()) {
|
|
|
+ double floating = BasementClimateUtil.getTempDiffWithOutdoor();
|
|
|
+ temp707 = weatherData.get("temperature") + floating;
|
|
|
} else {
|
|
|
- temp707 = ThreadLocalRandom.current().nextDouble(TEMP_RANGE_MIN, TEMP_RANGE_MAX);
|
|
|
+ temp707 = BasementClimateUtil.getCurrentTempRange();
|
|
|
}
|
|
|
- simData.put("wd", formatNumber(temp707, FORMAT_2_2));
|
|
|
+ simData.put("wd", temp707);
|
|
|
break;
|
|
|
|
|
|
case 708:
|
|
|
- simData.put("sd", formatNumber(ThreadLocalRandom.current().nextDouble(HUMIDITY_RANGE_MIN, HUMIDITY_RANGE_MAX), FORMAT_2_2));
|
|
|
+ double humi708 = 0.0;
|
|
|
+ if (!weatherData.isEmpty()) {
|
|
|
+ double floating = BasementClimateUtil.getHumiDiffWithOutdoor();
|
|
|
+ humi708 = weatherData.get("humidity") + floating;
|
|
|
+ } else {
|
|
|
+ humi708 = BasementClimateUtil.getCurrentHumiRange();
|
|
|
+ }
|
|
|
+ simData.put("sd", humi708);
|
|
|
break;
|
|
|
|
|
|
case 709:
|
|
|
@@ -355,6 +426,7 @@ public class DeviceDataQuery {
|
|
|
break;
|
|
|
|
|
|
case 704:
|
|
|
+ case 705:
|
|
|
double aVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
|
|
|
simData.put("aVoltage", formatNumber(aVoltage, FORMAT_3_2));
|
|
|
double bVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
|
|
|
@@ -399,6 +471,10 @@ public class DeviceDataQuery {
|
|
|
simData.put("wy", 0);
|
|
|
break;
|
|
|
|
|
|
+ case 716:
|
|
|
+ simData.put("sensorValue", formatNumber(ThreadLocalRandom.current().nextDouble(SEWAGE_LEVEL_MIN, SEWAGE_LEVEL_MAX), FORMAT_1_2));
|
|
|
+ break;
|
|
|
+
|
|
|
default:
|
|
|
log.warn("未知设备类型:{},无法生成模拟数据", deviceType);
|
|
|
continue;
|
|
|
@@ -413,33 +489,86 @@ public class DeviceDataQuery {
|
|
|
}
|
|
|
|
|
|
private void addNoiseToNumericFields(JSONObject data, Integer deviceType) {
|
|
|
- // 定义各字段的噪声比例或范围(可根据设备类型定制)
|
|
|
Map<String, Double> noiseConfig = getNoiseConfigByDeviceType(deviceType);
|
|
|
|
|
|
+ // 这些字段永远不加噪
|
|
|
+ Set<String> skipFields = new HashSet<>(Arrays.asList("realtime", "device_id", "device_uuid"));
|
|
|
+
|
|
|
for (String key : data.keySet()) {
|
|
|
+ if (skipFields.contains(key)) continue;
|
|
|
+ if (isDiscreteField(key)) continue;
|
|
|
+
|
|
|
Object value = data.get(key);
|
|
|
- if (value instanceof Number) {
|
|
|
- double original = ((Number) value).doubleValue();
|
|
|
- // 默认 ±2%
|
|
|
- double noiseFactor = noiseConfig.getOrDefault(key, 0.02);
|
|
|
+ Double original = null;
|
|
|
|
|
|
- // 避免对状态类字段(如 0/1)加噪
|
|
|
- if (isDiscreteField(key)) {
|
|
|
- continue;
|
|
|
+ // 【关键】支持 Number 和 String 两种类型
|
|
|
+ if (value instanceof Number) {
|
|
|
+ original = ((Number) value).doubleValue();
|
|
|
+ } else if (value instanceof String) {
|
|
|
+ try {
|
|
|
+ original = Double.parseDouble((String) value);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ System.out.println("跳过字段(非数字): " + key + " = " + value);
|
|
|
+ continue; // 不是数字字符串,跳过
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- double noise = original * noiseFactor * (ThreadLocalRandom.current().nextDouble(-1, 1));
|
|
|
- double newValue = original + noise;
|
|
|
+ if (original == null) continue;
|
|
|
|
|
|
- // 根据原格式保留小数位(这里简化处理,也可记录原格式)
|
|
|
+ // 每个设备、每个字段、每次调用都是独立的随机噪声 ✅
|
|
|
+ double noiseFactor = noiseConfig.getOrDefault(key, 0.02);
|
|
|
+ double noise = original * noiseFactor * (ThreadLocalRandom.current().nextDouble(-1, 1));
|
|
|
+ double newValue = original + noise;
|
|
|
+
|
|
|
+ // System.out.println("加噪: " + key + " | 原值=" + original + " | 噪声=" + noise + " | 新值=" + newValue);
|
|
|
+
|
|
|
+ // 保持原类型:原来是 String 就格式化回 String,原来是 Number 就放 Double
|
|
|
+ if (value instanceof String) {
|
|
|
+ data.put(key, formatNumber(newValue, getFormatByKey(key)));
|
|
|
+ } else {
|
|
|
data.put(key, newValue);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据字段名获取对应的格式化器
|
|
|
+ */
|
|
|
+ private DecimalFormat getFormatByKey(String key) {
|
|
|
+ switch (key) {
|
|
|
+ case "aVoltage":
|
|
|
+ case "bVoltage":
|
|
|
+ case "cVoltage":
|
|
|
+ case "aElectricity":
|
|
|
+ case "bElectricity":
|
|
|
+ case "cElectricity":
|
|
|
+ return FORMAT_3_2;
|
|
|
+ case "totalPower":
|
|
|
+ return FORMAT_4_2;
|
|
|
+ case "line1TEMP":
|
|
|
+ case "Line2TEMP":
|
|
|
+ case "Line3TEMP":
|
|
|
+ case "Line4TEMP":
|
|
|
+ case "wd":
|
|
|
+ case "sd":
|
|
|
+ case "o2":
|
|
|
+ return FORMAT_2_2;
|
|
|
+ case "co2":
|
|
|
+ return FORMAT_0_3;
|
|
|
+ case "leakageCurrent":
|
|
|
+ return FORMAT_4_2;
|
|
|
+ case "sensorValue":
|
|
|
+ return FORMAT_1_2;
|
|
|
+ default:
|
|
|
+ return null; // 不格式化,保持原样
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private boolean isDiscreteField(String field) {
|
|
|
- // 这些字段是状态码,不应加噪
|
|
|
- return field.equalsIgnoreCase("leach_status")
|
|
|
+ return field.equalsIgnoreCase("realtime")
|
|
|
+ || field.equalsIgnoreCase("device_id")
|
|
|
+ || field.equalsIgnoreCase("device_uuid")
|
|
|
+ || field.equalsIgnoreCase("leach_status")
|
|
|
|| field.equalsIgnoreCase("sensorValue")
|
|
|
|| field.equalsIgnoreCase("qx")
|
|
|
|| field.equalsIgnoreCase("cd")
|
|
|
@@ -452,7 +581,7 @@ public class DeviceDataQuery {
|
|
|
Map<String, Double> config = new HashMap<>();
|
|
|
switch (deviceType) {
|
|
|
case 707: // 温度
|
|
|
- config.put("wd", 0.03); // ±3%
|
|
|
+ config.put("wd", 0.03); //
|
|
|
break;
|
|
|
case 708: // 湿度
|
|
|
config.put("sd", 0.05);
|