From 33521b17c9502e3dc7807ad41294b87d4a42b174 Mon Sep 17 00:00:00 2001 From: tianyongbao Date: Tue, 14 Apr 2026 22:02:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=B5=E8=AF=9D=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=8A=9F=E8=83=BD=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=EF=BC=8C=E5=AE=8C=E5=96=84=E6=9C=BA=E5=88=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../intc/iot/handler/DeviceDataHandler.java | 48 ++++++++++++++----- .../iot/mapper/AquWarnCallNoticeMapper.java | 32 +++++++++++++ .../impl/WarnCallNoticeServiceImpl.java | 22 +++++++-- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java index 1476edc..34dc641 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java @@ -834,10 +834,12 @@ public class DeviceDataHandler { ); if (response != null && response.isSuccess()) { - // 更新通知记录状态 + // 更新通知记录状态,同时将 callTime 更新为实际发出时间 + // 超时判断基于 callTime,必须使用实际发出时间而非预计呼叫时间 callNotice.setCallStatus(1); // 呼叫成功,等待结果反馈 callNotice.setCallId(response.getCallId()); callNotice.setOutId(outId); + callNotice.setCallTime(LocalDateTime.now()); // 记录实际发出时间,用于超时判断 warnCallNoticeMapper.updateById(callNotice); log.info("[告警] 呼叫成功: {}", callNotice.getMobilePhone()); @@ -2238,7 +2240,7 @@ public class DeviceDataHandler { new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(Device::getSerialNum, deviceName) .select(Device::getId, Device::getIotId, Device::getSerialNum, Device::getDeviceName, - Device::getPondId, Device::getValueDissolvedOxygen) + Device::getPondId, Device::getValueDissolvedOxygen, Device::getUserId) .last("LIMIT 1") ); if (currentDevice == null || currentDevice.getId() == null) { @@ -2334,7 +2336,8 @@ public class DeviceDataHandler { deviceMapper.selectOne( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .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") ) ); @@ -2356,7 +2359,9 @@ public class DeviceDataHandler { && effectiveDissolvedOxygen <= linkedCtrl.getOxyLowerValue()) { log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≤ 下限={},开启开关(本机上报={})", deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue(), dissolvedOxygen); - setDeviceSwitchLinkedOpen(device, switches, true); + setDeviceSwitchLinkedOpen(device, switches, true, + String.format("溶解氧%.2f ≤ 下限%.2f", + effectiveDissolvedOxygen, linkedCtrl.getOxyLowerValue())); } // 溶解氧上限联动(带滞回防止重复触发) @@ -2385,7 +2390,9 @@ public class DeviceDataHandler { linkedCtrlMapper.updateById(updateCtrl); log.info("[联动控制] 触发设备={} 塘口最低溶解氧={} ≥ 上限={},关闭开关", deviceName, effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue()); - setDeviceSwitchLinkedOpen(device, switches, false); + setDeviceSwitchLinkedOpen(device, switches, false, + String.format("溶解氧%.2f ≥ 上限%.2f", + effectiveDissolvedOxygen, linkedCtrl.getOxyUpperValue())); } } } @@ -2396,13 +2403,14 @@ public class DeviceDataHandler { /** * 按联动控制开启/关闭一组开关(参考 C# SetDeviceSwitchLinkedOpen) - * 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime。 + * 下发成功后更新 DeviceSwitch.isOpen 和 lastTurnTime,并写入操作记录。 * - * @param device 设备信息(含 iotId) - * @param switches 待操作的开关列表 - * @param open true=开启, false=关闭 + * @param device 设备信息(含 iotId、userId) + * @param switches 待操作的开关列表 + * @param open true=开启, false=关闭 + * @param opReasonDesc 操作原因描述(用于操作记录 message 前缀) */ - private void setDeviceSwitchLinkedOpen(Device device, List switches, boolean open) { + private void setDeviceSwitchLinkedOpen(Device device, List switches, boolean open, String opReasonDesc) { if (device == null || device.getIotId() == null || switches == null || switches.isEmpty()) { return; } @@ -2432,16 +2440,32 @@ public class DeviceDataHandler { boolean success = iotCloudService.setProperty(device.getIotId(), properties, false, 1); if (success) { 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) { 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); + // 写入操作记录 + 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("[联动控制] 设备={} 开关下发成功:{} -> {}", - device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(), - properties.keySet(), open ? "开启" : "关闭"); + deviceLabel, properties.keySet(), open ? "开启" : "关闭"); } else { log.warn("[联动控制] 设备={} 开关下发失败:{}", device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(), diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/mapper/AquWarnCallNoticeMapper.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/mapper/AquWarnCallNoticeMapper.java index 14b7589..4d09c31 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/mapper/AquWarnCallNoticeMapper.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/mapper/AquWarnCallNoticeMapper.java @@ -104,6 +104,38 @@ public interface AquWarnCallNoticeMapper extends BaseMapper { @Select("SELECT * FROM aqu_call_notice WHERE call_status = 0 AND call_time <= #{now} ORDER BY call_time ASC") List 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 查询关联的第一条告警消息内容 * 用于定时任务重发时构建 TTS 参数 diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/WarnCallNoticeServiceImpl.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/WarnCallNoticeServiceImpl.java index 95bfbbd..a289c35 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/WarnCallNoticeServiceImpl.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/WarnCallNoticeServiceImpl.java @@ -56,10 +56,11 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService { /** * 回执超时时间(分钟) - * VMS 回执延迟较长,设为 15 分钟,给 VMS 平台充足的回调缓冲时间 - * 注意:超时判断基于 callTime(预计呼叫时间),实际呼叫发起后回调可能延迟数分钟到达 + * 从呼叫实际发出(callTime 已修正为真实发出时间)到收到 VMS 回执的最长等待时间。 + * 正常链路:通话约 30s + VMS 生成回执约 1~2min,3 分钟内完成。 + * 设为 5 分钟,兼顾回执延迟容忍与批次重试及时性(批次间隔 3 分钟)。 */ - private static final int CALLBACK_TIMEOUT_MINUTES = 15; + private static final int CALLBACK_TIMEOUT_MINUTES = 5; @Override @Transactional(rollbackFor = Exception.class) @@ -258,6 +259,20 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService { int sentCount = 0; for (AquWarnCallNotice notice : pendingList) { 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(如"设备离线"、"溶解氧")而非完整 message,避免长字符串被 TTS 逐字播报 String warnTitle = warnCallNoticeMapper.selectWarnTitleByCallNoticeId(notice.getId()); @@ -291,6 +306,7 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService { notice.setCallStatus(CALL_STATUS_CALL_SUCCESS); notice.setCallId(response.getCallId()); notice.setOutId(outId); + notice.setCallTime(LocalDateTime.now()); // 记录实际发出时间,用于超时判断 warnCallNoticeMapper.updateById(notice); sentCount++; log.info("[待发通知] 呼叫成功: {} - 记录ID: {}", notice.getMobilePhone(), notice.getId());