feat: 新增太阳能控制下发指令功能。

This commit is contained in:
tianyongbao
2026-07-01 01:02:17 +08:00
parent 573bac7505
commit 2930829ee9
4 changed files with 670 additions and 118 deletions

View File

@@ -45,8 +45,11 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 设备数据处理器
@@ -59,9 +62,15 @@ import java.util.Map;
@ConditionalOnBean(IDeviceSensorDataService.class)
public class DeviceDataHandler {
public static final String PENDING_SOLAR_SALINITY_KEY_PREFIX = "device:pending:solar:salinity:";
public static final String PENDING_SOLAR_PROPERTY_KEY_PREFIX = "device:pending:solar:property:";
private static final int DEVICE_TYPE_SOLAR_CONTROLLER = 3;
/**
* 太阳能网控上线后缓存属性下发的延迟时间(毫秒)
* 设备上线后 MQTT 订阅需要一定时间完成,延迟后再下发设置避免失败
*/
private static final long SOLAR_PROPERTY_DELAY_MS = 400L;
/**
* TDengine 服务,用于存储设备时序数据
*/
@@ -127,6 +136,11 @@ public class DeviceDataHandler {
*/
private final AliyunIotProperties aliyunIotProperties;
/**
* 调度线程池,用于延迟执行任务
*/
private final ScheduledExecutorService scheduledExecutorService;
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
@@ -246,7 +260,7 @@ public class DeviceDataHandler {
if (params != null) {
handleSalinitySet(deviceName, params);
}
// flushPendingSolarSalinity(deviceName, "属性上报");
// flushPendingSolarProperties(deviceName, "属性上报");
if (params != null && params.size() > 0) {
try {
@@ -960,71 +974,16 @@ public class DeviceDataHandler {
log.info("[离线告警] 设备已上线,取消离线告警: {}", deviceName);
}
// 设备上线后直接补发缓存盐度
flushPendingSolarSalinity(deviceName, "生命周期上线");
// 太阳能网控上线后延迟下发缓存属性设备MQTT订阅需要一定时间完成立即下发可能失败
scheduledExecutorService.schedule(
() -> flushPendingSolarProperties(deviceName, "生命周期上线"),
SOLAR_PROPERTY_DELAY_MS, TimeUnit.MILLISECONDS);
// 同步更新 Device.warnCode清除离线和休眠标志位
setDeviceWarnCodeBit(deviceName, DefineDeviceWarnCode.DeviceOffline, false);
setDeviceWarnCodeBit(deviceName, DefineDeviceWarnCode.DeviceSleeping, false);
}
private void flushPendingSolarSalinity(String deviceName, String trigger) {
Device device = findDeviceByOnlineName(deviceName);
if (device == null) {
log.debug("[太阳能盐度] 上线设备未绑定,跳过补发: deviceName={}", deviceName);
return;
}
if (!Integer.valueOf(DEVICE_TYPE_SOLAR_CONTROLLER).equals(device.getDeviceType())) {
log.debug("[太阳能盐度] 非太阳能网控设备,跳过补发: deviceName={}, deviceId={}, deviceType={}",
deviceName, device.getId(), device.getDeviceType());
return;
}
try {
PendingSolarSalinity pendingSalinity = findPendingSolarSalinity(deviceName, device);
if (pendingSalinity == null) {
logNoPendingSolarSalinity(trigger, deviceName, device);
return;
}
JSONObject pending = JSONUtil.parseObj(pendingSalinity.rawPending);
Double salinityCompensation = pending.getDouble("salinityCompensation");
String iotId = pending.getStr("iotId");
Long deviceId = pending.getLong("deviceId");
if (salinityCompensation == null || iotId == null || deviceId == null) {
log.warn("[太阳能盐度] 缓存数据不完整,跳过补发: deviceName={}, pending={}", deviceName, pending);
return;
}
log.info("[太阳能盐度] 太阳能网控{},准备补发缓存盐度: deviceName={}, cacheKey={}, deviceId={}, salinityCompensation={}",
trigger, deviceName, pendingSalinity.redisKey, deviceId, salinityCompensation);
Map<String, Object> properties = new HashMap<>();
properties.put("salinitySet", salinityCompensation);
boolean success = iotCloudService.setProperty(iotId, properties, true, 2);
if (!success) {
log.warn("[太阳能盐度] {}补发失败,保留缓存: deviceName={}, deviceId={}, salinityCompensation={}",
trigger, deviceName, deviceId, salinityCompensation);
return;
}
Device updateDevice = new Device();
updateDevice.setId(deviceId);
updateDevice.setSalinityCompensation(salinityCompensation);
int updated = deviceMapper.updateById(updateDevice);
if (updated > 0) {
clearPendingSolarSalinity(deviceName, device, pendingSalinity.redisKey);
log.info("[太阳能盐度] {}补发成功并清除缓存: deviceName={}, cacheKey={}, deviceId={}, salinityCompensation={}",
trigger, deviceName, pendingSalinity.redisKey, deviceId, salinityCompensation);
} else {
log.warn("[太阳能盐度] {}补发成功但设备表更新失败,保留缓存以便重试: deviceName={}, deviceId={}",
trigger, deviceName, deviceId);
}
} catch (Exception e) {
log.error("[太阳能盐度] {}补发异常: deviceName={}, err={}", trigger, deviceName, e.getMessage(), e);
}
}
private Device findDeviceByOnlineName(String deviceName) {
if (StrUtil.isBlank(deviceName)) {
return null;
@@ -1041,47 +1000,196 @@ public class DeviceDataHandler {
);
}
private PendingSolarSalinity findPendingSolarSalinity(String deviceName, Device device) {
private void flushPendingSolarProperties(String deviceName, String trigger) {
Device device = findDeviceByOnlineName(deviceName);
if (device == null) {
log.debug("[太阳能属性] 上线设备未绑定,跳过补发: deviceName={}", deviceName);
return;
}
if (!Integer.valueOf(DEVICE_TYPE_SOLAR_CONTROLLER).equals(device.getDeviceType())) {
log.debug("[太阳能属性] 非太阳能网控设备,跳过补发: deviceName={}, deviceId={}, deviceType={}",
deviceName, device.getId(), device.getDeviceType());
return;
}
try {
PendingSolarProperty pendingProperty = findPendingSolarProperty(device);
if (pendingProperty == null) {
logNoPendingSolarProperty(trigger, deviceName, device);
return;
}
if (pendingProperty.iotId == null || pendingProperty.deviceId == null || pendingProperty.properties == null) {
log.warn("[太阳能属性] 缓存数据不完整,跳过补发: deviceName={}, cacheKey={}",
deviceName, pendingProperty.redisKey);
return;
}
if (pendingProperty.properties.isEmpty()) {
log.warn("[太阳能属性] 缓存属性为空,跳过补发: deviceName={}, cacheKey={}",
deviceName, pendingProperty.redisKey);
return;
}
log.info("[太阳能属性] 太阳能网控{},准备补发缓存属性: deviceName={}, cacheKey={}, deviceId={}, properties={}",
trigger, deviceName, pendingProperty.redisKey, pendingProperty.deviceId, pendingProperty.properties);
boolean success = iotCloudService.setProperty(pendingProperty.iotId, pendingProperty.properties, true, 2);
if (!success) {
log.warn("[太阳能属性] {}补发失败,保留缓存: deviceName={}, deviceId={}, properties={}",
trigger, deviceName, pendingProperty.deviceId, pendingProperty.properties);
return;
}
syncDeviceFieldsAfterPropertySet(pendingProperty.deviceId, pendingProperty.properties);
clearPendingSolarProperty(pendingProperty);
log.info("[太阳能属性] {}补发成功并清除缓存: deviceName={}, cacheKey={}, deviceId={}, properties={}",
trigger, deviceName, pendingProperty.redisKey, pendingProperty.deviceId, pendingProperty.properties);
} catch (Exception e) {
log.error("[太阳能属性] {}补发异常: deviceName={}, err={}", trigger, deviceName, e.getMessage(), e);
}
}
private PendingSolarProperty findPendingSolarProperty(Device device) {
String serialNum = device.getSerialNum();
if (StrUtil.isBlank(serialNum)) {
return null;
}
// 太阳能盐度缓存为跨上下文共享数据HTTP线程写入 + AMQP消费线程读取
// 必须忽略租户前缀,避免写入带租户前缀而读取不带前缀导致 key 不匹配
return TenantHelper.ignore(() -> {
String redisKey = PENDING_SOLAR_SALINITY_KEY_PREFIX + serialNum;
String redisKey = PENDING_SOLAR_PROPERTY_KEY_PREFIX + serialNum;
Object rawPending = RedisUtils.getCacheObject(redisKey);
return rawPending != null ? new PendingSolarSalinity(redisKey, rawPending) : null;
if (rawPending == null) {
return null;
}
Long deviceId = device.getId();
String iotId = device.getIotId();
Map<String, Object> properties = new LinkedHashMap<>();
try {
JSONObject pending = JSONUtil.parseObj(rawPending);
Long cachedDeviceId = pending.getLong("deviceId");
String cachedIotId = pending.getStr("iotId");
if (cachedDeviceId != null) {
deviceId = cachedDeviceId;
}
if (StrUtil.isNotBlank(cachedIotId)) {
iotId = cachedIotId;
}
Object propertiesObj = pending.get("properties");
if (propertiesObj != null) {
JSONObject propertiesJson = JSONUtil.parseObj(propertiesObj);
for (String key : propertiesJson.keySet()) {
properties.put(key, propertiesJson.get(key));
}
}
} catch (Exception e) {
log.warn("[太阳能属性] 解析缓存属性失败: serialNum={}, err={}", serialNum, e.getMessage());
}
return new PendingSolarProperty(redisKey, deviceId, iotId, properties);
});
}
private boolean hasPendingSolarSalinity(String deviceName, Device device) {
return findPendingSolarSalinity(deviceName, device) != null;
private boolean hasPendingSolarProperty(Device device, String identifier) {
if (device == null || StrUtil.isBlank(identifier)) {
return false;
}
PendingSolarProperty pendingProperty = findPendingSolarProperty(device);
return pendingProperty != null
&& pendingProperty.properties != null
&& pendingProperty.properties.containsKey(identifier);
}
private void logNoPendingSolarSalinity(String trigger, String deviceName, Device device) {
private void logNoPendingSolarProperty(String trigger, String deviceName, Device device) {
if ("生命周期上线".equals(trigger)) {
log.info("[太阳能盐度] 太阳能网控{},未找到待补发缓存: deviceName={}, deviceId={}, serialNum={}, iotId={}",
log.info("[太阳能属性] 太阳能网控{},未找到待补发缓存: deviceName={}, deviceId={}, serialNum={}, iotId={}",
trigger, deviceName, device.getId(), device.getSerialNum(), device.getIotId());
return;
}
log.debug("[太阳能盐度] 太阳能网控{},未找到待补发缓存: deviceName={}, deviceId={}, serialNum={}, iotId={}",
log.debug("[太阳能属性] 太阳能网控{},未找到待补发缓存: deviceName={}, deviceId={}, serialNum={}, iotId={}",
trigger, deviceName, device.getId(), device.getSerialNum(), device.getIotId());
}
private void clearPendingSolarSalinity(String deviceName, Device device, String matchedRedisKey) {
// 忽略租户前缀,确保与写入侧 key 一致(写入在 HTTP 线程,删除在 AMQP 线程)
TenantHelper.ignore(() -> RedisUtils.deleteObject(matchedRedisKey));
private void clearPendingSolarProperty(PendingSolarProperty pendingProperty) {
if (pendingProperty == null) {
return;
}
TenantHelper.ignore(() -> {
if (StrUtil.isNotBlank(pendingProperty.redisKey)) {
RedisUtils.deleteObject(pendingProperty.redisKey);
}
});
}
private static class PendingSolarSalinity {
private final String redisKey;
private final Object rawPending;
private void syncDeviceFieldsAfterPropertySet(Long deviceId, Map<String, Object> properties) {
if (deviceId == null || properties == null || properties.isEmpty()) {
return;
}
Device updateDevice = new Device();
updateDevice.setId(deviceId);
boolean needUpdate = false;
private PendingSolarSalinity(String redisKey, Object rawPending) {
if (properties.containsKey("salinitySet")) {
Double value = toDouble(properties.get("salinitySet"));
if (value != null) {
updateDevice.setSalinityCompensation(value);
needUpdate = true;
}
}
if (properties.containsKey("phase_compensation")) {
Double value = toDouble(properties.get("phase_compensation"));
if (value != null) {
updateDevice.setPhaseCompensation(value);
needUpdate = true;
}
}
if (properties.containsKey("phasedif_compensation")) {
Double value = toDouble(properties.get("phasedif_compensation"));
if (value != null) {
updateDevice.setPhasedifCompensation(value);
needUpdate = true;
}
}
if (properties.containsKey("temperature_compensation")) {
Double value = toDouble(properties.get("temperature_compensation"));
if (value != null) {
updateDevice.setTemperatureCompensation(value);
needUpdate = true;
}
}
if (needUpdate) {
deviceMapper.updateById(updateDevice);
}
}
private Double toDouble(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.doubleValue();
}
try {
return Double.valueOf(value.toString());
} catch (Exception e) {
return null;
}
}
private static class PendingSolarProperty {
private final String redisKey;
private final Long deviceId;
private final String iotId;
private final Map<String, Object> properties;
private PendingSolarProperty(String redisKey, Long deviceId, String iotId, Map<String, Object> properties) {
this.redisKey = redisKey;
this.rawPending = rawPending;
this.deviceId = deviceId;
this.iotId = iotId;
this.properties = properties;
}
}
@@ -2748,15 +2856,15 @@ public class DeviceDataHandler {
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.select(Device::getId)
.select(Device::getId, Device::getIotId, Device::getSerialNum)
.last("LIMIT 1")
);
if (device == null) {
log.debug("设备不存在,无法更新盐度补偿设定值: {}", deviceName);
return;
}
if (hasPendingSolarSalinity(deviceName, device)) {
log.debug("[太阳能盐度] 存在待补发盐度跳过设备旧salinitySet同步: deviceName={}, reportSalinitySet={}",
if (hasPendingSolarProperty(device, "salinitySet")) {
log.debug("[太阳能属性] 存在待补发salinitySet跳过设备旧salinitySet同步: deviceName={}, reportSalinitySet={}",
deviceName, salinitySet);
return;
}