|
|
@@ -1,9 +1,12 @@
|
|
|
package com.zksy.lamp.controller;
|
|
|
|
|
|
import com.zksy.common.core.domain.Result;
|
|
|
+import com.zksy.common.exception.CommonException;
|
|
|
import com.zksy.lamp.server.ExecutionServer;
|
|
|
+import io.swagger.annotations.Api;
|
|
|
import io.swagger.annotations.ApiOperation;
|
|
|
import io.swagger.annotations.ApiParam;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
@@ -15,120 +18,240 @@ import java.time.Instant;
|
|
|
import java.time.LocalTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.time.format.DateTimeParseException;
|
|
|
-import java.util.Date;
|
|
|
-import java.util.Objects;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
/**
|
|
|
* @author Administrator
|
|
|
* @version 1.0
|
|
|
* @project dh-server-micro
|
|
|
- * @description 路灯服务
|
|
|
+ * @description 路灯服务控制器
|
|
|
* @date 2025/2/10 15:00:46
|
|
|
*/
|
|
|
@RequestMapping("/execution")
|
|
|
@RestController
|
|
|
+@Api(tags = "路灯控制接口")
|
|
|
+@Slf4j // 引入日志注解,替代手动创建Logger
|
|
|
public class ExecutionController {
|
|
|
+
|
|
|
@Autowired
|
|
|
private ExecutionServer server;
|
|
|
+
|
|
|
@Autowired
|
|
|
private RedisTemplate<String, String> redisTemplate;
|
|
|
|
|
|
+ // 常量抽取:避免魔法值,提高可维护性
|
|
|
+ private static final String LOCK_ON_KEY = "lock:on";
|
|
|
+ private static final String LOCK_OFF_KEY = "lock:off";
|
|
|
+ private static final long LOCK_DURATION_MS = 3600000; // 1小时(毫秒)
|
|
|
+ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
|
|
|
+
|
|
|
+
|
|
|
@GetMapping("/firstRelay")
|
|
|
- @ApiOperation(value = "第一路继电器闭合、断开", notes = "第一路继电器闭合、断开")
|
|
|
- public Result<?> firstRelay(@ApiParam(value = "第一路继电器(0-断开,1-闭合)", required = true)
|
|
|
- @RequestParam(value = "value") Integer value) {
|
|
|
- String data = "AT+STACH1=" + value + "\r\n";
|
|
|
- String msg = server.ExecutionData(data);
|
|
|
- return Result.ok(msg);
|
|
|
+ @ApiOperation(value = "第一路继电器控制", notes = "控制第一路继电器闭合(1)或断开(0)")
|
|
|
+ public Result<?> firstRelay(
|
|
|
+ @ApiParam(value = "控制值(0-断开,1-闭合)", required = true, allowableValues = "0,1")
|
|
|
+ @RequestParam Integer value) {
|
|
|
+ // 复用通用继电器控制方法,减少重复代码
|
|
|
+ return controlRelay(1, value);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
@GetMapping("/secondRelay")
|
|
|
- @ApiOperation(value = "第二路继电器闭合、断开", notes = "第二路继电器闭合、断开")
|
|
|
- public Result<?> secondRelay(@ApiParam(value = "第二路继电器(0-断开,1-闭合)", required = true)
|
|
|
- @RequestParam(value = "value") Integer value) {
|
|
|
- String data = "AT+STACH2=" + value + "\r\n";
|
|
|
- String msg = server.ExecutionData(data);
|
|
|
- return Result.ok(msg);
|
|
|
+ @ApiOperation(value = "第二路继电器控制", notes = "控制第二路继电器闭合(1)或断开(0)")
|
|
|
+ public Result<?> secondRelay(
|
|
|
+ @ApiParam(value = "控制值(0-断开,1-闭合)", required = true, allowableValues = "0,1")
|
|
|
+ @RequestParam Integer value) {
|
|
|
+ // 复用通用继电器控制方法
|
|
|
+ return controlRelay(2, value);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
@GetMapping("/timingOn")
|
|
|
- @ApiOperation(value = "定时开启", notes = "定时开启")
|
|
|
- public Result<?> timingOn(@ApiParam(value = "定时开启时间,时:分:秒,示例:17:00:00", required = true)
|
|
|
- @RequestParam(value = "value") String value) {
|
|
|
+ @ApiOperation(value = "定时开启", notes = "设置定时开启时间(格式:HH:mm:ss,每天执行)")
|
|
|
+ public Result<?> timingOn(
|
|
|
+ @ApiParam(value = "定时开启时间(示例:17:00:00)", required = true)
|
|
|
+ @RequestParam String value) {
|
|
|
+ // 校验时间格式
|
|
|
if (!isValidTime(value)) {
|
|
|
return Result.error(400, "无效的时间格式,请使用 HH:mm:ss 格式");
|
|
|
}
|
|
|
|
|
|
- if (Boolean.TRUE.equals(redisTemplate.hasKey("lock:on"))) {
|
|
|
- Instant now = Instant.now();
|
|
|
- long currentTime = now.toEpochMilli();
|
|
|
- String lockTimeStr = redisTemplate.opsForValue().get("lock:on");
|
|
|
- if (lockTimeStr != null) {
|
|
|
- try {
|
|
|
- long lockTime = Long.parseLong(lockTimeStr);
|
|
|
- long remainingTime = (lockTime - currentTime) / 1000;
|
|
|
- return Result.error(429, "一小时只能发布一次还剩" + remainingTime + "秒");
|
|
|
- } catch (NumberFormatException e) {
|
|
|
- return Result.error(500, "内部服务器错误");
|
|
|
- }
|
|
|
- }
|
|
|
+ // 校验频率限制(复用通用方法)
|
|
|
+ Result<?> rateLimitResult = checkRateLimit(LOCK_ON_KEY);
|
|
|
+ if (rateLimitResult != null) {
|
|
|
+ return rateLimitResult;
|
|
|
}
|
|
|
|
|
|
- long t = Instant.now().toEpochMilli();
|
|
|
- long lockTime = 3600000; // 3600000 毫秒 = 1 小时
|
|
|
- String data = "AT+AUTOCONT=8,task1,[CYC:1],[T:3,0|1|2|3|4|5|6," + value + "],[DO:0,1,1,100000,100000,1000000],[N:1,0]\r\n";
|
|
|
- String msg = server.ExecutionData(data);
|
|
|
- redisTemplate.opsForValue().set("lock:on", String.valueOf(t + lockTime), lockTime, TimeUnit.MILLISECONDS);
|
|
|
- return Result.ok(msg);
|
|
|
+ // 构建指令并执行
|
|
|
+ String data = buildTimingCommand("task1", value, 1); // 1表示开启
|
|
|
+ return executeWithRateLimit(data, LOCK_ON_KEY);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
@GetMapping("/timedShutdown")
|
|
|
- @ApiOperation(value = "定时关闭", notes = "定时关闭")
|
|
|
- public Result<?> timedShutdown(@ApiParam(value = "定时关闭时间,时:分:秒,示例:06:00:00", required = true)
|
|
|
- @RequestParam(value = "value") String value) {
|
|
|
+ @ApiOperation(value = "定时关闭", notes = "设置定时关闭时间(格式:HH:mm:ss,每天执行)")
|
|
|
+ public Result<?> timedShutdown(
|
|
|
+ @ApiParam(value = "定时关闭时间(示例:06:00:00)", required = true)
|
|
|
+ @RequestParam String value) {
|
|
|
+ // 校验时间格式
|
|
|
if (!isValidTime(value)) {
|
|
|
return Result.error(400, "无效的时间格式,请使用 HH:mm:ss 格式");
|
|
|
}
|
|
|
|
|
|
- if (Boolean.TRUE.equals(redisTemplate.hasKey("lock:off"))) {
|
|
|
- Instant now = Instant.now();
|
|
|
- long currentTime = now.toEpochMilli();
|
|
|
- String lockTimeStr = redisTemplate.opsForValue().get("lock:off");
|
|
|
- if (lockTimeStr != null) {
|
|
|
- try {
|
|
|
- long lockTime = Long.parseLong(lockTimeStr);
|
|
|
- long remainingTime = (lockTime - currentTime) / 1000;
|
|
|
- return Result.error(429, "一小时只能发布一次还剩" + remainingTime + "秒");
|
|
|
- } catch (NumberFormatException e) {
|
|
|
- return Result.error(500, "内部服务器错误");
|
|
|
+ // 校验频率限制
|
|
|
+ Result<?> rateLimitResult = checkRateLimit(LOCK_OFF_KEY);
|
|
|
+ if (rateLimitResult != null) {
|
|
|
+ return rateLimitResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建指令并执行
|
|
|
+ String data = buildTimingCommand("task2", value, 0); // 0表示关闭
|
|
|
+ return executeWithRateLimit(data, LOCK_OFF_KEY);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @GetMapping("/queryScheduledTasks")
|
|
|
+ @ApiOperation(value = "查询定时任务", notes = "查询所有已设置的定时任务")
|
|
|
+ public Result<?> queryScheduledTasks() {
|
|
|
+ try {
|
|
|
+ String data = "AT+AUTOCONT=0\r\n";
|
|
|
+ log.info("查询定时任务,发送指令:{}", data);
|
|
|
+ String msg = server.executeData(data);
|
|
|
+ log.info("查询定时任务成功,响应:{}", msg);
|
|
|
+ return Result.ok(msg);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return handleException("查询定时任务", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // ------------------------------ 私有工具方法(复用逻辑) ------------------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通用继电器控制方法
|
|
|
+ *
|
|
|
+ * @param relayNum 继电器编号(1/2)
|
|
|
+ * @param value 控制值(0/1)
|
|
|
+ */
|
|
|
+ private Result<?> controlRelay(int relayNum, Integer value) {
|
|
|
+ // 参数校验
|
|
|
+ if (value == null || (value != 0 && value != 1)) {
|
|
|
+ return Result.error(400, "参数错误,仅支持 0(断开)或 1(闭合)");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String data = String.format("AT+STACH%d=%d\r\n", relayNum, value);
|
|
|
+ log.info("控制第{}路继电器,发送指令:{}", relayNum, data);
|
|
|
+ String msg = server.executeData(data);
|
|
|
+ log.info("控制第{}路继电器成功,响应:{}", relayNum, msg);
|
|
|
+ return Result.ok(msg);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return handleException("控制第" + relayNum + "路继电器", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查频率限制(1小时内只能执行一次)
|
|
|
+ *
|
|
|
+ * @param lockKey redis锁键名
|
|
|
+ * @return 若触发限制则返回错误Result,否则返回null
|
|
|
+ */
|
|
|
+ private Result<?> checkRateLimit(String lockKey) {
|
|
|
+ try {
|
|
|
+ if (Boolean.TRUE.equals(redisTemplate.hasKey(lockKey))) {
|
|
|
+ long currentTime = Instant.now().toEpochMilli();
|
|
|
+ String lockTimeStr = redisTemplate.opsForValue().get(lockKey);
|
|
|
+ // 防御性判空,避免空指针
|
|
|
+ if (lockTimeStr == null) {
|
|
|
+ log.warn("redis锁键{}存在,但值为null,清除无效锁", lockKey);
|
|
|
+ redisTemplate.delete(lockKey);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ long lockTime = Long.parseLong(lockTimeStr);
|
|
|
+ long remainingTime = (lockTime - currentTime) / 1000;
|
|
|
+ if (remainingTime > 0) {
|
|
|
+ return Result.error(429, "一小时内只能操作一次,还剩" + remainingTime + "秒");
|
|
|
+ } else {
|
|
|
+ // 锁已过期,清除旧锁
|
|
|
+ redisTemplate.delete(lockKey);
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
+ return null;
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.error("解析redis锁时间失败,key:{}", lockKey, e);
|
|
|
+ return Result.error(500, "系统异常:时间解析失败");
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- long t = Instant.now().toEpochMilli();
|
|
|
- long lockTime = 3600000; // 3600000 毫秒 = 1 小时
|
|
|
- String data = "AT+AUTOCONT=8,task2,[CYC:1],[T:3,0|1|2|3|4|5|6," + value + "],[DO:0,1,0,100000,100000,1000000],[N:1,0]\r\n";
|
|
|
- String msg = server.ExecutionData(data);
|
|
|
- redisTemplate.opsForValue().set("lock:off", String.valueOf(t + lockTime), lockTime, TimeUnit.MILLISECONDS);
|
|
|
- return Result.ok(msg);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行指令并设置频率限制锁
|
|
|
+ *
|
|
|
+ * @param data 要发送的指令
|
|
|
+ * @param lockKey redis锁键名
|
|
|
+ */
|
|
|
+ private Result<?> executeWithRateLimit(String data, String lockKey) {
|
|
|
+ try {
|
|
|
+ log.info("发送定时指令:{}", data);
|
|
|
+ String msg = server.executeData(data);
|
|
|
+ // 设置锁(当前时间+1小时)
|
|
|
+ long expireTime = Instant.now().toEpochMilli() + LOCK_DURATION_MS;
|
|
|
+ redisTemplate.opsForValue().set(lockKey, String.valueOf(expireTime), LOCK_DURATION_MS, TimeUnit.MILLISECONDS);
|
|
|
+ log.info("定时指令执行成功,响应:{},已设置锁{}", msg, lockKey);
|
|
|
+ return Result.ok(msg);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return handleException("执行定时指令", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建定时任务指令
|
|
|
+ *
|
|
|
+ * @param taskName 任务名(task1/task2)
|
|
|
+ * @param time 定时时间(HH:mm:ss)
|
|
|
+ * @param action 动作(1-开启,0-关闭)
|
|
|
+ */
|
|
|
+ private String buildTimingCommand(String taskName, String time, int action) {
|
|
|
+ return String.format(
|
|
|
+ "AT+AUTOCONT=8,%s,[CYC:1],[T:3,0|1|2|3|4|5|6,%s],[DO:0,1,%d,100000,100000,1000000],[N:1,0]\r\n",
|
|
|
+ taskName, time, action
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验时间格式是否为HH:mm:ss
|
|
|
+ */
|
|
|
private boolean isValidTime(String time) {
|
|
|
try {
|
|
|
- LocalTime.parse(time, DateTimeFormatter.ofPattern("HH:mm:ss"));
|
|
|
+ LocalTime.parse(time, TIME_FORMATTER);
|
|
|
return true;
|
|
|
} catch (DateTimeParseException e) {
|
|
|
+ log.warn("无效的时间格式:{},正确格式应为HH:mm:ss", time);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
- @GetMapping("/queryScheduledTasks")
|
|
|
- @ApiOperation(value = "查询定时", notes = "查询定时")
|
|
|
- public Result<?> queryScheduledTasks() {
|
|
|
- String data = "AT+AUTOCONT=0\r\n";
|
|
|
- String msg = server.ExecutionData(data);
|
|
|
- return Result.ok(msg);
|
|
|
+ /**
|
|
|
+ * 统一异常处理方法
|
|
|
+ *
|
|
|
+ * @param operation 操作描述(用于日志)
|
|
|
+ * @param e 异常对象
|
|
|
+ */
|
|
|
+ private Result<?> handleException(String operation, Exception e) {
|
|
|
+ log.error("{}失败", operation, e); // 打印完整堆栈,便于排查
|
|
|
+ if (e instanceof CommonException) {
|
|
|
+ CommonException ce = (CommonException) e;
|
|
|
+ return Result.error(ce.getCode(), ce.getMessage());
|
|
|
+ } else if (e instanceof RuntimeException) {
|
|
|
+ return Result.error(500, e.getMessage());
|
|
|
+ } else {
|
|
|
+ return Result.error(500, "系统异常:" + e.getMessage() + ",请联系管理员");
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
+}
|