Compare commits

..

2 Commits

Author SHA1 Message Date
tianyongbao
07495f9270 fix:三相输入缺相,缺相无告警,bug修复。 2026-04-14 22:31:17 +08:00
tianyongbao
9eb2ec90c6 fix: 电话通知逻辑,功能修改,完善机制。 2026-04-14 22:31:14 +08:00
3 changed files with 111 additions and 16 deletions

View File

@@ -834,10 +834,12 @@ public class DeviceDataHandler {
); );
if (response != null && response.isSuccess()) { if (response != null && response.isSuccess()) {
// 更新通知记录状态 // 更新通知记录状态,同时将 callTime 更新为实际发出时间
// 超时判断基于 callTime必须使用实际发出时间而非预计呼叫时间
callNotice.setCallStatus(1); // 呼叫成功,等待结果反馈 callNotice.setCallStatus(1); // 呼叫成功,等待结果反馈
callNotice.setCallId(response.getCallId()); callNotice.setCallId(response.getCallId());
callNotice.setOutId(outId); callNotice.setOutId(outId);
callNotice.setCallTime(LocalDateTime.now()); // 记录实际发出时间,用于超时判断
warnCallNoticeMapper.updateById(callNotice); warnCallNoticeMapper.updateById(callNotice);
log.info("[告警] 呼叫成功: {}", callNotice.getMobilePhone()); log.info("[告警] 呼叫成功: {}", callNotice.getMobilePhone());
@@ -1397,7 +1399,30 @@ public class DeviceDataHandler {
String message = null; String message = null;
int switchIndex = 0; int switchIndex = 0;
if (errorCode >= DefineDeviceErrorCode.Three_Switch1LosePhaseA if (errorCode >= DefineDeviceErrorCode.Three_InputLosePhaseA
&& errorCode <= DefineDeviceErrorCode.Three_InputLosePhaseC) {
// 三相输入缺相1001-1003强制关闭所有4路开关
String phaseName = DefineDeviceErrorCode.getPhaseName(errorCode - DefineDeviceErrorCode.Three_InputLosePhaseA);
for (int i = 1; i <= 4; i++) {
forceSwitchOff(device, i);
}
if (warnTrigger) {
title = "控制器输入缺相";
message = String.format("%s塘口中%s(%s)输入缺%s相",
pondName, displayName, device.getSerialNum(), phaseName);
}
} else if (errorCode >= DefineDeviceErrorCode.Three_PhaseA_OverVoltage
&& errorCode <= DefineDeviceErrorCode.Three_PhaseC_UnderVoltage) {
// 三相过压欠压1004-1009不强制关闭开关
boolean isOver = (errorCode - DefineDeviceErrorCode.Three_PhaseA_OverVoltage) % 2 == 0;
int phaseIndex = (errorCode - DefineDeviceErrorCode.Three_PhaseA_OverVoltage) / 2;
String phaseName = DefineDeviceErrorCode.getPhaseName(phaseIndex);
if (warnTrigger) {
title = isOver ? "控制器输入过压" : "控制器输入欠压";
message = String.format("%s塘口中%s(%s)%s相%s",
pondName, displayName, device.getSerialNum(), phaseName, isOver ? "过压" : "欠压");
}
} else if (errorCode >= DefineDeviceErrorCode.Three_Switch1LosePhaseA
&& errorCode <= DefineDeviceErrorCode.Three_Switch4LosePhaseC) { && errorCode <= DefineDeviceErrorCode.Three_Switch4LosePhaseC) {
// 三相开关输出缺相 // 三相开关输出缺相
int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1LosePhaseA; int tempIndex = errorCode - DefineDeviceErrorCode.Three_Switch1LosePhaseA;
@@ -2238,7 +2263,7 @@ public class DeviceDataHandler {
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>() new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getSerialNum, deviceName) .eq(Device::getSerialNum, deviceName)
.select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName, .select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName,
Device::getPondId, Device::getValueDissolvedOxygen) Device::getPondId, Device::getValueDissolvedOxygen, Device::getUserId)
.last("LIMIT 1") .last("LIMIT 1")
); );
if (currentDevice == null || currentDevice.getId() == null) { if (currentDevice == null || currentDevice.getId() == null) {
@@ -2334,7 +2359,8 @@ public class DeviceDataHandler {
deviceMapper.selectOne( deviceMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>() new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
.eq(Device::getId, id) .eq(Device::getId, id)
.select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName) .select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName,
Device::getUserId)
.last("LIMIT 1") .last("LIMIT 1")
) )
); );
@@ -2356,7 +2382,9 @@ public class DeviceDataHandler {
&& effectiveDissolvedOxygen <= linkedCtrl.getOxyLowerValue()) { && effectiveDissolvedOxygen <= linkedCtrl.getOxyLowerValue()) {
log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≤ 下限={},开启开关(本机上报={}", log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≤ 下限={},开启开关(本机上报={}",
deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue(), dissolvedOxygen); deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue(), dissolvedOxygen);
setDeviceSwitchLinkedOpen(device, switches, true); setDeviceSwitchLinkedOpen(device, switches, true,
String.format("溶解氧%.2f ≤ 下限%.2f",
effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue()));
} }
// 溶解氧上限联动(带滞回防止重复触发) // 溶解氧上限联动(带滞回防止重复触发)
@@ -2385,7 +2413,9 @@ public class DeviceDataHandler {
linkedCtrlMapper.updateById(updateCtrl); linkedCtrlMapper.updateById(updateCtrl);
log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≥ 上限={},关闭开关", log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≥ 上限={},关闭开关",
deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue()); deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue());
setDeviceSwitchLinkedOpen(device, switches, false); setDeviceSwitchLinkedOpen(device, switches, false,
String.format("溶解氧%.2f ≥ 上限%.2f",
effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue()));
} }
} }
} }
@@ -2396,13 +2426,14 @@ public class DeviceDataHandler {
/** /**
* 按联动控制开启/关闭一组开关(参考 C# SetDeviceSwitchLinkedOpen * 按联动控制开启/关闭一组开关(参考 C# SetDeviceSwitchLinkedOpen
* 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime。 * 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime,并写入操作记录
* *
* @param device 设备信息(含 iotId * @param device 设备信息(含 iotId、userId
* @param switches 待操作的开关列表 * @param switches 待操作的开关列表
* @param open true=开启, false=关闭 * @param open true=开启, false=关闭
* @param opReasonDesc 操作原因描述(用于操作记录 message 前缀)
*/ */
private void setDeviceSwitchLinkedOpen(Device device, List<com.intc.fishery.domain.DeviceSwitch> switches, boolean open) { private void setDeviceSwitchLinkedOpen(Device device, List<com.intc.fishery.domain.DeviceSwitch> switches, boolean open, String opReasonDesc) {
if (device == null || device.getIotId() == null || switches == null || switches.isEmpty()) { if (device == null || device.getIotId() == null || switches == null || switches.isEmpty()) {
return; return;
} }
@@ -2432,16 +2463,32 @@ public class DeviceDataHandler {
boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 1); boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 1);
if (success) { if (success) {
java.util.Date now = new java.util.Date(); java.util.Date now = new java.util.Date();
String deviceLabel = device.getSerialNum() != null ? device.getSerialNum() : device.getIotId();
for (com.intc.fishery.domain.DeviceSwitch sw : toChange) { for (com.intc.fishery.domain.DeviceSwitch sw : toChange) {
com.intc.fishery.domain.DeviceSwitch updateSw = new com.intc.fishery.domain.DeviceSwitch(); com.intc.fishery.domain.DeviceSwitch updateSw = new com.intc.fishery.domain.DeviceSwitch();
updateSw.setId(sw.getId()); updateSw.setId(sw.getId());
updateSw.setIsOpen(targetState); updateSw.setIsOpen(targetState);
updateSw.setLastTurnTime(now); updateSw.setLastTurnTime(now);
deviceSwitchMapper.updateById(updateSw); deviceSwitchMapper.updateById(updateSw);
// 写入操作记录
if (device.getUserId() != null) {
String dName = device.getDeviceName() != null ? device.getDeviceName() : deviceLabel;
String switchName = sw.getSwitchName() != null ? sw.getSwitchName() : ("Switch" + sw.getIndex());
String reasonPart = opReasonDesc != null ? "" + opReasonDesc + "" : "";
com.intc.fishery.utils.MessageOpRecordUtil.addMessageOpRecordLinked(
device.getUserId(),
"开关操作",
String.format("%s(%s)的开关%s%s%s%s。",
dName, deviceLabel,
sw.getIndex() != null ? sw.getIndex() + " " : "",
switchName,
open ? "开启" : "关闭",
reasonPart)
);
}
} }
log.info("[联动控制] 设备={} 开关下发成功:{} -> {}", log.info("[联动控制] 设备={} 开关下发成功:{} -> {}",
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(), deviceLabel, properties.keySet(), open ? "开启" : "关闭");
properties.keySet(), open ? "开启" : "关闭");
} else { } else {
log.warn("[联动控制] 设备={} 开关下发失败:{}", log.warn("[联动控制] 设备={} 开关下发失败:{}",
device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(), device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(),

View File

@@ -104,6 +104,38 @@ public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
@Select("SELECT * FROM aqu_call_notice WHERE call_status = 0 AND call_time <= #{now} ORDER BY call_time ASC") @Select("SELECT * FROM aqu_call_notice WHERE call_status = 0 AND call_time <= #{now} ORDER BY call_time ASC")
List<AquWarnCallNotice> selectDuePendingNotices(@Param("now") LocalDateTime now); List<AquWarnCallNotice> selectDuePendingNotices(@Param("now") LocalDateTime now);
/**
* 查询指定设备在指定时间之后是否已有「等待回执中callStatus=1」或「回执成功callStatus=3」的通知记录
* 用于定时任务发出待发通知前的拦截判断:
* - callStatus=1前一条已发出但回执未到不应重复拨打等其超时后再发
* - callStatus=3已成功直接取消后续记录
*
* @param deviceId 设备ID
* @param afterTime 批次窗口起始时间
* @return 满足条件的通知数量
*/
@Select("SELECT COUNT(1) FROM aqu_call_notice " +
"WHERE device_id = #{deviceId} " +
"AND call_status IN (1, 3) " +
"AND call_time >= #{afterTime}")
long countWaitingOrSuccessByDeviceAfter(@Param("deviceId") Long deviceId,
@Param("afterTime") LocalDateTime afterTime);
/**
* 查询指定设备在指定时间之后是否已有「回执成功」的通知记录(不区分 title
* 用于定时任务发出待发通知前,判断同批次是否已有成功回执,避免重复拨打
*
* @param deviceId 设备ID
* @param afterTime 起始时间(通常取批次第一条记录的 callTime
* @return 满足条件的通知数量
*/
@Select("SELECT COUNT(1) FROM aqu_call_notice " +
"WHERE device_id = #{deviceId} " +
"AND call_status = 3 " +
"AND call_time >= #{afterTime}")
long countSuccessByDeviceAfter(@Param("deviceId") Long deviceId,
@Param("afterTime") LocalDateTime afterTime);
/** /**
* 通过 callNoticeId 查询关联的第一条告警消息内容 * 通过 callNoticeId 查询关联的第一条告警消息内容
* 用于定时任务重发时构建 TTS 参数 * 用于定时任务重发时构建 TTS 参数

View File

@@ -56,10 +56,11 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
/** /**
* 回执超时时间(分钟) * 回执超时时间(分钟)
* VMS 回执延迟较长,设为 15 分钟,给 VMS 平台充足的回调缓冲时间 * 从呼叫实际发出callTime 已修正为真实发出时间)到收到 VMS 回执的最长等待时间
* 注意:超时判断基于 callTime预计呼叫时间实际呼叫发起后回调可能延迟数分钟到达 * 正常链路:通话约 30s + VMS 生成回执约 1~2min3 分钟内完成。
* 设为 5 分钟,兼顾回执延迟容忍与批次重试及时性(批次间隔 3 分钟)。
*/ */
private static final int CALLBACK_TIMEOUT_MINUTES = 15; private static final int CALLBACK_TIMEOUT_MINUTES = 5;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -258,6 +259,20 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
int sentCount = 0; int sentCount = 0;
for (AquWarnCallNotice notice : pendingList) { for (AquWarnCallNotice notice : pendingList) {
try { try {
// 发出前先检查同设备同批次窗口内的状态
// 批次窗口 = 当前记录的 callTime 往前 CALLBACK_TIMEOUT_MINUTES 分钟(足够覆盖整个批次)
// - status=1等待回执中前一条已发出但回执未到不重复拨打等其超时后再发
// - status=3已成功已有成功回执跳过该条记录
if (notice.getDeviceId() != null) {
LocalDateTime batchWindowStart = notice.getCallTime().minusMinutes(CALLBACK_TIMEOUT_MINUTES);
long blockingCount = warnCallNoticeMapper.countWaitingOrSuccessByDeviceAfter(
notice.getDeviceId(), batchWindowStart);
if (blockingCount > 0) {
log.info("[待发通知] 设备 {} 同批次内已有等待回执或成功的记录跳过记录ID: {}", notice.getDeviceId(), notice.getId());
continue;
}
}
// 查询关联的告警标题title作为 TTS warnMessage 参数 // 查询关联的告警标题title作为 TTS warnMessage 参数
// 使用 title如"设备离线"、"溶解氧")而非完整 message避免长字符串被 TTS 逐字播报 // 使用 title如"设备离线"、"溶解氧")而非完整 message避免长字符串被 TTS 逐字播报
String warnTitle = warnCallNoticeMapper.selectWarnTitleByCallNoticeId(notice.getId()); String warnTitle = warnCallNoticeMapper.selectWarnTitleByCallNoticeId(notice.getId());
@@ -291,6 +306,7 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
notice.setCallStatus(CALL_STATUS_CALL_SUCCESS); notice.setCallStatus(CALL_STATUS_CALL_SUCCESS);
notice.setCallId(response.getCallId()); notice.setCallId(response.getCallId());
notice.setOutId(outId); notice.setOutId(outId);
notice.setCallTime(LocalDateTime.now()); // 记录实际发出时间,用于超时判断
warnCallNoticeMapper.updateById(notice); warnCallNoticeMapper.updateById(notice);
sentCount++; sentCount++;
log.info("[待发通知] 呼叫成功: {} - 记录ID: {}", notice.getMobilePhone(), notice.getId()); log.info("[待发通知] 呼叫成功: {} - 记录ID: {}", notice.getMobilePhone(), notice.getId());