generateQrCode(@NotEmpty(message = "设备编号不能为空")
+ @PathVariable String[] ids) {
+ return toAjax(appDeviceService.generateQrCode(List.of(ids)));
+ }
+
/**
* 删除设备信息
*
- * @param ids 主键串
+ * @param
*/
@SaCheckPermission("app:device:remove")
@Log(title = "设备信息", businessType = BusinessType.DELETE)
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java
index c0781d1..d835fdf 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppDevice.java
@@ -1,6 +1,5 @@
package org.dromara.app.domain;
-import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -18,7 +17,7 @@ public class AppDevice extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
- @TableId(value = "device_no", type = IdType.INPUT)
+ @TableId(value = "device_no")
private String deviceNo;
private String bindTokenHash;
@@ -29,6 +28,8 @@ public class AppDevice extends BaseEntity {
private String deviceImg;
+ private String qrcode;
+
private String status;
private String workStatus;
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java
index aa96ad1..303d33c 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/AppWateringLog.java
@@ -1,13 +1,13 @@
package org.dromara.app.domain;
-import org.dromara.common.mybatis.core.domain.BaseEntity;
-import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
-import java.util.Date;
-import com.fasterxml.jackson.annotation.JsonFormat;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
+import java.util.Date;
/**
* 浇水记录对象 app_watering_log
@@ -74,6 +74,11 @@ public class AppWateringLog extends BaseEntity {
*/
private String triggerType;
+ /**
+ * 备注
+ */
+ private String remark;
+
/**
* 创建时间
*/
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java
index 52ca084..262a115 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppDeviceBo.java
@@ -1,6 +1,7 @@
package org.dromara.app.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.app.domain.AppDevice;
@@ -8,8 +9,6 @@ import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
import java.util.Date;
@Data
@@ -17,14 +16,13 @@ import java.util.Date;
@AutoMapper(target = AppDevice.class, reverseConvertGenerate = false)
public class AppDeviceBo extends BaseEntity {
- @NotBlank(message = "设备编号不能为空", groups = { AddGroup.class, EditGroup.class })
+ @NotBlank(message = "设备编号不能为空", groups = { EditGroup.class })
private String deviceNo;
private String bindToken;
private String bindTokenHash;
- @NotNull(message = "用户id不能为空", groups = { AddGroup.class, EditGroup.class })
private Long userId;
/**
@@ -37,6 +35,11 @@ public class AppDeviceBo extends BaseEntity {
*/
private String deviceImg;
+ /**
+ * 二维码
+ */
+ private String qrcode;
+
/**
* 状态 1-在线 0-离线 2-到期 3-故障
*/
@@ -85,6 +88,7 @@ public class AppDeviceBo extends BaseEntity {
/**
* mac地址
*/
+ @NotBlank(message = "MAC地址不能为空", groups = { AddGroup.class })
private String macAddress;
/**
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java
index 72627de..c825607 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/bo/AppWateringLogBo.java
@@ -1,15 +1,14 @@
package org.dromara.app.domain.bo;
-import org.dromara.app.domain.AppWateringLog;
-import org.dromara.common.mybatis.core.domain.BaseEntity;
-import org.dromara.common.core.validate.AddGroup;
-import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
-import jakarta.validation.constraints.*;
+import org.dromara.app.domain.AppWateringLog;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
import java.util.Date;
-import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 浇水记录业务对象 app_watering_log
@@ -73,6 +72,11 @@ public class AppWateringLogBo extends BaseEntity {
*/
private String triggerType;
+ /**
+ * 备注
+ */
+ private String remark;
+
/**
* 创建时间
*/
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java
index 8cb8167..4eb2742 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppDeviceVo.java
@@ -30,6 +30,9 @@ public class AppDeviceVo implements Serializable {
@ExcelProperty(value = "设备图片")
private String deviceImg;
+ @ExcelProperty(value = "二维码")
+ private String qrcode;
+
@ExcelProperty(value = "状态")
private String status;
@@ -59,6 +62,9 @@ public class AppDeviceVo implements Serializable {
@ExcelProperty(value = "mac地址")
private String macAddress;
+ @ExcelProperty(value = "用户名称")
+ private String nickName;
+
@ExcelProperty(value = "到期时间")
private Date expirationTime;
}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java
index a5ddcd5..0701537 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/AppWateringLogVo.java
@@ -1,14 +1,10 @@
package org.dromara.app.domain.vo;
-import java.util.Date;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import org.dromara.app.domain.AppWateringLog;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
-import org.dromara.common.excel.annotation.ExcelDictFormat;
-import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
+import org.dromara.app.domain.AppWateringLog;
import java.io.Serial;
import java.io.Serializable;
@@ -91,6 +87,12 @@ public class AppWateringLogVo implements Serializable {
private String triggerType;
private String deviceName;
+ /**
+ * 备注
+ */
+ @ExcelProperty(value = "备注")
+ private String remark;
+
/**
* 创建时间
*/
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/DashboardStatsVo.java b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/DashboardStatsVo.java
new file mode 100644
index 0000000..68f67e7
--- /dev/null
+++ b/water-modules/water-app/src/main/java/org/dromara/app/domain/vo/DashboardStatsVo.java
@@ -0,0 +1,42 @@
+package org.dromara.app.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 仪表盘统计指标 VO。
+ *
+ * 对应前端首页 8 个统计卡片的数据。
+ *
+ * @author water team
+ */
+@Data
+public class DashboardStatsVo implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 设备总数 */
+ private Long totalDevices;
+
+ /** 在线设备数(status='1') */
+ private Long onlineDevices;
+
+ /** 待机设备数(status='2') */
+ private Long standbyDevices;
+
+ /** 离线设备数(status='0') */
+ private Long offlineDevices;
+
+ /** 故障设备数(status='3') */
+ private Long faultDevices;
+
+ /** 工作中设备数(workStatus='1') */
+ private Long workingDevices;
+
+ /** 空闲设备数(workStatus='0') */
+ private Long idleDevices;
+
+ /** 未绑定用户设备数(user_id IS NULL) */
+ private Long unboundDevices;
+}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceCommandAckHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceCommandAckHandler.java
index 8d9beee..a76859a 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceCommandAckHandler.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceCommandAckHandler.java
@@ -18,9 +18,11 @@ public class DeviceCommandAckHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/ack$");
private final IDeviceCommandAckHandler delegate;
+ private final DeviceIdentityResolver deviceIdentityResolver;
- public DeviceCommandAckHandler(IDeviceCommandAckHandler delegate) {
+ public DeviceCommandAckHandler(IDeviceCommandAckHandler delegate, DeviceIdentityResolver deviceIdentityResolver) {
this.delegate = delegate;
+ this.deviceIdentityResolver = deviceIdentityResolver;
}
@Override
@@ -29,7 +31,12 @@ public class DeviceCommandAckHandler implements MqttTopicHandler {
}
@Override
- public void handle(String deviceNo, String payload) {
+ public void handle(String deviceIdentity, String payload) {
+ String deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
+ if (deviceNo == null) {
+ log.warn("[MQTT] ACK 未找到设备 设备标识={} 消息体={}", deviceIdentity, payload);
+ return;
+ }
delegate.handleAck(deviceNo, payload);
}
}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceDataHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceDataHandler.java
index d81a24d..078c2fe 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceDataHandler.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceDataHandler.java
@@ -10,6 +10,7 @@ import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
+import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@@ -18,6 +19,7 @@ import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
@@ -31,12 +33,14 @@ public class DeviceDataHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/power$");
private final IAppDeviceService appDeviceService;
private final AppDeviceMapper appDeviceMapper;
+ private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";
@Value("${mqtt.command-ack.device-status-cache-prefix}")
private String deviceStatusCachePrefix;
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
private int deviceStatusCacheTtlSeconds;
+ private final DeviceIdentityResolver deviceIdentityResolver;
@Override
public Pattern topicPattern() {
@@ -44,8 +48,15 @@ public class DeviceDataHandler implements MqttTopicHandler {
}
@Override
- public void handle(String deviceNo, String payload) {
+ public void handle(String deviceIdentity, String payload) {
+ String deviceNo = null;
try {
+ deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
+ if (deviceNo == null) {
+ log.warn("[MQTT] 设备电量上报未找到设备 时间={} 设备标识={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, payload);
+ return;
+ }
Map dto = JsonUtils.parseObject(payload, Map.class);
if (dto == null || dto.get("powerLevel") == null) {
log.warn("[MQTT] 设备电量上报缺少电量字段 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
@@ -63,7 +74,8 @@ public class DeviceDataHandler implements MqttTopicHandler {
refreshDeviceOnline(deviceNo);
log.info("[MQTT] 设备电量更新 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
- log.error("[MQTT] 设备电量更新失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
+ log.error("[MQTT] 设备电量更新失败 时间={} 设备标识={} 设备编号={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
}
}
@@ -72,19 +84,29 @@ public class DeviceDataHandler implements MqttTopicHandler {
* 写入 Redis 后 TTL 到期自动过期 = 离线
*/
private void refreshDeviceOnline(String deviceNo) {
- Map statusCache = new HashMap<>();
- statusCache.put("deviceNo", deviceNo);
- statusCache.put("status", "1");
- statusCache.put("lastReportTime", Instant.now().toString());
- RedisUtils.setCacheObject(
- deviceStatusCachePrefix + deviceNo,
- statusCache,
- Duration.ofSeconds(deviceStatusCacheTtlSeconds)
- );
- appDeviceMapper.update(null,
- new LambdaUpdateWrapper()
- .set(AppDevice::getStatus, "1")
- .eq(AppDevice::getDeviceNo, deviceNo)
- );
+ RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
+ boolean locked = false;
+ try {
+ lock.lock(10, TimeUnit.SECONDS);
+ locked = true;
+ Map statusCache = new HashMap<>();
+ statusCache.put("deviceNo", deviceNo);
+ statusCache.put("status", "1");
+ statusCache.put("lastReportTime", Instant.now().toString());
+ RedisUtils.setCacheObject(
+ deviceStatusCachePrefix + deviceNo,
+ statusCache,
+ Duration.ofSeconds(deviceStatusCacheTtlSeconds)
+ );
+ appDeviceMapper.update(null,
+ new LambdaUpdateWrapper()
+ .set(AppDevice::getStatus, "1")
+ .eq(AppDevice::getDeviceNo, deviceNo)
+ );
+ } finally {
+ if (locked && lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ }
}
}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceIdentityResolver.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceIdentityResolver.java
new file mode 100644
index 0000000..812ad62
--- /dev/null
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceIdentityResolver.java
@@ -0,0 +1,45 @@
+package org.dromara.app.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.app.domain.AppDevice;
+import org.dromara.app.mapper.AppDeviceMapper;
+import org.dromara.common.core.utils.StringUtils;
+import org.springframework.stereotype.Component;
+
+import java.util.Locale;
+
+/**
+ * 解析 MQTT Topic 中的设备标识。
+ *
+ * Topic 第一段允许传设备编号或 MAC 地址。业务处理统一转换为数据库中的设备编号。
+ */
+@Component
+@RequiredArgsConstructor
+public class DeviceIdentityResolver {
+
+ private final AppDeviceMapper appDeviceMapper;
+
+ public AppDevice resolve(String identity) {
+ if (StringUtils.isBlank(identity)) {
+ return null;
+ }
+ String text = identity.trim();
+ AppDevice device = appDeviceMapper.selectById(text);
+ if (device != null) {
+ return device;
+ }
+ return appDeviceMapper.selectByMac(normalizeMacAddress(text));
+ }
+
+ public String resolveDeviceNo(String identity) {
+ AppDevice device = resolve(identity);
+ return device == null ? null : device.getDeviceNo();
+ }
+
+ public String normalizeMacAddress(String macAddress) {
+ if (StringUtils.isBlank(macAddress)) {
+ return null;
+ }
+ return macAddress.trim().toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java
index 1f8f012..323de32 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/DeviceRegisterHandler.java
@@ -5,38 +5,50 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.bo.AppDeviceBo;
+import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
+import org.dromara.app.service.IDeviceCommandPublisher;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
+import org.redisson.api.RLock;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
- * Device registration handler, matches /{deviceNo}/publish/register.
+ * 设备注册处理器,匹配 /{deviceMac}/publish/register。
*/
@Slf4j
@Component
@RequiredArgsConstructor
-public class DeviceRegisterHandler implements MqttTopicHandler {
+public class
+DeviceRegisterHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/register$");
private final IAppDeviceService appDeviceService;
private final AppDeviceMapper appDeviceMapper;
+ private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";
+ private final ObjectProvider commandPublisherProvider;
@Value("${mqtt.command-ack.device-status-cache-prefix}")
private String deviceStatusCachePrefix;
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds}")
private int deviceStatusCacheTtlSeconds;
+ private final DeviceIdentityResolver deviceIdentityResolver;
+ @Value("${mqtt.topics.publish-prefix:}")
+ private String mqttPublishPrefix;
@Override
public Pattern topicPattern() {
@@ -44,22 +56,40 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
}
@Override
- public void handle(String deviceNo, String payload) {
+ public void handle(String deviceIdentity, String payload) {
try {
Map dto = JsonUtils.parseObject(payload, Map.class);
- AppDeviceBo device = buildRegisterDevice(deviceNo, dto);
+ AppDevice exists = deviceIdentityResolver.resolve(deviceIdentity);
+ String normalizedDeviceMac = resolveDeviceMac(deviceIdentity, dto);
+ if (exists == null && normalizedDeviceMac != null) {
+ exists = appDeviceMapper.selectByMac(normalizedDeviceMac);
+ }
+ if (exists == null) {
+ log.warn("[MQTT] 设备注册失败,设备标识未入库 时间={} 设备标识={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, payload);
+ return;
+ }
+
+ String deviceNo = exists.getDeviceNo();
+ if (normalizedDeviceMac == null) {
+ normalizedDeviceMac = deviceIdentityResolver.normalizeMacAddress(exists.getMacAddress());
+ }
+ AppDeviceBo device = buildRegisterDevice(deviceNo, normalizedDeviceMac, dto);
appDeviceService.registerByMqtt(device);
- // 注册即在线
refreshDeviceOnline(deviceNo);
- log.info("[MQTT] 设备注册 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
+ String commandTarget = normalizedDeviceMac == null ? deviceIdentity : normalizedDeviceMac;
+ sendDeviceNoToDevice(commandTarget, deviceNo);
+ log.info("[MQTT] 设备注册 时间={} MAC={} 设备编号={} 消息体={}",
+ HandlerLogTime.now(), normalizedDeviceMac, deviceNo, payload);
} catch (Exception e) {
- log.error("[MQTT] 设备注册失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
+ log.error("[MQTT] 设备注册失败 时间={} 设备标识={} 消息体={}", HandlerLogTime.now(), deviceIdentity, payload, e);
}
}
- private AppDeviceBo buildRegisterDevice(String topicDeviceNo, Map dto) {
+ private AppDeviceBo buildRegisterDevice(String deviceNo, String deviceMac, Map dto) {
AppDeviceBo device = new AppDeviceBo();
- device.setDeviceNo(topicDeviceNo);
+ device.setDeviceNo(deviceNo);
+ device.setMacAddress(deviceMac);
device.setStatus("1"); // 注册即在线
if (dto == null) {
return device;
@@ -68,11 +98,59 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
device.setPowerLevel(valueAsString(dto.get("powerLevel")));
device.setDeviceEm(valueAsString(dto.get("deviceEm")));
device.setDeviceSn(valueAsString(dto.get("deviceSn")));
- device.setMacAddress(firstNotBlank(dto.get("deviceMac"), dto.get("macAddress")));
device.setFwVer(firstNotBlank(dto.get("version"), dto.get("fwVer")));
return device;
}
+ private String resolveDeviceMac(String topicDeviceMac, Map dto) {
+ String payloadMac = dto == null ? null : firstNotBlank(dto.get("deviceMac"), dto.get("macAddress"));
+ return normalizeMacAddress(firstNotBlank(payloadMac, topicDeviceMac));
+ }
+
+ private String normalizeMacAddress(String macAddress) {
+ if (macAddress == null) {
+ return null;
+ }
+ String text = macAddress.trim();
+ return text.isBlank() ? null : text.toLowerCase(Locale.ROOT);
+ }
+
+ private void sendDeviceNoToDevice(String deviceMac, String deviceNo) {
+ IDeviceCommandPublisher commandPublisher = commandPublisherProvider.getIfAvailable();
+ if (commandPublisher == null) {
+ log.warn("[MQTT] 设备编号下发失败,未找到命令发布器 时间={} MAC={} 设备编号={}",
+ HandlerLogTime.now(), deviceMac, deviceNo);
+ return;
+ }
+
+ try {
+ DeviceCommand command = new DeviceCommand();
+ command.setDeviceNo(deviceNo);
+ command.setCommandType("registerDeviceNo");
+ command.setTopic(buildCommandTopic(deviceMac));
+ command.getPayload().put("deviceNo", deviceNo);
+ command.getPayload().put("deviceMac", deviceMac);
+
+ String commandId = commandPublisher.send(command);
+ log.info("[MQTT] 设备编号已下发 时间={} MAC={} 设备编号={} 命令编号={}",
+ HandlerLogTime.now(), deviceMac, deviceNo, commandId);
+ } catch (Exception e) {
+ log.error("[MQTT] 设备编号下发失败 时间={} MAC={} 设备编号={}",
+ HandlerLogTime.now(), deviceMac, deviceNo, e);
+ }
+ }
+
+ private String buildCommandTopic(String deviceMac) {
+ if (mqttPublishPrefix == null || mqttPublishPrefix.isBlank()) {
+ return "/" + deviceMac + "/subscriber/cmd";
+ }
+ String normalizedPrefix = mqttPublishPrefix.startsWith("/") ? mqttPublishPrefix : "/" + mqttPublishPrefix;
+ if (normalizedPrefix.endsWith("/")) {
+ normalizedPrefix = normalizedPrefix.substring(0, normalizedPrefix.length() - 1);
+ }
+ return normalizedPrefix + "/" + deviceMac + "/subscriber/cmd";
+ }
+
private String firstNotBlank(Object first, Object second) {
String firstText = valueAsString(first);
if (firstText != null && !firstText.isBlank()) {
@@ -90,19 +168,29 @@ public class DeviceRegisterHandler implements MqttTopicHandler {
* 刷新设备在线状态到 Redis
*/
private void refreshDeviceOnline(String deviceNo) {
- Map statusCache = new HashMap<>();
- statusCache.put("deviceNo", deviceNo);
- statusCache.put("status", "1");
- statusCache.put("lastReportTime", Instant.now().toString());
- RedisUtils.setCacheObject(
- deviceStatusCachePrefix + deviceNo,
- statusCache,
- Duration.ofSeconds(deviceStatusCacheTtlSeconds)
- );
- appDeviceMapper.update(null,
- new LambdaUpdateWrapper()
- .set(AppDevice::getStatus, "1")
- .eq(AppDevice::getDeviceNo, deviceNo)
- );
+ RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
+ boolean locked = false;
+ try {
+ lock.lock(10, TimeUnit.SECONDS);
+ locked = true;
+ Map statusCache = new HashMap<>();
+ statusCache.put("deviceNo", deviceNo);
+ statusCache.put("status", "1");
+ statusCache.put("lastReportTime", Instant.now().toString());
+ RedisUtils.setCacheObject(
+ deviceStatusCachePrefix + deviceNo,
+ statusCache,
+ Duration.ofSeconds(deviceStatusCacheTtlSeconds)
+ );
+ appDeviceMapper.update(null,
+ new LambdaUpdateWrapper()
+ .set(AppDevice::getStatus, "1")
+ .eq(AppDevice::getDeviceNo, deviceNo)
+ );
+ } finally {
+ if (locked && lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ }
}
}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/ErromesHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/ErromesHandler.java
index 2bc92c9..1946794 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/handler/ErromesHandler.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/ErromesHandler.java
@@ -18,6 +18,7 @@ import java.util.regex.Pattern;
public class ErromesHandler implements MqttTopicHandler {
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/error$");
+ private final DeviceIdentityResolver deviceIdentityResolver;
@Override
public Pattern topicPattern() {
@@ -25,12 +26,19 @@ public class ErromesHandler implements MqttTopicHandler {
}
@Override
- public void handle(String deviceNo, String payload) {
+ public void handle(String deviceIdentity, String payload) {
+ String deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
try {
+ if (deviceNo == null) {
+ log.warn("[MQTT] 设备异常告警未找到设备 时间={} 设备标识={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, payload);
+ return;
+ }
Map dto = JsonUtils.parseObject(payload, Map.class);
log.info("[MQTT] 设备异常告警 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, dto);
} catch (Exception e) {
- log.error("[MQTT] 设备异常处理失败 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo, e);
+ log.error("[MQTT] 设备异常处理失败 时间={} 设备标识={} 设备编号={}",
+ HandlerLogTime.now(), deviceIdentity, deviceNo, e);
}
}
}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java
index 899e8df..e4421b7 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/KeyFinishHandler.java
@@ -1,9 +1,12 @@
package org.dromara.app.handler;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.AppDeviceVo;
+import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.mqtt.MqttTopicHandler;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
@@ -14,6 +17,7 @@ import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
@@ -29,6 +33,8 @@ public class KeyFinishHandler implements MqttTopicHandler {
private final IAppWateringLogService appWateringLogService;
private final IAppDeviceService appDeviceService;
+ private final AppSchedulingDeviceMapper schedulingDeviceMapper;
+ private final DeviceIdentityResolver deviceIdentityResolver;
@Override
public Pattern topicPattern() {
@@ -36,14 +42,26 @@ public class KeyFinishHandler implements MqttTopicHandler {
}
@Override
- public void handle(String deviceNo, String payload) {
+ public void handle(String deviceIdentity, String payload) {
+ String deviceNo = null;
try {
+ deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
+ if (deviceNo == null) {
+ log.warn("[MQTT] 按键浇水完成上报未找到设备 时间={} 设备标识={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, payload);
+ return;
+ }
Map dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
- appWateringLogService.insertByBo(logBo);
+ if ("0".equals(logBo.getTriggerType())) {
+ appWateringLogService.confirmScheduleLog(logBo);
+ } else {
+ appWateringLogService.insertByBo(logBo);
+ }
log.info("[MQTT] 按键浇水完成上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
- log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
+ log.error("[MQTT] 按键浇水完成上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
}
}
@@ -73,9 +91,11 @@ public class KeyFinishHandler implements MqttTopicHandler {
logBo.setTriggerType("1");
if ("schedule".equals(valueAsString(dto.get("triggerON")))){
logBo.setTriggerType("0");
+ logBo.setScheduleId(queryScheduleId(topicDeviceNo));
}
logBo.setCreateTime(receivedAt);
logBo.setEndTime(endTime);
+ logBo.setRemark("0".equals(logBo.getTriggerType()) ? "设备上报排程浇水完成" : "设备按键完成浇水");
return logBo;
}
@@ -84,6 +104,21 @@ public class KeyFinishHandler implements MqttTopicHandler {
return device == null ? null : device.getUserId();
}
+ private Long queryScheduleId(String deviceNo) {
+ List schedulingDevices = schedulingDeviceMapper.selectList(
+ Wrappers.lambdaQuery()
+ .eq(AppSchedulingDevice::getDeviceNo, deviceNo)
+ );
+ if (schedulingDevices == null || schedulingDevices.isEmpty()) {
+ log.warn("[MQTT] 按键浇水完成上报未找到设备绑定的排程 时间={} 设备编号={}", HandlerLogTime.now(), deviceNo);
+ return 0L;
+ }
+ if (schedulingDevices.size() > 1) {
+ log.warn("[MQTT] 按键浇水完成上报匹配到多个排程 时间={} 设备编号={} 数量={}", HandlerLogTime.now(), deviceNo, schedulingDevices.size());
+ }
+ return schedulingDevices.get(0).getScheduleId();
+ }
+
private Date parseStartTime(String startTime) {
if (StringUtils.isBlank(startTime)) {
return new Date();
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java b/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java
index 8d58cea..2c1399b 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/handler/ScheduleFinishHandler.java
@@ -12,6 +12,7 @@ import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Component;
import java.text.ParseException;
@@ -34,6 +35,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
private final IAppWateringLogService appWateringLogService;
private final IAppDeviceService appDeviceService;
private final AppSchedulingDeviceMapper schedulingDeviceMapper;
+ private final DeviceIdentityResolver deviceIdentityResolver;
@Override
public Pattern topicPattern() {
@@ -41,14 +43,23 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
}
@Override
- public void handle(String deviceNo, String payload) {
+ public void handle(String deviceIdentity, String payload) {
+ String deviceNo = null;
try {
+ deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
+ if (deviceNo == null) {
+ log.warn("[MQTT] 浇水排程上报未找到设备 时间={} 设备标识={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, payload);
+ return;
+ }
Map dto = JsonUtils.parseObject(payload, Map.class);
AppWateringLogBo logBo = buildWateringLog(deviceNo, dto);
- appWateringLogService.insertByBo(logBo);
+ logBo.setUserId(LoginHelper.getUserId());
+ appWateringLogService.confirmScheduleLog(logBo);
log.info("[MQTT] 浇水排程上报已保存 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
} catch (Exception e) {
- log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload, e);
+ log.error("[MQTT] 浇水排程上报处理失败 时间={} 设备标识={} 设备编号={} 消息体={}",
+ HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
}
}
@@ -82,6 +93,7 @@ public class ScheduleFinishHandler implements MqttTopicHandler {
}
logBo.setCreateTime(receivedAt);
logBo.setEndTime(endTime);
+ logBo.setRemark("排程浇水完成");
return logBo;
}
diff --git a/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java b/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java
index 69377ec..b254563 100644
--- a/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java
+++ b/water-modules/water-app/src/main/java/org/dromara/app/mapper/AppDeviceMapper.java
@@ -1,12 +1,17 @@
package org.dromara.app.mapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.dromara.app.domain.AppDevice;
+import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.vo.AppDeviceVo;
+import org.dromara.app.domain.vo.DashboardStatsVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import java.util.List;
+
/**
* 设备信息
Mapper接口
@@ -16,6 +21,154 @@ Mapper接口
*/
public interface AppDeviceMapper extends BaseMapperPlus {
+ @Select("""
+
+ """)
+ Page selectDeviceVoPage(Page page, @Param("bo") AppDeviceBo bo);
+
+ @Select("""
+
+ """)
+ List selectDeviceVoList(@Param("bo") AppDeviceBo bo);
+
@Update("""