fix: 开关定时任务,后台定时扫描执行。

This commit is contained in:
tianyongbao
2026-06-30 17:26:21 +08:00
parent f0f6494574
commit 573bac7505
5 changed files with 336 additions and 76 deletions

View File

@@ -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<String> test() {
@@ -2258,84 +2263,12 @@ public class IotController extends BaseController {
@PutMapping("/switch/turn_switch")
public R<Void> 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<Device>()
.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<String, Object> 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<DeviceSwitch>()
.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());

View File

@@ -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;
}
}

View File

@@ -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<TimingCtrl> listEnabledDailyTimingCtrls();
}

View File

@@ -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<DeviceSwitch>()
.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<Device>()
.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<String, Object> 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<DeviceSwitch>()
.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<TimingCtrl> 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<TimingCtrl> listEnabledDailyTimingCtrls() {
return timingCtrlMapper.selectList(
new LambdaQueryWrapper<TimingCtrl>()
.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));
}
}

View File

@@ -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);
}
}
}