fix: 开关定时任务,后台定时扫描执行。
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user