From 70bd17439baf5fa165cb0cafe529a10c1608c5e3 Mon Sep 17 00:00:00 2001 From: tianyongbao Date: Tue, 30 Jun 2026 17:26:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BC=80=E5=85=B3=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=EF=BC=8C=E5=90=8E=E5=8F=B0=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E6=89=A7=E8=A1=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../intc/iot/controller/IotController.java | 85 +------ .../intc/iot/service/SwitchTurnResult.java | 34 +++ .../iot/service/TimingCtrlExecuteService.java | 38 +++ .../impl/TimingCtrlExecuteServiceImpl.java | 218 ++++++++++++++++++ .../intc/iot/task/TimingCtrlExecuteTask.java | 37 +++ 5 files changed, 336 insertions(+), 76 deletions(-) create mode 100644 intc-modules/intc-iot/src/main/java/com/intc/iot/service/SwitchTurnResult.java create mode 100644 intc-modules/intc-iot/src/main/java/com/intc/iot/service/TimingCtrlExecuteService.java create mode 100644 intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/TimingCtrlExecuteServiceImpl.java create mode 100644 intc-modules/intc-iot/src/main/java/com/intc/iot/task/TimingCtrlExecuteTask.java diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java index 461538b..41f9282 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/controller/IotController.java @@ -41,6 +41,8 @@ import com.intc.iot.service.IotDeviceService; import com.intc.iot.service.MqttService; import com.intc.iot.service.VmsMnsConsumerService; import com.intc.iot.service.VmsNoticeService; +import com.intc.iot.service.SwitchTurnResult; +import com.intc.iot.service.TimingCtrlExecuteService; import com.intc.iot.handler.DeviceDataHandler; import com.intc.iot.service.WarnCallNoticeService; import com.intc.iot.utils.AliyunAmqpSignUtil; @@ -140,6 +142,9 @@ public class IotController extends BaseController { @Autowired(required = false) private IotCloudService iotCloudService; + @Autowired(required = false) + private TimingCtrlExecuteService timingCtrlExecuteService; + @Operation(summary = "测试接口") @GetMapping("/test") public R test() { @@ -2258,84 +2263,12 @@ public class IotController extends BaseController { @PutMapping("/switch/turn_switch") public R turnSwitch(@RequestBody ReqTurnOpen request) { try { - if (deviceSwitchMapper == null || deviceMapper == null) { + if (timingCtrlExecuteService == null) { return R.fail("系统配置未完成"); } - - Long userId = LoginHelper.getUserId(); - Long switchId = request.getId(); - Integer targetStatus = request.getIsOpen(); - - // 查询开关信息 - DeviceSwitch deviceSwitch = deviceSwitchMapper.selectById(switchId); - if (deviceSwitch == null) { - return R.fail("开关不存在"); - } - - // 查询关联的设备信息 - Device device = deviceMapper.selectOne( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(Device::getId, deviceSwitch.getDeviceId()) - .select(Device::getId, Device::getUserId, Device::getIotId, Device::getSerialNum, - Device::getDeviceName, Device::getWarnCode, Device::getDeadTime) - ); - - if (device == null) { - return R.fail("开关不存在或无权限访问"); - } - - // 检查设备是否到期 - if (device.getDeadTime() != null && new Date().after(device.getDeadTime())) { - return R.fail("设备服务已过期"); - } - - // 防抖处理:检查距离上次操作是否小于3秒 - if (deviceSwitch.getLastTurnTime() != null) { - long timeDiff = System.currentTimeMillis() - deviceSwitch.getLastTurnTime().getTime(); - if (timeDiff < 3000) { - return R.fail("操作过于频繁,请稍后再试"); - } - } - - // 如果开关状态已经是目标状态,无需操作 - if (deviceSwitch.getIsOpen() != null && deviceSwitch.getIsOpen().equals(targetStatus)) { - return R.ok(); - } - - // 检查设备是否离线(warnCode为99表示离线) - if (device.getWarnCode() == null || device.getWarnCode() == 99) { - return R.fail("设备离线或已关机"); - } - - // 检查IoT云服务是否可用 - if (iotCloudService == null) { - return R.fail("物联网服务未启用"); - } - - // 构造IoT属性 - Map properties = new java.util.HashMap<>(); - properties.put(IOTPropertyName.SWITCH_INDEX + deviceSwitch.getIndex(), targetStatus); - - // 调用物联网服务设置属性 - boolean success = iotCloudService.setProperty(device.getIotId(), properties, true, 2); - if (!success) { - return R.fail("设置开关失败"); - } - - // 更新数据库中的开关状态和最后操作时间 - deviceSwitchMapper.update(null, - new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() - .eq(DeviceSwitch::getId, switchId) - .set(DeviceSwitch::getIsOpen, targetStatus) - .set(DeviceSwitch::getLastTurnTime, new Date()) - ); - - // 记录操作日志 - String operation = targetStatus == 1 ? "开启" : "关闭"; - log.info("开关操作:{}({})的开关{}:{}", - device.getDeviceName(), device.getSerialNum(), deviceSwitch.getSwitchName(), operation); - - return R.ok(); + SwitchTurnResult result = timingCtrlExecuteService.executeSwitch( + request.getId(), request.getIsOpen(), "开关操作", null, false); + return result.isSuccess() ? R.ok() : R.fail(result.getMessage()); } catch (Exception e) { log.error("开关控制失败: {}", e.getMessage(), e); return R.fail("开关控制失败: " + e.getMessage()); diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/SwitchTurnResult.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/SwitchTurnResult.java new file mode 100644 index 0000000..9a49383 --- /dev/null +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/SwitchTurnResult.java @@ -0,0 +1,34 @@ +package com.intc.iot.service; + +import lombok.Data; + +/** + * 开关执行结果 + */ +@Data +public class SwitchTurnResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 结果描述 + */ + private String message; + + public static SwitchTurnResult ok(String message) { + SwitchTurnResult result = new SwitchTurnResult(); + result.setSuccess(true); + result.setMessage(message); + return result; + } + + public static SwitchTurnResult fail(String message) { + SwitchTurnResult result = new SwitchTurnResult(); + result.setSuccess(false); + result.setMessage(message); + return result; + } +} diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/TimingCtrlExecuteService.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/TimingCtrlExecuteService.java new file mode 100644 index 0000000..22506b9 --- /dev/null +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/TimingCtrlExecuteService.java @@ -0,0 +1,38 @@ +package com.intc.iot.service; + +import com.intc.fishery.domain.TimingCtrl; + +import java.util.List; + +/** + * 定时控制执行服务 + */ +public interface TimingCtrlExecuteService { + + /** + * 按开关ID执行开/关控制 + * + * @param switchId 开关ID + * @param targetStatus 目标状态,1=开启,0=关闭 + * @param opTypeLabel 操作类型描述 + * @param opMessagePrefix 操作记录前缀 + * @param timingOp 是否按定时任务记录 + * @return 执行结果 + */ + SwitchTurnResult executeSwitch(Long switchId, Integer targetStatus, String opTypeLabel, + String opMessagePrefix, boolean timingOp); + + /** + * 扫描并执行当天到点的循环定时任务 + * + * @return 执行数量 + */ + int scanAndExecuteDailyTimingCtrl(); + + /** + * 查询启用中的每天循环定时控制 + * + * @return 定时控制列表 + */ + List listEnabledDailyTimingCtrls(); +} diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/TimingCtrlExecuteServiceImpl.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/TimingCtrlExecuteServiceImpl.java new file mode 100644 index 0000000..e297dd1 --- /dev/null +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/service/impl/TimingCtrlExecuteServiceImpl.java @@ -0,0 +1,218 @@ +package com.intc.iot.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.intc.common.redis.utils.RedisUtils; +import com.intc.fishery.constant.DefineDeviceWarnCode; +import com.intc.fishery.domain.Device; +import com.intc.fishery.domain.DeviceSwitch; +import com.intc.fishery.domain.TimingCtrl; +import com.intc.fishery.mapper.DeviceMapper; +import com.intc.fishery.mapper.DeviceSwitchMapper; +import com.intc.fishery.mapper.TimingCtrlMapper; +import com.intc.fishery.utils.MessageOpRecordUtil; +import com.intc.iot.constant.IOTPropertyName; +import com.intc.iot.service.IotCloudService; +import com.intc.iot.service.SwitchTurnResult; +import com.intc.iot.service.TimingCtrlExecuteService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 定时控制执行服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnBean(IotCloudService.class) +public class TimingCtrlExecuteServiceImpl implements TimingCtrlExecuteService { + + private static final String DAILY_EXEC_LOCK_PREFIX = "fishery:timingCtrl:daily:exec:"; + private static final DateTimeFormatter DATE_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final long SCAN_WINDOW_MINUTES = 5L; + + private final DeviceMapper deviceMapper; + private final DeviceSwitchMapper deviceSwitchMapper; + private final TimingCtrlMapper timingCtrlMapper; + private final IotCloudService iotCloudService; + + @Override + public SwitchTurnResult executeSwitch(Long switchId, Integer targetStatus, String opTypeLabel, + String opMessagePrefix, boolean timingOp) { + try { + if (switchId == null || targetStatus == null) { + return SwitchTurnResult.fail("参数不能为空"); + } + + DeviceSwitch deviceSwitch = deviceSwitchMapper.selectOne( + new LambdaQueryWrapper() + .eq(DeviceSwitch::getId, switchId) + .select(DeviceSwitch::getId, DeviceSwitch::getDeviceId, DeviceSwitch::getIndex, + DeviceSwitch::getSwitchName, DeviceSwitch::getIsOpen, DeviceSwitch::getLastTurnTime) + ); + if (deviceSwitch == null) { + return SwitchTurnResult.fail("开关不存在"); + } + + if (deviceSwitch.getIsOpen() != null && deviceSwitch.getIsOpen().equals(targetStatus)) { + return SwitchTurnResult.ok("开关已处于目标状态"); + } + + if (deviceSwitch.getLastTurnTime() != null) { + long timeDiff = System.currentTimeMillis() - deviceSwitch.getLastTurnTime().getTime(); + if (timeDiff < 3000) { + return SwitchTurnResult.fail("操作过于频繁,请稍后再试"); + } + } + + Device device = deviceMapper.selectOne( + new LambdaQueryWrapper() + .eq(Device::getId, deviceSwitch.getDeviceId()) + .select(Device::getId, Device::getUserId, Device::getIotId, Device::getSerialNum, + Device::getDeviceName, Device::getWarnCode, Device::getDeadTime) + ); + if (device == null) { + return SwitchTurnResult.fail("开关不存在或无权限访问"); + } + + if (device.getDeadTime() != null && new Date().after(device.getDeadTime())) { + return SwitchTurnResult.fail("设备服务已过期"); + } + + if (device.getWarnCode() == null || device.getWarnCode() == 99 + || (device.getWarnCode() & DefineDeviceWarnCode.DeviceOffline) != 0) { + return SwitchTurnResult.fail("设备离线或已关机"); + } + + if (iotCloudService == null) { + return SwitchTurnResult.fail("物联网服务未启用"); + } + + Map properties = new HashMap<>(); + properties.put(IOTPropertyName.SWITCH_INDEX + deviceSwitch.getIndex(), targetStatus); + + boolean success = iotCloudService.setProperty(device.getIotId(), properties, true, 2); + if (!success) { + return SwitchTurnResult.fail("设置开关失败"); + } + + Date now = new Date(); + deviceSwitchMapper.update(null, + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(DeviceSwitch::getId, switchId) + .set(DeviceSwitch::getIsOpen, targetStatus) + .set(DeviceSwitch::getLastTurnTime, now) + ); + + if (device.getUserId() != null && opTypeLabel != null && timingOp) { + String deviceLabel = device.getDeviceName() != null ? device.getDeviceName() : device.getSerialNum(); + String switchLabel = deviceSwitch.getSwitchName() != null + ? deviceSwitch.getSwitchName() + : ("Switch" + deviceSwitch.getIndex()); + String operation = targetStatus == 1 ? "开启" : "关闭"; + String message = String.format("%s(%s)的开关%s:%s%s。", + deviceLabel, device.getSerialNum(), switchLabel, operation, + opMessagePrefix != null ? "(" + opMessagePrefix + ")" : ""); + + MessageOpRecordUtil.addMessageOpRecordTimingLoop(device.getUserId(), opTypeLabel, message); + } + + log.info("[SwitchTurn] 设备={} 开关{} {}成功", + device.getSerialNum() != null ? device.getSerialNum() : device.getIotId(), + deviceSwitch.getSwitchName(), + targetStatus == 1 ? "开启" : "关闭"); + return SwitchTurnResult.ok("操作成功"); + } catch (Exception e) { + log.error("[SwitchTurn] 开关执行失败: {}", e.getMessage(), e); + return SwitchTurnResult.fail("开关控制失败: " + e.getMessage()); + } + } + + @Override + public int scanAndExecuteDailyTimingCtrl() { + List list = listEnabledDailyTimingCtrls(); + if (list.isEmpty()) { + return 0; + } + + LocalDateTime now = LocalDateTime.now(); + int executedCount = 0; + for (TimingCtrl timingCtrl : list) { + try { + if (timingCtrl.getSwitchId() == null || timingCtrl.getOpenTime() == null || timingCtrl.getCloseTime() == null) { + continue; + } + + LocalTime openTime = toLocalTime(timingCtrl.getOpenTime()); + LocalTime closeTime = toLocalTime(timingCtrl.getCloseTime()); + if (isDueInLookbackWindow(now, openTime)) { + if (tryLock(timingCtrl.getId(), now.toLocalDate(), "open")) { + SwitchTurnResult result = executeSwitch(timingCtrl.getSwitchId(), 1, + "定时控制管理", "定时任务到达开启时间", true); + if (result.isSuccess()) { + executedCount++; + } else { + log.warn("[TimingCtrl] 开启执行失败,timingCtrlId={}, msg={}", timingCtrl.getId(), result.getMessage()); + } + } + } + + if (isDueInLookbackWindow(now, closeTime)) { + if (tryLock(timingCtrl.getId(), now.toLocalDate(), "close")) { + SwitchTurnResult result = executeSwitch(timingCtrl.getSwitchId(), 0, + "定时控制管理", "定时任务到达关闭时间", true); + if (result.isSuccess()) { + executedCount++; + } else { + log.warn("[TimingCtrl] 关闭执行失败,timingCtrlId={}, msg={}", timingCtrl.getId(), result.getMessage()); + } + } + } + } catch (Exception e) { + log.error("[TimingCtrl] 处理定时控制失败,id={}: {}", timingCtrl.getId(), e.getMessage(), e); + } + } + + return executedCount; + } + + @Override + public List listEnabledDailyTimingCtrls() { + return timingCtrlMapper.selectList( + new LambdaQueryWrapper() + .eq(TimingCtrl::getIsOpen, 1L) + .eq(TimingCtrl::getLoopType, 1L) + .select(TimingCtrl::getId, TimingCtrl::getSwitchId, TimingCtrl::getOpenTime, + TimingCtrl::getCloseTime, TimingCtrl::getLoopType, TimingCtrl::getIsOpen) + ); + } + + private static LocalTime toLocalTime(Date date) { + return LocalDateTime.ofInstant(date.toInstant(), java.time.ZoneId.systemDefault()).toLocalTime(); + } + + private static boolean isDueInLookbackWindow(LocalDateTime now, LocalTime scheduleTime) { + LocalDateTime scheduleDateTime = LocalDateTime.of(now.toLocalDate(), scheduleTime); + if (scheduleDateTime.isAfter(now)) { + scheduleDateTime = scheduleDateTime.minusDays(1); + } + LocalDateTime lowerBound = now.minusMinutes(SCAN_WINDOW_MINUTES); + return !scheduleDateTime.isBefore(lowerBound) && !scheduleDateTime.isAfter(now); + } + + private boolean tryLock(Long timingCtrlId, LocalDate date, String action) { + String redisKey = DAILY_EXEC_LOCK_PREFIX + timingCtrlId + ":" + date.format(DATE_KEY_FORMATTER) + ":" + action; + return RedisUtils.setObjectIfAbsent(redisKey, "1", Duration.ofMinutes(5)); + } +} diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/task/TimingCtrlExecuteTask.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/task/TimingCtrlExecuteTask.java new file mode 100644 index 0000000..45cd2c4 --- /dev/null +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/task/TimingCtrlExecuteTask.java @@ -0,0 +1,37 @@ +package com.intc.iot.task; + +import com.intc.iot.service.TimingCtrlExecuteService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 开关定时任务执行扫描 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnBean(TimingCtrlExecuteService.class) +public class TimingCtrlExecuteTask { + + private static final long SCAN_DELAY_MS = 300000L; + + private final TimingCtrlExecuteService timingCtrlExecuteService; + + /** + * 每5分钟扫描一次启用中的每天循环定时控制 + */ + @Scheduled(fixedDelay = SCAN_DELAY_MS) + public void scanDailyTimingCtrl() { + try { + int count = timingCtrlExecuteService.scanAndExecuteDailyTimingCtrl(); + if (count > 0) { + log.info("[TimingCtrlTask] 本次扫描执行了 {} 条定时控制", count); + } + } catch (Exception e) { + log.error("[TimingCtrlTask] 扫描定时控制失败: {}", e.getMessage(), e); + } + } +}