fix: 修改设备故障码记录相关逻辑。

This commit is contained in:
tianyongbao
2026-03-22 22:21:38 +08:00
parent a5d6394dc2
commit 0a6fed1b01
4 changed files with 1124 additions and 163 deletions

View File

@@ -16,11 +16,18 @@ import com.intc.fishery.domain.AquUser;
import com.intc.fishery.domain.Device;
import com.intc.fishery.domain.MessageWarn;
import com.intc.fishery.domain.Pond;
import com.intc.fishery.constant.DefineDeviceWarnCode;
import com.intc.fishery.mapper.AquUserMapper;
import com.intc.fishery.domain.DeviceErrorCode;
import com.intc.fishery.domain.DeviceSwitch;
import com.intc.fishery.constant.DefineDeviceErrorCode;
import com.intc.fishery.mapper.DeviceMapper;
import com.intc.fishery.mapper.DeviceSwitchMapper;
import com.intc.fishery.mapper.DeviceErrorCodeMapper;
import com.intc.fishery.mapper.LinkedCtrlMapper;
import com.intc.fishery.mapper.MessageWarnMapper;
import com.intc.fishery.mapper.PondMapper;
import com.intc.iot.service.IotCloudService;
import com.intc.tdengine.domain.DeviceSensorData;
import com.intc.tdengine.service.IDeviceSensorDataService;
import com.intc.iot.config.AliyunIotProperties;
@@ -89,6 +96,21 @@ public class DeviceDataHandler {
*/
private final PondMapper pondMapper;
/**
* 设备故障码 Mapper
*/
private final DeviceErrorCodeMapper deviceErrorCodeMapper;
/**
* 联动控制 Mapper
*/
private final LinkedCtrlMapper linkedCtrlMapper;
/**
* IoT 云端服务(下发属性指令)
*/
private final IotCloudService iotCloudService;
/**
* VMS 语音通知服务
*/
@@ -191,6 +213,14 @@ public class DeviceDataHandler {
// 判断是否为控制器设备使用配置文件中的ProductKey
boolean isController = controllerProductKey.equals(productKey);
// 收到设备属性上报,说明设备在线,清除离线标志
clearDeviceOfflineFlag(deviceName);
// 处理探头错误码sensorErrorCode 位掩码:更新 Device.warnCode 中探头离线位)
if (params != null && params.containsKey("sensorErrorCode")) {
handleSensorErrorCode(deviceName, params);
}
if (params != null && params.size() > 0) {
try {
// 如果是控制器,需要区分处理
@@ -199,6 +229,10 @@ public class DeviceDataHandler {
if (hasSwitchData(params)) {
// 开关数据:更新开关状态和电压电流
handleSwitchData(deviceName, params);
// 控制器故障码处理C# itemController.errorCode
if (params.containsKey("errorCode")) {
handleControllerErrorCode(deviceName, params);
}
} else if (hasSensorData(params)) {
// 传感器数据写入TDengine
sensorData = convertToSensorData(productKey, deviceName, params);
@@ -209,6 +243,11 @@ public class DeviceDataHandler {
// 更新设备表的实时数据
updateDeviceRealTimeData(deviceName, sensorData);
// 联动控制:根据溶解氧数据控制开关
if (sensorData.getDissolvedOxygen() != null) {
handleLinkedCtrl(deviceName, sensorData.getDissolvedOxygen());
}
}
} else {
// 既不是开关数据也不是传感器数据
@@ -225,6 +264,11 @@ public class DeviceDataHandler {
// 更新设备表的实时数据
updateDeviceRealTimeData(deviceName, sensorData);
// 联动控制:根据溶解氧数据控制开关
if (sensorData.getDissolvedOxygen() != null) {
handleLinkedCtrl(deviceName, sensorData.getDissolvedOxygen());
}
}
} else {
log.debug("数据字段不足3个已过滤: {} (字段数: {})", deviceName, params.size());
@@ -830,6 +874,9 @@ public class DeviceDataHandler {
int ttlMinutes = offlineDelayMinutes * OFFLINE_WAIT_TTL_MULTIPLIER;
RedisUtils.setCacheObject(redisKey, offlineTime, Duration.ofMinutes(ttlMinutes));
log.info("[离线告警] 设备离线,已记录等待标记,将在 {} 分钟后触发告警: {}", offlineDelayMinutes, deviceName);
// 同步更新 Device.warnCode写入离线标志位
setDeviceWarnCodeBit(deviceName, DefineDeviceWarnCode.DeviceOffline, true);
}
/**
@@ -844,6 +891,9 @@ public class DeviceDataHandler {
if (removed) {
log.info("[离线告警] 设备已上线,取消离线告警: {}", deviceName);
}
// 同步更新 Device.warnCode清除离线标志位
setDeviceWarnCodeBit(deviceName, DefineDeviceWarnCode.DeviceOffline, false);
}
/**
@@ -1094,25 +1144,703 @@ public class DeviceDataHandler {
}
/**
* 触发设备故障告警
* 针对设备事件error的紧急告警
* 触发设备故障告警(对应 IoT 事件路径 error 事件)
* 参考 C# CreateNoticeCall 逻辑:查设备→查塘口→写告警消息→创建电话通知记录→立即发送
*
* @param deviceName 设备名称
* @param params 事件参数
*/
private void triggerDeviceErrorAlarm(String deviceName, JSONObject params) {
try {
String errorMsg = params != null ? params.toString() : "设备故障";
log.error("[设备故障] 设备: {}, 故障信息: {}", deviceName, errorMsg);
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.last("LIMIT 1")
);
if (device == null) {
log.debug("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907\u4e0d\u5b58\u5728\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
// TODO: 实现紧急告警逻辑
// 1. 查询设备信息和用户
// 2. 创建紧急告警记录
// 3. 立即发送语音通知
// 4. 可选:发送短信、推送消息等多种通知方式
Long deviceId = device.getId();
Long userId = device.getUserId();
if (userId == null) {
log.debug("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907\u672a\u7ed1\u5b9a\u7528\u6237\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
if (device.getPondId() == null) {
log.debug("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907\u672a\u7ed1\u5b9a\u5858\u53e3\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
Pond pond = pondMapper.selectById(device.getPondId());
if (pond == null) {
log.debug("[\u8bbe\u5907\u6545\u969c] \u5858\u53e3\u4e0d\u5b58\u5728\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
String pondName = pond.getPondName();
String displayName = device.getDeviceName() != null ? device.getDeviceName() : deviceName;
// \u89e3\u6790\u4e8b\u4ef6\u53c2\u6570\u83b7\u53d6\u6545\u969c\u7801
int errorCode = params != null && params.containsKey("errorCode")
? params.getInt("errorCode", 0) : 0;
String errorDesc = DefineDeviceErrorCode.getErrorMessage(errorCode);
if (StrUtil.isBlank(errorDesc)) {
errorDesc = "\u8bbe\u5907\u6545\u969c(code=" + errorCode + ")";
}
String warnMsg = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u53d1\u751f\u6545\u969c\uff1a%s",
pondName, displayName, deviceName, errorDesc);
log.error("[\u8bbe\u5907\u6545\u969c] \u8bbe\u5907: {}, \u6545\u969c\u4fe1\u606f: {}", deviceName, warnMsg);
// \u83b7\u53d6\u544a\u8b66\u624b\u673a\u53f7
AquUser aquUser = aquUserMapper.selectById(userId);
if (aquUser == null) {
log.warn("[\u8bbe\u5907\u6545\u969c] \u7528\u6237\u4e0d\u5b58\u5728\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
java.util.List<String> phoneList = parseWarnPhoneList(aquUser.getWarnPhoneJson());
if (phoneList == null || phoneList.isEmpty()) {
log.warn("[\u8bbe\u5907\u6545\u969c] \u7528\u6237\u672a\u914d\u7f6e\u544a\u8b66\u624b\u673a\u53f7\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
// \u53cc\u91cd\u6291\u5236
String errorTitle = "\u63a7\u5236\u5668\u6545\u969c";
int maxBatchMinutesErr = CALL_NOTICE_RETRY_COUNT * CALL_NOTICE_MINUTE_INTERVAL * 2;
LocalDateTime activeBatchStartErr = LocalDateTime.now().minusMinutes(maxBatchMinutesErr);
long activeCountErr = warnCallNoticeMapper.countActiveByDeviceAndTitleAfter(deviceId, errorTitle, activeBatchStartErr);
if (activeCountErr > 0) {
log.debug("[\u8bbe\u5907\u6545\u969c] \u5f53\u524d\u6709\u6d3b\u8dc3\u901a\u77e5\u6279\u6b21\u5728\u8fdb\u884c\uff0c\u8df3\u8fc7: {}", deviceName);
return;
}
LocalDateTime suppressTimeErr = LocalDateTime.now().minusHours(aliyunIotProperties.getCallSuccessSuppressHours());
long recentCountErr = warnCallNoticeMapper.countSuccessByDeviceAndTitleAfter(deviceId, errorTitle, suppressTimeErr);
if (recentCountErr > 0) {
log.debug("[\u8bbe\u5907\u6545\u969c] {}h\u5185\u5df2\u6709\u56de\u6267\u6210\u529f\u7684\u901a\u77e5\uff0c\u8df3\u8fc7: {}", aliyunIotProperties.getCallSuccessSuppressHours(), deviceName);
return;
}
// \u5199\u5165\u544a\u8b66\u6d88\u606f
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, errorTitle, WARN_TYPE_DISSOLVED_OXYGEN, warnMsg);
messageWarnMapper.insert(warn);
// \u521b\u5efa\u7535\u8bdd\u901a\u77e5\u8bb0\u5f55
LocalDateTime baseTime = LocalDateTime.now();
int index = 0;
AquWarnCallNotice firstCallNotice = null;
for (int retry = 0; retry < CALL_NOTICE_RETRY_COUNT; retry++) {
for (String phoneNum : phoneList) {
if (StrUtil.isBlank(phoneNum) || phoneNum.length() < 11) {
continue;
}
LocalDateTime callTime = baseTime.plusMinutes((long) CALL_NOTICE_MINUTE_INTERVAL * index);
index++;
AquWarnCallNotice callNotice = new AquWarnCallNotice();
callNotice.setUserId(userId);
callNotice.setMobilePhone(phoneNum);
callNotice.setDeviceId(deviceId);
callNotice.setPondName(pondName);
callNotice.setCallTime(callTime);
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
warnCallNoticeMapper.insert(callNotice);
if (warn.getId() != null) {
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
mapRecord.setMessageWarnId(warn.getId());
mapRecord.setCallNoticeId(callNotice.getId());
mapMessageWarnCallNoticeMapper.insert(mapRecord);
}
if (firstCallNotice == null) {
firstCallNotice = callNotice;
}
}
}
if (firstCallNotice != null) {
sendVoiceNotification(firstCallNotice, displayName, errorTitle);
}
log.info("[\u8bbe\u5907\u6545\u969c] \u5df2\u89e6\u53d1\u901a\u77e5 - \u8bbe\u5907\uff1a{}, \u5858\u53e3\uff1a{}, \u624b\u673a\u53f7\u6570\uff1a{}",
deviceName, pondName, phoneList.size());
} catch (Exception e) {
log.error("[设备故障] 触发故障告警失败: {}", e.getMessage(), e);
log.error("[\u8bbe\u5907\u6545\u969c] \u89e6\u53d1\u6545\u969c\u544a\u8b66\u5931\u8d25: {}", e.getMessage(), e);
}
}
/**
* 处理控制器上报的 errorCode属性上报路径对应 C# itemController.errorCode
* <ul>
* <li>errorCode == 0清除所有故障码ClearErrorCode</li>
* <li>errorCode/100%10 == 2删除指定故障码RemoveErrorCodeerrorCode -= 200</li>
* <li>errorCode/100%10 == 0新增故障码CreateErrorCode</li>
* </ul>
*
* @param deviceName 设备名称serialNum
* @param params 上报参数
*/
private void handleControllerErrorCode(String deviceName, JSONObject params) {
try {
Integer rawCode = params.getInt("errorCode");
if (rawCode == null) {
log.warn("[故障码] 解析 errorCode 失败: {}", deviceName);
return;
}
int errorCode = rawCode;
// 查询设备基本信息
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.last("LIMIT 1")
);
if (device == null) {
return;
}
if (errorCode == 0) {
// 清除所有故障码
clearErrorCodeRecords(device);
log.info("[故障码] 设备={} 清除所有故障码", deviceName);
} else {
int tempIndex = errorCode / 100 % 10;
if (tempIndex == 2) {
// 故障恢复
int realCode = errorCode - 200;
removeErrorCodeRecord(device, realCode);
log.info("[故障码] 设备={} 清除故障码={}", deviceName, realCode);
} else if (tempIndex == 0) {
// 新增故障
createErrorCodeRecord(device, errorCode);
log.info("[故障码] 设备={} 新增故障码={}", deviceName, errorCode);
} else {
log.warn("[故障码] 设备={} 无法解析故障码={}", deviceName, errorCode);
}
}
} catch (Exception e) {
log.error("[故障码] 处理失败[{}]: {}", deviceName, e.getMessage(), e);
}
}
/**
* 创建设备故障码记录,并根据故障类型生成告警消息和电话通知
* 对应 C# CreateErrorCode 逻辑
*
* @param device 设备信息
* @param errorCode 故障码
*/
private void createErrorCodeRecord(Device device, int errorCode) {
try {
Long deviceId = device.getId();
if (deviceId == null) {
return;
}
// 检查是否已存在相同故障码
long existCount = deviceErrorCodeMapper.selectCount(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DeviceErrorCode>()
.eq(DeviceErrorCode::getDeviceId, deviceId)
.eq(DeviceErrorCode::getErrorCode, errorCode)
);
if (existCount > 0) {
log.debug("[故障码] 设备={} 故障码={} 已存在,跳过", device.getSerialNum(), errorCode);
return;
}
// 查询塘口(如果设备在塘口中)
String pondName = null;
if (device.getPondId() != null) {
Pond pond = pondMapper.selectById(device.getPondId());
if (pond != null) {
pondName = pond.getPondName();
}
}
String displayName = device.getDeviceName() != null ? device.getDeviceName() : device.getSerialNum();
// 设备在塘口中且非山机才触发告警
boolean isOffline = device.getWarnCode() != null
&& (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) != 0;
boolean warnTrigger = !isOffline && pondName != null;
// 根据故障码范围分类生成告警内容
String title = null;
String message = null;
int switchIndex = 0;
if (errorCode >= DefineDeviceErrorCode.Three_Switch1LosePhaseA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4LosePhaseC) {
// 三相开关输出缺相
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1LosePhaseA;
String phaseName = DefineDeviceErrorCode.getPhaseName(tempIndex % 3);
switchIndex = tempIndex / 3 + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
// 缺相故障强制关闭开关
forceSwitchOff(device, switchIndex);
if (warnTrigger) {
String swPondName = (sw != null && sw.getPondId() != null)
? getPondName(sw.getPondId()) : pondName;
String swName = sw != null && sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u7f3a\u76f8";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa\u7f3a%s\u76f8",
swPondName, displayName, device.getSerialNum(), switchIndex, swName, phaseName);
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1OverElectricA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4OverElectricC) {
// 三相过流
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1OverElectricA;
String phaseName = DefineDeviceErrorCode.getPhaseName(tempIndex % 3);
switchIndex = tempIndex / 3 + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u8fc7\u6d41";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa%s\u76f8\u8fc7\u6d41",
swPondName, displayName, device.getSerialNum(), switchIndex, swName, phaseName);
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1UnderElectricA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4UnderElectricC) {
// 三相欠流
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1UnderElectricA;
String phaseName = DefineDeviceErrorCode.getPhaseName(tempIndex % 3);
switchIndex = tempIndex / 3 + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u6b20\u6d41";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa%s\u76f8\u6b20\u6d41",
swPondName, displayName, device.getSerialNum(), switchIndex, swName, phaseName);
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1ElectricEmpty
&& errorCode <= DefineDeviceErrorCode.Three_Switch4ElectricEmpty) {
// 三相空载
switchIndex = errorCode - DefineDeviceErrorCode.Three_Switch1ElectricEmpty + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u7a7a\u8f7d";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa\u7a7a\u8f7d",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
}
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1OverElectric
&& errorCode <= DefineDeviceErrorCode.One_Switch4OverElectric) {
// 单相过流
switchIndex = errorCode - DefineDeviceErrorCode.One_Switch1OverElectric + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u8fc7\u6d41";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa\u8fc7\u6d41",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
}
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1UnderElectric
&& errorCode <= DefineDeviceErrorCode.One_Switch4UnderElectric) {
// 单相欠流
switchIndex = errorCode - DefineDeviceErrorCode.One_Switch1UnderElectric + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u6b20\u6d41";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa\u6b20\u6d41",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
}
} else if (errorCode >= DefineDeviceErrorCode.One_Switch1ElectricEmpty
&& errorCode <= DefineDeviceErrorCode.One_Switch4ElectricEmpty) {
// 单相空载
switchIndex = errorCode - DefineDeviceErrorCode.One_Switch1ElectricEmpty + 1;
DeviceSwitch sw = getDeviceSwitchByIndex(deviceId, switchIndex);
boolean electricWarnOpen = sw != null && sw.getElectricWarnOpen() != null && sw.getElectricWarnOpen() == 1;
warnTrigger = warnTrigger && electricWarnOpen;
if (warnTrigger) {
String swPondName = (sw.getPondId() != null) ? getPondName(sw.getPondId()) : pondName;
String swName = sw.getSwitchName() != null ? sw.getSwitchName() : ("\u5f00\u5173" + switchIndex);
title = "\u63a7\u5236\u5668\u5f00\u5173\u8f93\u51fa\u7a7a\u8f7d";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u7684\u5f00\u5173%d(%s)\u8f93\u51fa\u7a7a\u8f7d",
swPondName, displayName, device.getSerialNum(), switchIndex, swName);
}
} else if (errorCode == DefineDeviceErrorCode.PowerOff) {
// 断电:已存在离线状态则不触发断电告警
boolean alreadyOffline = device.getWarnCode() != null
&& (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) != 0;
warnTrigger = warnTrigger && !alreadyOffline;
// 置位 ErrorPowerOff warnCode
setDeviceWarnCodeBit(device.getSerialNum(), DefineDeviceWarnCode.ErrorPowerOff, true);
if (warnTrigger) {
title = "\u63a7\u5236\u5668\u65ad\u7535";
message = String.format("%s\u5858\u53e3\u4e2d%s(%s)\u65ad\u7535",
pondName, displayName, device.getSerialNum());
}
} else {
warnTrigger = false;
}
// 创建告警消息和电话通知
if (warnTrigger && title != null && message != null) {
Long userId = device.getUserId();
if (userId != null) {
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, title, WARN_TYPE_DISSOLVED_OXYGEN, message);
messageWarnMapper.insert(warn);
// 创建电话通知记录
createCallNotices(device, pondName, warn);
}
}
// 写入 DeviceErrorCode 数据库记录
DeviceErrorCode data = new DeviceErrorCode();
data.setDeviceId(deviceId);
data.setSwitchIndex(switchIndex);
data.setErrorCode(errorCode);
deviceErrorCodeMapper.insert(data);
} catch (Exception e) {
log.error("[故障码] 创建故障码记录失败[{}]: {}", device.getSerialNum(), e.getMessage(), e);
}
}
/**
* 删除设备指定故障码记录(对应 C# RemoveErrorCode
*
* @param device 设备信息
* @param errorCode 故障码
*/
private void removeErrorCodeRecord(Device device, int errorCode) {
try {
if (device.getId() == null) {
return;
}
// PowerOff 恢复时清除 ErrorPowerOff warnCode 位
if (errorCode == DefineDeviceErrorCode.PowerOff) {
setDeviceWarnCodeBit(device.getSerialNum(), DefineDeviceWarnCode.ErrorPowerOff, false);
}
int rows = deviceErrorCodeMapper.delete(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DeviceErrorCode>()
.eq(DeviceErrorCode::getDeviceId, device.getId())
.eq(DeviceErrorCode::getErrorCode, errorCode)
);
log.debug("[故障码] 设备={} 删除故障码={}, 影响行数={}", device.getSerialNum(), errorCode, rows);
} catch (Exception e) {
log.error("[故障码] 删除故障码失败[{}]: {}", device.getSerialNum(), e.getMessage(), e);
}
}
/**
* 清除设备所有故障码记录(对应 C# ClearErrorCode
* 同时清除 ErrorPowerOff warnCode 位
*
* @param device 设备信息
*/
private void clearErrorCodeRecords(Device device) {
try {
if (device.getId() == null) {
return;
}
deviceErrorCodeMapper.delete(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DeviceErrorCode>()
.eq(DeviceErrorCode::getDeviceId, device.getId())
);
// 清除 ErrorPowerOff 位
setDeviceWarnCodeBit(device.getSerialNum(), DefineDeviceWarnCode.ErrorPowerOff, false);
log.debug("[故障码] 设备={} 清除所有故障码完成", device.getSerialNum());
} catch (Exception e) {
log.error("[故障码] 清除故障码失败[{}]: {}", device.getSerialNum(), e.getMessage(), e);
}
}
/**
* 根据开关序号查询 DeviceSwitch
*/
private DeviceSwitch getDeviceSwitchByIndex(Long deviceId, int switchIndex) {
try {
return deviceSwitchMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DeviceSwitch>()
.eq(DeviceSwitch::getDeviceId, deviceId)
.eq(DeviceSwitch::getIndex, switchIndex)
.last("LIMIT 1")
);
} catch (Exception e) {
return null;
}
}
/**
* 根据塘口ID获取塘口名称
*/
private String getPondName(Long pondId) {
if (pondId == null) {
return null;
}
try {
Pond pond = pondMapper.selectById(pondId);
return pond != null ? pond.getPondName() : null;
} catch (Exception e) {
return null;
}
}
/**
* 强制关闭开关(故障导致的强制关闭)
*/
private void forceSwitchOff(Device device, int switchIndex) {
try {
if (device.getIotId() == null) {
return;
}
Map<String, Object> properties = new HashMap<>();
properties.put("Switch" + switchIndex, 0);
boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 1);
if (success) {
// 更新DB中的开关状态
DeviceSwitch sw = getDeviceSwitchByIndex(device.getId(), switchIndex);
if (sw != null) {
DeviceSwitch updateSw = new DeviceSwitch();
updateSw.setId(sw.getId());
updateSw.setIsOpen(0);
updateSw.setLastTurnTime(new java.util.Date());
deviceSwitchMapper.updateById(updateSw);
}
log.info("[故障码] 设备={} 开关{} 已强制关闭", device.getSerialNum(), switchIndex);
}
} catch (Exception e) {
log.error("[故障码] 强制关闭开关失败: {}", e.getMessage());
}
}
/**
* 创建电话通知记录(内部辅助方法)
*
* @param device 设备信息
* @param pondName 塘口名称
* @param warn 告警消息记录
*/
private void createCallNotices(Device device, String pondName, MessageWarn warn) {
try {
Long userId = device.getUserId();
if (userId == null) {
return;
}
AquUser aquUser = aquUserMapper.selectById(userId);
if (aquUser == null) {
return;
}
java.util.List<String> phoneList = parseWarnPhoneList(aquUser.getWarnPhoneJson());
if (phoneList == null || phoneList.isEmpty()) {
return;
}
LocalDateTime baseTime = LocalDateTime.now();
int index = 0;
AquWarnCallNotice firstCallNotice = null;
for (int retry = 0; retry < CALL_NOTICE_RETRY_COUNT; retry++) {
for (String phoneNum : phoneList) {
if (StrUtil.isBlank(phoneNum) || phoneNum.length() < 11) {
continue;
}
LocalDateTime callTime = baseTime.plusMinutes((long) CALL_NOTICE_MINUTE_INTERVAL * index);
index++;
AquWarnCallNotice callNotice = new AquWarnCallNotice();
callNotice.setUserId(userId);
callNotice.setMobilePhone(phoneNum);
callNotice.setDeviceId(device.getId());
callNotice.setPondName(pondName);
callNotice.setCallTime(callTime);
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
warnCallNoticeMapper.insert(callNotice);
if (warn.getId() != null) {
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
mapRecord.setMessageWarnId(warn.getId());
mapRecord.setCallNoticeId(callNotice.getId());
mapMessageWarnCallNoticeMapper.insert(mapRecord);
}
if (firstCallNotice == null) {
firstCallNotice = callNotice;
}
}
}
String displayName = device.getDeviceName() != null ? device.getDeviceName() : device.getSerialNum();
if (firstCallNotice != null) {
sendVoiceNotification(firstCallNotice, displayName, warn.getTitle());
}
} catch (Exception e) {
log.error("[故障码] 创建电话通知失败: {}", e.getMessage());
}
}
/**
* 处理探头错误码(参考 C# sensorErrorCode 逻辑)
* sensorErrorCode 是设备上报的一个整数位掩码,每一位对应一种探头的离线状态。
* 对 DetectorOxyOffline 特殊处理:新增离线时写入 Redis 待告警标记,恢复时删除。
* 对 DetectorPHOffline / DetectorSalinityOffline直接更新 warnCode 位。
*
* @param deviceName 设备名称serialNum
* @param params 上报参数
*/
private void handleSensorErrorCode(String deviceName, JSONObject params) {
try {
Integer rawCode = params.getInt("sensorErrorCode");
if (rawCode == null) {
log.warn("[探头错误码] 解析 sensorErrorCode 失败: {}", deviceName);
return;
}
int sensorErrorCode = rawCode;
// 查询设备,需要 id / warnCode / isOxygenUsed / pondId
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.select(Device::getId, Device::getWarnCode, Device::getIsOxygenUsed, Device::getPondId)
.last("LIMIT 1")
);
if (device == null) {
return;
}
int currentCode = device.getWarnCode() != null ? device.getWarnCode() : 0;
int newCode = currentCode;
// 1. DetectorOxyOffline溶解氧探头离线——带延迟告警
boolean oxyOfflineNow = (sensorErrorCode & DefineDeviceWarnCode.DetectorOxyOffline) != 0;
boolean oxyOfflinePrev = (currentCode & DefineDeviceWarnCode.DetectorOxyOffline) != 0;
if (oxyOfflineNow) {
if (!oxyOfflinePrev) {
// 新增离线:置位 + 写入 Redis 待告警标记
newCode |= DefineDeviceWarnCode.DetectorOxyOffline;
// 只有开启了溶氧检测且处于塘口中才创建待告警
if (device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1
&& device.getPondId() != null) {
String redisKey = "device:detector:oxy:offline:wait:" + deviceName;
if (!Boolean.TRUE.equals(com.intc.common.redis.utils.RedisUtils.hasKey(redisKey))) {
String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER);
int offlineDelayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
com.intc.common.redis.utils.RedisUtils.setCacheObject(
redisKey, offlineTime,
java.time.Duration.ofMinutes((long) offlineDelayMinutes * OFFLINE_WAIT_TTL_MULTIPLIER)
);
log.info("[探头错误码] 溶解氧探头离线,已写入待告警标记: {}", deviceName);
}
}
}
} else {
if (oxyOfflinePrev) {
// 恢复:清位 + 删除 Redis 待告警标记
newCode &= ~DefineDeviceWarnCode.DetectorOxyOffline;
String redisKey = "device:detector:oxy:offline:wait:" + deviceName;
com.intc.common.redis.utils.RedisUtils.deleteObject(redisKey);
log.info("[探头错误码] 溶解氧探头恢复,已删除待告警标记: {}", deviceName);
}
}
// 2. DetectorPHOffline直接更新位
if ((sensorErrorCode & DefineDeviceWarnCode.DetectorPHOffline) != 0) {
newCode |= DefineDeviceWarnCode.DetectorPHOffline;
} else {
newCode &= ~DefineDeviceWarnCode.DetectorPHOffline;
}
// 3. DetectorSalinityOffline直接更新位
if ((sensorErrorCode & DefineDeviceWarnCode.DetectorSalinityOffline) != 0) {
newCode |= DefineDeviceWarnCode.DetectorSalinityOffline;
} else {
newCode &= ~DefineDeviceWarnCode.DetectorSalinityOffline;
}
// 有变化才更新数据库
if (newCode != currentCode) {
Device update = new Device();
update.setId(device.getId());
update.setWarnCode(newCode);
deviceMapper.updateById(update);
log.debug("[探头错误码] 设备={} warnCode: {} -> {}", deviceName, currentCode, newCode);
}
} catch (Exception e) {
log.error("[探头错误码] 处理失败[{}]: {}", deviceName, e.getMessage(), e);
}
}
/**
* 清除设备离线标志(收到属性上报时调用)
*
* @param deviceName 设备名称serialNum
*/
private void clearDeviceOfflineFlag(String deviceName) {
try {
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.select(Device::getId, Device::getWarnCode)
.last("LIMIT 1")
);
if (device == null || device.getWarnCode() == null) {
return;
}
// 只有当前有离线标志时才更新,避免无意义的 SQL
if ((device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) != 0) {
Device update = new Device();
update.setId(device.getId());
update.setWarnCode(device.getWarnCode() & ~DefineDeviceWarnCode.DeviceOffline);
deviceMapper.updateById(update);
log.debug("[在线] 收到属性上报,已清除设备离线标志: {}", deviceName);
}
} catch (Exception e) {
log.error("[在线] 清除设备离线标志失败[{}]: {}", deviceName, e.getMessage());
}
}
/**
* 更新 Device.warnCode 指定位
*
* @param deviceName 设备名称serialNum
* @param bit 要操作的位DefineDeviceWarnCode 中的常量)
* @param set true=置位, false=清位
*/
private void setDeviceWarnCodeBit(String deviceName, int bit, boolean set) {
try {
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.select(Device::getId, Device::getWarnCode)
.last("LIMIT 1")
);
if (device == null) {
return;
}
int currentCode = device.getWarnCode() != null ? device.getWarnCode() : 0;
int newCode = set ? (currentCode | bit) : (currentCode & ~bit);
if (newCode == currentCode) {
return; // 无变化,不更新
}
Device update = new Device();
update.setId(device.getId());
update.setWarnCode(newCode);
deviceMapper.updateById(update);
log.debug("[warnCode] 设备={}, {}位={}, {}→{}",
deviceName, bit, set ? "置位" : "清位", currentCode, newCode);
} catch (Exception e) {
log.error("[warnCode] 更新设备告警码失败[{}]: {}", deviceName, e.getMessage());
}
}
@@ -1398,22 +2126,17 @@ public class DeviceDataHandler {
*/
private void updateDeviceRealTimeData(String deviceName, DeviceSensorData sensorData) {
try {
// 查询设备
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.last("LIMIT 1")
);
if (device == null) {
log.debug("设备不存在,无法更新实时数据: {}", deviceName);
return;
}
// 更新实时数据
Device updateDevice = new Device();
updateDevice.setId(device.getId());
if (sensorData.getDissolvedOxygen() != null) {
updateDevice.setValueDissolvedOxygen(sensorData.getDissolvedOxygen());
}
@@ -1429,8 +2152,6 @@ public class DeviceDataHandler {
if (sensorData.getSalinity() != null) {
updateDevice.setValueSalinity(sensorData.getSalinity());
}
// 执行更新
int updated = deviceMapper.updateById(updateDevice);
if (updated > 0) {
log.debug("设备实时数据已更新: {}", deviceName);
@@ -1440,4 +2161,149 @@ public class DeviceDataHandler {
}
}
/**
* 溶解氧联动控制(参考 C# SetDeviceSwitchLinkedOpen 逻辑)
* <ul>
* <li>溶解氧 ≤ 下限且下限开关已开:开启联动开关(曝气机等)</li>
* <li>溶解氧 ≥ 上限且上限开关已开且未触发过:关闭联动开关,设置 isOxyUpperTrigger=1</li>
* <li>溶解氧 ≤ 上限-0.3且已触发过:重置 isOxyUpperTrigger=0</li>
* </ul>
*
* @param deviceName 设备名称serialNum
* @param dissolvedOxygen 当前溶解氧値
*/
private void handleLinkedCtrl(String deviceName, double dissolvedOxygen) {
try {
// 查询设备(需要 id 和 iotId
Device device = deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName)
.select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName)
.last("LIMIT 1")
);
if (device == null || device.getId() == null) {
return;
}
// 查询该设备的所有联动控制配置
List<com.intc.fishery.domain.LinkedCtrl> linkedCtrls = linkedCtrlMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.intc.fishery.domain.LinkedCtrl>()
.eq(com.intc.fishery.domain.LinkedCtrl::getDeviceId, device.getId())
);
if (linkedCtrls == null || linkedCtrls.isEmpty()) {
return;
}
for (com.intc.fishery.domain.LinkedCtrl linkedCtrl : linkedCtrls) {
// 查询该联动控制关联的所有开关
List<com.intc.fishery.domain.DeviceSwitch> switches = deviceSwitchMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.intc.fishery.domain.DeviceSwitch>()
.eq(com.intc.fishery.domain.DeviceSwitch::getLinkedCtrlId, linkedCtrl.getId())
);
if (switches == null || switches.isEmpty()) {
continue;
}
// 溶解氧下限联动:溶解氧 ≤ 下限就开启开关
if (linkedCtrl.getOxyLowerOpen() != null && linkedCtrl.getOxyLowerOpen() == 1
&& linkedCtrl.getOxyLowerValue() != null
&& dissolvedOxygen <= linkedCtrl.getOxyLowerValue()) {
log.info("[联动控制] 设备={} 溶解氧={} ≤ 下限={},开启开关",
deviceName, dissolvedOxygen, linkedCtrl.getOxyLowerValue());
setDeviceSwitchLinkedOpen(device, switches, true);
}
// 溶解氧上限联动(带滞回防止重复触发)
boolean isOxyUpperTrigger = linkedCtrl.getIsOxyUpperTrigger() != null
&& linkedCtrl.getIsOxyUpperTrigger() == 1;
if (isOxyUpperTrigger) {
// 已触发过:溶解氧降回 上限-0.3 则重置触发标志
if (linkedCtrl.getOxyUpperValue() != null
&& dissolvedOxygen <= linkedCtrl.getOxyUpperValue() - 0.3) {
com.intc.fishery.domain.LinkedCtrl updateCtrl = new com.intc.fishery.domain.LinkedCtrl();
updateCtrl.setId(linkedCtrl.getId());
updateCtrl.setIsOxyUpperTrigger(0);
linkedCtrlMapper.updateById(updateCtrl);
log.info("[联动控制] 设备={} 溶解氧={} 降回至 上限-0.3={},重置触发标志",
deviceName, dissolvedOxygen, linkedCtrl.getOxyUpperValue() - 0.3);
}
} else {
// 未触发:溶解氧 ≥ 上限则关闭开关
if (linkedCtrl.getOxyUpperOpen() != null && linkedCtrl.getOxyUpperOpen() == 1
&& linkedCtrl.getOxyUpperValue() != null
&& dissolvedOxygen >= linkedCtrl.getOxyUpperValue()) {
// 设置触发标志并关闭开关
com.intc.fishery.domain.LinkedCtrl updateCtrl = new com.intc.fishery.domain.LinkedCtrl();
updateCtrl.setId(linkedCtrl.getId());
updateCtrl.setIsOxyUpperTrigger(1);
linkedCtrlMapper.updateById(updateCtrl);
log.info("[联动控制] 设备={} 溶解氧={} ≥ 上限={},关闭开关",
deviceName, dissolvedOxygen, linkedCtrl.getOxyUpperValue());
setDeviceSwitchLinkedOpen(device, switches, false);
}
}
}
} catch (Exception e) {
log.error("[联动控制] 处理失败[{}]: {}", deviceName, e.getMessage(), e);
}
}
/**
* 按联动控制开启/关闭一组开关(参考 C# SetDeviceSwitchLinkedOpen
* 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime。
*
* @param device 设备信息(含 iotId
* @param switches 待操作的开关列表
* @param open true=开启, false=关闭
*/
private void setDeviceSwitchLinkedOpen(Device device, List<com.intc.fishery.domain.DeviceSwitch> switches, boolean open) {
if (device == null || device.getIotId() == null || switches == null || switches.isEmpty()) {
return;
}
try {
int targetState = open ? 1 : 0;
// 过滤出当前就已是目标状态的开关(不需要操作)
List<com.intc.fishery.domain.DeviceSwitch> toChange = new java.util.ArrayList<>();
for (com.intc.fishery.domain.DeviceSwitch sw : switches) {
if (sw.getIsOpen() == null || sw.getIsOpen() != targetState) {
toChange.add(sw);
}
}
if (toChange.isEmpty()) {
log.debug("[联动控制] 开关已全部处于目标状态({}),无需操作", open ? "开启" : "关闭");
return;
}
// 构建属性 Map{"Switch1": 1, "Switch2": 0, ...}
Map<String, Object> properties = new HashMap<>();
for (com.intc.fishery.domain.DeviceSwitch sw : toChange) {
if (sw.getIndex() != null) {
properties.put("Switch" + sw.getIndex(), targetState);
}
}
// 调用 IoT 云端服务下发开关指令
boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 1);
if (success) {
java.util.Date now = new java.util.Date();
for (com.intc.fishery.domain.DeviceSwitch sw : toChange) {
com.intc.fishery.domain.DeviceSwitch updateSw = new com.intc.fishery.domain.DeviceSwitch();
updateSw.setId(sw.getId());
updateSw.setIsOpen(targetState);
updateSw.setLastTurnTime(now);
deviceSwitchMapper.updateById(updateSw);
}
log.info("[联动控制] 设备={} 开关下发成功:{} -> {}",
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(),
properties.keySet(), open ? "开启" : "关闭");
} else {
log.warn("[联动控制] 设备={} 开关下发失败:{}",
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(),
properties.keySet());
}
} catch (Exception e) {
log.error("[联动控制] 下发开关指令失败[{}]: {}", device.getIotId(), e.getMessage(), e);
}
}
}