diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java index 8d6a79178..d266eadc6 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/DateIntervalEnum.java @@ -16,6 +16,7 @@ import java.util.Arrays; @AllArgsConstructor public enum DateIntervalEnum implements ArrayValuable { + HOUR(0, "小时"), // 特殊:字典里,暂时不会有这个枚举!!!因为大多数情况下,用不到这个间隔 DAY(1, "天"), WEEK(2, "周"), MONTH(3, "月"), diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java index 26b396168..4cbd4b618 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -8,6 +8,7 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; +import java.sql.Timestamp; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -16,8 +17,7 @@ import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.List; -import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN; -import static cn.hutool.core.date.DatePattern.createFormatter; +import static cn.hutool.core.date.DatePattern.*; /** * 时间工具类,用于 {@link LocalDateTime} @@ -82,6 +82,21 @@ public class LocalDateTimeUtils { return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)}; } + /** + * 判指定断时间,是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, Timestamp time) { + if (startTime == null || endTime == null || time == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTimeUtil.of(time), startTime, endTime); + } + /** * 判指定断时间,是否在该时间范围内 * @@ -234,6 +249,11 @@ public class LocalDateTimeUtils { // 2. 循环,生成时间范围 List timeRanges = new ArrayList<>(); switch (intervalEnum) { + case HOUR: + while (startTime.isBefore(endTime)) { + timeRanges.add(new LocalDateTime[]{startTime, startTime.plusHours(1).minusNanos(1)}); + startTime = startTime.plusHours(1); + } case DAY: while (startTime.isBefore(endTime)) { timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); @@ -297,6 +317,8 @@ public class LocalDateTimeUtils { // 2. 循环,生成时间范围 switch (intervalEnum) { + case HOUR: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATETIME_MINUTE_PATTERN); case DAY: return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); case WEEK: diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java index 8bb876591..1da94691b 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java @@ -3,18 +3,22 @@ package cn.iocoder.yudao.framework.common.util.json; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer; +import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.lang.reflect.Type; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -32,7 +36,11 @@ public class JsonUtils { objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值 - objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化 + // 解决 LocalDateTime 的序列化 + SimpleModule simpleModule = new JavaTimeModule() + .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + objectMapper.registerModules(simpleModule); } /** @@ -99,6 +107,18 @@ public class JsonUtils { } } + public static T parseObject(byte[] text, Type type) { + if (ArrayUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + /** * 将字符串解析成指定类型的对象 * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java index c9ab3e541..b80244456 100644 --- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java @@ -69,9 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration { @ConditionalOnBean(AbstractRedisStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 public RedisPendingMessageResendJob redisPendingMessageResendJob(List> listeners, RedisMQTemplate redisTemplate, - @Value("${spring.application.name}") String groupName, RedissonClient redissonClient) { - return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); + return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient); } /** @@ -141,14 +140,14 @@ public class YudaoRedisMQConsumerAutoConfiguration { * * @return 消费者名字 */ - private static String buildConsumerName() { + public static String buildConsumerName() { return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); } /** * 校验 Redis 版本号,是否满足最低的版本号要求! */ - private static void checkRedisVersion(RedisTemplate redisTemplate) { + public static void checkRedisVersion(RedisTemplate redisTemplate) { // 获得 Redis 版本 Properties info = redisTemplate.execute((RedisCallback) RedisServerCommands::info); String version = MapUtil.getStr(info, "redis_version"); diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java index cb4e3991f..bb16be0ee 100644 --- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/job/RedisPendingMessageResendJob.java @@ -35,7 +35,6 @@ public class RedisPendingMessageResendJob { private final List> listeners; private final RedisMQTemplate redisTemplate; - private final String groupName; private final RedissonClient redissonClient; /** @@ -64,13 +63,13 @@ public class RedisPendingMessageResendJob { private void execute() { StreamOperations ops = redisTemplate.getRedisTemplate().opsForStream(); listeners.forEach(listener -> { - PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName)); + PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), listener.getGroup())); // 每个消费者的 pending 队列消息数量 Map pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer(); pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> { log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount); // 每个消费者的 pending消息的详情信息 - PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount); + PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(listener.getGroup(), consumerName), Range.unbounded(), pendingMessageCount); if (pendingMessages.isEmpty()) { return; } @@ -91,7 +90,7 @@ public class RedisPendingMessageResendJob { .ofObject(records.get(0).getValue()) // 设置内容 .withStreamKey(listener.getStreamKey())); // ack 消息消费完成 - redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0)); + redisTemplate.getRedisTemplate().opsForStream().acknowledge(listener.getGroup(), records.get(0)); log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId()); }); }); diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java index 3e656af3f..ba1aa9697 100644 --- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/core/stream/AbstractRedisStreamMessageListener.java @@ -53,6 +53,12 @@ public abstract class AbstractRedisStreamMessageListener message) { // 消费消息 diff --git a/yudao-module-iot/pom.xml b/yudao-module-iot/pom.xml index 34a6dce3e..32b004424 100644 --- a/yudao-module-iot/pom.xml +++ b/yudao-module-iot/pom.xml @@ -9,8 +9,9 @@ yudao-module-iot-api - yudao-module-iot-biz - yudao-module-iot-plugins + yudao-module-iot-core + yudao-module-iot-server + yudao-module-iot-gateway 4.0.0 diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java deleted file mode 100644 index e88706ac5..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java +++ /dev/null @@ -1,93 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.enums.ApiConstants; -import jakarta.validation.Valid; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; - -/** - * 设备数据 Upstream 上行 API - * - * 目的:设备 -> 插件 -> 服务端 - * - * @author haohao - */ -public interface IotDeviceUpstreamApi { - - String PREFIX = ApiConstants.PREFIX + "/device/upstream"; - - // ========== 设备相关 ========== - - /** - * 更新设备状态 - * - * @param updateReqDTO 更新设备状态 DTO - */ - @PostMapping(PREFIX + "/update-state") - CommonResult updateDeviceState(@Valid @RequestBody IotDeviceStateUpdateReqDTO updateReqDTO); - - /** - * 上报设备属性数据 - * - * @param reportReqDTO 上报设备属性数据 DTO - */ - @PostMapping(PREFIX + "/report-property") - CommonResult reportDeviceProperty(@Valid @RequestBody IotDevicePropertyReportReqDTO reportReqDTO); - - /** - * 上报设备事件数据 - * - * @param reportReqDTO 设备事件 - */ - @PostMapping(PREFIX + "/report-event") - CommonResult reportDeviceEvent(@Valid @RequestBody IotDeviceEventReportReqDTO reportReqDTO); - - // TODO @芋艿:这个需要 plugins 接入下 - /** - * 注册设备 - * - * @param registerReqDTO 注册设备 DTO - */ - @PostMapping(PREFIX + "/register") - CommonResult registerDevice(@Valid @RequestBody IotDeviceRegisterReqDTO registerReqDTO); - - // TODO @芋艿:这个需要 plugins 接入下 - /** - * 注册子设备 - * - * @param registerReqDTO 注册子设备 DTO - */ - @PostMapping(PREFIX + "/register-sub") - CommonResult registerSubDevice(@Valid @RequestBody IotDeviceRegisterSubReqDTO registerReqDTO); - - // TODO @芋艿:这个需要 plugins 接入下 - /** - * 注册设备拓扑 - * - * @param addReqDTO 注册设备拓扑 DTO - */ - @PostMapping(PREFIX + "/add-topology") - CommonResult addDeviceTopology(@Valid @RequestBody IotDeviceTopologyAddReqDTO addReqDTO); - - // TODO @芋艿:考虑 http 认证 - /** - * 认证 Emqx 连接 - * - * @param authReqDTO 认证 Emqx 连接 DTO - */ - @PostMapping(PREFIX + "/authenticate-emqx-connection") - CommonResult authenticateEmqxConnection(@Valid @RequestBody IotDeviceEmqxAuthReqDTO authReqDTO); - - // ========== 插件相关 ========== - - /** - * 心跳插件实例 - * - * @param heartbeatReqDTO 心跳插件实例 DTO - */ - @PostMapping(PREFIX + "/heartbeat-plugin-instance") - CommonResult heartbeatPluginInstance(@Valid @RequestBody IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java deleted file mode 100644 index 9624b671e..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import java.util.Map; - -/** - * IoT 设备【配置】设置 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDeviceConfigSetReqDTO extends IotDeviceDownstreamAbstractReqDTO { - - /** - * 配置 - */ - @NotNull(message = "配置不能为空") - private Map config; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java deleted file mode 100644 index e78bea6fb..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -/** - * IoT 设备下行的抽象 Request DTO - * - * @author 芋道源码 - */ -@Data -public abstract class IotDeviceDownstreamAbstractReqDTO { - - /** - * 请求编号 - */ - private String requestId; - - /** - * 产品标识 - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - /** - * 设备名称 - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java deleted file mode 100644 index 8eccec42e..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; - -import cn.hutool.core.map.MapUtil; -import lombok.Data; - -import java.util.Map; - -/** - * IoT 设备【OTA】升级下发 Request DTO(更新固件消息) - * - * @author 芋道源码 - */ -@Data -public class IotDeviceOtaUpgradeReqDTO extends IotDeviceDownstreamAbstractReqDTO { - - /** - * 固件编号 - */ - private Long firmwareId; - /** - * 固件版本 - */ - private String version; - - /** - * 签名方式 - * - * 例如说:MD5、SHA256 - */ - private String signMethod; - /** - * 固件文件签名 - */ - private String fileSign; - /** - * 固件文件大小 - */ - private Long fileSize; - /** - * 固件文件 URL - */ - private String fileUrl; - - /** - * 自定义信息,建议使用 JSON 格式 - */ - private String information; - - public static IotDeviceOtaUpgradeReqDTO build(Map map) { - return new IotDeviceOtaUpgradeReqDTO() - .setFirmwareId(MapUtil.getLong(map, "firmwareId")).setVersion((String) map.get("version")) - .setSignMethod((String) map.get("signMethod")).setFileSign((String) map.get("fileSign")) - .setFileSize(MapUtil.getLong(map, "fileSize")).setFileUrl((String) map.get("fileUrl")) - .setInformation((String) map.get("information")); - } - - public static Map build(IotDeviceOtaUpgradeReqDTO dto) { - return MapUtil.builder() - .put("firmwareId", dto.getFirmwareId()).put("version", dto.getVersion()) - .put("signMethod", dto.getSignMethod()).put("fileSign", dto.getFileSign()) - .put("fileSize", dto.getFileSize()).put("fileUrl", dto.getFileUrl()) - .put("information", dto.getInformation()) - .build(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java deleted file mode 100644 index d9ae96321..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.List; - -// TODO @芋艿:从 server => plugin => device 是否有必要?从阿里云 iot 来看,没有这个功能?! -// TODO @芋艿:是不是改成 read 更好?在看看阿里云的 topic 设计 -/** - * IoT 设备【属性】获取 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDevicePropertyGetReqDTO extends IotDeviceDownstreamAbstractReqDTO { - - /** - * 属性标识数组 - */ - @NotEmpty(message = "属性标识数组不能为空") - private List identifiers; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java deleted file mode 100644 index 170fe80f6..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.Map; - -/** - * IoT 设备【属性】设置 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDevicePropertySetReqDTO extends IotDeviceDownstreamAbstractReqDTO { - - /** - * 属性参数 - */ - @NotEmpty(message = "属性参数不能为空") - private Map properties; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java deleted file mode 100644 index 0a2b3f0bf..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.Map; - -/** - * IoT 设备【服务】调用 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDeviceServiceInvokeReqDTO extends IotDeviceDownstreamAbstractReqDTO { - - /** - * 服务标识 - */ - @NotEmpty(message = "服务标识不能为空") - private String identifier; - /** - * 调用参数 - */ - private Map params; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java deleted file mode 100644 index 34e6283d9..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.Map; - -/** - * IoT 设备【事件】上报 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDeviceEventReportReqDTO extends IotDeviceUpstreamAbstractReqDTO { - - /** - * 事件标识 - */ - @NotEmpty(message = "事件标识不能为空") - private String identifier; - /** - * 事件参数 - */ - private Map params; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java deleted file mode 100644 index a88a72e91..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import lombok.Data; - -// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/progress -/** - * IoT 设备【OTA】升级进度 Request DTO(上报更新固件进度) - * - * @author 芋道源码 - */ -@Data -public class IotDeviceOtaProgressReqDTO extends IotDeviceUpstreamAbstractReqDTO { - - /** - * 固件编号 - */ - private Long firmwareId; - - /** - * 升级状态 - * - * 枚举 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} - */ - private Integer status; - /** - * 升级进度,百分比 - */ - private Integer progress; - - /** - * 升级进度描述 - */ - private String description; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java deleted file mode 100644 index 6328704e5..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/pull -/** - * IoT 设备【OTA】升级下拉 Request DTO(拉取固件更新) - * - * @author 芋道源码 - */ -public class IotDeviceOtaPullReqDTO { - - /** - * 固件编号 - */ - private Long firmwareId; - - /** - * 固件版本 - */ - private String version; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java deleted file mode 100644 index 2b3b91c98..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/report -/** - * IoT 设备【OTA】上报 Request DTO(上报固件版本) - * - * @author 芋道源码 - */ -public class IotDeviceOtaReportReqDTO { - - /** - * 固件编号 - */ - private Long firmwareId; - - /** - * 固件版本 - */ - private String version; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java deleted file mode 100644 index 4a276bd22..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.Map; - -/** - * IoT 设备【属性】上报 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDevicePropertyReportReqDTO extends IotDeviceUpstreamAbstractReqDTO { - - /** - * 属性参数 - */ - @NotEmpty(message = "属性参数不能为空") - private Map properties; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java deleted file mode 100644 index cab55e832..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import lombok.Data; - -/** - * IoT 设备【注册】自己 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDeviceRegisterReqDTO extends IotDeviceUpstreamAbstractReqDTO { -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java deleted file mode 100644 index 0b826fbb1..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.List; - -/** - * IoT 设备【注册】子设备 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDeviceRegisterSubReqDTO extends IotDeviceUpstreamAbstractReqDTO { - - // TODO @芋艿:看看要不要优化命名 - /** - * 子设备数组 - */ - @NotEmpty(message = "子设备不能为空") - private List params; - - /** - * 设备信息 - */ - @Data - public static class Device { - - /** - * 产品标识 - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - - /** - * 设备名称 - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java deleted file mode 100644 index 38c479a57..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * IoT 设备【状态】更新 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotDeviceStateUpdateReqDTO extends IotDeviceUpstreamAbstractReqDTO { - - /** - * 设备状态 - */ - @NotNull(message = "设备状态不能为空") - @InEnum(IotDeviceStateEnum.class) // 只使用:在线、离线 - private Integer state; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java deleted file mode 100644 index 18efe7d48..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.util.List; - -// TODO @芋艿:要写清楚,是来自设备网关,还是设备。 -/** - * IoT 设备【拓扑】添加 Request DTO - */ -@Data -public class IotDeviceTopologyAddReqDTO extends IotDeviceUpstreamAbstractReqDTO { - - // TODO @芋艿:看看要不要优化命名 - /** - * 子设备数组 - */ - @NotEmpty(message = "子设备不能为空") - private List params; - - /** - * 设备信息 - */ - @Data - public static class Device { - - /** - * 产品标识 - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - - /** - * 设备名称 - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - - // TODO @芋艿:阿里云还有 sign 签名 - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java deleted file mode 100644 index a0c8ce92a..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * IoT 设备上行的抽象 Request DTO - * - * @author 芋道源码 - */ -@Data -public abstract class IotDeviceUpstreamAbstractReqDTO { - - /** - * 请求编号 - */ - private String requestId; - - /** - * 插件实例的进程编号 - */ - private String processId; - - /** - * 产品标识 - */ - @NotEmpty(message = "产品标识不能为空") - private String productKey; - /** - * 设备名称 - */ - @NotEmpty(message = "设备名称不能为空") - private String deviceName; - - /** - * 上报时间 - */ - @JsonSerialize(using = TimestampLocalDateTimeSerializer.class) // 解决 iot plugins 序列化 LocalDateTime 是数组,导致无法解析的问题 - private LocalDateTime reportTime; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java deleted file mode 100644 index 9125b5f24..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * IoT 插件实例心跳 Request DTO - * - * @author 芋道源码 - */ -@Data -public class IotPluginInstanceHeartbeatReqDTO { - - /** - * 请求编号 - */ - @NotEmpty(message = "请求编号不能为空") - private String processId; - - /** - * 插件包标识符 - */ - @NotEmpty(message = "插件包标识符不能为空") - private String pluginKey; - - /** - * 插件实例所在 IP - */ - @NotEmpty(message = "插件实例所在 IP 不能为空") - private String hostIp; - /** - * 插件实例的进程编号 - */ - @NotNull(message = "插件实例的进程编号不能为空") - private Integer downstreamPort; - - /** - * 是否在线 - */ - @NotNull(message = "是否在线不能为空") - private Boolean online; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java deleted file mode 100644 index cb946cd89..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO 芋艿:占位 - */ -package cn.iocoder.yudao.module.iot.api.device.dto; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java index 7da0c665b..65d049636 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java @@ -1,6 +1,4 @@ /** - * 占位 - * - * TODO 芋艿:后续删除 + * iot API 包,定义暴露给其它模块的 API */ package cn.iocoder.yudao.module.iot.api; diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java index d8f0cc60d..4f07ddfc1 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java @@ -7,16 +7,19 @@ package cn.iocoder.yudao.module.iot.enums; */ public class DictTypeConstants { + public static final String NET_TYPE = "iot_net_type"; + public static final String LOCATION_TYPE = "iot_location_type"; + public static final String CODEC_TYPE = "iot_codec_type"; + public static final String PRODUCT_STATUS = "iot_product_status"; public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; - public static final String NET_TYPE = "iot_net_type"; - public static final String PROTOCOL_TYPE = "iot_protocol_type"; - public static final String DATA_FORMAT = "iot_data_format"; - public static final String VALIDATE_TYPE = "iot_validate_type"; public static final String DEVICE_STATE = "iot_device_state"; - - public static final String IOT_DATA_BRIDGE_DIRECTION_ENUM = "iot_data_bridge_direction_enum"; - public static final String IOT_DATA_BRIDGE_TYPE_ENUM = "iot_data_bridge_type_enum"; + + public static final String ALERT_LEVEL = "iot_alert_level"; + + public static final String OTA_TASK_DEVICE_SCOPE = "iot_ota_task_device_scope"; + public static final String OTA_TASK_STATUS = "iot_ota_task_status"; + public static final String OTA_TASK_RECORD_STATUS = "iot_ota_task_record_status"; } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 230baca3f..d1cf60e20 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -14,6 +14,7 @@ public interface ErrorCodeConstants { ErrorCode PRODUCT_KEY_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在"); ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除"); ErrorCode PRODUCT_STATUS_NOT_ALLOW_THING_MODEL = new ErrorCode(1_050_001_003, "产品状是发布状态,不允许操作物模型"); + ErrorCode PRODUCT_DELETE_FAIL_HAS_DEVICE = new ErrorCode(1_050_001_004, "产品下存在设备,不允许删除"); // ========== 产品物模型 1-050-002-000 ============ ErrorCode THING_MODEL_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在"); @@ -30,7 +31,8 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在"); ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备"); ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); - ErrorCode DEVICE_DOWNSTREAM_FAILED = new ErrorCode(1_050_003_007, "执行失败,原因:{}"); + ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); + ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); // ========== 产品分类 1-050-004-000 ========== ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); @@ -39,37 +41,42 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); - // ========== 插件配置 1-050-006-000 ========== - ErrorCode PLUGIN_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "插件配置不存在"); - ErrorCode PLUGIN_INSTALL_FAILED = new ErrorCode(1_050_006_001, "插件安装失败"); - ErrorCode PLUGIN_INSTALL_FAILED_FILE_NAME_NOT_MATCH = new ErrorCode(1_050_006_002, "插件安装失败,文件名与原插件id不匹配"); - ErrorCode PLUGIN_CONFIG_DELETE_FAILED_RUNNING = new ErrorCode(1_050_006_003, "请先停止插件"); - ErrorCode PLUGIN_STATUS_INVALID = new ErrorCode(1_050_006_004, "插件状态无效"); - ErrorCode PLUGIN_CONFIG_KEY_DUPLICATE = new ErrorCode(1_050_006_005, "插件标识已存在"); - ErrorCode PLUGIN_START_FAILED = new ErrorCode(1_050_006_006, "插件启动失败"); - ErrorCode PLUGIN_STOP_FAILED = new ErrorCode(1_050_006_007, "插件停止失败"); - - // ========== 插件实例 1-050-007-000 ========== - - // ========== 固件相关 1-050-008-000 ========== + // ========== OTA 固件相关 1-050-008-000 ========== ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); ErrorCode OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE = new ErrorCode(1_050_008_001, "产品版本号重复"); - ErrorCode OTA_UPGRADE_TASK_NOT_EXISTS = new ErrorCode(1_050_008_100, "升级任务不存在"); - ErrorCode OTA_UPGRADE_TASK_NAME_DUPLICATE = new ErrorCode(1_050_008_101, "升级任务名称重复"); - ErrorCode OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY = new ErrorCode(1_050_008_102, "设备编号列表不能为空"); - ErrorCode OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY = new ErrorCode(1_050_008_103, "设备列表不能为空"); - ErrorCode OTA_UPGRADE_TASK_CANNOT_CANCEL = new ErrorCode(1_050_008_104, "升级任务不能取消"); + // ========== OTA 升级任务相关 1-050-008-100 ========== - ErrorCode OTA_UPGRADE_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_200, "升级记录不存在"); - ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); - ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_202, "升级记录不能重试"); + ErrorCode OTA_TASK_NOT_EXISTS = new ErrorCode(1_050_008_100, "升级任务不存在"); + ErrorCode OTA_TASK_CREATE_FAIL_NAME_DUPLICATE = new ErrorCode(1_050_008_101, "创建 OTA 任务失败,原因:任务名称重复"); + ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_FIRMWARE_EXISTS = new ErrorCode(1_050_008_102, + "创建 OTA 任务失败,原因:设备({})已经是该固件版本"); + ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_OTA_IN_PROCESS = new ErrorCode(1_050_008_102, + "创建 OTA 任务失败,原因:设备({})已经在升级中..."); + ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_EMPTY = new ErrorCode(1_050_008_103, "创建 OTA 任务失败,原因:没有可升级的设备"); + ErrorCode OTA_TASK_CANCEL_FAIL_STATUS_END = new ErrorCode(1_050_008_104, "取消 OTA 任务失败,原因:任务状态不是进行中"); - // ========== MQTT 通信相关 1-050-009-000 ========== - ErrorCode MQTT_TOPIC_ILLEGAL = new ErrorCode(1_050_009_000, "topic illegal"); + // ========== OTA 升级任务记录相关 1-050-008-200 ========== - // ========== IoT 数据桥梁 1-050-010-000 ========== - ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); + ErrorCode OTA_TASK_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_200, "升级记录不存在"); + ErrorCode OTA_TASK_RECORD_CANCEL_FAIL_STATUS_ERROR = new ErrorCode(1_050_008_201, "取消 OTA 升级记录失败,原因:记录状态不是进行中"); + ErrorCode OTA_TASK_RECORD_UPDATE_PROGRESS_FAIL_NO_EXISTS = new ErrorCode(1_050_008_202, "更新 OTA 升级记录进度失败,原因:该设备没有进行中的升级记录"); + + // ========== IoT 数据流转规则 1-050-010-000 ========== + ErrorCode DATA_RULE_NOT_EXISTS = new ErrorCode(1_050_010_000, "数据流转规则不存在"); + + // ========== IoT 数据流转目的 1-050-011-000 ========== + ErrorCode DATA_SINK_NOT_EXISTS = new ErrorCode(1_050_011_000, "数据桥梁不存在"); + ErrorCode DATA_SINK_DELETE_FAIL_USED_BY_RULE = new ErrorCode(1_050_011_001, "数据流转目的正在被数据流转规则使用,无法删除"); + + // ========== IoT 场景联动 1-050-012-000 ========== + ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_012_000, "场景联动不存在"); + + // ========== IoT 告警配置 1-050-013-000 ========== + ErrorCode ALERT_CONFIG_NOT_EXISTS = new ErrorCode(1_050_013_000, "IoT 告警配置不存在"); + + // ========== IoT 告警记录 1-050-014-000 ========== + ErrorCode ALERT_RECORD_NOT_EXISTS = new ErrorCode(1_050_014_000, "IoT 告警记录不存在"); } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java similarity index 58% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java index 3fdd53234..d70aea5c6 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/alert/IotAlertReceiveTypeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.rule; +package cn.iocoder.yudao.module.iot.enums.alert; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; @@ -7,21 +7,22 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * IoT 告警配置的接收方式枚举 + * IoT 告警的接收方式枚举 * * @author 芋道源码 */ @RequiredArgsConstructor @Getter -public enum IotAlertConfigReceiveTypeEnum implements ArrayValuable { +public enum IotAlertReceiveTypeEnum implements ArrayValuable { SMS(1), // 短信 MAIL(2), // 邮箱 - NOTIFY(3); // 通知 + NOTIFY(3); // 站内信 + // TODO 待实现(欢迎 pull request):webhook 4 private final Integer type; - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotAlertConfigReceiveTypeEnum::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotAlertReceiveTypeEnum::getType).toArray(Integer[]::new); @Override public Integer[] array() { diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java index 6de9359ba..e9dbe2f65 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java @@ -7,11 +7,12 @@ import lombok.RequiredArgsConstructor; /** * IoT 设备消息标识符枚举 */ +@Deprecated @Getter @RequiredArgsConstructor public enum IotDeviceMessageIdentifierEnum { - PROPERTY_GET("get"), // 下行 TODO 芋艿:【讨论】貌似这个“上行”更合理?device 主动拉取配置。和 IotDevicePropertyGetReqDTO 一样的配置 + PROPERTY_GET("get"), // 下行 PROPERTY_SET("set"), // 下行 PROPERTY_REPORT("report"), // 上行 diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java index 0354157ed..9131210ab 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java @@ -9,6 +9,7 @@ import java.util.Arrays; /** * IoT 设备消息类型枚举 */ +@Deprecated @Getter @RequiredArgsConstructor public enum IotDeviceMessageTypeEnum implements ArrayValuable { diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskDeviceScopeEnum.java similarity index 74% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskDeviceScopeEnum.java index 6dccbb041..d9ec270ed 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskDeviceScopeEnum.java @@ -7,18 +7,19 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * IoT OTA 升级任务的范围枚举 + * IoT OTA 升级任务的设备范围枚举 * * @author haohao */ @RequiredArgsConstructor @Getter -public enum IotOtaUpgradeTaskScopeEnum implements ArrayValuable { +public enum IotOtaTaskDeviceScopeEnum implements ArrayValuable { ALL(1), // 全部设备:只包括当前产品下的设备,不包括未来创建的设备 SELECT(2); // 指定设备 - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeTaskScopeEnum::getScope).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotOtaTaskDeviceScopeEnum::getScope).toArray(Integer[]::new); /** * 范围 diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskRecordStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskRecordStatusEnum.java new file mode 100644 index 000000000..0f95eb79c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskRecordStatusEnum.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * IoT OTA 升级任务记录的状态枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaTaskRecordStatusEnum implements ArrayValuable { + + PENDING(0), // 待推送 + PUSHED(10), // 已推送 + UPGRADING(20), // 升级中 + SUCCESS(30), // 升级成功 + FAILURE(40), // 升级失败 + CANCELED(50),; // 升级取消 + + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotOtaTaskRecordStatusEnum::getStatus).toArray(Integer[]::new); + + public static final Set IN_PROCESS_STATUSES = SetUtils.asSet( + PENDING.getStatus(), + PUSHED.getStatus(), + UPGRADING.getStatus()); + + public static final List PRIORITY_STATUSES = Arrays.asList( + SUCCESS.getStatus(), + PENDING.getStatus(), PUSHED.getStatus(), UPGRADING.getStatus(), + FAILURE.getStatus(), CANCELED.getStatus()); + + /** + * 状态 + */ + private final Integer status; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static IotOtaTaskRecordStatusEnum of(Integer status) { + return ArrayUtil.firstMatch(o -> o.getStatus().equals(status), values()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskStatusEnum.java similarity index 51% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskStatusEnum.java index e809a7e5b..fc16e55a8 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaTaskStatusEnum.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.enums.ota; - import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -8,25 +7,23 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * IoT OTA 升级记录的范围枚举 + * IoT OTA 升级任务的状态 * - * @author haohao + * @author 芋道源码 */ @RequiredArgsConstructor @Getter -public enum IotOtaUpgradeRecordStatusEnum implements ArrayValuable { +public enum IotOtaTaskStatusEnum implements ArrayValuable { - PENDING(0), // 待推送 - PUSHED(10), // 已推送 - UPGRADING(20), // 升级中 - SUCCESS(30), // 升级成功 - FAILURE(40), // 升级失败 - CANCELED(50),; // 已取消 + IN_PROGRESS(10), // 进行中(升级中) + END(20), // 已结束(包括全部成功、部分成功) + CANCELED(30),; // 已取消(一般是主动取消任务) - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeRecordStatusEnum::getStatus).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()) + .map(IotOtaTaskStatusEnum::getStatus).toArray(Integer[]::new); /** - * 范围 + * 状态 */ private final Integer status; diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java deleted file mode 100644 index 78af16cb2..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.ota; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT OTA 升级任务的范围枚举 - * - * @author haohao - */ -@RequiredArgsConstructor -@Getter -public enum IotOtaUpgradeTaskStatusEnum implements ArrayValuable { - - IN_PROGRESS(10), // 进行中:升级中 - COMPLETED(20), // 已完成:已结束,全部升级完成 - INCOMPLETE(21), // 未完成:已结束,部分升级完成 - CANCELED(30),; // 已取消:一般是主动取消任务 - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeTaskStatusEnum::getStatus).toArray(Integer[]::new); - - /** - * 范围 - */ - private final Integer status; - - @Override - public Integer[] array() { - return ARRAYS; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java deleted file mode 100644 index b6ef4f0cc..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.plugin; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT 部署方式枚举 - * - * @author haohao - */ -@RequiredArgsConstructor -@Getter -public enum IotPluginDeployTypeEnum implements ArrayValuable { - - JAR(0, "JAR 部署"), - STANDALONE(1, "独立部署"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginDeployTypeEnum::getDeployType).toArray(Integer[]::new); - - /** - * 部署方式 - */ - private final Integer deployType; - /** - * 部署方式名 - */ - private final String name; - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java deleted file mode 100644 index 7e3fa657e..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.plugin; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT 插件状态枚举 - * - * @author haohao - */ -@RequiredArgsConstructor -@Getter -public enum IotPluginStatusEnum implements ArrayValuable { - - STOPPED(0, "停止"), - RUNNING(1, "运行"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginStatusEnum::getStatus).toArray(Integer[]::new); - - /** - * 状态 - */ - private final Integer status; - /** - * 状态名 - */ - private final String name; - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java deleted file mode 100644 index ec0b72f9f..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.plugin; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Arrays; - -/** - * IoT 插件类型枚举 - * - * @author haohao - */ -@AllArgsConstructor -@Getter -public enum IotPluginTypeEnum implements ArrayValuable { - - NORMAL(0, "普通插件"), - DEVICE(1, "设备插件"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginTypeEnum::getType).toArray(Integer[]::new); - - /** - * 类型 - */ - private final Integer type; - /** - * 类型名 - */ - private final String name; - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotDataFormatEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotDataFormatEnum.java deleted file mode 100644 index 0cfe1c9f4..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotDataFormatEnum.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.product; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Arrays; - -/** - * 产品数据格式枚举类 - * - * @author ahh - * @see 阿里云 - 什么是消息解析 - */ -@AllArgsConstructor -@Getter -public enum IotDataFormatEnum implements ArrayValuable { - - JSON(0, "标准数据格式(JSON)"), - CUSTOMIZE(1, "透传/自定义"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataFormatEnum::getType).toArray(Integer[]::new); - - /** - * 类型 - */ - private final Integer type; - /** - * 描述 - */ - private final String description; - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java index 2a15d16a4..11989ec71 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java @@ -7,18 +7,19 @@ import lombok.Getter; import java.util.Arrays; /** - * IoT 数据校验级别枚举类 + * IoT 定位方式枚举类 * - * @author ahh + * @author alwayssuper */ @AllArgsConstructor @Getter -public enum IotValidateTypeEnum implements ArrayValuable { +public enum IotLocationTypeEnum implements ArrayValuable { - WEAK(0, "弱校验"), - NONE(1, "免校验"); + IP(1, "IP 定位"), + DEVICE(2, "设备上报"), + MANUAL(3, "手动定位"); - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotValidateTypeEnum::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new); /** * 类型 diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java deleted file mode 100644 index d24dea92e..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.product; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Arrays; - -/** - * IoT 接入网关协议枚举类 - * - * @author ahh - */ -@AllArgsConstructor -@Getter -public enum IotProtocolTypeEnum implements ArrayValuable { - - CUSTOM(0, "自定义"), - MODBUS(1, "Modbus"), - OPC_UA(2, "OPC UA"), - ZIGBEE(3, "ZigBee"), - BLE(4, "BLE"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(Integer[]::new); - - /** - * 类型 - */ - private final Integer type; - /** - * 描述 - */ - private final String description; - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java deleted file mode 100644 index 78fc8452e..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.rule; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT 数据桥接的类型枚举 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Getter -public enum IotDataBridgeTypeEnum implements ArrayValuable { - - HTTP(1, "HTTP"), - TCP(2, "TCP"), - WEBSOCKET(3, "WEBSOCKET"), - - MQTT(10, "MQTT"), - - DATABASE(20, "DATABASE"), - REDIS_STREAM(21, "REDIS_STREAM"), - - ROCKETMQ(30, "ROCKETMQ"), - RABBITMQ(31, "RABBITMQ"), - KAFKA(32, "KAFKA"); - - private final Integer type; - - private final String name; - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataBridgeTypeEnum::getType).toArray(Integer[]::new); - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java new file mode 100644 index 000000000..45a557db6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 数据目的的类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotDataSinkTypeEnum implements ArrayValuable { + + HTTP(1, "HTTP"), + TCP(2, "TCP"), // TODO @puhui999:待实现; + WEBSOCKET(3, "WebSocket"), // TODO @puhui999:待实现; + + MQTT(10, "MQTT"), // TODO 待实现; + + DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。 + REDIS(21, "Redis"), + + ROCKETMQ(30, "RocketMQ"), + RABBITMQ(31, "RabbitMQ"), + KAFKA(32, "Kafka"); + + private final Integer type; + + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataSinkTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRedisDataStructureEnum.java similarity index 53% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRedisDataStructureEnum.java index a9d445fd2..4195b0843 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRedisDataStructureEnum.java @@ -7,20 +7,26 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * IoT 数据桥接的方向枚举 + * IoT Redis 数据结构类型枚举 * - * @author 芋道源码 + * @author HUIHUI */ @RequiredArgsConstructor @Getter -public enum IotDataBridgeDirectionEnum implements ArrayValuable { +public enum IotRedisDataStructureEnum implements ArrayValuable { - INPUT(1), // 输入 - OUTPUT(2); // 输出 + STREAM(1, "Stream"), + HASH(2, "Hash"), + LIST(3, "List"), + SET(4, "Set"), + ZSET(5, "ZSet"), + STRING(6, "String"); private final Integer type; - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataBridgeDirectionEnum::getType).toArray(Integer[]::new); + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRedisDataStructureEnum::getType).toArray(Integer[]::new); @Override public Integer[] array() { diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java deleted file mode 100644 index 2bdf7d0ed..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.rule; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT 规则场景的触发类型枚举 - * - * 设备触发,定时触发 - */ -@RequiredArgsConstructor -@Getter -public enum IotRuleSceneActionTypeEnum implements ArrayValuable { - - DEVICE_CONTROL(1), // 设备执行 - ALERT(2), // 告警执行 - DATA_BRIDGE(3); // 桥接执行 - - private final Integer type; - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneActionTypeEnum::getType).toArray(Integer[]::new); - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java deleted file mode 100644 index a420a21d5..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.rule; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -/** - * IoT 场景流转的触发类型枚举 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -@Getter -public enum IotRuleSceneTriggerTypeEnum implements ArrayValuable { - - DEVICE(1), // 设备触发 - TIMER(2); // 定时触发 - - private final Integer type; - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerTypeEnum::getType).toArray(Integer[]::new); - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java new file mode 100644 index 000000000..ad3b4cf17 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleActionTypeEnum.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 规则场景的触发类型枚举 + * + * 设备触发,定时触发 + */ +@RequiredArgsConstructor +@Getter +public enum IotSceneRuleActionTypeEnum implements ArrayValuable { + + /** + * 设备属性设置 + * + * 对应 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} + */ + DEVICE_PROPERTY_SET(1), + /** + * 设备服务调用 + * + * 对应 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} + */ + DEVICE_SERVICE_INVOKE(2), + + /** + * 告警触发 + */ + ALERT_TRIGGER(100), + /** + * 告警恢复 + */ + ALERT_RECOVER(101), + + ; + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotSceneRuleActionTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionOperatorEnum.java similarity index 57% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionOperatorEnum.java index 5ed90ccae..9bf90cff6 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionOperatorEnum.java @@ -8,13 +8,13 @@ import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * IoT 场景触发条件参数的操作符枚举 + * IoT 场景触发条件的操作符枚举 * * @author 芋道源码 */ @RequiredArgsConstructor @Getter -public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayValuable { +public enum IotSceneRuleConditionOperatorEnum implements ArrayValuable { EQUALS("=", "#source == #value"), NOT_EQUALS("!=", "!(#source == #value)"), @@ -32,12 +32,28 @@ public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayVa NOT_BETWEEN("not between", "(#source < #values.get(0)) || (#source > #values.get(1))"), LIKE("like", "#source.contains(#value)"), // 字符串匹配 - NOT_NULL("not null", "#source != null && #source.length() > 0"); // 非空 + NOT_NULL("not null", "#source != null && #source.length() > 0"), // 非空 + + // ========== 特殊:不放在字典里 ========== + + // TODO @puhui999:@芋艿:需要测试下 + DATE_TIME_GREATER_THAN("date_time_>", "#source > #value"), // 在时间之后:时间戳 + DATE_TIME_LESS_THAN("date_time_<", "#source < #value"), // 在时间之前:时间戳 + DATE_TIME_BETWEEN("date_time_between", // 在时间之间:时间戳 + "(#source >= #values.get(0)) && (#source <= #values.get(1))"), + + // TODO @puhui999:@芋艿:需要测试下 + TIME_GREATER_THAN("time_>", "#source.isAfter(#value)"), // 在当日时间之后:HH:mm:ss + TIME_LESS_THAN("time_<", "#source.isBefore(#value)"), // 在当日时间之前:HH:mm:ss + TIME_BETWEEN("time_between", // 在当日时间之间:HH:mm:ss + "(#source >= #values.get(0)) && (#source <= #values.get(1))"), + + ; private final String operator; private final String springExpression; - public static final String[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerConditionParameterOperatorEnum::getOperator).toArray(String[]::new); + public static final String[] ARRAYS = Arrays.stream(values()).map(IotSceneRuleConditionOperatorEnum::getOperator).toArray(String[]::new); /** * Spring 表达式 - 原始值 @@ -50,9 +66,9 @@ public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayVa /** * Spring 表达式 - 目标值数组 */ - public static final String SPRING_EXPRESSION_VALUE_List = "values"; + public static final String SPRING_EXPRESSION_VALUE_LIST = "values"; - public static IotRuleSceneTriggerConditionParameterOperatorEnum operatorOf(String operator) { + public static IotSceneRuleConditionOperatorEnum operatorOf(String operator) { return ArrayUtil.firstMatch(item -> item.getOperator().equals(operator), values()); } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionTypeEnum.java new file mode 100644 index 000000000..81d7e6e1f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionTypeEnum.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 条件类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotSceneRuleConditionTypeEnum implements ArrayValuable { + + DEVICE_STATE(1, "设备状态"), + DEVICE_PROPERTY(2, "设备属性"), + + CURRENT_TIME(100, "当前时间"), + + ; + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotSceneRuleConditionTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static IotSceneRuleConditionTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java new file mode 100644 index 000000000..bfc84c9f6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 场景流转的触发类型枚举 + * + * 为什么不直接使用 IotDeviceMessageMethodEnum 呢? + * 原因是,物模型属性上报,存在批量上报的情况,不只对应一个 method!!! + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable { + + // TODO @芋艿:后续“对应”部分,要 @下,等包结构梳理完; + /** + * 设备上下线变更 + * + * 对应 IotDeviceMessageMethodEnum.STATE_UPDATE + */ + DEVICE_STATE_UPDATE(1), + /** + * 物模型属性上报 + * + * 对应 IotDeviceMessageMethodEnum.DEVICE_PROPERTY_POST + */ + DEVICE_PROPERTY_POST(2), + /** + * 设备事件上报 + * + * 对应 IotDeviceMessageMethodEnum.DEVICE_EVENT_POST + */ + DEVICE_EVENT_POST(3), + /** + * 设备服务调用 + * + * 对应 IotDeviceMessageMethodEnum.DEVICE_SERVICE_INVOKE + */ + DEVICE_SERVICE_INVOKE(4), + + /** + * 定时触发 + */ + TIMER(100) + + ; + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotSceneRuleTriggerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static IotSceneRuleTriggerTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java deleted file mode 100644 index 9f54d60e8..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package cn.iocoder.yudao.module.iot; - -import cn.hutool.script.ScriptUtil; -import javax.script.Bindings; -import javax.script.ScriptEngine; -import javax.script.ScriptException; - -/** - * TODO 芋艿:测试脚本的接入 - */ -public class ScriptTest { - - public static void main2(String[] args) { - // 创建一个 Groovy 脚本引擎 - ScriptEngine engine = ScriptUtil.createGroovyEngine(); - - // 创建绑定参数 - Bindings bindings = engine.createBindings(); - bindings.put("name", "Alice"); - bindings.put("age", 30); - - // 定义一个稍微复杂的 Groovy 脚本 - String script = "def greeting = 'Hello, ' + name + '!';\n" + - "def ageInFiveYears = age + 5;\n" + - "def message = greeting + ' In five years, you will be ' + ageInFiveYears + ' years old.';\n" + - "return message.toUpperCase();\n"; - - try { - // 执行脚本并获取结果 - Object result = engine.eval(script, bindings); - System.out.println(result); // 输出: HELLO, ALICE! IN FIVE YEARS, YOU WILL BE 35 YEARS OLD. - } catch (ScriptException e) { - e.printStackTrace(); - } - } - - public static void main(String[] args) { - // 创建一个 JavaScript 脚本引擎 - ScriptEngine jsEngine = ScriptUtil.createJsEngine(); - - // 创建绑定参数 - Bindings jsBindings = jsEngine.createBindings(); - jsBindings.put("name", "Bob"); - jsBindings.put("age", 25); - - // 定义一个简单的 JavaScript 脚本 - String jsScript = "var greeting = 'Hello, ' + name + '!';\n" + - "var ageInTenYears = age + 10;\n" + - "var message = greeting + ' In ten years, you will be ' + ageInTenYears + ' years old.';\n" + - "message.toUpperCase();\n"; - - try { - // 执行脚本并获取结果 - Object jsResult = jsEngine.eval(jsScript, jsBindings); - System.out.println(jsResult); // 输出: HELLO, BOB! IN TEN YEARS, YOU WILL BE 35 YEARS OLD. - } catch (ScriptException e) { - e.printStackTrace(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java deleted file mode 100644 index 25faa1a6b..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; -import jakarta.annotation.Resource; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RestController; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -/** - * * 设备数据 Upstream 上行 API 实现类 - */ -@RestController -@Validated -public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { - - @Resource - private IotDeviceUpstreamService deviceUpstreamService; - @Resource - private IotPluginInstanceService pluginInstanceService; - - // ========== 设备相关 ========== - - @Override - public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { - deviceUpstreamService.updateDeviceState(updateReqDTO); - return success(true); - } - - @Override - public CommonResult reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { - deviceUpstreamService.reportDeviceProperty(reportReqDTO); - return success(true); - } - - @Override - public CommonResult reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { - deviceUpstreamService.reportDeviceEvent(reportReqDTO); - return success(true); - } - - @Override - public CommonResult registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { - deviceUpstreamService.registerDevice(registerReqDTO); - return success(true); - } - - @Override - public CommonResult registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { - deviceUpstreamService.registerSubDevice(registerReqDTO); - return success(true); - } - - @Override - public CommonResult addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { - deviceUpstreamService.addDeviceTopology(addReqDTO); - return success(true); - } - - @Override - public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { - boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); - return success(result); - } - - // ========== 插件相关 ========== - - @Override - public CommonResult heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { - pluginInstanceService.heartbeatPluginInstance(heartbeatReqDTO); - return success(true); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java deleted file mode 100644 index 07852180d..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 占位 - * - * TODO 芋艿:后续删除 - */ -package cn.iocoder.yudao.module.iot.api; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http deleted file mode 100644 index c1190cec1..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http +++ /dev/null @@ -1,75 +0,0 @@ -### 请求 /iot/device/downstream 接口(服务调用) => 成功 -POST {{baseUrl}}/iot/device/downstream -Content-Type: application/json -tenant-id: {{adminTenentId}} -Authorization: Bearer {{token}} - -{ - "id": 25, - "type": "service", - "identifier": "temperature", - "data": { - "xx": "yy" - } -} - -### 请求 /iot/device/downstream 接口(属性设置) => 成功 -POST {{baseUrl}}/iot/device/downstream -Content-Type: application/json -tenant-id: {{adminTenentId}} -Authorization: Bearer {{token}} - -{ - "id": 25, - "type": "property", - "identifier": "set", - "data": { - "xx": "yy" - } -} - -### 请求 /iot/device/downstream 接口(属性获取) => 成功 -POST {{baseUrl}}/iot/device/downstream -Content-Type: application/json -tenant-id: {{adminTenentId}} -Authorization: Bearer {{token}} - -{ - "id": 25, - "type": "property", - "identifier": "get", - "data": ["xx", "yy"] -} - -### 请求 /iot/device/downstream 接口(配置设置) => 成功 -POST {{baseUrl}}/iot/device/downstream -Content-Type: application/json -tenant-id: {{adminTenentId}} -Authorization: Bearer {{token}} - -{ - "id": 25, - "type": "config", - "identifier": "set" -} - -### 请求 /iot/device/downstream 接口(OTA 升级) => 成功 -POST {{baseUrl}}/iot/device/downstream -Content-Type: application/json -tenant-id: {{adminTenentId}} -Authorization: Bearer {{token}} - -{ - "id": 25, - "type": "ota", - "identifier": "upgrade", - "data": { - "firmwareId": 1, - "version": "1.0.0", - "signMethod": "MD5", - "fileSign": "d41d8cd98f00b204e9800998ecf8427e", - "fileSize": 1024, - "fileUrl": "http://example.com/firmware.bin", - "information": "{\"desc\":\"升级到最新版本\"}" - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java deleted file mode 100644 index 81d1bff94..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogRespVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; -import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT 设备日志") -@RestController -@RequestMapping("/iot/device/log") -@Validated -public class IotDeviceLogController { - - @Resource - private IotDeviceLogService deviceLogService; - - @GetMapping("/page") - @Operation(summary = "获得设备日志分页") - @PreAuthorize("@ss.hasPermission('iot:device:log-query')") - public CommonResult> getDeviceLogPage(@Valid IotDeviceLogPageReqVO pageReqVO) { - PageResult pageResult = deviceLogService.getDeviceLogPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotDeviceLogRespVO.class)); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java deleted file mode 100644 index 47bf325dd..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; -import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - -@Tag(name = "管理后台 - IoT 设备属性") -@RestController -@RequestMapping("/iot/device/property") -@Validated -public class IotDevicePropertyController { - - @Resource - private IotDevicePropertyService devicePropertyService; - @Resource - private IotThingModelService thingModelService; - @Resource - private IotDeviceService deviceService; - - @GetMapping("/latest") - @Operation(summary = "获取设备属性最新属性") - @Parameters({ - @Parameter(name = "deviceId", description = "设备编号", required = true), - @Parameter(name = "identifier", description = "标识符"), - @Parameter(name = "name", description = "名称") - }) - @PreAuthorize("@ss.hasPermission('iot:device:property-query')") - public CommonResult> getLatestDeviceProperties( - @RequestParam("deviceId") Long deviceId, - @RequestParam(value = "identifier", required = false) String identifier, - @RequestParam(value = "name", required = false) String name) { - Map properties = devicePropertyService.getLatestDeviceProperties(deviceId); - - // 拼接数据 - IotDeviceDO device = deviceService.getDevice(deviceId); - Assert.notNull(device, "设备不存在"); - List thingModels = thingModelService.getThingModelListByProductId(device.getProductId()); - return success(convertList(properties.entrySet(), entry -> { - IotThingModelDO thingModel = CollUtil.findOne(thingModels, - item -> item.getIdentifier().equals(entry.getKey())); - if (thingModel == null || thingModel.getProperty() == null) { - return null; - } - if (StrUtil.isNotEmpty(identifier) && !StrUtil.contains(thingModel.getIdentifier(), identifier)) { - return null; - } - if (StrUtil.isNotEmpty(name) && !StrUtil.contains(thingModel.getName(), name)) { - return null; - } - // 构建对象 - IotDevicePropertyDO property = entry.getValue(); - return new IotDevicePropertyRespVO().setProperty(thingModel.getProperty()) - .setValue(property.getValue()).setUpdateTime(LocalDateTimeUtil.toEpochMilli(property.getUpdateTime())); - })); - } - - @GetMapping("/history-page") - @Operation(summary = "获取设备属性历史数据") - @PreAuthorize("@ss.hasPermission('iot:device:property-query')") - public CommonResult> getHistoryDevicePropertyPage( - @Valid IotDevicePropertyHistoryPageReqVO pageReqVO) { - Assert.notEmpty(pageReqVO.getIdentifier(), "标识符不能为空"); - return success(devicePropertyService.getHistoryDevicePropertyPage(pageReqVO)); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java deleted file mode 100644 index eefaeffeb..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.control; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备下行 Request VO") // 服务调用、属性设置、属性获取等 -@Data -public class IotDeviceDownstreamReqVO { - - @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") - @NotNull(message = "设备编号不能为空") - private Long id; - - @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") - @NotEmpty(message = "消息类型不能为空") - @InEnum(IotDeviceMessageTypeEnum.class) - private String type; - - @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") - @NotEmpty(message = "标识符不能为空") - private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举类 - - @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) - private Object data; // 例如说:服务调用的 params、属性设置的 properties - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java deleted file mode 100644 index 778d75bba..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.control; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备上行 Request VO") // 属性上报、事件上报、状态变更等 -@Data -public class IotDeviceUpstreamReqVO { - - @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") - @NotNull(message = "设备编号不能为空") - private Long id; - - @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") - @NotEmpty(message = "消息类型不能为空") - @InEnum(IotDeviceMessageTypeEnum.class) - private String type; - - @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") - @NotEmpty(message = "标识符不能为空") - private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举类 - - @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) - private Object data; // 例如说:属性上报的 properties、事件上报的 params - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java deleted file mode 100644 index fcf36994f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备日志分页查询 Request VO") -@Data -public class IotDeviceLogPageReqVO extends PageParam { - - @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123") - @NotEmpty(message = "设备标识不能为空") - private String deviceKey; - - @Schema(description = "消息类型", example = "property") - private String type; // 参见 IotDeviceMessageTypeEnum 枚举,精准匹配 - - @Schema(description = "标识符", example = "temperature") - private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举,模糊匹配 - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java deleted file mode 100644 index 6e6639ede..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -@Schema(description = "管理后台 - IoT 设备日志 Response VO") -@Data -public class IotDeviceLogRespVO { - - @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") - private String id; - - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "product123") - private String productKey; - - @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123") - private String deviceKey; - - @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") - private String type; - - @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") - private String identifier; - - @Schema(description = "日志内容", requiredMode = Schema.RequiredMode.REQUIRED) - private String content; - - @Schema(description = "上报时间", requiredMode = Schema.RequiredMode.REQUIRED) - private LocalDateTime reportTime; - - @Schema(description = "记录时间戳", requiredMode = Schema.RequiredMode.REQUIRED) - private LocalDateTime ts; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java deleted file mode 100644 index 751b4d57e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; - -import cn.idev.excel.annotation.ExcelIgnoreUnannotated; -import cn.idev.excel.annotation.ExcelProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备 MQTT 连接参数 Response VO") -@Data -@ExcelIgnoreUnannotated -public class IotDeviceMqttConnectionParamsRespVO { - - @Schema(description = "MQTT 客户端 ID", example = "24602") - @ExcelProperty("MQTT 客户端 ID") - private String mqttClientId; - - @Schema(description = "MQTT 用户名", example = "芋艿") - @ExcelProperty("MQTT 用户名") - private String mqttUsername; - - @Schema(description = "MQTT 密码") - @ExcelProperty("MQTT 密码") - private String mqttPassword; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java deleted file mode 100644 index f6bc526ac..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordRespVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; -import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT OTA 升级记录") -@RestController -@RequestMapping("/iot/ota-upgrade-record") -@Validated -public class IotOtaUpgradeRecordController { - - @Resource - private IotOtaUpgradeRecordService upgradeRecordService; - - @GetMapping("/get-statistics") - @Operation(summary = "固件升级设备统计") - @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") - @Parameter(name = "firmwareId", description = "固件编号", required = true, example = "1024") - public CommonResult> getOtaUpgradeRecordStatistics(@RequestParam(value = "firmwareId") Long firmwareId) { - return success(upgradeRecordService.getOtaUpgradeRecordStatistics(firmwareId)); - } - - @GetMapping("/get-count") - @Operation(summary = "获得升级记录分页 tab 数量") - @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") - public CommonResult> getOtaUpgradeRecordCount( - @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { - return success(upgradeRecordService.getOtaUpgradeRecordCount(pageReqVO)); - } - - @GetMapping("/page") - @Operation(summary = "获得升级记录分页") - @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") - public CommonResult> getUpgradeRecordPage( - @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { - PageResult pageResult = upgradeRecordService.getUpgradeRecordPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotOtaUpgradeRecordRespVO.class)); - } - - @GetMapping("/get") - @Operation(summary = "获得升级记录") - @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") - @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") - public CommonResult getUpgradeRecord(@RequestParam("id") Long id) { - IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordService.getUpgradeRecord(id); - return success(BeanUtils.toBean(upgradeRecord, IotOtaUpgradeRecordRespVO.class)); - } - - @PutMapping("/retry") - @Operation(summary = "重试升级记录") - @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:retry')") - @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") - public CommonResult retryUpgradeRecord(@RequestParam("id") Long id) { - upgradeRecordService.retryUpgradeRecord(id); - return success(true); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java deleted file mode 100644 index e248e8027..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; -import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeTaskService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT OTA 升级任务") -@RestController -@RequestMapping("/iot/ota-upgrade-task") -@Validated -public class IotOtaUpgradeTaskController { - - @Resource - private IotOtaUpgradeTaskService upgradeTaskService; - - @PostMapping("/create") - @Operation(summary = "创建升级任务") - @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:create')") - public CommonResult createUpgradeTask(@Valid @RequestBody IotOtaUpgradeTaskSaveReqVO createReqVO) { - return success(upgradeTaskService.createUpgradeTask(createReqVO)); - } - - @PostMapping("/cancel") - @Operation(summary = "取消升级任务") - @Parameter(name = "id", description = "升级任务编号", required = true) - @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:cancel')") - public CommonResult cancelUpgradeTask(@RequestParam("id") Long id) { - upgradeTaskService.cancelUpgradeTask(id); - return success(true); - } - - @GetMapping("/page") - @Operation(summary = "获得升级任务分页") - @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')") - public CommonResult> getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO) { - PageResult pageResult = upgradeTaskService.getUpgradeTaskPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotOtaUpgradeTaskRespVO.class)); - } - - @GetMapping("/get") - @Operation(summary = "获得升级任务") - @Parameter(name = "id", description = "升级任务编号", required = true, example = "1024") - @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')") - public CommonResult getUpgradeTask(@RequestParam("id") Long id) { - IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(id); - return success(BeanUtils.toBean(upgradeTask, IotOtaUpgradeTaskRespVO.class)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java deleted file mode 100644 index 50c2ece15..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Schema(description = "管理后台 - IoT OTA 固件创建 Request VO") -@Data -public class IotOtaFirmwareCreateReqVO { - - @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") - @NotEmpty(message = "固件名称不能为空") - private String name; - - @Schema(description = "固件描述", example = "某品牌型号固件,测试用") - private String description; - - @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") - @NotEmpty(message = "版本号不能为空") - private String version; - - @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") - @NotNull(message = "产品编号不能为空") - private String productId; - - @Schema(description = "签名方式", example = "MD5") - // TODO @li:是不是必传哈 - private String signMethod; - - @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip") - @NotEmpty(message = "固件文件 URL 不能为空") - private String fileUrl; - - @Schema(description = "自定义信息,建议使用 JSON 格式", example = "{\"key1\":\"value1\",\"key2\":\"value2\"}") - private String information; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java deleted file mode 100644 index 735618781..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; - -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import com.fhs.core.trans.anno.Trans; -import com.fhs.core.trans.constant.TransType; -import com.fhs.core.trans.vo.VO; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Data -@Schema(description = "管理后台 - IoT OTA 固件 Response VO") -public class IotOtaFirmwareRespVO implements VO { - - /** - * 固件编号 - */ - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") - private Long id; - /** - * 固件名称 - */ - @Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件") - private String name; - /** - * 固件描述 - */ - @Schema(description = "固件描述") - private String description; - /** - * 版本号 - */ - @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") - private String version; - - /** - * 产品编号 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} - */ - @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") - @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"}) - private String productId; - /** - * 产品标识 - *

- * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} - */ - @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key") - private String productKey; - /** - * 产品名称 - */ - @Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品") - private String productName; - /** - * 签名方式 - *

- * 例如说:MD5、SHA256 - */ - @Schema(description = "签名方式", example = "MD5") - private String signMethod; - /** - * 固件文件签名 - */ - @Schema(description = "固件文件签名", example = "1024") - private String fileSign; - /** - * 固件文件大小 - */ - @Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024") - private Long fileSize; - /** - * 固件文件 URL - */ - @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn") - private String fileUrl; - /** - * 自定义信息,建议使用 JSON 格式 - */ - @Schema(description = "自定义信息,建议使用 JSON 格式") - private String information; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java deleted file mode 100644 index 2b21b3079..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Data -@Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO") -public class IotOtaUpgradeRecordPageReqVO extends PageParam { - - // TODO @li:已经有注解,不用重复注释 - /** - * 升级任务编号字段。 - *

- * 该字段用于标识升级任务的唯一编号,不能为空。 - */ - @Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024") - @NotNull(message = "升级任务编号不能为空") - private Long taskId; - - /** - * 设备标识字段。 - *

- * 该字段用于标识设备的名称,通常用于区分不同的设备。 - */ - @Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1") - private String deviceName; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java deleted file mode 100644 index db6737feb..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java +++ /dev/null @@ -1,109 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record; - -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; -import com.fhs.core.trans.anno.Trans; -import com.fhs.core.trans.constant.TransType; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Data -@Schema(description = "管理后台 - IoT OTA 升级记录 Response VO") -public class IotOtaUpgradeRecordRespVO { - - /** - * 升级记录编号 - */ - @Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024") - private Long id; - /** - * 固件编号 - *

- * 关联 {@link IotOtaFirmwareDO#getId()} - */ - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") - @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"}) - private Long firmwareId; - /** - * 固件版本 - */ - @Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0") - private String firmwareVersion; - /** - * 任务编号 - *

- * 关联 {@link IotOtaUpgradeTaskDO#getId()} - */ - @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") - private Long taskId; - /** - * 产品标识 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} - */ - @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot") - private String productKey; - /** - * 设备名称 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} - */ - @Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot") - private String deviceName; - /** - * 设备编号 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} - */ - @Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024") - private String deviceId; - /** - * 来源的固件编号 - *

- * 关联 {@link IotDeviceDO#getFirmwareId()} - */ - @Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024") - @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"}) - private Long fromFirmwareId; - /** - * 来源的固件版本 - */ - @Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0") - private String fromFirmwareVersion; - /** - * 升级状态 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} - */ - @Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"}) - private Integer status; - /** - * 升级进度,百分比 - */ - @Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10") - private Integer progress; - /** - * 升级进度描述 - *

- * 注意,只记录设备最后一次的升级进度描述 - * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 - */ - @Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10") - private String description; - /** - * 升级开始时间 - */ - @Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") - private LocalDateTime startTime; - /** - * 升级结束时间 - */ - @Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") - private LocalDateTime endTime; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java deleted file mode 100644 index d2b1926aa..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Data -@Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO") -public class IotOtaUpgradeTaskPageReqVO extends PageParam { - - /** - * 任务名称字段,用于描述任务的名称 - */ - @Schema(description = "任务名称", example = "升级任务") - private String name; - - /** - * 固件编号字段,用于唯一标识固件,不能为空 - */ - @NotNull(message = "固件编号不能为空") - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") - private Long firmwareId; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java deleted file mode 100644 index dbc29618f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; - -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import com.fhs.core.trans.vo.VO; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; -import java.util.List; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Data -@Schema(description = "管理后台 - IoT OTA 升级任务 Response VO") -public class IotOtaUpgradeTaskRespVO implements VO { - - /** - * 任务编号 - */ - @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") - private Long id; - /** - * 任务名称 - */ - @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") - private String name; - /** - * 任务描述 - */ - @Schema(description = "任务描述", example = "升级任务") - private String description; - /** - * 固件编号 - *

- * 关联 {@link IotOtaFirmwareDO#getId()} - */ - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") - private Long firmwareId; - /** - * 任务状态 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} - */ - @Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"}) - private Integer status; - /** - * 任务状态名称 - */ - @Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中") - private String statusName; - /** - * 升级范围 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} - */ - @Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"}) - private Integer scope; - /** - * 设备数量 - */ - @Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024") - private Long deviceCount; - /** - * 选中的设备编号数组 - *

- * 关联 {@link IotDeviceDO#getId()} - */ - @Schema(description = "选中的设备编号数组", example = "1024") - private List deviceIds; - /** - * 选中的设备名字数组 - *

- * 关联 {@link IotDeviceDO#getDeviceName()} - */ - @Schema(description = "选中的设备名字数组", example = "1024") - private List deviceNames; - /** - * 创建时间 - */ - @Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") - private LocalDateTime createTime; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java deleted file mode 100644 index 0ace17a04..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import java.util.List; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -@Data -@Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO") -public class IotOtaUpgradeTaskSaveReqVO { - - // TODO @li:已经有注解,不用重复注释 - // TODO @li: @Schema 写在参数校验前面。先有定义;其他的,也检查下; - - /** - * 任务名称 - */ - @NotEmpty(message = "任务名称不能为空") - @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") - private String name; - - /** - * 任务描述 - */ - @Schema(description = "任务描述", example = "升级任务") - private String description; - - /** - * 固件编号 - *

- * 关联 {@link IotOtaFirmwareDO#getId()} - */ - @NotNull(message = "固件编号不能为空") - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") - private Long firmwareId; - - /** - * 升级范围 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} - */ - @NotNull(message = "升级范围不能为空") - @InEnum(value = IotOtaUpgradeTaskScopeEnum.class) - @Schema(description = "升级范围", requiredMode = REQUIRED, example = "1") - private Integer scope; - - /** - * 选中的设备编号数组 - *

- * 关联 {@link IotDeviceDO#getId()} - */ - @Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]") - private List deviceIds; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java deleted file mode 100644 index e21b10241..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java +++ /dev/null @@ -1,90 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigImportReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigStatusReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT 插件配置") -@RestController -@RequestMapping("/iot/plugin-config") -@Validated -public class PluginConfigController { - - @Resource - private IotPluginConfigService pluginConfigService; - - @PostMapping("/create") - @Operation(summary = "创建插件配置") - @PreAuthorize("@ss.hasPermission('iot:plugin-config:create')") - public CommonResult createPluginConfig(@Valid @RequestBody PluginConfigSaveReqVO createReqVO) { - return success(pluginConfigService.createPluginConfig(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新插件配置") - @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") - public CommonResult updatePluginConfig(@Valid @RequestBody PluginConfigSaveReqVO updateReqVO) { - pluginConfigService.updatePluginConfig(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除插件配置") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:plugin-config:delete')") - public CommonResult deletePluginConfig(@RequestParam("id") Long id) { - pluginConfigService.deletePluginConfig(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得插件配置") - @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:plugin-config:query')") - public CommonResult getPluginConfig(@RequestParam("id") Long id) { - IotPluginConfigDO pluginConfig = pluginConfigService.getPluginConfig(id); - return success(BeanUtils.toBean(pluginConfig, PluginConfigRespVO.class)); - } - - @GetMapping("/page") - @Operation(summary = "获得插件配置分页") - @PreAuthorize("@ss.hasPermission('iot:plugin-config:query')") - public CommonResult> getPluginConfigPage(@Valid PluginConfigPageReqVO pageReqVO) { - PageResult pageResult = pluginConfigService.getPluginConfigPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, PluginConfigRespVO.class)); - } - - @PostMapping("/upload-file") - @Operation(summary = "上传插件文件") - @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") - public CommonResult uploadFile(@Valid PluginConfigImportReqVO reqVO) { - pluginConfigService.uploadFile(reqVO.getId(), reqVO.getFile()); - return success(true); - } - - @PutMapping("/update-status") - @Operation(summary = "修改插件状态") - @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") - public CommonResult updatePluginConfigStatus(@Valid @RequestBody PluginConfigStatusReqVO reqVO) { - pluginConfigService.updatePluginStatus(reqVO.getId(), reqVO.getStatus()); - return success(true); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java deleted file mode 100644 index b9b277a54..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import org.springframework.web.multipart.MultipartFile; - -@Schema(description = "管理后台 - IoT 插件上传 Request VO") -@Data -public class PluginConfigImportReqVO { - - @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") - private Long id; - - @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "插件文件不能为空") - private MultipartFile file; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java deleted file mode 100644 index 1666d5d6b..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 插件配置分页 Request VO") -@Data -public class PluginConfigPageReqVO extends PageParam { - - @Schema(description = "插件名称", example = "http") - private String name; - - @Schema(description = "状态", example = "1") - @InEnum(IotPluginStatusEnum.class) - private Integer status; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java deleted file mode 100644 index 2b8c4dcde..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -@Schema(description = "管理后台 - IoT 插件配置 Response VO") -@Data -public class PluginConfigRespVO { - - @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") - private Long id; - - @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") - private String pluginKey; - - @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") - private String name; - - @Schema(description = "描述", example = "你猜") - private String description; - - @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - private Integer deployType; - - @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) - private String fileName; - - @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) - private String version; - - @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - private Integer type; - - @Schema(description = "设备插件协议类型") - private String protocol; - - @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer status; - - @Schema(description = "插件配置项描述信息") - private String configSchema; - - @Schema(description = "插件配置信息") - private String config; - - @Schema(description = "插件脚本") - private String script; - - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - private LocalDateTime createTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java deleted file mode 100644 index e48869d64..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 插件配置新增/修改 Request VO") -@Data -public class PluginConfigSaveReqVO { - - // TODO @haohao:新增的字段有点多,每个都需要哇? - - // TODO @haohao:一些枚举字段,需要加枚举校验。例如说,deployType、status、type 等 - - @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") - private Long id; - - @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") - private String pluginKey; - - @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") - private String name; - - @Schema(description = "描述", example = "你猜") - private String description; - - @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - private Integer deployType; - - @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) - private String fileName; - - @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) - private String version; - - @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - private Integer type; - - @Schema(description = "设备插件协议类型") - private String protocol; - - @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) - @InEnum(IotPluginStatusEnum.class) - private Integer status; - - @Schema(description = "插件配置项描述信息") - private String configSchema; - - @Schema(description = "插件配置信息") - private String config; - - @Schema(description = "插件脚本") - private String script; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java deleted file mode 100644 index eae4aa0a2..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 插件配置状态 Request VO") -@Data -public class PluginConfigStatusReqVO { - - @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") - private Long id; - - @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) - @InEnum(IotPluginStatusEnum.class) - private Integer status; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java deleted file mode 100644 index e58b88856..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.instance; - -import lombok.*; -import io.swagger.v3.oas.annotations.media.Schema; -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import org.springframework.format.annotation.DateTimeFormat; -import java.time.LocalDateTime; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; - -// TODO @haohao:后续需要使用下 -@Schema(description = "管理后台 - IoT 插件实例分页 Request VO") -@Data -public class PluginInstancePageReqVO extends PageParam { - - @Schema(description = "插件主程序编号", example = "23738") - private String mainId; - - @Schema(description = "插件id", example = "26498") - private Long pluginId; - - @Schema(description = "插件主程序所在ip") - private String ip; - - @Schema(description = "插件主程序端口") - private Integer port; - - @Schema(description = "心跳时间,心路时间超过30秒需要剔除") - private Long heartbeatAt; - - @Schema(description = "创建时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] createTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java deleted file mode 100644 index cba59fdaf..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.instance; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -// TODO @haohao:后续需要使用下 -@Schema(description = "管理后台 - IoT 插件实例 Response VO") -@Data -public class PluginInstanceRespVO { - - @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23864") - private Long id; - - @Schema(description = "插件主程序id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738") - private String mainId; - - @Schema(description = "插件id", requiredMode = Schema.RequiredMode.REQUIRED, example = "26498") - private Long pluginId; - - @Schema(description = "插件主程序所在ip", requiredMode = Schema.RequiredMode.REQUIRED) - private String ip; - - @Schema(description = "插件主程序端口", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer port; - - @Schema(description = "心跳时间,心路时间超过30秒需要剔除", requiredMode = Schema.RequiredMode.REQUIRED) - private Long heartbeatAt; - - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - private LocalDateTime createTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java deleted file mode 100644 index 95e50a4a2..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java +++ /dev/null @@ -1,72 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT 数据桥梁") -@RestController -@RequestMapping("/iot/data-bridge") -@Validated -public class IotDataBridgeController { - - @Resource - private IotDataBridgeService dataBridgeService; - - @PostMapping("/create") - @Operation(summary = "创建数据桥梁") - @PreAuthorize("@ss.hasPermission('iot:data-bridge:create')") - public CommonResult createDataBridge(@Valid @RequestBody IotDataBridgeSaveReqVO createReqVO) { - return success(dataBridgeService.createDataBridge(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新数据桥梁") - @PreAuthorize("@ss.hasPermission('iot:data-bridge:update')") - public CommonResult updateDataBridge(@Valid @RequestBody IotDataBridgeSaveReqVO updateReqVO) { - dataBridgeService.updateDataBridge(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除数据桥梁") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:data-bridge:delete')") - public CommonResult deleteDataBridge(@RequestParam("id") Long id) { - dataBridgeService.deleteDataBridge(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得数据桥梁") - @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:data-bridge:query')") - public CommonResult getDataBridge(@RequestParam("id") Long id) { - IotDataBridgeDO dataBridge = dataBridgeService.getDataBridge(id); - return success(BeanUtils.toBean(dataBridge, IotDataBridgeRespVO.class)); - } - - @GetMapping("/page") - @Operation(summary = "获得数据桥梁分页") - @PreAuthorize("@ss.hasPermission('iot:data-bridge:query')") - public CommonResult> getDataBridgePage(@Valid IotDataBridgePageReqVO pageReqVO) { - PageResult pageResult = dataBridgeService.getDataBridgePage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotDataBridgeRespVO.class)); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java deleted file mode 100644 index 04e2f4570..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule; - -import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "管理后台 - IoT 规则场景") -@RestController -@RequestMapping("/iot/rule-scene") -@Validated -public class IotRuleSceneController { - - @Resource - private IotRuleSceneService ruleSceneService; - - @GetMapping("/test") - @PermitAll - public void test() { - ruleSceneService.test(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java deleted file mode 100644 index 38e04b2eb..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; - -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -@Schema(description = "管理后台 - IoT 数据桥梁 Response VO") -@Data -public class IotDataBridgeRespVO { - - @Schema(description = "桥梁编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") - private Long id; - - @Schema(description = "桥梁名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") - private String name; - - @Schema(description = "桥梁描述", example = "随便") - private String description; - - @Schema(description = "桥梁状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Integer status; - - @Schema(description = "桥梁方向", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer direction; - - @Schema(description = "桥梁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Integer type; - - @Schema(description = "桥梁配置") - private IotDataBridgeAbstractConfig config; - - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - private LocalDateTime createTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java deleted file mode 100644 index 8441701af..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; - -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeDirectionEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 数据桥梁新增/修改 Request VO") -@Data -public class IotDataBridgeSaveReqVO { - - @Schema(description = "桥梁编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") - private Long id; - - @Schema(description = "桥梁名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") - @NotEmpty(message = "桥梁名称不能为空") - private String name; - - @Schema(description = "桥梁描述", example = "随便") - private String description; - - @Schema(description = "桥梁状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @NotNull(message = "桥梁状态不能为空") - @InEnum(CommonStatusEnum.class) - private Integer status; - - @Schema(description = "桥梁方向", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "桥梁方向不能为空") - @InEnum(IotDataBridgeDirectionEnum.class) - private Integer direction; - - @Schema(description = "桥梁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "桥梁类型不能为空") - @InEnum(IotDataBridgeTypeEnum.class) - private Integer type; - - @Schema(description = "桥梁配置") - @NotNull(message = "桥梁配置不能为空") - private IotDataBridgeAbstractConfig config; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java deleted file mode 100644 index 527e79b35..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; - -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import lombok.Data; - -/** - * IoT IotDataBridgeConfig 抽象类 - * - * 用于表示数据桥梁配置数据的通用类型,根据具体的 "type" 字段动态映射到对应的子类 - * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 - * - * @author HUIHUI - */ -@Data -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) -@JsonSubTypes({ - @JsonSubTypes.Type(value = IotDataBridgeHttpConfig.class, name = "1"), - @JsonSubTypes.Type(value = IotDataBridgeMqttConfig.class, name = "10"), - @JsonSubTypes.Type(value = IotDataBridgeRedisStreamMQConfig.class, name = "21"), - @JsonSubTypes.Type(value = IotDataBridgeRocketMQConfig.class, name = "30"), - @JsonSubTypes.Type(value = IotDataBridgeRabbitMQConfig.class, name = "31"), - @JsonSubTypes.Type(value = IotDataBridgeKafkaMQConfig.class, name = "32"), -}) -public abstract class IotDataBridgeAbstractConfig { - - /** - * 配置类型 - * - * 枚举 {@link IotDataBridgeTypeEnum#getType()} - */ - private String type; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java deleted file mode 100644 index 3c9bb330f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; - -import lombok.Data; - -// TODO @puhui999:MQ 可以去掉哈。stream 更精准 -/** - * IoT Redis Stream 配置 {@link IotDataBridgeAbstractConfig} 实现类 - * - * @author HUIHUI - */ -@Data -public class IotDataBridgeRedisStreamMQConfig extends IotDataBridgeAbstractConfig { - - /** - * Redis 服务器地址 - */ - private String host; - /** - * 端口 - */ - private Integer port; - /** - * 密码 - */ - private String password; - /** - * 数据库索引 - */ - private Integer database; - - /** - * 主题 - */ - private String topic; -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java deleted file mode 100644 index f397e0acd..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// TODO @芋艿:占位 -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java deleted file mode 100644 index 15d2abccc..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.util.List; -import java.util.Map; - -@Schema(description = "管理后台 - IoT 设备上下行消息数量统计 Response VO") -@Data -public class IotStatisticsDeviceMessageSummaryRespVO { - - @Schema(description = "每小时上行数据数量统计") - private List> upstreamCounts; - - @Schema(description = "每小时下行数据数量统计") - private List> downstreamCounts; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java deleted file mode 100644 index 741f77f3a..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 统计 Request VO") -@Data -public class IotStatisticsReqVO { - - // TODO @super:前端传递的时候,还是通过 startTime 和 endTime 传递。后端转成 Long - - @Schema(description = "查询起始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1658486600000") - @NotNull(message = "查询起始时间不能为空") - private Long startTime; - - @Schema(description = "查询结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1758486600000") - @NotNull(message = "查询结束时间不能为空") - private Long endTime; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java deleted file mode 100644 index 925bc6719..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * IoT 物模型数据类型为布尔型或枚举型的 DataSpec 定义 - * - * 数据类型,取值为 bool 或 enum。 - * - * @author HUIHUI - */ -@Data -@EqualsAndHashCode(callSuper = true) -@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 -public class ThingModelBoolOrEnumDataSpecs extends ThingModelDataSpecs { - - // TODO @puhui999:要不写下参数校验?这样,注释可以简洁一点 - /** - * 枚举项的名称。 - * 可包含中文、大小写英文字母、数字、下划线(_)和短划线(-) - * 必须以中文、英文字母或数字开头,长度不超过 20 个字符 - */ - private String name; - /** - * 枚举值。 - */ - private Integer value; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http deleted file mode 100644 index 84446b0ce..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http +++ /dev/null @@ -1,112 +0,0 @@ -### 请求 /iot/think-model-function/create 接口 => 成功 -POST {{baseUrl}}/iot/think-model-function/create -Content-Type: application/json -tenant-id: {{adminTenantId}} -Authorization: Bearer {{token}} - -{ - "productId": 1001, - "productKey": "smart-sensor-001", - "identifier": "Temperature", - "name": "温度", - "description": "当前温度值", - "type": 1, - "property": { - "identifier": "Temperature", - "name": "温度", - "accessMode": "r", - "required": true, - "dataType": { - "type": "float", - "specs": { - "min": -10.0, - "max": 100.0, - "step": 0.1, - "unit": "℃" - } - }, - "description": "当前温度值" - } -} - -### 请求 /iot/think-model-function/create 接口 => 成功 -POST {{baseUrl}}/iot/think-model-function/create -Content-Type: application/json -tenant-id: {{adminTenantId}} -Authorization: Bearer {{token}} - -{ - "productId": 1001, - "productKey": "smart-sensor-001", - "identifier": "Humidity", - "name": "湿度", - "description": "当前湿度值", - "type": 1, - "property": { - "identifier": "Humidity", - "name": "湿度", - "accessMode": "r", - "required": true, - "dataType": { - "type": "float", - "specs": { - "min": 0.0, - "max": 100.0, - "step": 0.1, - "unit": "%" - } - }, - "description": "当前湿度值" - } -} - - - - -### 请求 /iot/think-model-function/update 接口 => 成功 -PUT {{baseUrl}}/iot/think-model-function/update -Content-Type: application/json -tenant-id: {{adminTenantId}} -Authorization: Bearer {{token}} - -{ - "id": 11, - "productId": 1001, - "productKey": "smart-sensor-001", - "identifier": "Temperature", - "name": "温度", - "description": "当前温度值", - "type": 1, - "property": { - "identifier": "Temperature", - "name": "温度", - "accessMode": "r", - "required": true, - "dataType": { - "type": "float", - "specs": { - "min": -111.0, - "max": 222.0, - "step": 0.1, - "unit": "℃" - } - }, - "description": "当前温度值" - } -} - -### 请求 /iot/think-model-function/delete 接口 => 成功 -DELETE {{baseUrl}}/iot/think-model-function/delete?id=7 -tenant-id: {{adminTenantId}} -Authorization: Bearer {{token}} - -### 请求 /iot/think-model-function/get 接口 => 成功 -GET {{baseUrl}}/iot/think-model-function/get?id=10 -tenant-id: {{adminTenantId}} -Authorization: Bearer {{token}} - - -### 请求 /iot/think-model-function/list-by-product-id 接口 => 成功 -GET {{baseUrl}}/iot/think-model-function/list-by-product-id?productId=1001 -tenant-id: {{adminTenantId}} -Authorization: Bearer {{token}} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java deleted file mode 100644 index 55cfb19d4..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.device; - -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * IoT 设备日志数据 DO - * - * 目前使用 TDengine 存储 - * - * @author alwayssuper - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IotDeviceLogDO { - - /** - * 日志编号 - * - * 通过 {@link IdUtil#fastSimpleUUID()} 生成 - */ - private String id; - - /** - * 请求编号 - * - * 对应 {@link IotDeviceMessage#getRequestId()} 字段 - */ - private String requestId; - - /** - * 产品标识 - *

- * 关联 {@link IotProductDO#getProductKey()} - */ - private String productKey; - /** - * 设备名称 - * - * 关联 {@link IotDeviceDO#getDeviceName()} - */ - private String deviceName; - /** - * 设备标识 - *

- * 关联 {@link IotDeviceDO#getDeviceKey()}} - */ - private String deviceKey; // 非存储字段,用于 TDengine 的 TAG - - /** - * 日志类型 - * - * 枚举 {@link IotDeviceMessageTypeEnum} - */ - private String type; - /** - * 标识符 - * - * 枚举 {@link IotDeviceMessageIdentifierEnum} - */ - private String identifier; - - /** - * 数据内容 - * - * 存储具体的消息数据内容,通常是 JSON 格式 - */ - private String content; - /** - * 响应码 - * - * 目前只有 server 下行消息给 device 设备时,才会有响应码 - */ - private Integer code; - - /** - * 上报时间戳 - */ - private Long reportTime; - - /** - * 时序时间 - */ - private Long ts; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java deleted file mode 100644 index ff4f0e7a0..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java +++ /dev/null @@ -1,92 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.ota; - -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; - -import java.time.LocalDateTime; - -/** - * IoT OTA 升级记录 DO - * - * @author 芋道源码 - */ -@TableName(value = "iot_ota_upgrade_record", autoResultMap = true) -@KeySequence("iot_ota_upgrade_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IotOtaUpgradeRecordDO extends BaseDO { - - @TableId - private Long id; - - /** - * 固件编号 - * - * 关联 {@link IotOtaFirmwareDO#getId()} - */ - private Long firmwareId; - /** - * 任务编号 - * - * 关联 {@link IotOtaUpgradeTaskDO#getId()} - */ - private Long taskId; - - /** - * 产品标识 - * - * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} - */ - private String productKey; - /** - * 设备名称 - * - * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} - */ - private String deviceName; - /** - * 设备编号 - * - * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} - */ - private String deviceId; - /** - * 来源的固件编号 - * - * 关联 {@link IotDeviceDO#getFirmwareId()} - */ - private Long fromFirmwareId; - - /** - * 升级状态 - * - * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} - */ - private Integer status; - /** - * 升级进度,百分比 - */ - private Integer progress; - /** - * 升级进度描述 - * - * 注意,只记录设备最后一次的升级进度描述 - * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 - */ - private String description; - /** - * 升级开始时间 - */ - private LocalDateTime startTime; - /** - * 升级结束时间 - */ - private LocalDateTime endTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java deleted file mode 100644 index 221bdc56c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java +++ /dev/null @@ -1,72 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.ota; - -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import lombok.*; - -import java.util.List; - -/** - * IoT OTA 升级任务 DO - * - * @author 芋道源码 - */ -@TableName(value = "iot_ota_upgrade_task", autoResultMap = true) -@KeySequence("iot_ota_upgrade_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IotOtaUpgradeTaskDO extends BaseDO { - - /** - * 任务编号 - */ - @TableField - private Long id; - /** - * 任务名称 - */ - private String name; - /** - * 任务描述 - */ - private String description; - - /** - * 固件编号 - *

- * 关联 {@link IotOtaFirmwareDO#getId()} - */ - private Long firmwareId; - - /** - * 任务状态 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} - */ - private Integer status; - - /** - * 升级范围 - *

- * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} - */ - private Integer scope; - /** - * 设备数量 - */ - private Long deviceCount; - /** - * 选中的设备编号数组 - *

- * 关联 {@link IotDeviceDO#getId()} - */ - @TableField(typeHandler = JacksonTypeHandler.class) - private List deviceIds; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java deleted file mode 100644 index cb247fc30..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java +++ /dev/null @@ -1,93 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.plugin; - -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginTypeEnum; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; - -/** - * IoT 插件配置 DO - * - * @author 芋道源码 - */ -@TableName("iot_plugin_config") -@KeySequence("iot_plugin_config_seq") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IotPluginConfigDO extends TenantBaseDO { - - /** - * 主键 ID - */ - @TableId - private Long id; - /** - * 插件包标识符 - */ - private String pluginKey; - /** - * 插件名称 - */ - private String name; - /** - * 插件描述 - */ - private String description; - /** - * 部署方式 - *

- * 枚举 {@link IotPluginDeployTypeEnum} - */ - private Integer deployType; - // TODO @芋艿:如果是外置的插件,fileName 和 version 的选择~ - /** - * 插件包文件名 - */ - private String fileName; - /** - * 插件版本 - */ - private String version; - // TODO @芋艿:type 字典的定义 - /** - * 插件类型 - *

- * 枚举 {@link IotPluginTypeEnum} - */ - private Integer type; - /** - * 设备插件协议类型 - */ - // TODO @芋艿:枚举字段 - private String protocol; - // TODO @haohao:这个字段,是不是直接用 CommonStatus,开启、禁用;然后插件实例那,online 是否在线 - /** - * 状态 - *

- * 枚举 {@link CommonStatusEnum} - */ - private Integer status; - - // TODO @芋艿:configSchema、config 示例字段 - /** - * 插件配置项描述信息 - */ - private String configSchema; - /** - * 插件配置信息 - */ - private String config; - - // TODO @芋艿:script 后续的使用 - /** - * 插件脚本 - */ - private String script; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java deleted file mode 100644 index 34abe893e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.plugin; - -import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; - -import java.time.LocalDateTime; - -/** - * IoT 插件实例 DO - * - * @author 芋道源码 - */ -@TableName("iot_plugin_instance") -@KeySequence("iot_plugin_instance_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IotPluginInstanceDO extends TenantBaseDO { - - /** - * 主键 - */ - @TableId - private Long id; - /** - * 插件编号 - *

- * 关联 {@link IotPluginConfigDO#getId()} - */ - private Long pluginId; - /** - * 插件进程编号 - * - * 一般格式是:hostIp@processId@${uuid} - */ - private String processId; - - /** - * 插件实例所在 IP - */ - private String hostIp; - /** - * 设备下行端口 - */ - private Integer downstreamPort; - - /** - * 是否在线 - */ - private Boolean online; - /** - * 在线时间 - */ - private LocalDateTime onlineTime; - /** - * 离线时间 - */ - private LocalDateTime offlineTime; - /** - * 心跳时间 - * - * 目的:心路时间超过一定时间后,会被进行下线处理 - */ - private LocalDateTime heartbeatTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java deleted file mode 100644 index f50101a4e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java +++ /dev/null @@ -1,243 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.rule; - -import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import lombok.*; - -import java.util.List; -import java.util.Map; - -/** - * IoT 规则场景(场景联动) DO - * - * @author 芋道源码 - */ -@TableName("iot_rule_scene") -@KeySequence("iot_rule_scene_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class IotRuleSceneDO extends TenantBaseDO { - - /** - * 场景编号 - */ - @TableId - private Long id; - /** - * 场景名称 - */ - private String name; - /** - * 场景描述 - */ - private String description; - /** - * 场景状态 - * - * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} - */ - private Integer status; - - /** - * 触发器数组 - */ - @TableField(typeHandler = JacksonTypeHandler.class) - private List triggers; - - /** - * 执行器数组 - */ - @TableField(typeHandler = JacksonTypeHandler.class) - private List actions; - - /** - * 触发器配置 - */ - @Data - public static class TriggerConfig { - - /** - * 触发类型 - * - * 枚举 {@link IotRuleSceneTriggerTypeEnum} - */ - private Integer type; - - /** - * 产品标识 - * - * 关联 {@link IotProductDO#getProductKey()} - */ - private String productKey; - /** - * 设备名称数组 - * - * 关联 {@link IotDeviceDO#getDeviceName()} - */ - private List deviceNames; - - /** - * 触发条件数组 - * - * 必填:当 {@link #type} 为 {@link IotRuleSceneTriggerTypeEnum#DEVICE} 时 - * 条件与条件之间,是“或”的关系 - */ - private List conditions; - - /** - * CRON 表达式 - * - * 必填:当 {@link #type} 为 {@link IotRuleSceneTriggerTypeEnum#TIMER} 时 - */ - private String cronExpression; - - } - - /** - * 触发条件 - */ - @Data - public static class TriggerCondition { - - /** - * 消息类型 - * - * 枚举 {@link IotDeviceMessageTypeEnum} - */ - private String type; - /** - * 消息标识符 - * - * 枚举 {@link IotDeviceMessageIdentifierEnum} - */ - private String identifier; - - /** - * 参数数组 - * - * 参数与参数之间,是“或”的关系 - */ - private List parameters; - - } - - /** - * 触发条件参数 - */ - @Data - public static class TriggerConditionParameter { - - /** - * 标识符(属性、事件、服务) - * - * 关联 {@link IotThingModelDO#getIdentifier()} - */ - private String identifier; - - /** - * 操作符 - * - * 枚举 {@link IotRuleSceneTriggerConditionParameterOperatorEnum} - */ - private String operator; - - /** - * 比较值 - * - * 如果有多个值,则使用 "," 分隔,类似 "1,2,3"。 - * 例如说,{@link IotRuleSceneTriggerConditionParameterOperatorEnum#IN}、{@link IotRuleSceneTriggerConditionParameterOperatorEnum#BETWEEN} - */ - private String value; - - } - - /** - * 执行器配置 - */ - @Data - public static class ActionConfig { - - /** - * 执行类型 - * - * 枚举 {@link IotRuleSceneActionTypeEnum} - */ - private Integer type; - - /** - * 设备控制 - * - * 必填:当 {@link #type} 为 {@link IotRuleSceneActionTypeEnum#DEVICE_CONTROL} 时 - */ - private ActionDeviceControl deviceControl; - - /** - * 数据桥接编号 - * - * 必填:当 {@link #type} 为 {@link IotRuleSceneActionTypeEnum#DATA_BRIDGE} 时 - * 关联:{@link IotDataBridgeDO#getId()} - */ - private Long dataBridgeId; - - } - - /** - * 执行设备控制 - */ - @Data - public static class ActionDeviceControl { - - /** - * 产品标识 - * - * 关联 {@link IotProductDO#getProductKey()} - */ - private String productKey; - /** - * 设备名称数组 - * - * 关联 {@link IotDeviceDO#getDeviceName()} - */ - private List deviceNames; - - /** - * 消息类型 - * - * 枚举 {@link IotDeviceMessageTypeEnum#PROPERTY}、{@link IotDeviceMessageTypeEnum#SERVICE} - */ - private String type; - /** - * 消息标识符 - * - * 枚举 {@link IotDeviceMessageIdentifierEnum} - * - * 1. 属性设置:对应 {@link IotDeviceMessageIdentifierEnum#PROPERTY_SET} - * 2. 服务调用:对应 {@link IotDeviceMessageIdentifierEnum#SERVICE_INVOKE} - */ - private String identifier; - - /** - * 具体数据 - * - * 1. 属性设置:在 {@link #type} 是 {@link IotDeviceMessageTypeEnum#PROPERTY} 时,对应 properties - * 2. 服务调用:在 {@link #type} 是 {@link IotDeviceMessageTypeEnum#SERVICE} 时,对应 params - */ - private Map data; - - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java deleted file mode 100644 index 5e5d8200f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java +++ /dev/null @@ -1,159 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.ota; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -import java.util.List; -import java.util.Map; - -@Mapper -public interface IotOtaUpgradeRecordMapper extends BaseMapperX { - - // TODO @li:selectByFirmwareIdAndTaskIdAndDeviceId;让方法自解释 - /** - * 根据条件查询单个OTA升级记录 - * - * @param firmwareId 固件ID,可选参数,用于筛选固件ID匹配的记录 - * @param taskId 任务ID,可选参数,用于筛选任务ID匹配的记录 - * @param deviceId 设备ID,可选参数,用于筛选设备ID匹配的记录 - * @return 返回符合条件的单个OTA升级记录,如果不存在则返回null - */ - default IotOtaUpgradeRecordDO selectByConditions(Long firmwareId, Long taskId, String deviceId) { - // 使用LambdaQueryWrapperX构建查询条件,根据传入的参数动态添加查询条件 - return selectOne(new LambdaQueryWrapperX() - .eqIfPresent(IotOtaUpgradeRecordDO::getFirmwareId, firmwareId) - .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, taskId) - .eqIfPresent(IotOtaUpgradeRecordDO::getDeviceId, deviceId)); - } - - // TODO @li:这个是不是 groupby status 就 ok 拉? - /** - * 根据任务ID和设备名称查询OTA升级记录的状态统计信息。 - * 该函数通过SQL查询统计不同状态(0到5)的记录数量,并返回一个包含统计结果的Map列表。 - * - * @param taskId 任务ID,用于筛选特定任务的OTA升级记录。 - * @param deviceName 设备名称,支持模糊查询,用于筛选特定设备的OTA升级记录。 - * @return 返回一个Map列表,每个Map包含不同状态(0到5)的记录数量。 - */ - @Select("select count(case when status = 0 then 1 else 0) as `0` " + - "count(case when status = 1 then 1 else 0) as `1` " + - "count(case when status = 2 then 1 else 0) as `2` " + - "count(case when status = 3 then 1 else 0) as `3` " + - "count(case when status = 4 then 1 else 0) as `4` " + - "count(case when status = 5 then 1 else 0) as `5` " + - "from iot_ota_upgrade_record " + - "where task_id = #{taskId} " + - "and device_name like concat('%', #{deviceName}, '%') " + - "and status = #{status}") - List> selectOtaUpgradeRecordCount(@Param("taskId") Long taskId, - @Param("deviceName") String deviceName); - - /** - * 根据固件ID查询OTA升级记录的状态统计信息。 - * 该函数通过SQL查询统计不同状态(0到5)的记录数量,并返回一个包含统计结果的Map列表。 - * - * @param firmwareId 固件ID,用于筛选特定固件的OTA升级记录。 - * @return 返回一个Map列表,每个Map包含不同状态(0到5)的记录数量。 - */ - @Select("select count(case when status = 0 then 1 else 0) as `0` " + - "count(case when status = 1 then 1 else 0) as `1` " + - "count(case when status = 2 then 1 else 0) as `2` " + - "count(case when status = 3 then 1 else 0) as `3` " + - "count(case when status = 4 then 1 else 0) as `4` " + - "count(case when status = 5 then 1 else 0) as `5` " + - "from iot_ota_upgrade_record " + - "where firmware_id = #{firmwareId}") - List> selectOtaUpgradeRecordStatistics(Long firmwareId); - - // TODO @li:这里的注释,可以去掉哈 - /** - * 根据分页查询条件获取 OTA升级记录的分页结果 - * - * @param pageReqVO 分页查询请求参数,包含设备名称、任务ID等查询条件 - * @return 返回分页查询结果,包含符合条件的 OTA升级记录列表 - */ - // TODO @li:selectPage 就 ok 拉。 - default PageResult selectUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) { - // TODO @li:这里的注释,可以去掉哈;然后下面的“如果”。。。也没必要注释 - // 使用LambdaQueryWrapperX构建查询条件,并根据请求参数动态添加查询条件 - return selectPage(pageReqVO, new LambdaQueryWrapperX() - .likeIfPresent(IotOtaUpgradeRecordDO::getDeviceName, pageReqVO.getDeviceName()) // 如果设备名称存在,则添加模糊查询条件 - .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, pageReqVO.getTaskId())); // 如果任务ID存在,则添加等值查询条件 - } - - // TODO @li:这里的注释,可以去掉哈 - /** - * 根据任务ID和状态更新升级记录的状态 - *

- * 该函数用于将符合指定任务ID和状态的升级记录的状态更新为新的状态。 - * - * @param setStatus 要设置的新状态值,类型为Integer - * @param taskId 要更新的升级记录对应的任务ID,类型为Long - * @param whereStatus 用于筛选升级记录的当前状态值,类型为Integer - */ - // TODO @li:改成 updateByTaskIdAndStatus(taskId, status, IotOtaUpgradeRecordDO) 更通用一些。 - default void updateUpgradeRecordStatusByTaskIdAndStatus(Integer setStatus, Long taskId, Integer whereStatus) { - // 使用LambdaUpdateWrapper构建更新条件,将指定状态的记录更新为指定状态 - update(new LambdaUpdateWrapper() - .set(IotOtaUpgradeRecordDO::getStatus, setStatus) - .eq(IotOtaUpgradeRecordDO::getTaskId, taskId) - .eq(IotOtaUpgradeRecordDO::getStatus, whereStatus) - ); - } - - // TODO @li:参考上面的建议,调整下这个方法 - /** - * 根据状态查询符合条件的升级记录列表 - *

- * 该函数使用LambdaQueryWrapperX构建查询条件,查询指定状态的升级记录。 - * - * @param state 升级记录的状态,用于筛选符合条件的记录 - * @return 返回符合指定状态的升级记录列表,类型为List - */ - default List selectUpgradeRecordListByState(Integer state) { - // 使用LambdaQueryWrapperX构建查询条件,根据状态查询符合条件的升级记录 - return selectList(new LambdaQueryWrapperX() - .eq(IotOtaUpgradeRecordDO::getStatus, state)); - } - - // TODO @li:参考上面的建议,调整下这个方法 - /** - * 更新升级记录状态 - *

- * 该函数用于批量更新指定ID列表中的升级记录状态。通过传入的ID列表和状态值,使用LambdaUpdateWrapper构建更新条件, - * 并执行更新操作。 - * - * @param ids 需要更新的升级记录ID列表,类型为List。传入的ID列表中的记录将被更新。 - * @param status 要更新的状态值,类型为Integer。该值将被设置到符合条件的升级记录中。 - */ - default void updateUpgradeRecordStatus(List ids, Integer status) { - // 使用LambdaUpdateWrapper构建更新条件,设置状态字段,并根据ID列表进行筛选 - update(new LambdaUpdateWrapper() - .set(IotOtaUpgradeRecordDO::getStatus, status) - .in(IotOtaUpgradeRecordDO::getId, ids) - ); - } - - // TODO @li:参考上面的建议,调整下这个方法 - /** - * 根据任务ID查询升级记录列表 - *

- * 该函数通过任务ID查询符合条件的升级记录,并返回查询结果列表。 - * - * @param taskId 任务ID,用于筛选升级记录 - * @return 返回符合条件的升级记录列表,若未找到则返回空列表 - */ - default List selectUpgradeRecordListByTaskId(Long taskId) { - // 使用LambdaQueryWrapperX构建查询条件,根据任务ID查询符合条件的升级记录 - return selectList(new LambdaQueryWrapperX() - .eq(IotOtaUpgradeRecordDO::getTaskId, taskId)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java deleted file mode 100644 index d955b1361..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.ota; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -/** - * OTA 升级任务Mapper - * - * @author Shelly - */ -@Mapper -public interface IotOtaUpgradeTaskMapper extends BaseMapperX { - - /** - * 根据固件ID和任务名称查询升级任务列表。 - * - * @param firmwareId 固件ID,用于筛选升级任务 - * @param name 任务名称,用于筛选升级任务 - * @return 符合条件的升级任务列表 - */ - default List selectByFirmwareIdAndName(Long firmwareId, String name) { - return selectList(new LambdaQueryWrapperX() - .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, firmwareId) - .eqIfPresent(IotOtaUpgradeTaskDO::getName, name)); - } - - /** - * 分页查询升级任务列表,支持根据固件ID和任务名称进行筛选。 - * - * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件 - * @return 分页结果,包含符合条件的升级任务列表 - */ - default PageResult selectUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) { - return selectPage(pageReqVO, new LambdaQueryWrapperX() - .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, pageReqVO.getFirmwareId()) - .likeIfPresent(IotOtaUpgradeTaskDO::getName, pageReqVO.getName())); - } - - /** - * 根据任务状态查询升级任务列表 - *

- * 该函数通过传入的任务状态,查询数据库中符合条件的升级任务列表。 - * - * @param status 任务状态,用于筛选升级任务的状态值 - * @return 返回符合条件的升级任务列表,列表中的每个元素为 IotOtaUpgradeTaskDO 对象 - */ - default List selectUpgradeTaskByState(Integer status) { - return selectList(IotOtaUpgradeTaskDO::getStatus, status); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java deleted file mode 100644 index 0e2163a3f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.plugin; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -@Mapper -public interface IotPluginConfigMapper extends BaseMapperX { - - default PageResult selectPage(PluginConfigPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .likeIfPresent(IotPluginConfigDO::getName, reqVO.getName()) - .eqIfPresent(IotPluginConfigDO::getStatus, reqVO.getStatus()) - .orderByDesc(IotPluginConfigDO::getId)); - } - - default List selectListByStatusAndDeployType(Integer status, Integer deployType) { - return selectList(new LambdaQueryWrapperX() - .eq(IotPluginConfigDO::getStatus, status) - .eq(IotPluginConfigDO::getDeployType, deployType) - .orderByAsc(IotPluginConfigDO::getId)); - } - - default IotPluginConfigDO selectByPluginKey(String pluginKey) { - return selectOne(IotPluginConfigDO::getPluginKey, pluginKey); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java deleted file mode 100644 index 93ffe8728..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.plugin; - -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import org.apache.ibatis.annotations.Mapper; - -import java.time.LocalDateTime; -import java.util.List; - -// TODO @li:参考 IotOtaUpgradeRecordMapper 的写法 -@Mapper -public interface IotPluginInstanceMapper extends BaseMapperX { - - default IotPluginInstanceDO selectByProcessId(String processId) { - return selectOne(IotPluginInstanceDO::getProcessId, processId); - } - - default List selectListByHeartbeatTimeLt(LocalDateTime heartbeatTime) { - return selectList(new LambdaQueryWrapper() - .lt(IotPluginInstanceDO::getHeartbeatTime, heartbeatTime)); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java deleted file mode 100644 index 303579116..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.rule; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import org.apache.ibatis.annotations.Mapper; - -/** - * IoT 数据桥梁 Mapper - * - * @author HUIHUI - */ -@Mapper -public interface IotDataBridgeMapper extends BaseMapperX { - - default PageResult selectPage(IotDataBridgePageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .likeIfPresent(IotDataBridgeDO::getName, reqVO.getName()) - .eqIfPresent(IotDataBridgeDO::getStatus, reqVO.getStatus()) - .betweenIfPresent(IotDataBridgeDO::getCreateTime, reqVO.getCreateTime()) - .orderByDesc(IotDataBridgeDO::getId)); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java deleted file mode 100644 index e5e069a0c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.rule; - -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface IotRuleSceneMapper extends BaseMapperX { - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java deleted file mode 100644 index d09dac72d..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.redis; - -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; - -/** - * IoT Redis Key 枚举类 - * - * @author 芋道源码 - */ -public interface RedisKeyConstants { - - /** - * 设备属性的数据缓存,采用 HASH 结构 - *

- * KEY 格式:device_property:{deviceKey} - * HASH KEY:identifier 属性标识 - * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} - */ - String DEVICE_PROPERTY = "iot:device_property:%s"; - - /** - * 设备的最后上报时间,采用 ZSET 结构 - * - * KEY 格式:{deviceKey} - * SCORE:上报时间 - */ - String DEVICE_REPORT_TIMES = "iot:device_report_times"; - - /** - * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) - * - * KEY 格式:device_${productKey}_${deviceKey} - * VALUE 数据类型:String(JSON) - */ - String DEVICE = "iot:device"; - - /** - * 物模型的数据缓存,使用 Spring Cache 操作(忽略租户) - * - * KEY 格式:thing_model_${productKey} - * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO} 列表 - */ - String THING_MODEL_LIST = "iot:thing_model_list"; - - /** - * 设备插件的插件进程编号的映射,采用 HASH 结构 - * - * KEY 格式:device_plugin_instance_process_ids - * HASH KEY:${deviceKey} - * VALUE:插件进程编号,对应 {@link IotPluginInstanceDO#getProcessId()} 字段 - */ - String DEVICE_PLUGIN_INSTANCE_PROCESS_IDS = "iot:device_plugin_instance_process_ids"; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java deleted file mode 100644 index 32559d703..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.redis.plugin; - -import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; -import jakarta.annotation.Resource; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Repository; - -/** - * 设备插件的插件进程编号的缓存的 Redis DAO - */ -@Repository -public class DevicePluginProcessIdRedisDAO { - - @Resource - private StringRedisTemplate stringRedisTemplate; - - public void put(String deviceKey, String processId) { - stringRedisTemplate.opsForHash().put(RedisKeyConstants.DEVICE_PLUGIN_INSTANCE_PROCESS_IDS, deviceKey, processId); - } - - public String get(String deviceKey) { - return (String) stringRedisTemplate.opsForHash().get(RedisKeyConstants.DEVICE_PLUGIN_INSTANCE_PROCESS_IDS, deviceKey); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java deleted file mode 100644 index 96741e609..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java +++ /dev/null @@ -1,76 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.tdengine; - -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; -import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; -import com.baomidou.mybatisplus.annotation.InterceptorIgnore; -import com.baomidou.mybatisplus.core.metadata.IPage; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; -import java.util.Map; - -/** - * 设备日志 {@link IotDeviceLogDO} Mapper 接口 - */ -@Mapper -@TDengineDS -@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 -public interface IotDeviceLogMapper { - - /** - * 创建设备日志超级表 - */ - void createDeviceLogSTable(); - - /** - * 查询设备日志表是否存在 - * - * @return 存在则返回表名;不存在则返回 null - */ - String showDeviceLogSTable(); - - /** - * 插入设备日志数据 - * - * 如果子表不存在,会自动创建子表 - * - * @param log 设备日志数据 - */ - void insert(IotDeviceLogDO log); - - /** - * 获得设备日志分页 - * - * @param reqVO 分页查询条件 - * @return 设备日志列表 - */ - IPage selectPage(IPage page, - @Param("reqVO") IotDeviceLogPageReqVO reqVO); - - /** - * 统计设备日志数量 - * - * @param createTime 创建时间,如果为空,则统计所有日志数量 - * @return 日志数量 - */ - Long selectCountByCreateTime(@Param("createTime") Long createTime); - - // TODO @super:1)上行、下行,不写在 mapper 里,而是通过参数传递,这样,selectDeviceLogUpCountByHour、selectDeviceLogDownCountByHour 可以合并; - // TODO @super:2)不能只基于 identifier 来计算,而是要 type + identifier 成对 - /** - * 查询每个小时设备上行消息数量 - */ - List> selectDeviceLogUpCountByHour(@Param("deviceKey") String deviceKey, - @Param("startTime") Long startTime, - @Param("endTime") Long endTime); - - /** - * 查询每个小时设备下行消息数量 - */ - List> selectDeviceLogDownCountByHour(@Param("deviceKey") String deviceKey, - @Param("startTime") Long startTime, - @Param("endTime") Long endTime); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java deleted file mode 100644 index 0a2812ac8..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.module.iot.framework.plugin.config; - -import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStartRunner; -import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStateListener; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.spring.SpringPluginManager; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.nio.file.Paths; - -/** - * IoT 插件配置类 - * - * @author haohao - */ -@Configuration -@Slf4j -public class IotPluginConfiguration { - - @Bean - public IotPluginStartRunner pluginStartRunner(SpringPluginManager pluginManager, - IotPluginConfigService pluginConfigService) { - return new IotPluginStartRunner(pluginManager, pluginConfigService); - } - - // TODO @芋艿:需要 review 下 - @Bean - public SpringPluginManager pluginManager(@Value("${pf4j.pluginsDir:pluginsDir}") String pluginsDir) { - log.info("[init][实例化 SpringPluginManager]"); - SpringPluginManager springPluginManager = new SpringPluginManager(Paths.get(pluginsDir)) { - - @Override - public void startPlugins() { - // 禁用插件启动,避免插件启动时,启动所有插件 - log.info("[init][禁用默认启动所有插件]"); - } - - }; - springPluginManager.addPluginStateListener(new IotPluginStateListener()); - return springPluginManager; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java deleted file mode 100644 index 64d258514..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.yudao.module.iot.framework.plugin.core; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.spring.SpringPluginManager; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; - -import java.util.List; - -/** - * IoT 插件启动 Runner - * - * 用于 Spring Boot 启动时,启动 {@link IotPluginDeployTypeEnum#JAR} 部署类型的插件 - */ -@RequiredArgsConstructor -@Slf4j -public class IotPluginStartRunner implements ApplicationRunner { - - private final SpringPluginManager springPluginManager; - - private final IotPluginConfigService pluginConfigService; - - @Override - public void run(ApplicationArguments args) { - List pluginConfigList = TenantUtils.executeIgnore( - () -> pluginConfigService.getPluginConfigListByStatusAndDeployType( - IotPluginStatusEnum.RUNNING.getStatus(), IotPluginDeployTypeEnum.JAR.getDeployType())); - if (CollUtil.isEmpty(pluginConfigList)) { - log.info("[run][没有需要启动的插件]"); - return; - } - - // 遍历插件列表,逐个启动 - pluginConfigList.forEach(pluginConfig -> { - try { - log.info("[run][插件({}) 启动开始]", pluginConfig.getPluginKey()); - springPluginManager.startPlugin(pluginConfig.getPluginKey()); - log.info("[run][插件({}) 启动完成]", pluginConfig.getPluginKey()); - } catch (Exception e) { - log.error("[run][插件({}) 启动异常]", pluginConfig.getPluginKey(), e); - } - }); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java deleted file mode 100644 index bbc73c619..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.framework.plugin.core; - -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginStateEvent; -import org.pf4j.PluginStateListener; - -/** - * IoT 插件状态监听器,用于 log 插件的状态变化 - * - * @author haohao - */ -@Slf4j -public class IotPluginStateListener implements PluginStateListener { - - @Override - public void pluginStateChanged(PluginStateEvent event) { - log.info("[pluginStateChanged][插件({}) 状态变化,从 {} 变为 {}]", event.getPlugin().getPluginId(), - event.getOldState().toString(), event.getPluginState().toString()); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java deleted file mode 100644 index 0e3869e18..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.iocoder.yudao.module.iot.job.device; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; -import com.xxl.job.core.handler.annotation.XxlJob; -import jakarta.annotation.Resource; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -/** - * IoT 设备离线检查 Job - * - * 检测逻辑:设备最后一条 {@link cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage} 消息超过一定时间,则认为设备离线 - * - * @author 芋道源码 - */ -@Component -public class IotDeviceOfflineCheckJob { - - /** - * 设备离线超时时间 - * - * TODO 芋艿:暂定 10 分钟,后续看看要不要基于设备或者全局有配置文件 - */ - public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10); - - @Resource - private IotDeviceService deviceService; - @Resource - private IotDevicePropertyService devicePropertyService; - @Resource - private IotDeviceUpstreamService deviceUpstreamService; - - @XxlJob("iotDeviceOfflineCheckJob") - @TenantJob - public String execute(String param) { - // 1.1 获得在线设备列表 - List devices = deviceService.getDeviceListByState(IotDeviceStateEnum.ONLINE.getState()); - if (CollUtil.isEmpty(devices)) { - return JsonUtils.toJsonString(Collections.emptyList()); - } - // 1.2 获取超时的 deviceKey 集合 - Set timeoutDeviceKeys = devicePropertyService.getDeviceKeysByReportTime( - LocalDateTime.now().minus(OFFLINE_TIMEOUT)); - - // 2. 下线设备 - List offlineDeviceKeys = CollUtil.newArrayList(); - for (IotDeviceDO device : devices) { - if (!timeoutDeviceKeys.contains(device.getDeviceKey())) { - continue; - } - offlineDeviceKeys.add(device.getDeviceKey()); - // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 - deviceUpstreamService.updateDeviceState(((IotDeviceStateUpdateReqDTO) - new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) - .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) - .setState((IotDeviceStateEnum.OFFLINE.getState()))); - } - return JsonUtils.toJsonString(offlineDeviceKeys); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java deleted file mode 100644 index 963490a3a..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.module.iot.job.plugin; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; -import com.xxl.job.core.handler.annotation.XxlJob; -import jakarta.annotation.Resource; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.time.LocalDateTime; - -/** - * IoT 插件实例离线检查 Job - * - * @author 芋道源码 - */ -@Component -public class IotPluginInstancesJob { - - /** - * 插件离线超时时间 - * - * TODO 芋艿:暂定 10 分钟,后续看要不要做配置 - */ - public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10); - - @Resource - private IotPluginInstanceService pluginInstanceService; - - @XxlJob("iotPluginInstancesJob") - @TenantJob - public String execute(String param) { - int count = pluginInstanceService.offlineTimeoutPluginInstance( - LocalDateTime.now().minus(OFFLINE_TIMEOUT)); - return StrUtil.format("离线超时插件实例数量为: {}", count); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java deleted file mode 100644 index 594f9ef0b..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.module.iot.job.rule; - -import cn.hutool.core.map.MapUtil; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.quartz.JobExecutionContext; -import org.springframework.scheduling.quartz.QuartzJobBean; - -import java.util.Map; - -/** - * IoT 规则场景 Job,用于执行 {@link IotRuleSceneTriggerTypeEnum#TIMER} 类型的规则场景 - * - * @author 芋道源码 - */ -@Slf4j -public class IotRuleSceneJob extends QuartzJobBean { - - /** - * JobData Key - 规则场景编号 - */ - public static final String JOB_DATA_KEY_RULE_SCENE_ID = "ruleSceneId"; - - @Resource - private IotRuleSceneService ruleSceneService; - - @Override - protected void executeInternal(JobExecutionContext context) { - // 获得规则场景编号 - Long ruleSceneId = context.getMergedJobDataMap().getLong(JOB_DATA_KEY_RULE_SCENE_ID); - - // 执行规则场景 - ruleSceneService.executeRuleSceneByTimer(ruleSceneId); - } - - /** - * 创建 JobData Map - * - * @param ruleSceneId 规则场景编号 - * @return JobData Map - */ - public static Map buildJobDataMap(Long ruleSceneId) { - return MapUtil.of(JOB_DATA_KEY_RULE_SCENE_ID, ruleSceneId); - } - - /** - * 创建 Job 名字 - * - * @param ruleSceneId 规则场景编号 - * @return Job 名字 - */ - public static String buildJobName(Long ruleSceneId) { - return String.format("%s_%d", IotRuleSceneJob.class.getSimpleName(), ruleSceneId); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java deleted file mode 100644 index 297267791..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.mq.consumer.device; - -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -/** - * 针对 {@link IotDeviceMessage} 的消费者,记录设备日志 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotDeviceLogMessageConsumer { - - @Resource - private IotDeviceLogService deviceLogService; - - @EventListener - @Async - public void onMessage(IotDeviceMessage message) { - log.info("[onMessage][消息内容({})]", message); - deviceLogService.createDeviceLog(message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java deleted file mode 100644 index f0e49bd47..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.iocoder.yudao.module.iot.mq.consumer.device; - -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.util.Objects; - -/** - * 针对 {@link IotDeviceMessage} 的消费者,将离线的设备,自动标记为上线 - * - * 注意:只有设备上行消息,才会触发该逻辑 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotDeviceOnlineMessageConsumer { - - @Resource - private IotDeviceService deviceService; - - @Resource - private IotDeviceUpstreamService deviceUpstreamService; - - @EventListener - @Async - public void onMessage(IotDeviceMessage message) { - // 1.1 只处理上行消息。因为,只有设备上行的消息,才会触发设备上线的逻辑 - if (!isUpstreamMessage(message)) { - return; - } - // 1.2 如果设备已在线,则不做处理 - log.info("[onMessage][消息内容({})]", message); - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - message.getProductKey(), message.getDeviceName()); - if (device == null) { - log.error("[onMessage][消息({}) 对应的设备部存在]", message); - return; - } - if (IotDeviceStateEnum.isOnline(device.getState())) { - return; - } - - // 2. 标记设备为在线 - // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 - deviceUpstreamService.updateDeviceState(((IotDeviceStateUpdateReqDTO) - new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) - .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) - .setState((IotDeviceStateEnum.ONLINE.getState()))); - } - - private boolean isUpstreamMessage(IotDeviceMessage message) { - // 设备属性 - if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType()) - && Objects.equals(message.getIdentifier(), IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier())) { - return true; - } - // 设备事件 - if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) { - return true; - } - // 设备服务 - // noinspection RedundantIfStatement - if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.SERVICE.getType()) - && !StrUtil.endWith(message.getIdentifier(), IotDeviceMessageIdentifierEnum.SERVICE_REPLY_SUFFIX.getIdentifier())) { - return true; - } - return false; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java deleted file mode 100644 index bf9cc5332..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.yudao.module.iot.mq.consumer.device; - -import cn.hutool.core.util.ObjectUtil; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -import jakarta.annotation.Resource; - -/** - * 针对 {@link IotDeviceMessage} 的消费者,记录设备属性 - * - * @author alwayssuper - */ -@Component -@Slf4j -public class IotDevicePropertyMessageConsumer { - - @Resource - private IotDevicePropertyService deviceDataService; - - @EventListener - @Async - public void onMessage(IotDeviceMessage message) { - if (ObjectUtil.notEqual(message.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType()) - || ObjectUtil.notEqual(message.getIdentifier(), IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier())) { - return; - } - log.info("[onMessage][消息内容({})]", message); - - // 保存设备属性 - deviceDataService.saveDeviceProperty(message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java deleted file mode 100644 index e6ea3e22d..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.mq.consumer.rule; - -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -/** - * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotRuleSceneMessageHandler { - - @Resource - private IotRuleSceneService ruleSceneService; - - @EventListener - @Async - public void onMessage(IotDeviceMessage message) { - log.info("[onMessage][消息内容({})]", message); - ruleSceneService.executeRuleSceneByDevice(message); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java deleted file mode 100644 index 0e8309a82..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java +++ /dev/null @@ -1,76 +0,0 @@ -package cn.iocoder.yudao.module.iot.mq.message; - -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -// TODO @芋艿:参考阿里云的物模型,优化 IoT 上下行消息的设计,尽量保持一致(渐进式,不要一口气)! -/** - * IoT 设备消息 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class IotDeviceMessage { - - /** - * 请求编号 - */ - private String requestId; - - /** - * 设备信息 - */ - private String productKey; - /** - * 设备名称 - */ - private String deviceName; - /** - * 设备标识 - */ - private String deviceKey; - - /** - * 消息类型 - * - * 枚举 {@link IotDeviceMessageTypeEnum} - */ - private String type; - /** - * 标识符 - * - * 枚举 {@link IotDeviceMessageIdentifierEnum} - */ - private String identifier; - - /** - * 请求参数 - * - * 例如说:属性上报的 properties、事件上报的 params - */ - private Object data; - /** - * 响应码 - * - * 目前只有 server 下行消息给 device 设备时,才会有响应码 - */ - private Integer code; - - /** - * 上报时间 - */ - private LocalDateTime reportTime; - - /** - * 租户编号 - */ - private Long tenantId; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java deleted file mode 100644 index 11d5d96be..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.iot.mq.producer.device; - -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -/** - * IoT 设备相关消息的 Producer - * - * @author alwayssuper - * @since 2024/12/17 16:35 - */ -@Slf4j -@Component -public class IotDeviceProducer { - - @Resource - private ApplicationContext applicationContext; - - /** - * 发送 {@link IotDeviceMessage} 消息 - * - * @param thingModelMessage 物模型消息 - */ - public void sendDeviceMessage(IotDeviceMessage thingModelMessage) { - applicationContext.publishEvent(thingModelMessage); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java deleted file mode 100644 index 37d0ba016..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO 芋艿:临时占位 - */ -package cn.iocoder.yudao.module.iot.mq.producer; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java deleted file mode 100644 index f09604dea..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.control; - -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import jakarta.validation.Valid; - -/** - * IoT 设备下行 Service 接口 - * - * 目的:服务端 -> 插件 -> 设备 - * - * @author 芋道源码 - */ -public interface IotDeviceDownstreamService { - - /** - * 设备下行,可用于设备模拟 - * - * @param downstreamReqVO 设备下行请求 VO - * @return 下发消息 - */ - IotDeviceMessage downstreamDevice(@Valid IotDeviceDownstreamReqVO downstreamReqVO); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java deleted file mode 100644 index 3aab53de9..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java +++ /dev/null @@ -1,354 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.control; - -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.framework.common.exception.ServiceException; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.client.RestTemplate; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_DOWNSTREAM_FAILED; - -/** - * IoT 设备下行 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotDeviceDownstreamServiceImpl implements IotDeviceDownstreamService { - - @Resource - private IotDeviceService deviceService; - @Resource - private IotPluginInstanceService pluginInstanceService; - - @Resource - private RestTemplate restTemplate; - - @Resource - private IotDeviceProducer deviceProducer; - - @Override - public IotDeviceMessage downstreamDevice(IotDeviceDownstreamReqVO downstreamReqVO) { - // 校验设备是否存在 - IotDeviceDO device = deviceService.validateDeviceExists(downstreamReqVO.getId()); - // TODO @芋艿:离线设备,不允许推送 - // TODO 芋艿:父设备的处理 - IotDeviceDO parentDevice = null; - - // 服务调用 - if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.SERVICE.getType())) { - return invokeDeviceService(downstreamReqVO, device, parentDevice); - } - // 属性相关 - if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) { - // 属性设置 - if (Objects.equals(downstreamReqVO.getIdentifier(), - IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier())) { - return setDeviceProperty(downstreamReqVO, device, parentDevice); - } - // 属性设置 - if (Objects.equals(downstreamReqVO.getIdentifier(), - IotDeviceMessageIdentifierEnum.PROPERTY_GET.getIdentifier())) { - return getDeviceProperty(downstreamReqVO, device, parentDevice); - } - } - // 配置下发 - if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.CONFIG.getType()) - && Objects.equals(downstreamReqVO.getIdentifier(), - IotDeviceMessageIdentifierEnum.CONFIG_SET.getIdentifier())) { - return setDeviceConfig(downstreamReqVO, device, parentDevice); - } - // OTA 升级 - if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.OTA.getType())) { - return otaUpgrade(downstreamReqVO, device, parentDevice); - } - // TODO @芋艿:取消设备的网关的时,要不要下发 REGISTER_UNREGISTER_SUB ? - throw new IllegalArgumentException("不支持的下行消息类型:" + downstreamReqVO); - } - - /** - * 调用设备服务 - * - * @param downstreamReqVO 下行请求 - * @param device 设备 - * @param parentDevice 父设备 - * @return 下发消息 - */ - @SuppressWarnings("unchecked") - private IotDeviceMessage invokeDeviceService(IotDeviceDownstreamReqVO downstreamReqVO, - IotDeviceDO device, IotDeviceDO parentDevice) { - // 1. 参数校验 - if (!(downstreamReqVO.getData() instanceof Map)) { - throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); - } - // TODO @super:【可优化】过滤掉不合法的服务 - - // 2. 发送请求 - String url = String.format("sys/%s/%s/thing/service/%s", - getProductKey(device, parentDevice), getDeviceName(device, parentDevice), - downstreamReqVO.getIdentifier()); - IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO() - .setParams((Map) downstreamReqVO.getData()); - CommonResult result = requestPlugin(url, reqDTO, device); - - // 3. 发送设备消息 - IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) - .setType(IotDeviceMessageTypeEnum.SERVICE.getType()).setIdentifier(reqDTO.getIdentifier()) - .setData(reqDTO.getParams()); - sendDeviceMessage(message, device, result.getCode()); - - // 4. 如果不成功,抛出异常,提示用户 - if (result.isError()) { - log.error("[invokeDeviceService][设备({})服务调用失败,请求参数:({}),响应结果:({})]", - device.getDeviceKey(), reqDTO, result); - throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); - } - return message; - } - - /** - * 设置设备属性 - * - * @param downstreamReqVO 下行请求 - * @param device 设备 - * @param parentDevice 父设备 - * @return 下发消息 - */ - @SuppressWarnings("unchecked") - private IotDeviceMessage setDeviceProperty(IotDeviceDownstreamReqVO downstreamReqVO, - IotDeviceDO device, IotDeviceDO parentDevice) { - // 1. 参数校验 - if (!(downstreamReqVO.getData() instanceof Map)) { - throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); - } - // TODO @super:【可优化】过滤掉不合法的属性 - - // 2. 发送请求 - String url = String.format("sys/%s/%s/thing/service/property/set", - getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); - IotDevicePropertySetReqDTO reqDTO = new IotDevicePropertySetReqDTO() - .setProperties((Map) downstreamReqVO.getData()); - CommonResult result = requestPlugin(url, reqDTO, device); - - // 3. 发送设备消息 - IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) - .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) - .setData(reqDTO.getProperties()); - sendDeviceMessage(message, device, result.getCode()); - - // 4. 如果不成功,抛出异常,提示用户 - if (result.isError()) { - log.error("[setDeviceProperty][设备({})属性设置失败,请求参数:({}),响应结果:({})]", - device.getDeviceKey(), reqDTO, result); - throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); - } - return message; - } - - /** - * 获取设备属性 - * - * @param downstreamReqVO 下行请求 - * @param device 设备 - * @param parentDevice 父设备 - * @return 下发消息 - */ - @SuppressWarnings("unchecked") - private IotDeviceMessage getDeviceProperty(IotDeviceDownstreamReqVO downstreamReqVO, - IotDeviceDO device, IotDeviceDO parentDevice) { - // 1. 参数校验 - if (!(downstreamReqVO.getData() instanceof List)) { - throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 List 类型"); - } - // TODO @super:【可优化】过滤掉不合法的属性 - - // 2. 发送请求 - String url = String.format("sys/%s/%s/thing/service/property/get", - getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); - IotDevicePropertyGetReqDTO reqDTO = new IotDevicePropertyGetReqDTO() - .setIdentifiers((List) downstreamReqVO.getData()); - CommonResult result = requestPlugin(url, reqDTO, device); - - // 3. 发送设备消息 - IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) - .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) - .setData(reqDTO.getIdentifiers()); - sendDeviceMessage(message, device, result.getCode()); - - // 4. 如果不成功,抛出异常,提示用户 - if (result.isError()) { - log.error("[getDeviceProperty][设备({})属性获取失败,请求参数:({}),响应结果:({})]", - device.getDeviceKey(), reqDTO, result); - throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); - } - return message; - } - - /** - * 设置设备配置 - * - * @param downstreamReqVO 下行请求 - * @param device 设备 - * @param parentDevice 父设备 - * @return 下发消息 - */ - @SuppressWarnings({ "unchecked", "unused" }) - private IotDeviceMessage setDeviceConfig(IotDeviceDownstreamReqVO downstreamReqVO, - IotDeviceDO device, IotDeviceDO parentDevice) { - // 1. 参数转换,无需校验 - Map config = JsonUtils.parseObject(device.getConfig(), Map.class); - - // 2. 发送请求 - String url = String.format("sys/%s/%s/thing/service/config/set", - getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); - IotDeviceConfigSetReqDTO reqDTO = new IotDeviceConfigSetReqDTO() - .setConfig(config); - CommonResult result = requestPlugin(url, reqDTO, device); - - // 3. 发送设备消息 - IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) - .setType(IotDeviceMessageTypeEnum.CONFIG.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.CONFIG_SET.getIdentifier()) - .setData(reqDTO.getConfig()); - sendDeviceMessage(message, device, result.getCode()); - - // 4. 如果不成功,抛出异常,提示用户 - if (result.isError()) { - log.error("[setDeviceConfig][设备({})配置下发失败,请求参数:({}),响应结果:({})]", - device.getDeviceKey(), reqDTO, result); - throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); - } - return message; - } - - /** - * 设备 OTA 升级 - * - * @param downstreamReqVO 下行请求 - * @param device 设备 - * @param parentDevice 父设备 - * @return 下发消息 - */ - private IotDeviceMessage otaUpgrade(IotDeviceDownstreamReqVO downstreamReqVO, - IotDeviceDO device, IotDeviceDO parentDevice) { - // 1. 参数校验 - if (!(downstreamReqVO.getData() instanceof Map data)) { - throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); - } - - // 2. 发送请求 - String url = String.format("ota/%s/%s/upgrade", - getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); - IotDeviceOtaUpgradeReqDTO reqDTO = IotDeviceOtaUpgradeReqDTO.build(data); - CommonResult result = requestPlugin(url, reqDTO, device); - - // 3. 发送设备消息 - IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) - .setType(IotDeviceMessageTypeEnum.OTA.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.OTA_UPGRADE.getIdentifier()) - .setData(downstreamReqVO.getData()); - sendDeviceMessage(message, device, result.getCode()); - - // 4. 如果不成功,抛出异常,提示用户 - if (result.isError()) { - log.error("[otaUpgrade][设备({}) OTA 升级失败,请求参数:({}),响应结果:({})]", - device.getDeviceKey(), reqDTO, result); - throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); - } - return message; - } - - /** - * 请求插件 - * - * @param url URL - * @param reqDTO 请求参数,只需要设置子类的参数! - * @param device 设备 - * @return 响应结果 - */ - @SuppressWarnings({ "unchecked", "HttpUrlsUsage" }) - private CommonResult requestPlugin(String url, IotDeviceDownstreamAbstractReqDTO reqDTO, - IotDeviceDO device) { - // 获得设备对应的插件实例 - IotPluginInstanceDO pluginInstance = pluginInstanceService.getPluginInstanceByDeviceKey(device.getDeviceKey()); - if (pluginInstance == null) { - throw exception(DEVICE_DOWNSTREAM_FAILED, "设备找不到对应的插件实例"); - } - - // 补充通用参数 - reqDTO.setRequestId(IdUtil.fastSimpleUUID()); - - // 执行请求 - ResponseEntity> responseEntity; - try { - responseEntity = restTemplate.postForEntity( - String.format("http://%s:%d/%s", pluginInstance.getHostIp(), pluginInstance.getDownstreamPort(), - url), - reqDTO, (Class>) (Class) CommonResult.class); - Assert.isTrue(responseEntity.getStatusCode().is2xxSuccessful(), - "HTTP 状态码不是 2xx,而是" + responseEntity.getStatusCode()); - Assert.notNull(responseEntity.getBody(), "响应结果不能为空"); - } catch (Exception ex) { - log.error("[requestPlugin][设备({}) url({}) 下行消息失败,请求参数({})]", device.getDeviceKey(), url, reqDTO, ex); - throw exception(DEVICE_DOWNSTREAM_FAILED, ExceptionUtil.getMessage(ex)); - } - return responseEntity.getBody(); - } - - private void sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device, Integer code) { - // 1. 完善消息 - message.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) - .setDeviceKey(device.getDeviceKey()) - .setTenantId(device.getTenantId()); - Assert.notNull(message.getRequestId(), "requestId 不能为空"); - if (message.getReportTime() == null) { - message.setReportTime(LocalDateTime.now()); - } - message.setCode(code); - - // 2. 发送消息 - try { - deviceProducer.sendDeviceMessage(message); - log.info("[sendDeviceMessage][message({}) 发送消息成功]", message); - } catch (Exception e) { - log.error("[sendDeviceMessage][message({}) 发送消息失败]", message, e); - } - } - - private String getDeviceName(IotDeviceDO device, IotDeviceDO parentDevice) { - return parentDevice != null ? parentDevice.getDeviceName() : device.getDeviceName(); - } - - private String getProductKey(IotDeviceDO device, IotDeviceDO parentDevice) { - return parentDevice != null ? parentDevice.getProductKey() : device.getProductKey(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java deleted file mode 100644 index dba529df2..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java +++ /dev/null @@ -1,72 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.control; - -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; -import jakarta.validation.Valid; - -/** - * IoT 设备上行 Service 接口 - * - * 目的:设备 -> 插件 -> 服务端 - * - * @author 芋道源码 - */ -public interface IotDeviceUpstreamService { - - /** - * 设备上行,可用于设备模拟 - * - * @param simulatorReqVO 设备上行请求 VO - */ - void upstreamDevice(@Valid IotDeviceUpstreamReqVO simulatorReqVO); - - /** - * 更新设备状态 - * - * @param updateReqDTO 更新设备状态 DTO - */ - void updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO); - - /** - * 上报设备属性数据 - * - * @param reportReqDTO 上报设备属性数据 DTO - */ - void reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO); - - /** - * 上报设备事件数据 - * - * @param reportReqDTO 设备事件 - */ - void reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO); - - /** - * 注册设备 - * - * @param registerReqDTO 注册设备 DTO - */ - void registerDevice(IotDeviceRegisterReqDTO registerReqDTO); - - /** - * 注册子设备 - * - * @param registerReqDTO 注册子设备 DTO - */ - void registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO); - - /** - * 添加设备拓扑 - * - * @param addReqDTO 添加设备拓扑 DTO - */ - void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO); - - /** - * Emqx 连接认证 - * - * @param authReqDTO Emqx 连接认证 DTO - */ - boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java deleted file mode 100644 index 6c80e75ac..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java +++ /dev/null @@ -1,344 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.control; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.time.LocalDateTime; -import java.util.Map; -import java.util.Objects; - -/** - * IoT 设备上行 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { - - @Resource - private IotDeviceService deviceService; - @Resource - private IotDevicePropertyService devicePropertyService; - @Resource - private IotPluginInstanceService pluginInstanceService; - - @Resource - private IotDeviceProducer deviceProducer; - - @Override - @SuppressWarnings("unchecked") - public void upstreamDevice(IotDeviceUpstreamReqVO simulatorReqVO) { - // 1. 校验存在 - IotDeviceDO device = deviceService.validateDeviceExists(simulatorReqVO.getId()); - - // 2.1 情况一:属性上报 - String requestId = IdUtil.fastSimpleUUID(); - if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) { - reportDeviceProperty(((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO() - .setRequestId(requestId).setReportTime(LocalDateTime.now()) - .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) - .setProperties((Map) simulatorReqVO.getData())); - return; - } - // 2.2 情况二:事件上报 - if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) { - reportDeviceEvent(((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId) - .setReportTime(LocalDateTime.now()) - .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) - .setIdentifier(simulatorReqVO.getIdentifier()) - .setParams((Map) simulatorReqVO.getData())); - return; - } - // 2.3 情况三:状态变更 - if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.STATE.getType())) { - updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO() - .setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) - .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) - .setState((Integer) simulatorReqVO.getData())); - return; - } - throw new IllegalArgumentException("未知的类型:" + simulatorReqVO.getType()); - } - - @Override - public void updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { - Assert.isTrue(ObjectUtils.equalsAny(updateReqDTO.getState(), - IotDeviceStateEnum.ONLINE.getState(), IotDeviceStateEnum.OFFLINE.getState()), - "状态不合法"); - // 1.1 获得设备 - log.info("[updateDeviceState][更新设备状态: {}]", updateReqDTO); - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - updateReqDTO.getProductKey(), updateReqDTO.getDeviceName()); - if (device == null) { - log.error("[updateDeviceState][设备({}/{}) 不存在]", - updateReqDTO.getProductKey(), updateReqDTO.getDeviceName()); - return; - } - TenantUtils.execute(device.getTenantId(), () -> { - // 1.2 记录设备的最后时间 - updateDeviceLastTime(device, updateReqDTO); - // 1.3 当前状态一致,不处理 - if (Objects.equals(device.getState(), updateReqDTO.getState())) { - return; - } - - // 2. 更新设备状态 - deviceService.updateDeviceState(device.getId(), updateReqDTO.getState()); - - // 3. TODO 芋艿:子设备的关联 - - // 4. 发送设备消息 - IotDeviceMessage message = BeanUtils.toBean(updateReqDTO, IotDeviceMessage.class) - .setType(IotDeviceMessageTypeEnum.STATE.getType()) - .setIdentifier(ObjUtil.equals(updateReqDTO.getState(), IotDeviceStateEnum.ONLINE.getState()) - ? IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier() - : IotDeviceMessageIdentifierEnum.STATE_OFFLINE.getIdentifier()); - sendDeviceMessage(message, device); - }); - } - - @Override - public void reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { - // 1.1 获得设备 - log.info("[reportDeviceProperty][上报设备属性: {}]", reportReqDTO); - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); - if (device == null) { - log.error("[reportDeviceProperty][设备({}/{})不存在]", - reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); - return; - } - // 1.2 记录设备的最后时间 - updateDeviceLastTime(device, reportReqDTO); - - // 2. 发送设备消息 - IotDeviceMessage message = BeanUtils.toBean(reportReqDTO, IotDeviceMessage.class) - .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()) - .setData(reportReqDTO.getProperties()); - sendDeviceMessage(message, device); - } - - @Override - public void reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { - // 1.1 获得设备 - log.info("[reportDeviceEvent][上报设备事件: {}]", reportReqDTO); - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); - if (device == null) { - log.error("[reportDeviceEvent][设备({}/{})不存在]", - reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); - return; - } - // 1.2 记录设备的最后时间 - updateDeviceLastTime(device, reportReqDTO); - - // 2. 发送设备消息 - IotDeviceMessage message = BeanUtils.toBean(reportReqDTO, IotDeviceMessage.class) - .setType(IotDeviceMessageTypeEnum.EVENT.getType()) - .setIdentifier(reportReqDTO.getIdentifier()) - .setData(reportReqDTO.getParams()); - sendDeviceMessage(message, device); - } - - @Override - public void registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { - log.info("[registerDevice][注册设备: {}]", registerReqDTO); - registerDevice0(registerReqDTO.getProductKey(), registerReqDTO.getDeviceName(), null, registerReqDTO); - } - - private void registerDevice0(String productKey, String deviceName, Long gatewayId, - IotDeviceUpstreamAbstractReqDTO registerReqDTO) { - // 1.1 注册设备 - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); - boolean registerNew = device == null; - if (device == null) { - device = deviceService.createDevice(productKey, deviceName, gatewayId); - log.info("[registerDevice0][消息({}) 设备({}/{}) 成功注册]", registerReqDTO, productKey, device); - } else if (gatewayId != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) { - Long deviceId = device.getId(); - TenantUtils.execute(device.getTenantId(), - () -> deviceService.updateDeviceGateway(deviceId, gatewayId)); - log.info("[registerDevice0][消息({}) 设备({}/{}) 更新网关设备编号({})]", - registerReqDTO, productKey, device, gatewayId); - } - // 1.2 记录设备的最后时间 - updateDeviceLastTime(device, registerReqDTO); - - // 2. 发送设备消息 - if (registerNew) { - IotDeviceMessage message = BeanUtils.toBean(registerReqDTO, IotDeviceMessage.class) - .setType(IotDeviceMessageTypeEnum.REGISTER.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.REGISTER_REGISTER.getIdentifier()); - sendDeviceMessage(message, device); - } - } - - @Override - public void registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { - // 1.1 注册子设备 - log.info("[registerSubDevice][注册子设备: {}]", registerReqDTO); - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - if (device == null) { - log.error("[registerSubDevice][设备({}/{}) 不存在]", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); - return; - } - if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { - log.error("[registerSubDevice][设备({}/{}) 不是网关设备({}),无法进行注册]", - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName(), device); - return; - } - // 1.2 记录设备的最后时间 - updateDeviceLastTime(device, registerReqDTO); - - // 2. 处理子设备 - if (CollUtil.isNotEmpty(registerReqDTO.getParams())) { - registerReqDTO.getParams().forEach(subDevice -> registerDevice0( - subDevice.getProductKey(), subDevice.getDeviceName(), device.getId(), registerReqDTO)); - // TODO @芋艿:后续要处理,每个设备是否成功 - } - - // 3. 发送设备消息 - IotDeviceMessage message = BeanUtils.toBean(registerReqDTO, IotDeviceMessage.class) - .setType(IotDeviceMessageTypeEnum.REGISTER.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.REGISTER_REGISTER_SUB.getIdentifier()) - .setData(registerReqDTO.getParams()); - sendDeviceMessage(message, device); - } - - @Override - public void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { - // 1.1 获得设备 - log.info("[addDeviceTopology][添加设备拓扑: {}]", addReqDTO); - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - addReqDTO.getProductKey(), addReqDTO.getDeviceName()); - if (device == null) { - log.error("[addDeviceTopology][设备({}/{}) 不存在]", - addReqDTO.getProductKey(), addReqDTO.getDeviceName()); - return; - } - if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { - log.error("[addDeviceTopology][设备({}/{}) 不是网关设备({}),无法进行拓扑添加]", - addReqDTO.getProductKey(), addReqDTO.getDeviceName(), device); - return; - } - // 1.2 记录设备的最后时间 - updateDeviceLastTime(device, addReqDTO); - - // 2. 处理拓扑 - if (CollUtil.isNotEmpty(addReqDTO.getParams())) { - TenantUtils.execute(device.getTenantId(), () -> { - addReqDTO.getParams().forEach(subDevice -> { - IotDeviceDO subDeviceDO = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( - subDevice.getProductKey(), subDevice.getDeviceName()); - // TODO @芋艿:后续要处理,每个设备是否成功 - if (subDeviceDO == null) { - log.error("[addDeviceTopology][子设备({}/{}) 不存在]", - subDevice.getProductKey(), subDevice.getDeviceName()); - return; - } - deviceService.updateDeviceGateway(subDeviceDO.getId(), device.getId()); - log.info("[addDeviceTopology][子设备({}/{}) 添加到网关设备({}) 成功]", - subDevice.getProductKey(), subDevice.getDeviceName(), device); - }); - }); - } - - // 3. 发送设备消息 - IotDeviceMessage message = BeanUtils.toBean(addReqDTO, IotDeviceMessage.class) - .setType(IotDeviceMessageTypeEnum.TOPOLOGY.getType()) - .setIdentifier(IotDeviceMessageIdentifierEnum.TOPOLOGY_ADD.getIdentifier()) - .setData(addReqDTO.getParams()); - sendDeviceMessage(message, device); - } - - // TODO @芋艿:后续需要考虑,http 的认证 - @Override - public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { - log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); - // 1.1 校验设备是否存在。username 格式:${DeviceName}&${ProductKey} - String[] usernameParts = authReqDTO.getUsername().split("&"); - if (usernameParts.length != 2) { - log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); - return false; - } - String deviceName = usernameParts[0]; - String productKey = usernameParts[1]; - // 1.2 获得设备 - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); - if (device == null) { - log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", productKey, deviceName); - return false; - } - // TODO @haohao:需要记录,记录设备的最后时间 - - // 2. 校验密码 - String deviceSecret = device.getDeviceSecret(); - String clientId = authReqDTO.getClientId(); - MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); - // TODO 建议,先失败,return false; - if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { - log.info("[authenticateEmqxConnection][认证成功]"); - return true; - } - log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); - return false; - } - - private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) { - // 1. 【异步】记录设备与插件实例的映射 - pluginInstanceService.updateDevicePluginInstanceProcessIdAsync(device.getDeviceKey(), reqDTO.getProcessId()); - - // 2. 【异步】更新设备的最后时间 - devicePropertyService.updateDeviceReportTimeAsync(device.getDeviceKey(), LocalDateTime.now()); - } - - private void sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { - // 1. 完善消息 - message.setDeviceKey(device.getDeviceKey()) - .setTenantId(device.getTenantId()); - if (StrUtil.isEmpty(message.getRequestId())) { - message.setRequestId(IdUtil.fastSimpleUUID()); - } - if (message.getReportTime() == null) { - message.setReportTime(LocalDateTime.now()); - } - - // 2. 发送消息 - try { - deviceProducer.sendDeviceMessage(message); - log.info("[sendDeviceMessage][message({}) 发送消息成功]", message); - } catch (Exception e) { - log.error("[sendDeviceMessage][message({}) 发送消息失败]", message, e); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java deleted file mode 100644 index b79732911..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.data; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; - -import javax.annotation.Nullable; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -/** - * IoT 设备日志数据 Service 接口 - * - * @author alwayssuper - */ -public interface IotDeviceLogService { - - /** - * 初始化 TDengine 超级表 - * - * 系统启动时,会自动初始化一次 - */ - void defineDeviceLog(); - - /** - * 插入设备日志 - * - * @param message 设备数据 - */ - void createDeviceLog(IotDeviceMessage message); - - /** - * 获得设备日志分页 - * - * @param pageReqVO 分页查询 - * @return 设备日志分页 - */ - PageResult getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO); - - /** - * 获得设备日志数量 - * - * @param createTime 创建时间,如果为空,则统计所有日志数量 - * @return 日志数量 - */ - Long getDeviceLogCount(@Nullable LocalDateTime createTime); - - // TODO @super:deviceKey 是不是用不上哈? - /** - * 获得每个小时设备上行消息数量统计 - * - * @param deviceKey 设备标识 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return key: 时间戳, value: 消息数量 - */ - List> getDeviceLogUpCountByHour(@Nullable String deviceKey, - @Nullable Long startTime, - @Nullable Long endTime); - - /** - * 获得每个小时设备下行消息数量统计 - * - * @param deviceKey 设备标识 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return key: 时间戳, value: 消息数量 - */ - List> getDeviceLogDownCountByHour(@Nullable String deviceKey, - @Nullable Long startTime, - @Nullable Long endTime); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java deleted file mode 100644 index 2ed2312bb..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java +++ /dev/null @@ -1,112 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.data; - -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceLogMapper; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * IoT 设备日志数据 Service 实现类 - * - * @author alwayssuper - */ -@Service -@Slf4j -@Validated -public class IotDeviceLogServiceImpl implements IotDeviceLogService { - - @Resource - private IotDeviceLogMapper deviceLogMapper; - - @Override - public void defineDeviceLog() { - if (StrUtil.isNotEmpty(deviceLogMapper.showDeviceLogSTable())) { - log.info("[defineDeviceLog][设备日志超级表已存在,创建跳过]"); - return; - } - - log.info("[defineDeviceLog][设备日志超级表不存在,创建开始...]"); - deviceLogMapper.createDeviceLogSTable(); - log.info("[defineDeviceLog][设备日志超级表不存在,创建成功]"); - } - - @Override - public void createDeviceLog(IotDeviceMessage message) { - IotDeviceLogDO log = BeanUtils.toBean(message, IotDeviceLogDO.class) - .setId(IdUtil.fastSimpleUUID()) - .setContent(JsonUtils.toJsonString(message.getData())); - deviceLogMapper.insert(log); - } - - @Override - public PageResult getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO) { - try { - IPage page = deviceLogMapper.selectPage( - new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); - return new PageResult<>(page.getRecords(), page.getTotal()); - } catch (Exception exception) { - if (exception.getMessage().contains("Table does not exist")) { - return PageResult.empty(); - } - throw exception; - } - } - - @Override - public Long getDeviceLogCount(LocalDateTime createTime) { - return deviceLogMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); - } - - // TODO @super:加一个参数,Boolean upstream:true 上行,false 下行,null 不过滤 - @Override - public List> getDeviceLogUpCountByHour(String deviceKey, Long startTime, Long endTime) { - // TODO @super:不能只基于数据库统计。因为有一些小时,可能出现没数据的情况,导致前端展示的图是不全的。可以参考 CrmStatisticsCustomerService 来实现 - List> list = deviceLogMapper.selectDeviceLogUpCountByHour(deviceKey, startTime, endTime); - return list.stream() - .map(map -> { - // 从Timestamp获取时间戳 - Timestamp timestamp = (Timestamp) map.get("time"); - Long timeMillis = timestamp.getTime(); - // 消息数量转换 - Integer count = ((Number) map.get("data")).intValue(); - return MapUtil.of(timeMillis, count); - }) - .collect(Collectors.toList()); - } - - // TODO @super:getDeviceLogDownCountByHour 融合到 getDeviceLogUpCountByHour - @Override - public List> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) { - List> list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime); - return list.stream() - .map(map -> { - // 从Timestamp获取时间戳 - Timestamp timestamp = (Timestamp) map.get("time"); - Long timeMillis = timestamp.getTime(); - // 消息数量转换 - Integer count = ((Number) map.get("data")).intValue(); - return MapUtil.of(timeMillis, count); - }) - .collect(Collectors.toList()); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java deleted file mode 100644 index 2f0626865..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java +++ /dev/null @@ -1,71 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device.data; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import jakarta.validation.Valid; - -import java.time.LocalDateTime; -import java.util.Map; -import java.util.Set; - -/** - * IoT 设备【属性】数据 Service 接口 - * - * @author 芋道源码 - */ -public interface IotDevicePropertyService { - - // ========== 设备属性相关操作 ========== - - /** - * 定义设备属性数据的结构 - * - * @param productId 产品编号 - */ - void defineDevicePropertyData(Long productId); - - /** - * 保存设备数据 - * - * @param message 设备消息 - */ - void saveDeviceProperty(IotDeviceMessage message); - - /** - * 获得设备属性最新数据 - * - * @param deviceId 设备编号 - * @return 设备属性最新数据 - */ - Map getLatestDeviceProperties(Long deviceId); - - /** - * 获得设备属性历史数据 - * - * @param pageReqVO 分页请求 - * @return 设备属性历史数据 - */ - PageResult getHistoryDevicePropertyPage(@Valid IotDevicePropertyHistoryPageReqVO pageReqVO); - - // ========== 设备时间相关操作 ========== - - /** - * 获得最后上报时间小于指定时间的设备标识 - * - * @param maxReportTime 最大上报时间 - * @return 设备标识列表 - */ - Set getDeviceKeysByReportTime(LocalDateTime maxReportTime); - - /** - * 异步更新设备上报时间 - * - * @param deviceKey 设备标识 - * @param reportTime 上报时间 - */ - void updateDeviceReportTimeAsync(String deviceKey, LocalDateTime reportTime); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java deleted file mode 100644 index 99e3b382a..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.ota; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import jakarta.validation.Valid; - -// TODO @li:注释写的有点冗余,可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈 -/** - * OTA 固件管理 Service - * - * @author Shelly Chan - */ -public interface IotOtaFirmwareService { - - /** - * 创建 OTA 固件 - * - * @param saveReqVO OTA固件保存请求对象,包含固件的相关信息 - * @return 返回新创建的固件的ID - */ - Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO); - - /** - * 更新 OTA 固件信息 - * - * @param updateReqVO OTA固件保存请求对象,包含需要更新的固件信息 - */ - void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO); - - /** - * 根据 ID 获取 OTA 固件信息 - * - * @param id OTA固件的唯一标识符 - * @return 返回OTA固件的详细信息对象 - */ - IotOtaFirmwareDO getOtaFirmware(Long id); - - /** - * 分页查询 OTA 固件信息 - * - * @param pageReqVO 包含分页查询条件的请求对象 - * @return 返回分页查询结果,包含固件信息列表和分页信息 - */ - PageResult getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO); - - /** - * 验证物联网 OTA 固件是否存在 - * - * @param id 固件的唯一标识符 - * 该方法用于检查系统中是否存在与给定ID关联的物联网OTA固件信息 - * 主要目的是在进行固件更新操作前,确保目标固件已经存在并可以被访问 - * 如果固件不存在,该方法可能抛出异常或返回错误信息,具体行为未定义 - */ - IotOtaFirmwareDO validateFirmwareExists(Long id); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java deleted file mode 100644 index cbf900ac0..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java +++ /dev/null @@ -1,104 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.ota; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; -import jakarta.validation.Valid; - -import java.util.List; -import java.util.Map; - -// TODO @li:注释写的有点冗余,可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈 -/** - * IotOtaUpgradeRecordService 接口定义了与物联网设备OTA升级记录相关的操作。 - * 该接口提供了创建、更新、查询、统计和重试升级记录的功能。 - */ -public interface IotOtaUpgradeRecordService { - - /** - * 批量创建 OTA 升级记录 - * 该函数用于为指定的设备列表、固件ID和升级任务ID创建OTA升级记录。 - * - * @param deviceIds 设备ID列表,表示需要升级的设备集合。 - * @param firmwareId 固件ID,表示要升级到的固件版本。 - * @param upgradeTaskId 升级任务ID,表示此次升级任务的唯一标识。 - */ - void createOtaUpgradeRecordBatch(List deviceIds, Long firmwareId, Long upgradeTaskId); - - /** - * 获取 OTA 升级记录的数量统计 - * - * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录数量 - */ - Map getOtaUpgradeRecordCount(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO); - - /** - * 获取 OTA 升级记录的统计信息。 - * - * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录统计信息 - */ - Map getOtaUpgradeRecordStatistics(Long firmwareId); - - /** - * 重试指定的 OTA 升级记录 - * - * @param id 需要重试的升级记录的ID。 - */ - void retryUpgradeRecord(Long id); - - /** - * 获取指定 ID 的 OTA 升级记录的详细信息。 - * - * @param id 需要查询的升级记录的ID。 - * @return 返回包含升级记录详细信息的响应对象。 - */ - IotOtaUpgradeRecordDO getUpgradeRecord(Long id); - - /** - * 分页查询 OTA 升级记录。 - * - * @param pageReqVO 包含分页查询条件的请求对象,必须经过验证。 - * @return 返回包含分页查询结果的响应对象。 - */ - PageResult getUpgradeRecordPage(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO); - - /** - * 根据任务 ID 取消升级记录 - *

- * 该函数用于根据给定的任务ID,取消与该任务相关的升级记录。通常用于在任务执行失败或用户手动取消时, - * 清理或标记相关的升级记录为取消状态。 - * - * @param taskId 要取消升级记录的任务ID。该ID唯一标识一个任务,通常由任务管理系统生成。 - */ - void cancelUpgradeRecordByTaskId(Long taskId); - - // TODO @li:不要的方法,可以删除下哈。 - /** - * 根据升级状态获取升级记录列表 - * - * @param state 升级状态,用于筛选符合条件的升级记录 - * @return 返回符合指定状态的升级记录列表,列表中的元素为 {@link IotOtaUpgradeRecordDO} 对象 - */ - List getUpgradeRecordListByState(Integer state); - - /** - * 更新升级记录的状态。 - *

- * 该函数用于批量更新指定升级记录的状态。通过传入的ID列表和状态值,将对应的升级记录的状态更新为指定的值。 - * - * @param ids 需要更新状态的升级记录的ID列表。列表中的每个元素代表一个升级记录的ID。 - * @param status 要更新的状态值。该值应为有效的状态标识符,通常为整数类型。 - */ - void updateUpgradeRecordStatus(List ids, Integer status); - - /** - * 根据任务ID获取升级记录列表 - *

- * 该函数通过给定的任务ID,查询并返回与该任务相关的所有升级记录。 - * - * @param taskId 任务ID,用于指定需要查询的任务 - * @return 返回一个包含升级记录的列表,列表中的每个元素为IotOtaUpgradeRecordDO对象 - */ - List getUpgradeRecordListByTaskId(Long taskId); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java deleted file mode 100644 index 02ef39cdf..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java +++ /dev/null @@ -1,229 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.ota; - -import cn.hutool.core.convert.Convert; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; -import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeRecordMapper; -import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -// TODO @li:@Service、@Validated、@Slf4j,先用关键注解;2)类注释,简单写 -@Slf4j -@Service -@Validated -public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordService { - - @Resource - private IotOtaUpgradeRecordMapper upgradeRecordMapper; - // TODO @li:1)@Resource 写在 @Lazy 之前,先用关键注解;2)有必要的情况下,在写 @Lazy 注解。 - @Lazy - @Resource - private IotDeviceService deviceService; - @Lazy - @Resource - private IotOtaFirmwareService firmwareService; - @Lazy - @Resource - private IotOtaUpgradeTaskService upgradeTaskService; - - @Override - public void createOtaUpgradeRecordBatch(List deviceIds, Long firmwareId, Long upgradeTaskId) { - // 1. 校验升级记录信息是否存在,并且已经取消的任务可以重新开始 - // TODO @li:批量查询。。 - deviceIds.forEach(deviceId -> validateUpgradeRecordDuplicate(firmwareId, upgradeTaskId, String.valueOf(deviceId))); - - // 2.初始化OTA升级记录列表信息 - IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(upgradeTaskId); - IotOtaFirmwareDO firmware = firmwareService.getOtaFirmware(firmwareId); - List deviceList = deviceService.getDeviceListByIdList(deviceIds); - List upgradeRecordList = deviceList.stream().map(device -> { - IotOtaUpgradeRecordDO upgradeRecord = new IotOtaUpgradeRecordDO(); - upgradeRecord.setFirmwareId(firmware.getId()); - upgradeRecord.setTaskId(upgradeTask.getId()); - upgradeRecord.setProductKey(device.getProductKey()); - upgradeRecord.setDeviceName(device.getDeviceName()); - upgradeRecord.setDeviceId(Convert.toStr(device.getId())); - upgradeRecord.setFromFirmwareId(Convert.toLong(device.getFirmwareId())); - upgradeRecord.setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()); - upgradeRecord.setProgress(0); - return upgradeRecord; - }).toList(); - // 3.保存数据 - upgradeRecordMapper.insertBatch(upgradeRecordList); - // TODO @芋艿:在这里需要处理推送升级任务的逻辑 - - } - - // TODO @li:1)方法注释,简单写;2)父类写了注释,子类就不用写了。。。 - /** - * 获取OTA升级记录的数量统计。 - * 该方法根据传入的查询条件,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。 - * - * @param pageReqVO 包含查询条件的请求对象,主要包括任务ID和设备名称等信息。 - * @return 返回一个Map,其中键为状态常量,值为对应状态的记录数量。 - */ - @Override - @Transactional - public Map getOtaUpgradeRecordCount(IotOtaUpgradeRecordPageReqVO pageReqVO) { - // 分别查询不同状态的OTA升级记录数量 - List> upgradeRecordCountList = upgradeRecordMapper.selectOtaUpgradeRecordCount( - pageReqVO.getTaskId(), pageReqVO.getDeviceName()); - Map upgradeRecordCountMap = ObjectUtils.defaultIfNull(upgradeRecordCountList.get(0)); - Objects.requireNonNull(upgradeRecordCountMap); - return upgradeRecordCountMap.entrySet().stream().collect(Collectors.toMap( - entry -> Convert.toInt(entry.getKey()), - entry -> Convert.toLong(entry.getValue()))); - } - - // TODO @li:1)方法注释,简单写;2)父类写了注释,子类就不用写了。。。 - /** - * 获取指定固件ID的OTA升级记录统计信息。 - * 该方法通过查询数据库,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。 - * - * @param firmwareId 固件ID,用于指定需要统计的固件升级记录。 - * @return 返回一个Map,其中键为升级记录状态(如PENDING、PUSHED等),值为对应状态的记录数量。 - */ - @Override - @Transactional - public Map getOtaUpgradeRecordStatistics(Long firmwareId) { - // 查询并统计不同状态的OTA升级记录数量 - List> upgradeRecordStatisticsList = upgradeRecordMapper.selectOtaUpgradeRecordStatistics(firmwareId); - Map upgradeRecordStatisticsMap = ObjectUtils.defaultIfNull(upgradeRecordStatisticsList.get(0)); - Objects.requireNonNull(upgradeRecordStatisticsMap); - return upgradeRecordStatisticsMap.entrySet().stream().collect(Collectors.toMap( - entry -> Convert.toInt(entry.getKey()), - entry -> Convert.toLong(entry.getValue()))); - } - - @Override - public void retryUpgradeRecord(Long id) { - // 1.1.校验升级记录信息是否存在 - IotOtaUpgradeRecordDO upgradeRecord = validateUpgradeRecordExists(id); - // 1.2.校验升级记录是否可以重新升级 - validateUpgradeRecordCanRetry(upgradeRecord); - - // 2. 将一些数据重置,这样定时任务轮询就可以重启任务 - // TODO @li:更新的时候,wherestatus; - upgradeRecordMapper.updateById(new IotOtaUpgradeRecordDO() - .setId(upgradeRecord.getId()).setProgress(0) - .setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus())); - } - - @Override - public IotOtaUpgradeRecordDO getUpgradeRecord(Long id) { - return upgradeRecordMapper.selectById(id); - } - - @Override - public PageResult getUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) { - return upgradeRecordMapper.selectUpgradeRecordPage(pageReqVO); - } - - @Override - public void cancelUpgradeRecordByTaskId(Long taskId) { - // 暂定只有待推送的升级记录可以取消 TODO @芋艿:可以看看阿里云,哪些可以取消 - upgradeRecordMapper.updateUpgradeRecordStatusByTaskIdAndStatus( - IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus(), taskId, - IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()); - } - - @Override - public List getUpgradeRecordListByState(Integer state) { - return upgradeRecordMapper.selectUpgradeRecordListByState(state); - } - - @Override - public void updateUpgradeRecordStatus(List ids, Integer status) { - upgradeRecordMapper.updateUpgradeRecordStatus(ids, status); - } - - @Override - public List getUpgradeRecordListByTaskId(Long taskId) { - return upgradeRecordMapper.selectUpgradeRecordListByTaskId(taskId); - } - - /** - * 验证指定的升级记录是否存在。 - *

- * 该函数通过给定的ID查询升级记录,如果查询结果为空,则抛出异常,表示升级记录不存在。 - * - * @param id 升级记录的唯一标识符,类型为Long。 - * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,异常类型为OTA_UPGRADE_RECORD_NOT_EXISTS。 - */ - private IotOtaUpgradeRecordDO validateUpgradeRecordExists(Long id) { - // 根据ID查询升级记录 - IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectById(id); - // 如果查询结果为空,抛出异常 - if (upgradeRecord == null) { - throw exception(OTA_UPGRADE_RECORD_NOT_EXISTS); - } - return upgradeRecord; - } - - // TODO @li:注释有点冗余 - /** - * 校验固件升级记录是否重复。 - *

- * 该函数用于检查给定的固件ID、任务ID和设备ID是否已经存在未取消的升级记录。 - * 如果存在未取消的记录,则抛出异常,提示升级记录重复。 - * - * @param firmwareId 固件ID,用于标识特定的固件版本 - * @param taskId 任务ID,用于标识特定的升级任务 - * @param deviceId 设备ID,用于标识特定的设备 - */ - private void validateUpgradeRecordDuplicate(Long firmwareId, Long taskId, String deviceId) { - // 根据条件查询升级记录 - IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectByConditions(firmwareId, taskId, deviceId); - // 如果查询到升级记录且状态不是已取消,则抛出异常 - // TODO @li:if return,减少括号层级; - // TODO @li:ObjUtil.notEquals,尽量不用 !取否逻辑; - if (upgradeRecord != null) { - if (!IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus().equals(upgradeRecord.getStatus())) { - // TODO @li:提示的时候,需要把 deviceName 给提示出来,不然用户不知道哪个重复啦。 - throw exception(OTA_UPGRADE_RECORD_DUPLICATE); - } - } - } - - // TODO @li:注释有点冗余 - /** - * 验证升级记录是否可以重试。 - *

- * 该方法用于检查给定的升级记录是否处于允许重试的状态。如果升级记录的状态为 - * PENDING、PUSHED 或 UPGRADING,则抛出异常,表示不允许重试。 - * - * @param upgradeRecord 需要验证的升级记录对象,类型为 IotOtaUpgradeRecordDO - * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出 OTA_UPGRADE_RECORD_CANNOT_RETRY 异常 - */ - // TODO @li:这种一次性的方法(不复用的),其实一步一定要抽成小方法; - private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) { - // 检查升级记录的状态是否为 PENDING、PUSHED 或 UPGRADING - if (ObjectUtils.equalsAny(upgradeRecord.getStatus(), - IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(), - IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(), - IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) { - // 如果升级记录处于上述状态之一,则抛出异常,表示不允许重试 - throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java deleted file mode 100644 index a2a810bf0..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java +++ /dev/null @@ -1,68 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.ota; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; -import jakarta.validation.Valid; - -import java.util.List; - -/** - * IoT OTA升级任务 Service 接口 - * - * @author Shelly Chan - */ -public interface IotOtaUpgradeTaskService { - - /** - * 创建OTA升级任务 - * - * @param createReqVO OTA升级任务的创建请求对象,包含创建任务所需的信息 - * @return 创建成功的OTA升级任务的ID - */ - Long createUpgradeTask(@Valid IotOtaUpgradeTaskSaveReqVO createReqVO); - - /** - * 取消OTA升级任务 - * - * @param id 要取消的OTA升级任务的ID - */ - void cancelUpgradeTask(Long id); - - /** - * 根据ID获取OTA升级任务的详细信息 - * - * @param id OTA升级任务的ID - * @return OTA升级任务的详细信息对象 - */ - IotOtaUpgradeTaskDO getUpgradeTask(Long id); - - /** - * 分页查询OTA升级任务 - * - * @param pageReqVO OTA升级任务的分页查询请求对象,包含查询条件和分页信息 - * @return 分页查询结果,包含OTA升级任务列表和总记录数 - */ - PageResult getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO); - - /** - * 根据任务状态获取升级任务列表 - * - * @param state 任务状态,用于筛选符合条件的升级任务 - * @return 返回符合指定状态的升级任务列表,列表中的元素为 IotOtaUpgradeTaskDO 对象 - */ - List getUpgradeTaskByState(Integer state); - - /** - * 更新升级任务的状态。 - *

- * 该函数用于根据任务ID更新指定升级任务的状态。通常用于在任务执行过程中 - * 更新任务的状态,例如从“进行中”变为“已完成”或“失败”。 - * - * @param id 升级任务的唯一标识符,类型为Long。不能为null。 - * @param status 要更新的任务状态,类型为Integer。通常表示任务的状态码,如0表示未开始,1表示进行中,2表示已完成等。 - */ - void updateUpgradeTaskStatus(Long id, Integer status); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java deleted file mode 100644 index cee3ba516..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java +++ /dev/null @@ -1,207 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.ota; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; -import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeTaskMapper; -import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum; -import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -// TODO @li:完善注释、注解顺序 -@Slf4j -@Service -@Validated -public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService { - - @Resource - private IotOtaUpgradeTaskMapper upgradeTaskMapper; - - @Resource - @Lazy - private IotDeviceService deviceService; - @Resource - @Lazy - private IotOtaFirmwareService firmwareService; - @Resource - @Lazy - private IotOtaUpgradeRecordService upgradeRecordService; - - @Override - @Transactional(rollbackFor = Exception.class) - public Long createUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO) { - // 1.1 校验同一固件的升级任务名称不重复 - validateFirmwareTaskDuplicate(createReqVO.getFirmwareId(), createReqVO.getName()); - // 1.2 校验固件信息是否存在 - IotOtaFirmwareDO firmware = firmwareService.validateFirmwareExists(createReqVO.getFirmwareId()); - // 1.3 补全设备范围信息,并且校验是否又设备可以升级,如果没有设备可以升级,则报错 - validateScopeAndDevice(createReqVO.getScope(), createReqVO.getDeviceIds(), firmware.getProductId()); - - // 2. 保存 OTA 升级任务信息到数据库 - IotOtaUpgradeTaskDO upgradeTask = initOtaUpgradeTask(createReqVO, firmware.getProductId()); - upgradeTaskMapper.insert(upgradeTask); - - // 3. 生成设备升级记录信息并存储,等待定时任务轮询 - upgradeRecordService.createOtaUpgradeRecordBatch(upgradeTask.getDeviceIds(), firmware.getId(), upgradeTask.getId()); - return upgradeTask.getId(); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void cancelUpgradeTask(Long id) { - // 1.1 校验升级任务是否存在 - IotOtaUpgradeTaskDO upgradeTask = validateUpgradeTaskExists(id); - // 1.2 校验升级任务是否可以取消 - // TODO @li:ObjUtil notequals - if (!Objects.equals(upgradeTask.getStatus(), IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus())) { - throw exception(OTA_UPGRADE_TASK_CANNOT_CANCEL); - } - - // 2. 更新 OTA 升级任务状态为已取消 - upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder() - .id(id).status(IotOtaUpgradeTaskStatusEnum.CANCELED.getStatus()) - .build()); - - // 3. 更新 OTA 升级记录状态为已取消 - upgradeRecordService.cancelUpgradeRecordByTaskId(id); - } - - @Override - public IotOtaUpgradeTaskDO getUpgradeTask(Long id) { - return upgradeTaskMapper.selectById(id); - } - - @Override - public PageResult getUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) { - return upgradeTaskMapper.selectUpgradeTaskPage(pageReqVO); - } - - @Override - public List getUpgradeTaskByState(Integer state) { - return upgradeTaskMapper.selectUpgradeTaskByState(state); - } - - @Override - public void updateUpgradeTaskStatus(Long id, Integer status) { - upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder().id(id).status(status).build()); - } - - // TODO @li:注释有点冗余 - /** - * 校验固件升级任务是否重复 - *

- * 该方法用于检查给定固件ID和任务名称组合是否已存在于数据库中,如果存在则抛出异常, - * 表示任务名称对于该固件而言是重复的此检查确保用户不能创建具有相同名称的任务, - * 从而避免数据重复和混淆 - * - * @param firmwareId 固件的唯一标识符,用于区分不同的固件 - * @param taskName 升级任务的名称,用于与固件ID一起检查重复性 - * @throws cn.iocoder.yudao.framework.common.exception.ServerException 则抛出预定义的异常 - */ - private void validateFirmwareTaskDuplicate(Long firmwareId, String taskName) { - // 查询数据库中是否有相同固件ID和任务名称的升级任务存在 - List upgradeTaskList = upgradeTaskMapper.selectByFirmwareIdAndName(firmwareId, taskName); - // 如果查询结果不为空,说明存在重复的任务名称,抛出异常 - if (CollUtil.isNotEmpty(upgradeTaskList)) { - throw exception(OTA_UPGRADE_TASK_NAME_DUPLICATE); - } - } - - // TODO @li:注释有点冗余 - /** - * 验证升级任务的范围和设备列表的有效性。 - *

- * 根据升级任务的范围(scope),验证设备列表(deviceIds)或产品ID(productId)是否有效。 - * 如果范围是“选择设备”(SELECT),则必须提供设备列表;如果范围是“所有设备”(ALL),则必须根据产品ID获取设备列表,并确保列表不为空。 - * - * @param scope 升级任务的范围,参考 IotOtaUpgradeTaskScopeEnum 枚举值 - * @param deviceIds 设备ID列表,当范围为“选择设备”时,该列表不能为空 - * @param productId 产品ID,当范围为“所有设备”时,用于获取设备列表 - * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,抛出相应的异常 - */ - private void validateScopeAndDevice(Integer scope, List deviceIds, String productId) { - // TODO @li:if return - // 验证范围为“选择设备”时,设备列表不能为空 - if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) { - if (CollUtil.isEmpty(deviceIds)) { - throw exception(OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY); - } - } else if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { - // 验证范围为“所有设备”时,根据产品ID获取的设备列表不能为空 - List deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId)); - if (CollUtil.isEmpty(deviceList)) { - throw exception(OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY); - } - } - } - - // TODO @li:注释有点冗余 - /** - * 验证升级任务是否存在 - *

- * 通过查询数据库来验证给定ID的升级任务是否存在此方法主要用于确保后续操作所针对的升级任务是有效的 - * - * @param id 升级任务的唯一标识符如果为null或数据库中不存在对应的记录,则认为任务不存在 - * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果升级任务不存在,则抛出异常提示任务不存在 - */ - private IotOtaUpgradeTaskDO validateUpgradeTaskExists(Long id) { - // 查询数据库中是否有相同固件ID和任务名称的升级任务存在 - IotOtaUpgradeTaskDO upgradeTask = upgradeTaskMapper.selectById(id); - // 如果查询结果不为空,说明存在重复的任务名称,抛出异常 - if (Objects.isNull(upgradeTask)) { - throw exception(OTA_UPGRADE_TASK_NOT_EXISTS); - } - return upgradeTask; - } - - // TODO @li:注释有点冗余 - /** - * 初始化升级任务 - *

- * 根据请求参数创建升级任务对象,并根据选择的范围初始化设备数量 - * 如果选择特定设备进行升级,则设备数量为所选设备的总数 - * 如果选择全部设备进行升级,则设备数量为该固件对应产品下的所有设备总数 - * - * @param createReqVO 升级任务保存请求对象,包含创建升级任务所需的信息 - * @return 返回初始化后的升级任务对象 - */ - // TODO @li:一次性的方法,不用特别抽小方法 - private IotOtaUpgradeTaskDO initOtaUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO, String productId) { - // 将请求参数转换为升级任务对象 - IotOtaUpgradeTaskDO upgradeTask = BeanUtils.toBean(createReqVO, IotOtaUpgradeTaskDO.class); - // 初始化的时候,设置设备数量和状态 - upgradeTask.setDeviceCount(Convert.toLong(CollUtil.size(createReqVO.getDeviceIds()))) - .setStatus(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus()); - // 如果选择全选,则需要查询设备数量 - if (Objects.equals(createReqVO.getScope(), IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { - // 根据产品ID查询设备数量 - List deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId)); - // 设置升级任务的设备数量 - upgradeTask.setDeviceCount((long) deviceList.size()); - upgradeTask.setDeviceIds( - deviceList.stream().map(IotDeviceDO::getId).collect(Collectors.toList())); - } - // 返回初始化后的升级任务对象 - return upgradeTask; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java deleted file mode 100644 index 8b6610f15..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java +++ /dev/null @@ -1,100 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.plugin; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotEmpty; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; - -/** - * IoT 插件配置 Service 接口 - * - * @author haohao - */ -public interface IotPluginConfigService { - - /** - * 创建插件配置 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createPluginConfig(@Valid PluginConfigSaveReqVO createReqVO); - - /** - * 更新插件配置 - * - * @param updateReqVO 更新信息 - */ - void updatePluginConfig(@Valid PluginConfigSaveReqVO updateReqVO); - - /** - * 删除插件配置 - * - * @param id 编号 - */ - void deletePluginConfig(Long id); - - /** - * 获得插件配置 - * - * @param id 编号 - * @return 插件配置 - */ - IotPluginConfigDO getPluginConfig(Long id); - - /** - * 获得插件配置分页 - * - * @param pageReqVO 分页查询 - * @return 插件配置分页 - */ - PageResult getPluginConfigPage(PluginConfigPageReqVO pageReqVO); - - /** - * 上传插件的 JAR 包 - * - * @param id 插件id - * @param file 文件 - */ - void uploadFile(Long id, MultipartFile file); - - /** - * 更新插件的状态 - * - * @param id 插件id - * @param status 状态 {@link IotPluginStatusEnum} - */ - void updatePluginStatus(Long id, Integer status); - - /** - * 获得插件配置列表 - * - * @return 插件配置列表 - */ - List getPluginConfigList(); - - /** - * 根据状态和部署类型获得插件配置列表 - * - * @param status 状态 {@link IotPluginStatusEnum} - * @param deployType 部署类型 {@link IotPluginDeployTypeEnum} - * @return 插件配置列表 - */ - List getPluginConfigListByStatusAndDeployType(Integer status, Integer deployType); - - /** - * 根据插件包标识符获取插件配置 - * - * @param pluginKey 插件包标识符 - * @return 插件配置 - */ - IotPluginConfigDO getPluginConfigByPluginKey(@NotEmpty(message = "插件包标识符不能为空") String pluginKey); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java deleted file mode 100644 index 18376bc57..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java +++ /dev/null @@ -1,188 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.plugin; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginConfigMapper; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginWrapper; -import org.pf4j.spring.SpringPluginManager; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; -/** - * IoT 插件配置 Service 实现类 - * - * @author haohao - */ -@Service -@Validated -@Slf4j -public class IotPluginConfigServiceImpl implements IotPluginConfigService { - - @Resource - private IotPluginConfigMapper pluginConfigMapper; - - @Resource - private IotPluginInstanceService pluginInstanceService; - - @Resource - private SpringPluginManager springPluginManager; - - @Override - public Long createPluginConfig(PluginConfigSaveReqVO createReqVO) { - // 1. 校验插件标识唯一性:确保没有其他配置使用相同的 pluginKey(新建时 id 为 null) - validatePluginKeyUnique(null, createReqVO.getPluginKey()); - IotPluginConfigDO pluginConfig = BeanUtils.toBean(createReqVO, IotPluginConfigDO.class); - // 2. 插入插件配置到数据库 - pluginConfigMapper.insert(pluginConfig); - return pluginConfig.getId(); - } - - @Override - public void updatePluginConfig(PluginConfigSaveReqVO updateReqVO) { - // 1. 校验插件配置是否存在:根据传入 ID 判断记录是否存在 - validatePluginConfigExists(updateReqVO.getId()); - // 2. 校验插件标识唯一性:确保更新后的 pluginKey 没有被其他记录占用 - validatePluginKeyUnique(updateReqVO.getId(), updateReqVO.getPluginKey()); - // 3. 将更新请求对象转换为插件配置数据对象 - IotPluginConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotPluginConfigDO.class); - pluginConfigMapper.updateById(updateObj); - } - - /** - * 校验插件标识唯一性 - * - * @param id 当前插件配置的 ID(如果为 null 则说明为新建操作) - * @param pluginKey 待校验的插件标识 - */ - private void validatePluginKeyUnique(Long id, String pluginKey) { - // 1. 根据 pluginKey 从数据库中查询已有的插件配置 - IotPluginConfigDO pluginConfig = pluginConfigMapper.selectByPluginKey(pluginKey); - // 2. 如果查询到记录且记录的 ID 与当前 ID 不相同,则认为存在重复,抛出异常 - if (pluginConfig != null && !pluginConfig.getId().equals(id)) { - throw exception(PLUGIN_CONFIG_KEY_DUPLICATE); - } - } - - @Override - public void deletePluginConfig(Long id) { - // 1. 校验存在 - IotPluginConfigDO pluginConfigDO = validatePluginConfigExists(id); - // 2. 未开启状态,才允许删除 - if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginConfigDO.getStatus())) { - throw exception(PLUGIN_CONFIG_DELETE_FAILED_RUNNING); - } - - // 3. 卸载插件 - pluginInstanceService.stopAndUnloadPlugin(pluginConfigDO.getPluginKey()); - // 4. 删除插件文件 - pluginInstanceService.deletePluginFile(pluginConfigDO); - - // 5. 删除插件配置 - pluginConfigMapper.deleteById(id); - } - - /** - * 校验插件配置是否存在 - * - * @param id 插件配置编号 - * @return 插件配置 - */ - private IotPluginConfigDO validatePluginConfigExists(Long id) { - IotPluginConfigDO pluginConfig = pluginConfigMapper.selectById(id); - if (pluginConfig == null) { - throw exception(PLUGIN_CONFIG_NOT_EXISTS); - } - return pluginConfig; - } - - @Override - public IotPluginConfigDO getPluginConfig(Long id) { - return pluginConfigMapper.selectById(id); - } - - @Override - public PageResult getPluginConfigPage(PluginConfigPageReqVO pageReqVO) { - return pluginConfigMapper.selectPage(pageReqVO); - } - - @Override - public void uploadFile(Long id, MultipartFile file) { - // 1. 校验插件配置是否存在 - IotPluginConfigDO pluginConfigDO = validatePluginConfigExists(id); - - // 2.1 停止并卸载旧的插件 - pluginInstanceService.stopAndUnloadPlugin(pluginConfigDO.getPluginKey()); - // 2.2 上传新的插件文件,更新插件启用状态文件 - String pluginKeyNew = pluginInstanceService.uploadAndLoadNewPlugin(file); - - // 3. 校验 file 相关参数,是否完整 - validatePluginConfigFile(pluginKeyNew); - - // 4. 更新插件配置 - IotPluginConfigDO updatedPluginConfig = new IotPluginConfigDO() - .setId(pluginConfigDO.getId()) - .setPluginKey(pluginKeyNew) - .setStatus(IotPluginStatusEnum.STOPPED.getStatus()) // TODO @haohao:这个状态,是不是非 stop 哈? - .setFileName(file.getOriginalFilename()) - .setScript("") // TODO @haohao:这个设置为 "" 会不会覆盖数据里的哈?应该从插件里读取?未来? - .setConfigSchema(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription()) - .setVersion(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion()) - .setDescription(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription()); - pluginConfigMapper.updateById(updatedPluginConfig); - } - - /** - * 校验 file 相关参数 - * - * @param pluginKeyNew 插件标识符 - */ - private void validatePluginConfigFile(String pluginKeyNew) { - // TODO @haohao:校验 file 相关参数,是否完整,类似:version 之类是不是可以解析到 - PluginWrapper plugin = springPluginManager.getPlugin(pluginKeyNew); - if (plugin == null) { - throw exception(PLUGIN_INSTALL_FAILED); - } - if (plugin.getDescriptor().getVersion() == null) { - throw exception(PLUGIN_INSTALL_FAILED); - } - } - - @Override - public void updatePluginStatus(Long id, Integer status) { - // 1. 校验插件配置是否存在 - IotPluginConfigDO pluginConfigDo = validatePluginConfigExists(id); - - // 2. 更新插件状态 - pluginInstanceService.updatePluginStatus(pluginConfigDo, status); - - // 3. 更新数据库中的插件状态 - pluginConfigMapper.updateById(new IotPluginConfigDO().setId(id).setStatus(status)); - } - - @Override - public List getPluginConfigList() { - return pluginConfigMapper.selectList(); - } - - @Override - public List getPluginConfigListByStatusAndDeployType(Integer status, Integer deployType) { - return pluginConfigMapper.selectListByStatusAndDeployType(status, deployType); - } - - @Override - public IotPluginConfigDO getPluginConfigByPluginKey(String pluginKey) { - return pluginConfigMapper.selectByPluginKey(pluginKey); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java deleted file mode 100644 index 56e1bf0f0..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.plugin; - -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; -import org.springframework.web.multipart.MultipartFile; - -import java.time.LocalDateTime; - -/** - * IoT 插件实例 Service 接口 - * - * @author 芋道源码 - */ -public interface IotPluginInstanceService { - - /** - * 心跳插件实例 - * - * @param heartbeatReqDTO 心跳插件实例 DTO - */ - void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO); - - /** - * 离线超时插件实例 - * - * @param maxHeartbeatTime 最大心跳时间 - */ - int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime); - - /** - * 停止并卸载插件 - * - * @param pluginKey 插件标识符 - */ - void stopAndUnloadPlugin(String pluginKey); - - /** - * 删除插件文件 - * - * @param pluginConfigDO 插件配置 - */ - void deletePluginFile(IotPluginConfigDO pluginConfigDO); - - /** - * 上传并加载新的插件文件 - * - * @param file 插件文件 - * @return 插件标识符 - */ - String uploadAndLoadNewPlugin(MultipartFile file); - - /** - * 更新插件状态 - * - * @param pluginConfigDO 插件配置 - * @param status 新状态 - */ - void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status); - - // ========== 设备与插件的映射操作 ========== - - /** - * 更新设备对应的插件实例的进程编号 - * - * @param deviceKey 设备 Key - * @param processId 进程编号 - */ - void updateDevicePluginInstanceProcessIdAsync(String deviceKey, String processId); - - /** - * 获得设备对应的插件实例 - * - * @param deviceKey 设备 Key - * @return 插件实例 - */ - IotPluginInstanceDO getPluginInstanceByDeviceKey(String deviceKey); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java deleted file mode 100644 index 3c15ff774..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java +++ /dev/null @@ -1,231 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.plugin; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; -import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper; -import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO; -import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants; -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginState; -import org.pf4j.PluginWrapper; -import org.pf4j.spring.SpringPluginManager; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.time.LocalDateTime; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; - -/** - * IoT 插件实例 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { - - @Resource - @Lazy // 延迟加载,避免循环依赖 - private IotPluginConfigService pluginConfigService; - - @Resource - private IotPluginInstanceMapper pluginInstanceMapper; - - @Resource - private DevicePluginProcessIdRedisDAO devicePluginProcessIdRedisDAO; - - @Resource - private SpringPluginManager pluginManager; - - @Value("${pf4j.pluginsDir}") - private String pluginsDir; - - @Override - public void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { - // 情况一:已存在,则进行更新 - IotPluginInstanceDO instance = TenantUtils.executeIgnore( - () -> pluginInstanceMapper.selectByProcessId(heartbeatReqDTO.getProcessId())); - if (instance != null) { - IotPluginInstanceDO.IotPluginInstanceDOBuilder updateObj = IotPluginInstanceDO.builder().id(instance.getId()) - .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort()) - .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now()); - if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) { - if (Boolean.FALSE.equals(instance.getOnline())) { // 当前处于离线时,才需要更新上线时间 - updateObj.onlineTime(LocalDateTime.now()); - } - } else { - updateObj.offlineTime(LocalDateTime.now()); - } - TenantUtils.execute(instance.getTenantId(), - () -> pluginInstanceMapper.updateById(updateObj.build())); - return; - } - - // 情况二:不存在,则创建 - IotPluginConfigDO info = TenantUtils.executeIgnore( - () -> pluginConfigService.getPluginConfigByPluginKey(heartbeatReqDTO.getPluginKey())); - if (info == null) { - log.error("[heartbeatPluginInstance][心跳({}) 对应的插件不存在]", heartbeatReqDTO); - return; - } - IotPluginInstanceDO.IotPluginInstanceDOBuilder insertObj = IotPluginInstanceDO.builder() - .pluginId(info.getId()).processId(heartbeatReqDTO.getProcessId()) - .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort()) - .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now()); - if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) { - insertObj.onlineTime(LocalDateTime.now()); - } else { - insertObj.offlineTime(LocalDateTime.now()); - } - TenantUtils.execute(info.getTenantId(), - () -> pluginInstanceMapper.insert(insertObj.build())); - } - - @Override - public int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime) { - List list = pluginInstanceMapper.selectListByHeartbeatTimeLt(maxHeartbeatTime); - if (CollUtil.isEmpty(list)) { - return 0; - } - - // 更新插件实例为离线 - int count = 0; - for (IotPluginInstanceDO instance : list) { - pluginInstanceMapper.updateById(IotPluginInstanceDO.builder().id(instance.getId()) - .online(false).offlineTime(LocalDateTime.now()).build()); - count++; - } - return count; - } - - @Override - public void stopAndUnloadPlugin(String pluginKey) { - PluginWrapper plugin = pluginManager.getPlugin(pluginKey); - if (plugin == null) { - log.warn("插件不存在或已卸载: {}", pluginKey); - return; - } - if (plugin.getPluginState().equals(PluginState.STARTED)) { - pluginManager.stopPlugin(pluginKey); // 停止插件 - log.info("已停止插件: {}", pluginKey); - } - pluginManager.unloadPlugin(pluginKey); // 卸载插件 - log.info("已卸载插件: {}", pluginKey); - } - - @Override - public void deletePluginFile(IotPluginConfigDO pluginConfigDO) { - File file = new File(pluginsDir, pluginConfigDO.getFileName()); - if (!file.exists()) { - return; - } - try { - TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕 - if (!file.delete()) { - log.error("[deletePluginFile][删除插件文件({}) 失败]", pluginConfigDO.getFileName()); - } - } catch (InterruptedException e) { - log.error("[deletePluginFile][删除插件文件({}) 失败]", pluginConfigDO.getFileName(), e); - } - } - - @Override - public String uploadAndLoadNewPlugin(MultipartFile file) { - String pluginKeyNew; - // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载 - Path pluginsPath = Paths.get(pluginsDir); - try { - FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录 - String filename = file.getOriginalFilename(); - if (filename != null) { - Path jarPath = pluginsPath.resolve(filename); - Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件 - pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件 - log.info("已加载插件: {}", pluginKeyNew); - } else { - throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED); - } - } catch (IOException e) { - log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e); - throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e); - } catch (Exception e) { - log.error("[uploadAndLoadNewPlugin][加载插件失败]", e); - throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e); - } - return pluginKeyNew; - } - - @Override - public void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status) { - String pluginKey = pluginConfigDO.getPluginKey(); - PluginWrapper plugin = pluginManager.getPlugin(pluginKey); - - if (plugin == null) { - // 插件不存在且状态为停止,抛出异常 - if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginConfigDO.getStatus())) { - throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID); - } - return; - } - - // 启动插件 - if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) - && plugin.getPluginState() != PluginState.STARTED) { - try { - pluginManager.startPlugin(pluginKey); - } catch (Exception e) { - log.error("[updatePluginStatus][启动插件({}) 失败]", pluginKey, e); - throw exception(ErrorCodeConstants.PLUGIN_START_FAILED, e); - } - log.info("已启动插件: {}", pluginKey); - } - // 停止插件 - else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) - && plugin.getPluginState() == PluginState.STARTED) { - try { - pluginManager.stopPlugin(pluginKey); - } catch (Exception e) { - log.error("[updatePluginStatus][停止插件({}) 失败]", pluginKey, e); - throw exception(ErrorCodeConstants.PLUGIN_STOP_FAILED, e); - } - log.info("已停止插件: {}", pluginKey); - } - } - - // ========== 设备与插件的映射操作 ========== - - @Override - public void updateDevicePluginInstanceProcessIdAsync(String deviceKey, String processId) { - devicePluginProcessIdRedisDAO.put(deviceKey, processId); - } - - @Override - public IotPluginInstanceDO getPluginInstanceByDeviceKey(String deviceKey) { - String processId = devicePluginProcessIdRedisDAO.get(deviceKey); - if (StrUtil.isEmpty(processId)) { - return null; - } - return pluginInstanceMapper.selectByProcessId(processId); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java deleted file mode 100644 index 18069376b..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import jakarta.validation.Valid; - -/** - * IoT 数据桥梁 Service 接口 - * - * @author HUIHUI - */ -public interface IotDataBridgeService { - - /** - * 创建数据桥梁 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createDataBridge(@Valid IotDataBridgeSaveReqVO createReqVO); - - /** - * 更新数据桥梁 - * - * @param updateReqVO 更新信息 - */ - void updateDataBridge(@Valid IotDataBridgeSaveReqVO updateReqVO); - - /** - * 删除数据桥梁 - * - * @param id 编号 - */ - void deleteDataBridge(Long id); - - /** - * 获得数据桥梁 - * - * @param id 编号 - * @return 数据桥梁 - */ - IotDataBridgeDO getDataBridge(Long id); - - /** - * 获得数据桥梁分页 - * - * @param pageReqVO 分页查询 - * @return 数据桥梁分页 - */ - PageResult getDataBridgePage(IotDataBridgePageReqVO pageReqVO); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java deleted file mode 100644 index 9e439fc99..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataBridgeMapper; -import jakarta.annotation.Resource; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_BRIDGE_NOT_EXISTS; - -/** - * IoT 数据桥梁 Service 实现类 - * - * @author HUIHUI - */ -@Service -@Validated -public class IotDataBridgeServiceImpl implements IotDataBridgeService { - - @Resource - private IotDataBridgeMapper dataBridgeMapper; - - @Override - public Long createDataBridge(IotDataBridgeSaveReqVO createReqVO) { - // 插入 - IotDataBridgeDO dataBridge = BeanUtils.toBean(createReqVO, IotDataBridgeDO.class); - dataBridgeMapper.insert(dataBridge); - // 返回 - return dataBridge.getId(); - } - - @Override - public void updateDataBridge(IotDataBridgeSaveReqVO updateReqVO) { - // 校验存在 - validateDataBridgeExists(updateReqVO.getId()); - // 更新 - IotDataBridgeDO updateObj = BeanUtils.toBean(updateReqVO, IotDataBridgeDO.class); - dataBridgeMapper.updateById(updateObj); - } - - @Override - public void deleteDataBridge(Long id) { - // 校验存在 - validateDataBridgeExists(id); - // 删除 - dataBridgeMapper.deleteById(id); - } - - private void validateDataBridgeExists(Long id) { - if (dataBridgeMapper.selectById(id) == null) { - throw exception(DATA_BRIDGE_NOT_EXISTS); - } - } - - @Override - public IotDataBridgeDO getDataBridge(Long id) { - return dataBridgeMapper.selectById(id); - } - - @Override - public PageResult getDataBridgePage(IotDataBridgePageReqVO pageReqVO) { - return dataBridgeMapper.selectPage(pageReqVO); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java deleted file mode 100644 index 6927b1172..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule; - -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; - -import java.util.List; - -/** - * IoT 规则场景 Service 接口 - * - * @author 芋道源码 - */ -public interface IotRuleSceneService { - - /** - * 【缓存】获得指定设备的场景列表 - * - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @return 场景列表 - */ - List getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName); - - /** - * 基于 {@link IotRuleSceneTriggerTypeEnum#DEVICE} 场景,执行规则场景 - * - * @param message 消息 - */ - void executeRuleSceneByDevice(IotDeviceMessage message); - - /** - * 基于 {@link IotRuleSceneTriggerTypeEnum#TIMER} 场景,执行规则场景 - * - * @param id 场景编号 - */ - void executeRuleSceneByTimer(Long id); - - /** - * TODO 芋艿:测试方法,需要删除 - */ - void test(); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java deleted file mode 100644 index 2219d4bad..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java +++ /dev/null @@ -1,438 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.CharPool; -import cn.hutool.core.util.NumberUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.util.number.NumberUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; -import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotRuleSceneMapper; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; -import cn.iocoder.yudao.module.iot.job.rule.IotRuleSceneJob; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.rule.action.IotRuleSceneAction; -import jakarta.annotation.Resource; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.quartz.JobKey; -import org.quartz.Scheduler; -import org.quartz.SchedulerException; -import org.quartz.TriggerKey; -import org.quartz.impl.StdSchedulerFactory; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; - -/** - * IoT 规则场景 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotRuleSceneServiceImpl implements IotRuleSceneService { - - @Resource - private IotRuleSceneMapper ruleSceneMapper; - - @Resource - private List ruleSceneActions; - - @Resource(name = "iotSchedulerManager") - private IotSchedulerManager schedulerManager; - - // TODO 芋艿,缓存待实现 - @Override - @TenantIgnore // 忽略租户隔离:因为 IotRuleSceneMessageHandler 调用时,一般未传递租户,所以需要忽略 - public List getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) { - if (true) { - IotRuleSceneDO ruleScene01 = new IotRuleSceneDO(); - ruleScene01.setTriggers(CollUtil.newArrayList()); - IotRuleSceneDO.TriggerConfig trigger01 = new IotRuleSceneDO.TriggerConfig(); - trigger01.setType(IotRuleSceneTriggerTypeEnum.DEVICE.getType()); - trigger01.setConditions(CollUtil.newArrayList()); - // 属性 - IotRuleSceneDO.TriggerCondition condition01 = new IotRuleSceneDO.TriggerCondition(); - condition01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); - condition01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()); - condition01.setParameters(CollUtil.newArrayList()); -// IotRuleSceneDO.TriggerConditionParameter parameter010 = new IotRuleSceneDO.TriggerConditionParameter(); -// parameter010.setIdentifier("width"); -// parameter010.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); -// parameter010.setValue("abc"); -// condition01.getParameters().add(parameter010); - IotRuleSceneDO.TriggerConditionParameter parameter011 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter011.setIdentifier("width"); - parameter011.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); - parameter011.setValue("1"); - condition01.getParameters().add(parameter011); - IotRuleSceneDO.TriggerConditionParameter parameter012 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter012.setIdentifier("width"); - parameter012.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.getOperator()); - parameter012.setValue("2"); - condition01.getParameters().add(parameter012); - IotRuleSceneDO.TriggerConditionParameter parameter013 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter013.setIdentifier("width"); - parameter013.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.getOperator()); - parameter013.setValue("0"); - condition01.getParameters().add(parameter013); - IotRuleSceneDO.TriggerConditionParameter parameter014 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter014.setIdentifier("width"); - parameter014.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator()); - parameter014.setValue("0"); - condition01.getParameters().add(parameter014); - IotRuleSceneDO.TriggerConditionParameter parameter015 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter015.setIdentifier("width"); - parameter015.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.getOperator()); - parameter015.setValue("2"); - condition01.getParameters().add(parameter015); - IotRuleSceneDO.TriggerConditionParameter parameter016 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter016.setIdentifier("width"); - parameter016.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.getOperator()); - parameter016.setValue("2"); - condition01.getParameters().add(parameter016); - IotRuleSceneDO.TriggerConditionParameter parameter017 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter017.setIdentifier("width"); - parameter017.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.IN.getOperator()); - parameter017.setValue("1,2,3"); - condition01.getParameters().add(parameter017); - IotRuleSceneDO.TriggerConditionParameter parameter018 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter018.setIdentifier("width"); - parameter018.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.getOperator()); - parameter018.setValue("0,2,3"); - condition01.getParameters().add(parameter018); - IotRuleSceneDO.TriggerConditionParameter parameter019 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter019.setIdentifier("width"); - parameter019.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.getOperator()); - parameter019.setValue("1,3"); - condition01.getParameters().add(parameter019); - IotRuleSceneDO.TriggerConditionParameter parameter020 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter020.setIdentifier("width"); - parameter020.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.getOperator()); - parameter020.setValue("2,3"); - condition01.getParameters().add(parameter020); - trigger01.getConditions().add(condition01); - // 状态 - IotRuleSceneDO.TriggerCondition condition02 = new IotRuleSceneDO.TriggerCondition(); - condition02.setType(IotDeviceMessageTypeEnum.STATE.getType()); - condition02.setIdentifier(IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier()); - condition02.setParameters(CollUtil.newArrayList()); - trigger01.getConditions().add(condition02); - // 事件 - IotRuleSceneDO.TriggerCondition condition03 = new IotRuleSceneDO.TriggerCondition(); - condition03.setType(IotDeviceMessageTypeEnum.EVENT.getType()); - condition03.setIdentifier("xxx"); - condition03.setParameters(CollUtil.newArrayList()); - IotRuleSceneDO.TriggerConditionParameter parameter030 = new IotRuleSceneDO.TriggerConditionParameter(); - parameter030.setIdentifier("width"); - parameter030.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); - parameter030.setValue("1"); - trigger01.getConditions().add(condition03); - ruleScene01.getTriggers().add(trigger01); - // 动作 - ruleScene01.setActions(CollUtil.newArrayList()); - // 设备控制 - IotRuleSceneDO.ActionConfig action01 = new IotRuleSceneDO.ActionConfig(); - action01.setType(IotRuleSceneActionTypeEnum.DEVICE_CONTROL.getType()); - IotRuleSceneDO.ActionDeviceControl actionDeviceControl01 = new IotRuleSceneDO.ActionDeviceControl(); - actionDeviceControl01.setProductKey("4aymZgOTOOCrDKRT"); - actionDeviceControl01.setDeviceNames(ListUtil.of("small")); - actionDeviceControl01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); - actionDeviceControl01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()); - actionDeviceControl01.setData(MapUtil.builder() - .put("power", 1) - .put("color", "red") - .build()); - action01.setDeviceControl(actionDeviceControl01); -// ruleScene01.getActions().add(action01); // TODO 芋艿:先不测试了 - // 数据桥接(http) - IotRuleSceneDO.ActionConfig action02 = new IotRuleSceneDO.ActionConfig(); - action02.setType(IotRuleSceneActionTypeEnum.DATA_BRIDGE.getType()); - action02.setDataBridgeId(1L); - ruleScene01.getActions().add(action02); - return ListUtil.toList(ruleScene01); - } - - List list = ruleSceneMapper.selectList(); - // TODO @芋艿:需要考虑开启状态 - return filterList(list, ruleScene -> { - for (IotRuleSceneDO.TriggerConfig trigger : ruleScene.getTriggers()) { - if (ObjUtil.notEqual(trigger.getProductKey(), productKey)) { - continue; - } - if (CollUtil.isEmpty(trigger.getDeviceNames()) // 无设备名称限制 - || trigger.getDeviceNames().contains(deviceName)) { // 包含设备名称 - return true; - } - } - return false; - }); - } - - @Override - public void executeRuleSceneByDevice(IotDeviceMessage message) { - TenantUtils.execute(message.getTenantId(), () -> { - // 1. 获得设备匹配的规则场景 - List ruleScenes = getMatchedRuleSceneListByMessage(message); - if (CollUtil.isEmpty(ruleScenes)) { - return; - } - - // 2. 执行规则场景 - executeRuleSceneAction(message, ruleScenes); - }); - } - - @Override - public void executeRuleSceneByTimer(Long id) { - // 1.1 获得规则场景 -// IotRuleSceneDO scene = TenantUtils.executeIgnore(() -> ruleSceneMapper.selectById(id)); - // TODO @芋艿:这里,临时测试,后续删除。 - IotRuleSceneDO scene = new IotRuleSceneDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); - if (true) { - scene.setTenantId(1L); - IotRuleSceneDO.TriggerConfig triggerConfig = new IotRuleSceneDO.TriggerConfig(); - triggerConfig.setType(IotRuleSceneTriggerTypeEnum.TIMER.getType()); - scene.setTriggers(ListUtil.toList(triggerConfig)); - // 动作 - IotRuleSceneDO.ActionConfig action01 = new IotRuleSceneDO.ActionConfig(); - action01.setType(IotRuleSceneActionTypeEnum.DEVICE_CONTROL.getType()); - IotRuleSceneDO.ActionDeviceControl actionDeviceControl01 = new IotRuleSceneDO.ActionDeviceControl(); - actionDeviceControl01.setProductKey("4aymZgOTOOCrDKRT"); - actionDeviceControl01.setDeviceNames(ListUtil.of("small")); - actionDeviceControl01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); - actionDeviceControl01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()); - actionDeviceControl01.setData(MapUtil.builder() - .put("power", 1) - .put("color", "red") - .build()); - action01.setDeviceControl(actionDeviceControl01); - scene.setActions(ListUtil.toList(action01)); - } - if (scene == null) { - log.error("[executeRuleSceneByTimer][规则场景({}) 不存在]", id); - return; - } - if (CommonStatusEnum.isDisable(scene.getStatus())) { - log.info("[executeRuleSceneByTimer][规则场景({}) 已被禁用]", id); - return; - } - // 1.2 判断是否有定时触发器,避免脏数据 - IotRuleSceneDO.TriggerConfig config = CollUtil.findOne(scene.getTriggers(), - trigger -> ObjUtil.equals(trigger.getType(), IotRuleSceneTriggerTypeEnum.TIMER.getType())); - if (config == null) { - log.error("[executeRuleSceneByTimer][规则场景({}) 不存在定时触发器]", scene); - return; - } - - // 2. 执行规则场景 - TenantUtils.execute(scene.getTenantId(), - () -> executeRuleSceneAction(null, ListUtil.toList(scene))); - } - - /** - * 基于消息,获得匹配的规则场景列表 - * - * @param message 设备消息 - * @return 规则场景列表 - */ - private List getMatchedRuleSceneListByMessage(IotDeviceMessage message) { - // 1. 匹配设备 - // TODO @芋艿:可能需要 getSelf(); 缓存 - List ruleScenes = getRuleSceneListByProductKeyAndDeviceNameFromCache( - message.getProductKey(), message.getDeviceName()); - if (CollUtil.isEmpty(ruleScenes)) { - return ruleScenes; - } - - // 2. 匹配 trigger 触发器的条件 - return filterList(ruleScenes, ruleScene -> { - for (IotRuleSceneDO.TriggerConfig trigger : ruleScene.getTriggers()) { - // 2.1 非设备触发,不匹配 - if (ObjUtil.notEqual(trigger.getType(), IotRuleSceneTriggerTypeEnum.DEVICE.getType())) { - return false; - } - // TODO 芋艿:产品、设备的匹配,要不要这里在做一次???貌似和 1. 部分重复了 - // 2.2 条件为空,说明没有匹配的条件,因此不匹配 - if (CollUtil.isEmpty(trigger.getConditions())) { - return false; - } - // 2.3 多个条件,只需要满足一个即可 - IotRuleSceneDO.TriggerCondition matchedCondition = CollUtil.findOne(trigger.getConditions(), condition -> { - if (ObjUtil.notEqual(message.getType(), condition.getType()) - || ObjUtil.notEqual(message.getIdentifier(), condition.getIdentifier())) { - return false; - } - // 多个条件参数,必须全部满足。所以,下面的逻辑就是找到一个不满足的条件参数 - IotRuleSceneDO.TriggerConditionParameter notMatchedParameter = CollUtil.findOne(condition.getParameters(), - parameter -> !isTriggerConditionParameterMatched(message, parameter, ruleScene, trigger)); - return notMatchedParameter == null; - }); - if (matchedCondition == null) { - return false; - } - log.info("[getMatchedRuleSceneList][消息({}) 匹配到规则场景编号({}) 的触发器({})]", message, ruleScene.getId(), trigger); - return true; - } - return false; - }); - } - - // TODO @芋艿:【可优化】可以考虑增加下单测,边界太多了。 - /** - * 判断触发器的条件参数是否匹配 - * - * @param message 设备消息 - * @param parameter 触发器条件参数 - * @param ruleScene 规则场景(用于日志,无其它作用) - * @param trigger 触发器(用于日志,无其它作用) - * @return 是否匹配 - */ - @SuppressWarnings({"unchecked", "DataFlowIssue"}) - private boolean isTriggerConditionParameterMatched(IotDeviceMessage message, IotRuleSceneDO.TriggerConditionParameter parameter, - IotRuleSceneDO ruleScene, IotRuleSceneDO.TriggerConfig trigger) { - // 1.1 校验操作符是否合法 - IotRuleSceneTriggerConditionParameterOperatorEnum operator = - IotRuleSceneTriggerConditionParameterOperatorEnum.operatorOf(parameter.getOperator()); - if (operator == null) { - log.error("[isTriggerConditionParameterMatched][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]", - ruleScene.getId(), trigger, parameter.getOperator()); - return false; - } - // 1.2 校验消息是否包含对应的值 - String messageValue = MapUtil.getStr((Map) message.getData(), parameter.getIdentifier()); - if (messageValue == null) { - return false; - } - - // 2.1 构建 Spring 表达式的变量 - Map springExpressionVariables = new HashMap<>(); - try { - springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, messageValue); - springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, parameter.getValue()); - List parameterValues = StrUtil.splitTrim(parameter.getValue(), CharPool.COMMA); - springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, parameterValues); - // 特殊:解决数字的比较。因为 Spring 是基于它的 compareTo 方法,对数字的比较存在问题! - if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN, - IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN, - IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN, - IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS, - IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN, - IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS) - && NumberUtil.isNumber(messageValue) - && NumberUtils.isAllNumber(parameterValues)) { - springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, - NumberUtil.parseDouble(messageValue)); - springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, - NumberUtil.parseDouble(parameter.getValue())); - springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, - convertList(parameterValues, NumberUtil::parseDouble)); - } - // 2.2 计算 Spring 表达式 - return (Boolean) SpringExpressionUtils.parseExpression(operator.getSpringExpression(), springExpressionVariables); - } catch (Exception e) { - log.error("[isTriggerConditionParameterMatched][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}/{}) 计算异常]", - message, ruleScene.getId(), trigger, operator, springExpressionVariables, e); - return false; - } - } - - /** - * 执行规则场景的动作 - * - * @param message 设备消息 - * @param ruleScenes 规则场景列表 - */ - private void executeRuleSceneAction(IotDeviceMessage message, List ruleScenes) { - // 1. 遍历规则场景 - ruleScenes.forEach(ruleScene -> { - // 2. 遍历规则场景的动作 - ruleScene.getActions().forEach(actionConfig -> { - // 3.1 获取对应的动作 Action 数组 - List actions = filterList(ruleSceneActions, - action -> action.getType().getType().equals(actionConfig.getType())); - if (CollUtil.isEmpty(actions)) { - return; - } - // 3.2 执行动作 - actions.forEach(action -> { - try { - action.execute(message, actionConfig); - log.info("[executeRuleSceneAction][消息({}) 规则场景编号({}) 的执行动作({}) 成功]", - message, ruleScene.getId(), actionConfig); - } catch (Exception e) { - log.error("[executeRuleSceneAction][消息({}) 规则场景编号({}) 的执行动作({}) 执行异常]", - message, ruleScene.getId(), actionConfig, e); - } - }); - }); - }); - } - - @Override - @SneakyThrows - public void test() { - // TODO @芋艿:测试思路代码,记得删除!!! - // 1. Job 类:IotRuleSceneJob DONE - // 2. 参数:id DONE - // 3. jobHandlerName:IotRuleSceneJob + id DONE - - // 新增:addJob - // 修改:不存在 addJob、存在 updateJob - // 有 + 禁用:1)存在、停止;2)不存在:不处理;TODO 测试:直接暂停,是否可以???(结论:可以)pauseJob - // 有 + 开启:1)存在,更新;2)不存在,新增;结论:使用 save(addOrUpdateJob) - // 无 + 禁用、开启:1)存在,删除;TODO 测试:直接删除???(结论:可以)deleteJob - - // - if (false) { - Long id = 1L; - Map jobDataMap = IotRuleSceneJob.buildJobDataMap(id); - schedulerManager.addOrUpdateJob(IotRuleSceneJob.class, - IotRuleSceneJob.buildJobName(id), - "0/10 * * * * ?", - jobDataMap); - } - if (false) { - Long id = 1L; - schedulerManager.pauseJob(IotRuleSceneJob.buildJobName(id)); - } - if (true) { - Long id = 1L; - schedulerManager.deleteJob(IotRuleSceneJob.buildJobName(id)); - } - } - - public static void main2(String[] args) throws SchedulerException { -// System.out.println(QuartzJobBean.class); - Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); - scheduler.start(); - - String jobHandlerName = "123"; - // 暂停 Trigger 对象 - scheduler.pauseTrigger(new TriggerKey(jobHandlerName)); - // 取消并删除 Job 调度 - scheduler.unscheduleJob(new TriggerKey(jobHandlerName)); - scheduler.deleteJob(new JobKey(jobHandlerName)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java deleted file mode 100644 index c7b921c04..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action; - -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; - -import javax.annotation.Nullable; - -/** - * IoT 规则场景的场景执行器接口 - * - * @author 芋道源码 - */ -public interface IotRuleSceneAction { - - // TODO @芋艿:groovy 或者 javascript 实现数据的转换;可以考虑基于 hutool 的 ScriptUtil 做 - /** - * 执行场景 - * - * @param message 消息,允许空 - * 1. 空的情况:定时触发 - * 2. 非空的情况:设备触发 - * @param config 配置 - */ - void execute(@Nullable IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) throws Exception; - - /** - * 获得类型 - * - * @return 类型 - */ - IotRuleSceneActionTypeEnum getType(); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java deleted file mode 100644 index eadc17378..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action; - -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import org.springframework.stereotype.Component; - -import javax.annotation.Nullable; - -/** - * IoT 告警的 {@link IotRuleSceneAction} 实现类 - * - * @author 芋道源码 - */ -@Component -public class IotRuleSceneAlertAction implements IotRuleSceneAction { - - @Override - public void execute(@Nullable IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) { - // TODO @芋艿:待实现 - } - - @Override - public IotRuleSceneActionTypeEnum getType() { - return IotRuleSceneActionTypeEnum.ALERT; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java deleted file mode 100644 index b38e181f9..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action; - -import cn.hutool.core.lang.Assert; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService; -import cn.iocoder.yudao.module.iot.service.rule.action.databridge.IotDataBridgeExecute; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** - * IoT 数据桥梁的 {@link IotRuleSceneAction} 实现类 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotRuleSceneDataBridgeAction implements IotRuleSceneAction { - - @Resource - private IotDataBridgeService dataBridgeService; - @Resource - private List> dataBridgeExecutes; - - @Override - public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) throws Exception { - // 1.1 如果消息为空,直接返回 - if (message == null) { - return; - } - // 1.2 获得数据桥梁 - Assert.notNull(config.getDataBridgeId(), "数据桥梁编号不能为空"); - IotDataBridgeDO dataBridge = dataBridgeService.getDataBridge(config.getDataBridgeId()); - if (dataBridge == null || dataBridge.getConfig() == null) { - log.error("[execute][message({}) config({}) 对应的数据桥梁不存在]", message, config); - return; - } - if (CommonStatusEnum.isDisable(dataBridge.getStatus())) { - log.info("[execute][message({}) config({}) 对应的数据桥梁({}) 状态为禁用]", message, config, dataBridge); - return; - } - - // 2. 执行数据桥接操作 - for (IotDataBridgeExecute execute : dataBridgeExecutes) { - execute.execute(message, dataBridge); - } - } - - @Override - public IotRuleSceneActionTypeEnum getType() { - return IotRuleSceneActionTypeEnum.DATA_BRIDGE; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java deleted file mode 100644 index d8fd76b5e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action; - -import cn.hutool.core.lang.Assert; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -/** - * IoT 设备控制的 {@link IotRuleSceneAction} 实现类 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotRuleSceneDeviceControlAction implements IotRuleSceneAction { - - @Resource - private IotDeviceDownstreamService deviceDownstreamService; - @Resource - private IotDeviceService deviceService; - - @Override - public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) { - IotRuleSceneDO.ActionDeviceControl control = config.getDeviceControl(); - Assert.notNull(control, "设备控制配置不能为空"); - // 遍历每个设备,下发消息 - control.getDeviceNames().forEach(deviceName -> { - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(control.getProductKey(), deviceName); - if (device == null) { - log.error("[execute][message({}) config({}) 对应的设备不存在]", message, config); - return; - } - try { - IotDeviceMessage downstreamMessage = deviceDownstreamService.downstreamDevice(new IotDeviceDownstreamReqVO() - .setId(device.getId()).setType(control.getType()).setIdentifier(control.getIdentifier()) - .setData(control.getData())); - log.info("[execute][message({}) config({}) 下发消息({})成功]", message, config, downstreamMessage); - } catch (Exception e) { - log.error("[execute][message({}) config({}) 下发消息失败]", message, config, e); - } - }); - } - - @Override - public IotRuleSceneActionTypeEnum getType() { - return IotRuleSceneActionTypeEnum.DEVICE_CONTROL; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java deleted file mode 100644 index ce3d0f193..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; - -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; - - -/** - * IoT 数据桥梁的执行器 execute 接口 - * - * @author HUIHUI - */ -public interface IotDataBridgeExecute { - - /** - * 获取数据桥梁类型 - * - * @return 数据桥梁类型 - */ - Integer getType(); - - /** - * 执行数据桥梁操作 - * - * @param message 设备消息 - * @param dataBridge 数据桥梁 - */ - @SuppressWarnings({"unchecked"}) - default void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) throws Exception { - // 1.1 校验数据桥梁类型 - if (!getType().equals(dataBridge.getType())) { - return; - } - - // 1.2 执行对应的数据桥梁发送消息 - execute0(message, (Config) dataBridge.getConfig()); - } - - /** - * 【真正】执行数据桥梁操作 - * - * @param message 设备消息 - * @param config 桥梁配置 - */ - void execute0(IotDeviceMessage message, Config config) throws Exception; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java deleted file mode 100644 index efe08b1fc..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; - -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRabbitMQConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; - -/** - * RabbitMQ 的 {@link IotDataBridgeExecute} 实现类 - * - * @author HUIHUI - */ -@ConditionalOnClass(name = "com.rabbitmq.client.Channel") -@Component -@Slf4j -public class IotRabbitMQDataBridgeExecute extends - AbstractCacheableDataBridgeExecute { - - - @Override - public Integer getType() { - return IotDataBridgeTypeEnum.RABBITMQ.getType(); - } - - @Override - public void execute0(IotDeviceMessage message, IotDataBridgeRabbitMQConfig config) throws Exception { - // 1. 获取或创建 Channel - Channel channel = getProducer(config); - - // 2.1 声明交换机、队列和绑定关系 - channel.exchangeDeclare(config.getExchange(), "direct", true); - channel.queueDeclare(config.getQueue(), true, false, false, null); - channel.queueBind(config.getQueue(), config.getExchange(), config.getRoutingKey()); - - // 2.2 发送消息 - channel.basicPublish(config.getExchange(), config.getRoutingKey(), null, - message.toString().getBytes(StandardCharsets.UTF_8)); - log.info("[executeRabbitMQ][message({}) config({}) 发送成功]", message, config); - } - - @Override - @SuppressWarnings("resource") - protected Channel initProducer(IotDataBridgeRabbitMQConfig config) throws Exception { - // 1. 创建连接工厂 - ConnectionFactory factory = new ConnectionFactory(); - factory.setHost(config.getHost()); - factory.setPort(config.getPort()); - factory.setVirtualHost(config.getVirtualHost()); - factory.setUsername(config.getUsername()); - factory.setPassword(config.getPassword()); - - // 2. 创建连接 - Connection connection = factory.newConnection(); - - // 3. 创建信道 - return connection.createChannel(); - } - - @Override - protected void closeProducer(Channel channel) throws Exception { - if (channel.isOpen()) { - channel.close(); - } - Connection connection = channel.getConnection(); - if (connection.isOpen()) { - connection.close(); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java deleted file mode 100644 index 2aac76619..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java +++ /dev/null @@ -1,96 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; - -import cn.hutool.core.util.ReflectUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRedisStreamMQConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.extern.slf4j.Slf4j; -import org.redisson.Redisson; -import org.redisson.api.RedissonClient; -import org.redisson.config.Config; -import org.redisson.config.SingleServerConfig; -import org.redisson.spring.data.connection.RedissonConnectionFactory; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.stream.ObjectRecord; -import org.springframework.data.redis.connection.stream.StreamRecords; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.RedisSerializer; -import org.springframework.stereotype.Component; - -/** - * Redis Stream MQ 的 {@link IotDataBridgeExecute} 实现类 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class IotRedisStreamMQDataBridgeExecute extends - AbstractCacheableDataBridgeExecute> { - - @Override - public Integer getType() { - return IotDataBridgeTypeEnum.REDIS_STREAM.getType(); - } - - @Override - public void execute0(IotDeviceMessage message, IotDataBridgeRedisStreamMQConfig config) throws Exception { - // 1. 获取 RedisTemplate - RedisTemplate redisTemplate = getProducer(config); - - // 2. 创建并发送 Stream 记录 - ObjectRecord record = StreamRecords.newRecord() - .ofObject(message).withStreamKey(config.getTopic()); - String recordId = String.valueOf(redisTemplate.opsForStream().add(record)); - log.info("[executeRedisStream][消息发送成功] messageId: {}, config: {}", recordId, config); - } - - @Override - protected RedisTemplate initProducer(IotDataBridgeRedisStreamMQConfig config) { - // 1.1 创建 Redisson 配置 - Config redissonConfig = new Config(); - SingleServerConfig serverConfig = redissonConfig.useSingleServer() - .setAddress("redis://" + config.getHost() + ":" + config.getPort()) - .setDatabase(config.getDatabase()); - // 1.2 设置密码(如果有) - if (StrUtil.isNotBlank(config.getPassword())) { - serverConfig.setPassword(config.getPassword()); - } - - // TODO @huihui:看看能不能简化一些。按道理说,不用这么多的哈。 - // 2.1 创建 RedissonClient - RedissonClient redisson = Redisson.create(redissonConfig); - // 2.2 创建并配置 RedisTemplate - RedisTemplate template = new RedisTemplate<>(); - // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 - template.setConnectionFactory(new RedissonConnectionFactory(redisson)); - // 使用 String 序列化方式,序列化 KEY 。 - template.setKeySerializer(RedisSerializer.string()); - template.setHashKeySerializer(RedisSerializer.string()); - // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 - template.setValueSerializer(buildRedisSerializer()); - template.setHashValueSerializer(buildRedisSerializer()); - template.afterPropertiesSet();// 初始化 - return template; - } - - @Override - protected void closeProducer(RedisTemplate producer) throws Exception { - RedisConnectionFactory factory = producer.getConnectionFactory(); - if (factory != null) { - ((RedissonConnectionFactory) factory).destroy(); - } - } - - // TODO @huihui:看看能不能简化一些。按道理说,不用这么多的哈。 - public static RedisSerializer buildRedisSerializer() { - RedisSerializer json = RedisSerializer.json(); - // 解决 LocalDateTime 的序列化 - ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); - objectMapper.registerModules(new JavaTimeModule()); - return json; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java deleted file mode 100644 index d3ac77227..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; - -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRocketMQConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import lombok.extern.slf4j.Slf4j; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.client.producer.SendStatus; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.stereotype.Component; - -/** - * RocketMQ 的 {@link IotDataBridgeExecute} 实现类 - * - * @author HUIHUI - */ -@ConditionalOnClass(name = "org.apache.rocketmq.client.producer.DefaultMQProducer") -@Component -@Slf4j -public class IotRocketMQDataBridgeExecute extends - AbstractCacheableDataBridgeExecute { - - @Override - public Integer getType() { - return IotDataBridgeTypeEnum.ROCKETMQ.getType(); - } - - @Override - public void execute0(IotDeviceMessage message, IotDataBridgeRocketMQConfig config) throws Exception { - // 1. 获取或创建 Producer - DefaultMQProducer producer = getProducer(config); - - // 2.1 创建消息对象,指定Topic、Tag和消息体 - Message msg = new Message( - config.getTopic(), - config.getTags(), - message.toString().getBytes(RemotingHelper.DEFAULT_CHARSET) - ); - // 2.2 发送同步消息并处理结果 - SendResult sendResult = producer.send(msg); - // 2.3 处理发送结果 - if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { - log.info("[executeRocketMQ][message({}) config({}) 发送成功,结果({})]", message, config, sendResult); - } else { - log.error("[executeRocketMQ][message({}) config({}) 发送失败,结果({})]", message, config, sendResult); - } - } - - @Override - protected DefaultMQProducer initProducer(IotDataBridgeRocketMQConfig config) throws Exception { - DefaultMQProducer producer = new DefaultMQProducer(config.getGroup()); - producer.setNamesrvAddr(config.getNameServer()); - producer.start(); - return producer; - } - - @Override - protected void closeProducer(DefaultMQProducer producer) { - producer.shutdown(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java deleted file mode 100644 index 9487ff2de..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java +++ /dev/null @@ -1,373 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.thingmodel; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelParam; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; -import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert; -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; -import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; -import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; -import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; -import cn.iocoder.yudao.module.iot.enums.thingmodel.*; -import cn.iocoder.yudao.module.iot.service.product.IotProductService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.time.LocalDateTime; -import java.util.*; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -/** - * IoT 产品物模型 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotThingModelServiceImpl implements IotThingModelService { - - @Resource - private IotThingModelMapper thingModelMapper; - - @Resource - private IotProductService productService; - - @Override - @Transactional(rollbackFor = Exception.class) - public Long createThingModel(IotThingModelSaveReqVO createReqVO) { - // 1.1 校验功能标识符在同一产品下是否唯一 - validateIdentifierUnique(null, createReqVO.getProductId(), createReqVO.getIdentifier()); - // 1.2 功能名称在同一产品下是否唯一 - validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); - // 1.3 校验产品状态,发布状态下,不允许新增功能 - validateProductStatus(createReqVO.getProductId()); - - // 2. 插入数据库 - IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(createReqVO); - thingModelMapper.insert(thingModel); - - // 3. 如果创建的是属性,需要更新默认的事件和服务 - if (Objects.equals(createReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey()); - } - - // 4. 删除缓存 - deleteThingModelListCache(createReqVO.getProductKey()); - return thingModel.getId(); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateThingModel(IotThingModelSaveReqVO updateReqVO) { - // 1.1 校验功能是否存在 - validateProductThingModelMapperExists(updateReqVO.getId()); - // 1.2 校验功能标识符是否唯一 - validateIdentifierUnique(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); - // 1.3 校验产品状态,发布状态下,不允许操作功能 - validateProductStatus(updateReqVO.getProductId()); - - // 2. 更新数据库 - IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); - thingModelMapper.updateById(thingModel); - - // 3. 如果更新的是属性,需要更新默认的事件和服务 - if (Objects.equals(updateReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey()); - } - - // 4. 删除缓存 - deleteThingModelListCache(updateReqVO.getProductKey()); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteThingModel(Long id) { - // 1.1 校验功能是否存在 - IotThingModelDO thingModel = thingModelMapper.selectById(id); - if (thingModel == null) { - throw exception(THING_MODEL_NOT_EXISTS); - } - // 1.2 校验产品状态,发布状态下,不允许操作功能 - validateProductStatus(thingModel.getProductId()); - - // 2. 删除功能 - thingModelMapper.deleteById(id); - - // 3. 如果删除的是属性,需要更新默认的事件和服务 - if (Objects.equals(thingModel.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(thingModel.getProductId(), thingModel.getProductKey()); - } - - // 4. 删除缓存 - deleteThingModelListCache(thingModel.getProductKey()); - } - - @Override - public IotThingModelDO getThingModel(Long id) { - return thingModelMapper.selectById(id); - } - - @Override - public List getThingModelListByProductId(Long productId) { - return thingModelMapper.selectListByProductId(productId); - } - - @Override - @Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") - @TenantIgnore // 忽略租户信息,跨租户 productKey 是唯一的 - public List getThingModelListByProductKeyFromCache(String productKey) { - return thingModelMapper.selectListByProductKey(productKey); - } - - @Override - public PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO) { - return thingModelMapper.selectPage(pageReqVO); - } - - @Override - public List getThingModelList(IotThingModelListReqVO reqVO) { - return thingModelMapper.selectList(reqVO); - } - - /** - * 校验功能是否存在 - * - * @param id 功能编号 - */ - private void validateProductThingModelMapperExists(Long id) { - if (thingModelMapper.selectById(id) == null) { - throw exception(THING_MODEL_NOT_EXISTS); - } - } - - private void validateIdentifierUnique(Long id, Long productId, String identifier) { - // 1.0 情况一:创建时校验 - if (id == null) { - // 1.1 系统保留字段,不能用于标识符定义 - if (StrUtil.equalsAny(identifier, "set", "get", "post", "property", "event", "time", "value")) { - throw exception(THING_MODEL_IDENTIFIER_INVALID); - } - - // 1.2 校验唯一 - IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); - if (thingModel != null) { - throw exception(THING_MODEL_IDENTIFIER_EXISTS); - } - return; - } - - // 2.0 情况二:更新时校验 - IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); - if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) { - throw exception(THING_MODEL_IDENTIFIER_EXISTS); - } - } - - private void validateProductStatus(Long createReqVO) { - IotProductDO product = productService.validateProductExists(createReqVO); - if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { - throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL); - } - } - - private void validateNameUnique(Long productId, String name) { - IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndName(productId, name); - if (thingModel != null) { - throw exception(THING_MODEL_NAME_EXISTS); - } - } - - /** - * 创建默认的事件和服务 - * - * @param productId 产品编号 - * @param productKey 产品标识 - */ - public void createDefaultEventsAndServices(Long productId, String productKey) { - // 1. 获取当前属性列表 - List properties = thingModelMapper - .selectListByProductIdAndType(productId, IotThingModelTypeEnum.PROPERTY.getType()); - - // 2. 生成新的事件和服务列表 - List newThingModels = new ArrayList<>(); - // 2.1 生成属性上报事件 - ThingModelEvent propertyPostEvent = generatePropertyPostEvent(properties); - if (propertyPostEvent != null) { - newThingModels.add(buildEventThingModel(productId, productKey, propertyPostEvent, "属性上报事件")); - } - // 2.2 生成属性设置服务 - ThingModelService propertySetService = generatePropertySetService(properties); - if (propertySetService != null) { - newThingModels.add(buildServiceThingModel(productId, productKey, propertySetService, "属性设置服务")); - } - // 2.3 生成属性获取服务 - ThingModelService propertyGetService = generatePropertyGetService(properties); - if (propertyGetService != null) { - newThingModels.add(buildServiceThingModel(productId, productKey, propertyGetService, "属性获取服务")); - } - - // 3.1 获取数据库中的默认的旧事件和服务列表 - List oldThingModels = thingModelMapper.selectListByProductIdAndIdentifiersAndTypes( - productId, - Arrays.asList("post", "set", "get"), - Arrays.asList(IotThingModelTypeEnum.EVENT.getType(), IotThingModelTypeEnum.SERVICE.getType()) - ); - // 3.2 创建默认的事件和服务 - createDefaultEventsAndServices(oldThingModels, newThingModels); - } - - /** - * 创建默认的事件和服务 - */ - private void createDefaultEventsAndServices(List oldThingModels, - List newThingModels) { - // 使用 diffList 方法比较新旧列表 - List> diffResult = diffList(oldThingModels, newThingModels, - (oldVal, newVal) -> { - // 继续使用 identifier 和 type 进行比较:这样可以准确地匹配对应的功能对象。 - boolean same = Objects.equals(oldVal.getIdentifier(), newVal.getIdentifier()) - && Objects.equals(oldVal.getType(), newVal.getType()); - if (same) { - newVal.setId(oldVal.getId()); // 设置编号 - } - return same; - }); - // 批量添加、修改、删除 - if (CollUtil.isNotEmpty(diffResult.get(0))) { - thingModelMapper.insertBatch(diffResult.get(0)); - } - if (CollUtil.isNotEmpty(diffResult.get(1))) { - thingModelMapper.updateBatch(diffResult.get(1)); - } - if (CollUtil.isNotEmpty(diffResult.get(2))) { - thingModelMapper.deleteByIds(convertSet(diffResult.get(2), IotThingModelDO::getId)); - } - } - - /** - * 构建事件功能对象 - */ - private IotThingModelDO buildEventThingModel(Long productId, String productKey, - ThingModelEvent event, String description) { - return new IotThingModelDO().setProductId(productId).setProductKey(productKey) - .setIdentifier(event.getIdentifier()).setName(event.getName()).setDescription(description) - .setType(IotThingModelTypeEnum.EVENT.getType()).setEvent(event); - } - - /** - * 构建服务功能对象 - */ - private IotThingModelDO buildServiceThingModel(Long productId, String productKey, - ThingModelService service, String description) { - return new IotThingModelDO().setProductId(productId).setProductKey(productKey) - .setIdentifier(service.getIdentifier()).setName(service.getName()).setDescription(description) - .setType(IotThingModelTypeEnum.SERVICE.getType()).setService(service); - } - - // TODO @haohao:是不是不用生成这个?目前属性上报,是个批量接口 - - /** - * 生成属性上报事件 - */ - private ThingModelEvent generatePropertyPostEvent(List thingModels) { - // 没有属性则不生成 - if (CollUtil.isEmpty(thingModels)) { - return null; - } - - // 生成属性上报事件 - return new ThingModelEvent().setIdentifier("post").setName("属性上报").setMethod("thing.event.property.post") - .setType(IotThingModelServiceEventTypeEnum.INFO.getType()) - .setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT)); - } - - // TODO @haohao:是不是不用生成这个?目前属性上报,是个批量接口 - - /** - * 生成属性设置服务 - */ - private ThingModelService generatePropertySetService(List thingModels) { - // 1.1 过滤出所有可写属性 - thingModels = filterList(thingModels, thingModel -> - IotThingModelAccessModeEnum.READ_WRITE.getMode().equals(thingModel.getProperty().getAccessMode())); - // 1.2 没有可写属性则不生成 - if (CollUtil.isEmpty(thingModels)) { - return null; - } - - // 2. 生成属性设置服务 - return new ThingModelService().setIdentifier("set").setName("属性设置").setMethod("thing.service.property.set") - .setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType()) - .setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT)) - .setOutputParams(Collections.emptyList()); // 属性设置服务一般不需要输出参数 - } - - /** - * 生成属性获取服务 - */ - private ThingModelService generatePropertyGetService(List thingModels) { - // 1.1 没有属性则不生成 - if (CollUtil.isEmpty(thingModels)) { - return null; - } - - // 1.2 生成属性获取服务 - return new ThingModelService().setIdentifier("get").setName("属性获取").setMethod("thing.service.property.get") - .setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType()) - .setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT)) - .setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT)); - } - - /** - * 构建输入/输出参数列表 - * - * @param thingModels 属性列表 - * @return 输入/输出参数列表 - */ - private List buildInputOutputParam(List thingModels, - IotThingModelParamDirectionEnum direction) { - return convertList(thingModels, thingModel -> - BeanUtils.toBean(thingModel.getProperty(), ThingModelParam.class).setParaOrder(0) // TODO @puhui999: 先搞个默认值看看怎么个事 - .setDirection(direction.getDirection())); - } - - private void deleteThingModelListCache(String productKey) { - // 保证 Spring AOP 触发 - getSelf().deleteThingModelListCache0(productKey); - } - - @CacheEvict(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") - public void deleteThingModelListCache0(String productKey) { - } - - private IotThingModelServiceImpl getSelf() { - return SpringUtil.getBean(getClass()); - } - - // TODO @super:用不到,删除下; - @Override - public Long getThingModelCount(LocalDateTime createTime) { - return thingModelMapper.selectCountByCreateTime(createTime); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java deleted file mode 100644 index 01a6dba93..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package cn.iocoder.yudao.module.iot.util; - -import cn.hutool.crypto.digest.HMac; -import cn.hutool.crypto.digest.HmacAlgorithm; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.nio.charset.StandardCharsets; - -/** - * MQTT 签名工具类 - * - * 提供静态方法来计算 MQTT 连接参数 - */ -public class MqttSignUtils { - - /** - * 计算 MQTT 连接参数 - * - * @param productKey 产品密钥 - * @param deviceName 设备名称 - * @param deviceSecret 设备密钥 - * @return 包含 clientId, username, password 的结果对象 - */ - public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { - return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName); - } - - /** - * 计算 MQTT 连接参数 - * - * @param productKey 产品密钥 - * @param deviceName 设备名称 - * @param deviceSecret 设备密钥 - * @param clientId 客户端 ID - * @return 包含 clientId, username, password 的结果对象 - */ - public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { - String username = deviceName + "&" + productKey; - // 构建签名内容 - StringBuilder signContentBuilder = new StringBuilder() - .append("clientId").append(clientId) - .append("deviceName").append(deviceName) - .append("deviceSecret").append(deviceSecret) - .append("productKey").append(productKey); - - // 使用 HMac 计算签名 - byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8); - String signContent = signContentBuilder.toString(); - HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key); - String password = mac.digestHex(signContent); - - return new MqttSignResult(clientId, username, password); - } - - /** - * MQTT 签名结果类 - */ - @Getter - @AllArgsConstructor - public static class MqttSignResult { - - private final String clientId; - private final String username; - private final String password; - - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml deleted file mode 100644 index 932a9a862..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - CREATE STABLE IF NOT EXISTS device_log ( - ts TIMESTAMP, - id NCHAR(50), - product_key NCHAR(50), - device_name NCHAR(50), - type NCHAR(50), - identifier NCHAR(255), - content NCHAR(1024), - code INT, - report_time TIMESTAMP - ) TAGS ( - device_key NCHAR(50) - ) - - - - - - INSERT INTO device_log_${deviceKey} (ts, id, product_key, device_name, type, identifier, content, code, report_time) - USING device_log - TAGS ('${deviceKey}') - VALUES ( - NOW, - #{id}, - #{productKey}, - #{deviceName}, - #{type}, - #{identifier}, - #{content}, - #{code}, - #{reportTime} - ) - - - - - - - - - - - \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml deleted file mode 100644 index 8404729cc..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java deleted file mode 100644 index 38586afdd..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; - -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.*; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -import java.time.LocalDateTime; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -/** - * {@link IotDataBridgeExecute} 实现类的测试 - * - * @author HUIHUI - */ -@Disabled // 默认禁用,需要手动启用测试 -@Slf4j -public class IotDataBridgeExecuteTest extends BaseMockitoUnitTest { - - private IotDeviceMessage message; - - @Mock - private RestTemplate restTemplate; - - @InjectMocks - private IotHttpDataBridgeExecute httpDataBridgeExecute; - - @BeforeEach - public void setUp() { - // 创建共享的测试消息 - message = IotDeviceMessage.builder().requestId("TEST-001").reportTime(LocalDateTime.now()).tenantId(1L) - .productKey("testProduct").deviceName("testDevice").deviceKey("testDeviceKey") - .type("property").identifier("temperature").data("{\"value\": 60}") - .build(); - - // 配置 RestTemplate mock 返回成功响应 - // TODO @puhui999:这个应该放到 testHttpDataBridge 里 - when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(), any(Class.class))) - .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); - } - - @Test - public void testKafkaMQDataBridge() { - // 1. 创建执行器实例 - IotKafkaMQDataBridgeExecute action = new IotKafkaMQDataBridgeExecute(); - - // 2. 创建配置 - // TODO @puhui999:可以改成链式哈。 - IotDataBridgeKafkaMQConfig config = new IotDataBridgeKafkaMQConfig(); - config.setBootstrapServers("127.0.0.1:9092"); - config.setTopic("test-topic"); - config.setSsl(false); - config.setUsername(null); - config.setPassword(null); - - // 3. 执行两次测试,验证缓存 - log.info("[testKafkaMQDataBridge][第一次执行,应该会创建新的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - - log.info("[testKafkaMQDataBridge][第二次执行,应该会复用缓存的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - } - - @Test - public void testRabbitMQDataBridge() { - // 1. 创建执行器实例 - IotRabbitMQDataBridgeExecute action = new IotRabbitMQDataBridgeExecute(); - - // 2. 创建配置 - IotDataBridgeRabbitMQConfig config = new IotDataBridgeRabbitMQConfig(); - config.setHost("localhost"); - config.setPort(5672); - config.setVirtualHost("/"); - config.setUsername("admin"); - config.setPassword("123456"); - config.setExchange("test-exchange"); - config.setRoutingKey("test-key"); - config.setQueue("test-queue"); - - // 3. 执行两次测试,验证缓存 - log.info("[testRabbitMQDataBridge][第一次执行,应该会创建新的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - - log.info("[testRabbitMQDataBridge][第二次执行,应该会复用缓存的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - } - - @Test - public void testRedisStreamMQDataBridge() { - // 1. 创建执行器实例 - IotRedisStreamMQDataBridgeExecute action = new IotRedisStreamMQDataBridgeExecute(); - - // 2. 创建配置 - IotDataBridgeRedisStreamMQConfig config = new IotDataBridgeRedisStreamMQConfig(); - config.setHost("127.0.0.1"); - config.setPort(6379); - config.setDatabase(0); - config.setPassword("123456"); - config.setTopic("test-stream"); - - // 3. 执行两次测试,验证缓存 - log.info("[testRedisStreamMQDataBridge][第一次执行,应该会创建新的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - - log.info("[testRedisStreamMQDataBridge][第二次执行,应该会复用缓存的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - } - - @Test - public void testRocketMQDataBridge() { - // 1. 创建执行器实例 - IotRocketMQDataBridgeExecute action = new IotRocketMQDataBridgeExecute(); - - // 2. 创建配置 - IotDataBridgeRocketMQConfig config = new IotDataBridgeRocketMQConfig(); - config.setNameServer("127.0.0.1:9876"); - config.setGroup("test-group"); - config.setTopic("test-topic"); - config.setTags("test-tag"); - - // 3. 执行两次测试,验证缓存 - log.info("[testRocketMQDataBridge][第一次执行,应该会创建新的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - - log.info("[testRocketMQDataBridge][第二次执行,应该会复用缓存的 producer]"); - action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); - } - - @Test - public void testHttpDataBridge() throws Exception { - // 创建配置 - IotDataBridgeHttpConfig config = new IotDataBridgeHttpConfig(); - config.setUrl("https://doc.iocoder.cn/"); - config.setMethod(HttpMethod.GET.name()); - - // 执行测试 - log.info("[testHttpDataBridge][执行HTTP数据桥接测试]"); - httpDataBridgeExecute.execute(message, new IotDataBridgeDO().setType(httpDataBridgeExecute.getType()).setConfig(config)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-core/pom.xml b/yudao-module-iot/yudao-module-iot-core/pom.xml new file mode 100644 index 000000000..bebd5c7b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/pom.xml @@ -0,0 +1,72 @@ + + + + yudao-module-iot + cn.iocoder.cloud + ${revision} + + 4.0.0 + + yudao-module-iot-core + jar + + ${project.artifactId} + + iot 模块下,提供 iot-biz 和 iot-gateway 模块的核心功能。例如说: + 1. 消息总线:跨 iot-biz 和 iot-gateway 的设备消息。可选择使用 spring event、redis stream、rocketmq、kafka、rabbitmq 等。 + 2. 查询设备信息的通用 API + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mq + + + + org.springframework.data + spring-data-redis + true + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + org.springframework.amqp + spring-rabbit + true + + + + org.springframework.kafka + spring-kafka + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java new file mode 100644 index 000000000..29d540e73 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.core.biz; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; + +/** + * IoT 设备通用 API + * + * @author haohao + */ +public interface IotDeviceCommonApi { + + /** + * 设备认证 + * + * @param authReqDTO 认证请求 + * @return 认证结果 + */ + CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO); + + /** + * 获取设备信息 + * + * @param infoReqDTO 设备信息请求 + * @return 设备信息 + */ + CommonResult getDevice(IotDeviceGetReqDTO infoReqDTO); + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java similarity index 62% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java index 8762aae5b..9e62a2fc0 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java @@ -1,17 +1,15 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; +package cn.iocoder.yudao.module.iot.core.biz.dto; import jakarta.validation.constraints.NotEmpty; import lombok.Data; -// TODO @芋艿:要不要继承 IotDeviceUpstreamAbstractReqDTO -// TODO @芋艿:@haohao:后续其它认证的设计 /** - * IoT 认证 Emqx 连接 Request DTO + * IoT 设备认证 Request DTO * * @author 芋道源码 */ @Data -public class IotDeviceEmqxAuthReqDTO { +public class IotDeviceAuthReqDTO { /** * 客户端 ID diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceGetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceGetReqDTO.java new file mode 100644 index 000000000..981509dd6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceGetReqDTO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +/** + * IoT 设备信息查询 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceGetReqDTO { + + /** + * 设备编号 + */ + private Long id; + + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java new file mode 100644 index 000000000..add116780 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceRespDTO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.iot.core.biz.dto; + +import lombok.Data; + +/** + * IoT 设备信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceRespDTO { + + /** + * 设备编号 + */ + private Long id; + /** + * 产品标识 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 租户编号 + */ + private Long tenantId; + + // ========== 产品相关字段 ========== + + /** + * 产品编号 + */ + private Long productId; + /** + * 编解码器类型 + */ + private String codecType; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java new file mode 100644 index 000000000..a66a58ac3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Set; + +/** + * IoT 设备消息的方法枚举 + * + * @author haohao + */ +@Getter +@AllArgsConstructor +public enum IotDeviceMessageMethodEnum implements ArrayValuable { + + // ========== 设备状态 ========== + + STATE_UPDATE("thing.state.update", "设备状态更新", true), + + // TODO 芋艿:要不要加个 ping 消息; + + // ========== 设备属性 ========== + // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services + + PROPERTY_POST("thing.property.post", "属性上报", true), + PROPERTY_SET("thing.property.set", "属性设置", false), + + // ========== 设备事件 ========== + // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services + + EVENT_POST("thing.event.post", "事件上报", true), + + // ========== 设备服务调用 ========== + // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services + + SERVICE_INVOKE("thing.service.invoke", "服务调用", false), + + // ========== 设备配置 ========== + // 可参考:https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 + + CONFIG_PUSH("thing.config.push", "配置推送", true), + + // ========== OTA 固件 ========== + // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates + + OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), + OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), + ; + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod) + .toArray(String[]::new); + + /** + * 不进行 reply 回复的方法集合 + */ + public static final Set REPLY_DISABLED = Set.of( + STATE_UPDATE.getMethod(), + OTA_PROGRESS.getMethod() // 参考阿里云,OTA 升级进度上报,不进行回复 + ); + + private final String method; + + private final String name; + + private final Boolean upstream; + + @Override + public String[] array() { + return ARRAYS; + } + + public static IotDeviceMessageMethodEnum of(String method) { + return ArrayUtil.firstMatch(item -> item.getMethod().equals(method), + IotDeviceMessageMethodEnum.values()); + } + + public static boolean isReplyDisabled(String method) { + return REPLY_DISABLED.contains(method); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java new file mode 100644 index 000000000..e2fe8be20 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.core.enums; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 设备消息类型枚举 + */ +@Getter +@RequiredArgsConstructor +public enum IotDeviceMessageTypeEnum implements ArrayValuable { + + STATE("state"), // 设备状态 +// PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置 + OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级 + REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册 + TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new); + + /** + * 属性 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java similarity index 85% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java index 6ce2677db..d0ff8357e 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.device; +package cn.iocoder.yudao.module.iot.core.enums; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; @@ -39,4 +39,8 @@ public enum IotDeviceStateEnum implements ArrayValuable { return ONLINE.getState().equals(state); } + public static boolean isNotOnline(Integer state) { + return !isOnline(state); + } + } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java new file mode 100644 index 000000000..67ae67399 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusAutoConfiguration.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.config; + +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; +import cn.iocoder.yudao.framework.mq.redis.core.job.RedisStreamMessageCleanupJob; +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessage; +import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.local.IotLocalMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.redis.IotRedisMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.IotRocketMQMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.redisson.api.RedissonClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * IoT 消息总线自动配置 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableConfigurationProperties(IotMessageBusProperties.class) +@Slf4j +public class IotMessageBusAutoConfiguration { + + @Bean + public IotDeviceMessageProducer deviceMessageProducer(IotMessageBus messageBus) { + return new IotDeviceMessageProducer(messageBus); + } + + // ==================== Local 实现 ==================== + + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "local", matchIfMissing = true) + public static class IotLocalMessageBusConfiguration { + + @Bean + public IotLocalMessageBus iotLocalMessageBus(ApplicationContext applicationContext) { + log.info("[iotLocalMessageBus][创建 IoT Local 消息总线]"); + return new IotLocalMessageBus(applicationContext); + } + + } + + // ==================== RocketMQ 实现 ==================== + + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "rocketmq") + @ConditionalOnClass(RocketMQTemplate.class) + public static class IotRocketMQMessageBusConfiguration { + + @Bean + public IotRocketMQMessageBus iotRocketMQMessageBus(RocketMQProperties rocketMQProperties, + RocketMQTemplate rocketMQTemplate) { + log.info("[iotRocketMQMessageBus][创建 IoT RocketMQ 消息总线]"); + return new IotRocketMQMessageBus(rocketMQProperties, rocketMQTemplate); + } + + } + + // ==================== Redis 实现 ==================== + + /** + * 特殊:由于 YudaoRedisMQConsumerAutoConfiguration 关于 Redis stream 的消费是动态注册,所以这里只能拷贝相关的逻辑!!! + * + * @see cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "redis") + @ConditionalOnClass(RedisTemplate.class) + public static class IotRedisMessageBusConfiguration { + + @Bean + public IotRedisMessageBus iotRedisMessageBus(StringRedisTemplate redisTemplate) { + log.info("[iotRedisMessageBus][创建 IoT Redis 消息总线]"); + return new IotRedisMessageBus(redisTemplate); + } + + /** + * 创建 Redis Stream 重新消费的任务 + */ + @Bean + public RedisPendingMessageResendJob iotRedisPendingMessageResendJob(IotRedisMessageBus messageBus, + RedisMQTemplate redisTemplate, + RedissonClient redissonClient) { + List> listeners = getListeners(messageBus); + return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient); + } + + /** + * 创建 Redis Stream 消息清理任务 + */ + @Bean + public RedisStreamMessageCleanupJob iotRedisStreamMessageCleanupJob(IotRedisMessageBus messageBus, + RedisMQTemplate redisTemplate, + RedissonClient redissonClient) { + List> listeners = getListeners(messageBus); + return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient); + } + + private List> getListeners(IotRedisMessageBus messageBus) { + return convertList(messageBus.getSubscribers(), subscriber -> + new AbstractRedisStreamMessageListener<>(subscriber.getTopic(), subscriber.getGroup()) { + + @Override + public void onMessage(AbstractRedisStreamMessage message) { + throw new UnsupportedOperationException("不应该调用!!!"); + } + }); + } + + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java new file mode 100644 index 000000000..501eb2b0d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * IoT 消息总线配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties("yudao.iot.message-bus") +@Data +@Validated +public class IotMessageBusProperties { + + /** + * 消息总线类型 + * + * 可选值:local、redis、rocketmq、rabbitmq + */ + @NotNull(message = "IoT 消息总线类型不能为空") + private String type = "local"; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java new file mode 100644 index 000000000..c62146761 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageBus.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core; + +/** + * IoT 消息总线接口 + * + * 用于在 IoT 系统中发布和订阅消息,支持多种消息中间件实现 + * + * @author 芋道源码 + */ +public interface IotMessageBus { + + /** + * 发布消息到消息总线 + * + * @param topic 主题 + * @param message 消息内容 + */ + void post(String topic, Object message); + + /** + * 注册消息订阅者 + * + * @param subscriber 订阅者 + */ + void register(IotMessageSubscriber subscriber); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java new file mode 100644 index 000000000..23a055325 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/IotMessageSubscriber.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core; + +/** + * IoT 消息总线订阅者接口 + * + * 用于处理从消息总线接收到的消息 + * + * @author 芋道源码 + */ +public interface IotMessageSubscriber { + + /** + * @return 主题 + */ + String getTopic(); + + /** + * @return 分组 + */ + String getGroup(); + + /** + * 处理接收到的消息 + * + * @param message 消息内容 + */ + void onMessage(T message); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessage.java new file mode 100644 index 000000000..5a9841a75 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessage.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core.local; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class IotLocalMessage { + + private String topic; + + private Object message; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessageBus.java new file mode 100644 index 000000000..1fc608bc5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/IotLocalMessageBus.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core.local; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.EventListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 本地的 {@link IotMessageBus} 实现类 + * + * 注意:仅适用于单机场景!!! + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotLocalMessageBus implements IotMessageBus { + + private final ApplicationContext applicationContext; + + /** + * 订阅者映射表 + * Key: topic + */ + private final Map>> subscribers = new HashMap<>(); + + @Override + public void post(String topic, Object message) { + applicationContext.publishEvent(new IotLocalMessage(topic, message)); + } + + @Override + public void register(IotMessageSubscriber subscriber) { + String topic = subscriber.getTopic(); + List> topicSubscribers = subscribers.computeIfAbsent(topic, k -> new ArrayList<>()); + topicSubscribers.add(subscriber); + log.info("[register][topic({}/{}) 注册消费者({})成功]", + topic, subscriber.getGroup(), subscriber.getClass().getName()); + } + + @EventListener + @SuppressWarnings({"unchecked", "rawtypes"}) + public void onMessage(IotLocalMessage message) { + String topic = message.getTopic(); + List> topicSubscribers = subscribers.get(topic); + if (CollUtil.isEmpty(topicSubscribers)) { + return; + } + for (IotMessageSubscriber subscriber : topicSubscribers) { + try { + subscriber.onMessage(message.getMessage()); + } catch (Exception ex) { + log.error("[onMessage][topic({}/{}) message({}) 消费者({}) 处理异常]", + subscriber.getTopic(), subscriber.getGroup(), message.getMessage(), subscriber.getClass().getName(), ex); + } + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/redis/IotRedisMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/redis/IotRedisMessageBus.java new file mode 100644 index 000000000..fcaed5a87 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/redis/IotRedisMessageBus.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core.redis; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration.buildConsumerName; +import static cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration.checkRedisVersion; + +/** + * Redis 的 {@link IotMessageBus} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class IotRedisMessageBus implements IotMessageBus { + + private final RedisTemplate redisTemplate; + + private final StreamMessageListenerContainer> redisStreamMessageListenerContainer; + + @Getter + private final List> subscribers = new ArrayList<>(); + + public IotRedisMessageBus(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + checkRedisVersion(redisTemplate); + // 创建 options 配置 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() + .batchSize(10) // 一次性最多拉取多少条消息 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 + .build(); + // 创建 container 对象 + this.redisStreamMessageListenerContainer = + StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions); + } + + @PostConstruct + public void init() { + this.redisStreamMessageListenerContainer.start(); + } + + @PreDestroy + public void destroy() { + this.redisStreamMessageListenerContainer.stop(); + } + + @Override + public void post(String topic, Object message) { + redisTemplate.opsForStream().add(StreamRecords.newRecord() + .ofObject(JsonUtils.toJsonString(message)) // 设置内容 + .withStreamKey(topic)); // 设置 stream key + } + + @Override + public void register(IotMessageSubscriber subscriber) { + Type type = TypeUtil.getTypeArgument(subscriber.getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + + // 创建 listener 对应的消费者分组 + try { + redisTemplate.opsForStream().createGroup(subscriber.getTopic(), subscriber.getGroup()); + } catch (Exception ignore) { + } + // 创建 Consumer 对象 + String consumerName = buildConsumerName(); + Consumer consumer = Consumer.from(subscriber.getGroup(), consumerName); + // 设置 Consumer 消费进度,以最小消费进度为准 + StreamOffset streamOffset = StreamOffset.create(subscriber.getTopic(), ReadOffset.lastConsumed()); + // 设置 Consumer 监听 + StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest + .builder(streamOffset).consumer(consumer) + .autoAcknowledge(false) // 不自动 ack + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false + redisStreamMessageListenerContainer.register(builder.build(), message -> { + // 消费消息 + subscriber.onMessage(JsonUtils.parseObject(message.getValue(), type)); + // ack 消息消费完成 + redisTemplate.opsForStream().acknowledge(subscriber.getGroup(), message); + }); + this.subscribers.add(subscriber); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/IotRocketMQMessageBus.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/IotRocketMQMessageBus.java new file mode 100644 index 000000000..48218b251 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/IotRocketMQMessageBus.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq; + +import cn.hutool.core.util.TypeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; +import org.apache.rocketmq.spring.core.RocketMQTemplate; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * 基于 RocketMQ 的 {@link IotMessageBus} 实现类 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotRocketMQMessageBus implements IotMessageBus { + + private final RocketMQProperties rocketMQProperties; + + private final RocketMQTemplate rocketMQTemplate; + + /** + * 主题对应的消费者映射 + */ + private final List topicConsumers = new ArrayList<>(); + + /** + * 销毁时关闭所有消费者 + */ + @PreDestroy + public void destroy() { + for (DefaultMQPushConsumer consumer : topicConsumers) { + try { + consumer.shutdown(); + log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup()); + } catch (Exception e) { + log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e); + } + } + } + + @Override + public void post(String topic, Object message) { + // TODO @芋艿:需要 orderly! + SendResult result = rocketMQTemplate.syncSend(topic, JsonUtils.toJsonString(message)); + log.info("[post][topic({}) 发送消息({}) result({})]", topic, message, result); + } + + @Override + @SneakyThrows + public void register(IotMessageSubscriber subscriber) { + Type type = TypeUtil.getTypeArgument(subscriber.getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + + // 1.1 创建 DefaultMQPushConsumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(); + consumer.setNamesrvAddr(rocketMQProperties.getNameServer()); + consumer.setConsumerGroup(subscriber.getGroup()); + // 1.2 订阅主题 + consumer.subscribe(subscriber.getTopic(), "*"); + // 1.3 设置消息监听器 + consumer.setMessageListener((MessageListenerConcurrently) (messages, context) -> { + for (MessageExt messageExt : messages) { + try { + byte[] body = messageExt.getBody(); + subscriber.onMessage(JsonUtils.parseObject(body, type)); + } catch (Exception ex) { + log.error("[onMessage][topic({}/{}) message({}) 消费者({}) 处理异常]", + subscriber.getTopic(), subscriber.getGroup(), messageExt, subscriber.getClass().getName(), ex); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + // 1.4 启动消费者 + consumer.start(); + + // 2. 保存消费者引用 + topicConsumers.add(consumer); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java new file mode 100644 index 000000000..6821c0d16 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java @@ -0,0 +1,151 @@ +package cn.iocoder.yudao.module.iot.core.mq.message; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * IoT 设备消息 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IotDeviceMessage { + + /** + * 【消息总线】应用的设备消息 Topic,由 iot-gateway 发给 iot-biz 进行消费 + */ + public static final String MESSAGE_BUS_DEVICE_MESSAGE_TOPIC = "iot_device_message"; + + /** + * 【消息总线】设备消息 Topic,由 iot-biz 发送给 iot-gateway 的某个 "server"(protocol) 进行消费 + * + * 其中,%s 就是该"server"(protocol) 的标识 + */ + public static final String MESSAGE_BUS_GATEWAY_DEVICE_MESSAGE_TOPIC = MESSAGE_BUS_DEVICE_MESSAGE_TOPIC + "_%s"; + + /** + * 消息编号 + * + * 由后端生成,通过 {@link IotDeviceMessageUtils#generateMessageId()} + */ + private String id; + /** + * 上报时间 + * + * 由后端生成,当前时间 + */ + private LocalDateTime reportTime; + + /** + * 设备编号 + */ + private Long deviceId; + /** + * 租户编号 + */ + private Long tenantId; + + /** + * 服务编号,该消息由哪个 server 发送 + */ + private String serverId; + + // ========== codec(编解码)字段 ========== + + /** + * 请求编号 + * + * 由设备生成,对应阿里云 IoT 的 Alink 协议中的 id、华为云 IoTDA 协议的 request_id + */ + private String requestId; + /** + * 请求方法 + * + * 枚举 {@link IotDeviceMessageMethodEnum} + * 例如说:thing.property.report 属性上报 + */ + private String method; + /** + * 请求参数 + * + * 例如说:属性上报的 properties、事件上报的 params + */ + private Object params; + /** + * 响应结果 + */ + private Object data; + /** + * 响应错误码 + */ + private Integer code; + /** + * 返回结果信息 + */ + private String msg; + + // ========== 基础方法:只传递"codec(编解码)字段" ========== + + public static IotDeviceMessage requestOf(String method) { + return requestOf(null, method, null); + } + + public static IotDeviceMessage requestOf(String method, Object params) { + return requestOf(null, method, params); + } + + public static IotDeviceMessage requestOf(String requestId, String method, Object params) { + return of(requestId, method, params, null, null, null); + } + + public static IotDeviceMessage replyOf(String requestId, String method, + Object data, Integer code, String msg) { + if (code == null) { + code = GlobalErrorCodeConstants.SUCCESS.getCode(); + msg = GlobalErrorCodeConstants.SUCCESS.getMsg(); + } + return of(requestId, method, null, data, code, msg); + } + + public static IotDeviceMessage of(String requestId, String method, + Object params, Object data, Integer code, String msg) { + // 通用参数 + IotDeviceMessage message = new IotDeviceMessage() + .setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now()); + // 当前参数 + message.setRequestId(requestId).setMethod(method).setParams(params) + .setData(data).setCode(code).setMsg(msg); + return message; + } + + // ========== 核心方法:在 of 基础方法之上,添加对应 method ========== + + public static IotDeviceMessage buildStateUpdateOnline() { + return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), + MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState())); + } + + public static IotDeviceMessage buildStateOffline() { + return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(), + MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState())); + } + + public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize, + String fileDigestAlgorithm, String fileDigestValue) { + return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder() + .put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize) + .put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue) + .build()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/producer/IotDeviceMessageProducer.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/producer/IotDeviceMessageProducer.java new file mode 100644 index 000000000..e15241723 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/producer/IotDeviceMessageProducer.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.core.mq.producer; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import lombok.RequiredArgsConstructor; + +/** + * IoT 设备消息生产者 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class IotDeviceMessageProducer { + + private final IotMessageBus messageBus; + + /** + * 发送设备消息 + * + * @param message 设备消息 + */ + public void sendDeviceMessage(IotDeviceMessage message) { + messageBus.post(IotDeviceMessage.MESSAGE_BUS_DEVICE_MESSAGE_TOPIC, message); + } + + /** + * 发送网关设备消息 + * + * @param serverId 网关的 serverId 标识 + * @param message 设备消息 + */ + public void sendDeviceMessageToGateway(String serverId, IotDeviceMessage message) { + messageBus.post(IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(serverId), message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java new file mode 100644 index 000000000..2bc488007 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.core.util; + +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备【认证】的工具类,参考阿里云 + * + * @see 如何计算 MQTT 签名参数 + */ +public class IotDeviceAuthUtils { + + /** + * 认证信息 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class AuthInfo { + + /** + * 客户端 ID + */ + private String clientId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + } + + /** + * 设备信息 + */ + @Data + public static class DeviceInfo { + + private String productKey; + + private String deviceName; + + } + + public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) { + String clientId = buildClientId(productKey, deviceName); + String username = buildUsername(productKey, deviceName); + String content = "clientId" + clientId + + "deviceName" + deviceName + + "deviceSecret" + deviceSecret + + "productKey" + productKey; + String password = buildPassword(deviceSecret, content); + return new AuthInfo(clientId, username, password); + } + + private static String buildClientId(String productKey, String deviceName) { + return String.format("%s.%s", productKey, deviceName); + } + + private static String buildUsername(String productKey, String deviceName) { + return String.format("%s&%s", deviceName, productKey); + } + + private static String buildPassword(String deviceSecret, String content) { + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes()) + .digestHex(content); + } + + public static DeviceInfo parseUsername(String username) { + String[] usernameParts = username.split("&"); + if (usernameParts.length != 2) { + return null; + } + return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java new file mode 100644 index 000000000..5b7778ea0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.iot.core.util; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; + +import java.util.Map; + +/** + * IoT 设备【消息】的工具类 + * + * @author 芋道源码 + */ +public class IotDeviceMessageUtils { + + // ========== Message 相关 ========== + + public static String generateMessageId() { + return IdUtil.fastSimpleUUID(); + } + + /** + * 是否是上行消息:由设备发送 + * + * @param message 消息 + * @return 是否 + */ + @SuppressWarnings("SimplifiableConditionalExpression") + public static boolean isUpstreamMessage(IotDeviceMessage message) { + IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(message.getMethod()); + Assert.notNull(methodEnum, "无法识别的消息方法:" + message.getMethod()); + // 注意:回复消息时,需要取反 + return !isReplyMessage(message) ? methodEnum.getUpstream() : !methodEnum.getUpstream(); + } + + /** + * 是否是回复消息,通过 {@link IotDeviceMessage#getCode()} 非空进行识别 + * + * @param message 消息 + * @return 是否 + */ + public static boolean isReplyMessage(IotDeviceMessage message) { + return message.getCode() != null; + } + + /** + * 提取消息中的标识符 + * + * @param message 消息 + * @return 标识符 + */ + @SuppressWarnings("unchecked") + public static String getIdentifier(IotDeviceMessage message) { + if (message.getParams() == null) { + return null; + } + if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.EVENT_POST.getMethod(), + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod())) { + Map params = (Map) message.getParams(); + return MapUtil.getStr(params, "identifier"); + } else if (StrUtil.equalsAny(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { + Map params = (Map) message.getParams(); + return MapUtil.getStr(params, "state"); + } + return null; + } + + // ========== Topic 相关 ========== + + public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) { + return String.format(IotDeviceMessage.MESSAGE_BUS_GATEWAY_DEVICE_MESSAGE_TOPIC, serverId); + } + + /** + * 生成服务器编号 + * + * @param serverPort 服务器端口 + * @return 服务器编号 + */ + public static String generateServerId(Integer serverPort) { + String serverId = String.format("%s.%d", SystemUtil.getHostInfo().getAddress(), serverPort); + // 避免一些场景无法使用 . 符号,例如说 RocketMQ Topic + return serverId.replaceAll("\\.", "_"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-iot/yudao-module-iot-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..4c183f822 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.module.iot.core.messagebus.config.IotMessageBusAutoConfiguration \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/TestMessage.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/TestMessage.java new file mode 100644 index 000000000..e06c9ec04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/TestMessage.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core; + +import lombok.Data; + +@Data +public class TestMessage { + + private String nickname; + + private Integer age; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessageBusIntegrationTest.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessageBusIntegrationTest.java new file mode 100644 index 000000000..b282bc89e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/local/LocalIotMessageBusIntegrationTest.java @@ -0,0 +1,177 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core.local; + +import cn.iocoder.yudao.module.iot.core.messagebus.config.IotMessageBusAutoConfiguration; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link IotLocalMessageBus} 集成测试 + * + * @author 芋道源码 + */ +@SpringBootTest(classes = LocalIotMessageBusIntegrationTest.class) +@Import(IotMessageBusAutoConfiguration.class) +@TestPropertySource(properties = { + "yudao.iot.message-bus.type=local" +}) +@Slf4j +public class LocalIotMessageBusIntegrationTest { + + @Resource + private IotMessageBus messageBus; + + /** + * 1 topic 2 subscriber + */ + @Test + public void testSendMessageWithTwoSubscribers() throws InterruptedException { + // 准备 + String topic = "test-topic"; + String testMessage = "Hello IoT Message Bus!"; + // 用于等待消息处理完成 + CountDownLatch latch = new CountDownLatch(2); + // 用于记录接收到的消息 + AtomicInteger subscriber1Count = new AtomicInteger(0); + AtomicInteger subscriber2Count = new AtomicInteger(0); + + // 创建第一个订阅者 + IotMessageSubscriber subscriber1 = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic; + } + + @Override + public String getGroup() { + return "group1"; + } + + @Override + public void onMessage(String message) { + log.info("[订阅者1] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriber1Count.incrementAndGet(); + assertEquals(testMessage, message); + latch.countDown(); + } + + }; + // 创建第二个订阅者 + IotMessageSubscriber subscriber2 = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic; + } + + @Override + public String getGroup() { + return "group2"; + } + + @Override + public void onMessage(String message) { + log.info("[订阅者2] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriber2Count.incrementAndGet(); + assertEquals(testMessage, message); + latch.countDown(); + } + + }; + // 注册订阅者 + messageBus.register(subscriber1); + messageBus.register(subscriber2); + + // 发送消息 + log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage); + messageBus.post(topic, testMessage); + // 等待消息处理完成(最多等待 10 秒) + boolean completed = latch.await(10, TimeUnit.SECONDS); + + // 验证结果 + assertTrue(completed, "消息处理超时"); + assertEquals(1, subscriber1Count.get(), "订阅者 1 应该收到 1 条消息"); + assertEquals(1, subscriber2Count.get(), "订阅者 2 应该收到 1 条消息"); + log.info("[测试] 测试完成 - 订阅者 1 收到{}条消息,订阅者 2 收到{}条消息", subscriber1Count.get(), subscriber2Count.get()); + } + + /** + * 2 topic 2 subscriber + */ + @Test + public void testMultipleTopics() throws InterruptedException { + // 准备 + String topic1 = "device-status"; + String topic2 = "device-data"; + String message1 = "设备在线"; + String message2 = "温度:25°C"; + CountDownLatch latch = new CountDownLatch(2); + + // 创建订阅者 1 - 只订阅设备状态 + IotMessageSubscriber statusSubscriber = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic1; + } + + @Override + public String getGroup() { + return "status-group"; + } + + @Override + public void onMessage(String message) { + log.info("[状态订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + assertEquals(message1, message); + latch.countDown(); + } + + }; + // 创建订阅者 2 - 只订阅设备数据 + IotMessageSubscriber dataSubscriber = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic2; + } + + @Override + public String getGroup() { + return "data-group"; + } + + @Override + public void onMessage(String message) { + log.info("[数据订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + assertEquals(message2, message); + latch.countDown(); + } + + }; + // 注册订阅者到不同主题 + messageBus.register(statusSubscriber); + messageBus.register(dataSubscriber); + + // 发送消息到不同主题 + messageBus.post(topic1, message1); + messageBus.post(topic2, message2); + // 等待消息处理完成 + boolean completed = latch.await(10, TimeUnit.SECONDS); + assertTrue(completed, "消息处理超时"); + log.info("[测试] 多主题测试完成"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/RocketMQIotMessageBusTest.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/RocketMQIotMessageBusTest.java new file mode 100644 index 000000000..b7270f2fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/messagebus/core/rocketmq/RocketMQIotMessageBusTest.java @@ -0,0 +1,268 @@ +package cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq; + +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.iot.core.messagebus.config.IotMessageBusAutoConfiguration; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.messagebus.core.TestMessage; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link IotRocketMQMessageBus} 集成测试 + * + * @author 芋道源码 + */ +@SpringBootTest(classes = RocketMQIotMessageBusTest.class) +@Import({RocketMQAutoConfiguration.class, IotMessageBusAutoConfiguration.class}) +@TestPropertySource(properties = { + "yudao.iot.message-bus.type=rocketmq", + "rocketmq.name-server=127.0.0.1:9876", + "rocketmq.producer.group=test-rocketmq-group", + "rocketmq.producer.send-message-timeout=10000" +}) +@Slf4j +public class RocketMQIotMessageBusTest { + + @Resource + private IotMessageBus messageBus; + + /** + * 1 topic 1 subscriber(string) + */ + @Test + public void testSendMessageWithOneSubscriber() throws InterruptedException { + // 准备 + String topic = "test-topic-" + IdUtil.simpleUUID(); +// String topic = "test-topic-pojo"; + String testMessage = "Hello IoT Message Bus!"; + // 用于等待消息处理完成 + CountDownLatch latch = new CountDownLatch(1); + // 用于记录接收到的消息 + AtomicInteger subscriberCount = new AtomicInteger(0); + AtomicReference subscriberMessageRef = new AtomicReference<>(); + + // 发送消息(需要提前发,保证 RocketMQ 路由的创建) + log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage); + messageBus.post(topic, testMessage); + + // 创建订阅者 + IotMessageSubscriber subscriber1 = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic; + } + + @Override + public String getGroup() { + return "test-topic-" + IdUtil.simpleUUID() + "-consumer"; +// return "test-topic-consumer-01"; + } + + @Override + public void onMessage(String message) { + log.info("[订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriberCount.incrementAndGet(); + subscriberMessageRef.set(message); + assertEquals(testMessage, message); + latch.countDown(); + } + + }; + // 注册订阅者 + messageBus.register(subscriber1); + + // 等待消息处理完成(最多等待 5 秒) + boolean completed = latch.await(10, TimeUnit.SECONDS); + + // 验证结果 + assertTrue(completed, "消息处理超时"); + assertEquals(1, subscriberCount.get(), "订阅者应该收到 1 条消息"); + log.info("[测试] 测试完成 - 订阅者收到{}条消息", subscriberCount.get()); + assertEquals(testMessage, subscriberMessageRef.get(), "接收到的消息内容不匹配"); + } + + /** + * 1 topic 2 subscriber(pojo) + */ + @Test + public void testSendMessageWithTwoSubscribers() throws InterruptedException { + // 准备 + String topic = "test-topic-" + IdUtil.simpleUUID(); +// String topic = "test-topic-pojo"; + TestMessage testMessage = new TestMessage().setNickname("yunai").setAge(18); + // 用于等待消息处理完成 + CountDownLatch latch = new CountDownLatch(2); + // 用于记录接收到的消息 + AtomicInteger subscriber1Count = new AtomicInteger(0); + AtomicReference subscriber1MessageRef = new AtomicReference<>(); + AtomicInteger subscriber2Count = new AtomicInteger(0); + AtomicReference subscriber2MessageRef = new AtomicReference<>(); + + // 发送消息(需要提前发,保证 RocketMQ 路由的创建) + log.info("[测试] 发送消息 - Topic: {}, Message: {}", topic, testMessage); + messageBus.post(topic, testMessage); + + // 创建第一个订阅者 + IotMessageSubscriber subscriber1 = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic; + } + + @Override + public String getGroup() { + return "test-topic-" + IdUtil.simpleUUID() + "-consumer"; +// return "test-topic-consumer-01"; + } + + @Override + public void onMessage(TestMessage message) { + log.info("[订阅者1] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriber1Count.incrementAndGet(); + subscriber1MessageRef.set(message); + assertEquals(testMessage, message); + latch.countDown(); + } + + }; + // 创建第二个订阅者 + IotMessageSubscriber subscriber2 = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic; + } + + @Override + public String getGroup() { + return "test-topic-" + IdUtil.simpleUUID() + "-consumer"; +// return "test-topic-consumer-02"; + } + + @Override + public void onMessage(TestMessage message) { + log.info("[订阅者2] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriber2Count.incrementAndGet(); + subscriber2MessageRef.set(message); + assertEquals(testMessage, message); + latch.countDown(); + } + + }; + // 注册订阅者 + messageBus.register(subscriber1); + messageBus.register(subscriber2); + + // 等待消息处理完成(最多等待 5 秒) + boolean completed = latch.await(10, TimeUnit.SECONDS); + + // 验证结果 + assertTrue(completed, "消息处理超时"); + assertEquals(1, subscriber1Count.get(), "订阅者 1 应该收到 1 条消息"); + assertEquals(1, subscriber2Count.get(), "订阅者 2 应该收到 1 条消息"); + log.info("[测试] 测试完成 - 订阅者 1 收到{}条消息,订阅者2收到{}条消息", subscriber1Count.get(), subscriber2Count.get()); + assertEquals(testMessage, subscriber1MessageRef.get(), "接收到的消息内容不匹配"); + assertEquals(testMessage, subscriber2MessageRef.get(), "接收到的消息内容不匹配"); + } + + /** + * 2 topic 2 subscriber + */ + @Test + public void testMultipleTopics() throws InterruptedException { + // 准备 + String topic1 = "device-status-" + IdUtil.simpleUUID(); + String topic2 = "device-data-" + IdUtil.simpleUUID(); + String message1 = "设备在线"; + String message2 = "温度:25°C"; + CountDownLatch latch = new CountDownLatch(2); + AtomicInteger subscriber1Count = new AtomicInteger(0); + AtomicReference subscriber1MessageRef = new AtomicReference<>(); + AtomicInteger subscriber2Count = new AtomicInteger(0); + AtomicReference subscriber2MessageRef = new AtomicReference<>(); + + + // 发送消息到不同主题(需要提前发,保证 RocketMQ 路由的创建) + log.info("[测试] 发送消息 - Topic1: {}, Message1: {}", topic1, message1); + messageBus.post(topic1, message1); + log.info("[测试] 发送消息 - Topic2: {}, Message2: {}", topic2, message2); + messageBus.post(topic2, message2); + + // 创建订阅者 1 - 只订阅设备状态 + IotMessageSubscriber statusSubscriber = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic1; + } + + @Override + public String getGroup() { + return "status-group-" + IdUtil.simpleUUID(); + } + + @Override + public void onMessage(String message) { + log.info("[状态订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriber1Count.incrementAndGet(); + subscriber1MessageRef.set(message); + assertEquals(message1, message); + latch.countDown(); + } + + }; + // 创建订阅者 2 - 只订阅设备数据 + IotMessageSubscriber dataSubscriber = new IotMessageSubscriber<>() { + + @Override + public String getTopic() { + return topic2; + } + + @Override + public String getGroup() { + return "data-group-" + IdUtil.simpleUUID(); + } + + @Override + public void onMessage(String message) { + log.info("[数据订阅者] 收到消息 - Topic: {}, Message: {}", getTopic(), message); + subscriber2Count.incrementAndGet(); + subscriber2MessageRef.set(message); + assertEquals(message2, message); + latch.countDown(); + } + + }; + // 注册订阅者到不同主题 + messageBus.register(statusSubscriber); + messageBus.register(dataSubscriber); + + // 等待消息处理完成 + boolean completed = latch.await(10, TimeUnit.SECONDS); + + // 验证结果 + assertTrue(completed, "消息处理超时"); + assertEquals(1, subscriber1Count.get(), "状态订阅者应该收到 1 条消息"); + assertEquals(message1, subscriber1MessageRef.get(), "状态订阅者接收到的消息内容不匹配"); + assertEquals(1, subscriber2Count.get(), "数据订阅者应该收到 1 条消息"); + assertEquals(message2, subscriber2MessageRef.get(), "数据订阅者接收到的消息内容不匹配"); + log.info("[测试] 多主题测试完成 - 状态订阅者收到{}条消息,数据订阅者收到{}条消息", subscriber1Count.get(), subscriber2Count.get()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml new file mode 100644 index 000000000..9fb189701 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -0,0 +1,78 @@ + + + yudao-module-iot + cn.iocoder.cloud + ${revision} + + 4.0.0 + jar + yudao-module-iot-gateway + + ${project.artifactId} + + iot 模块下,设备网关: + ① 功能一:接收来自设备的消息,并进行解码(decode)后,发送到消息网关,提供给 iot-biz 进行处理 + ② 功能二:接收来自消息网关的消息(由 iot-biz 发送),并进行编码(encode)后,发送给设备 + + + + + cn.iocoder.cloud + yudao-module-iot-core + ${revision} + + + + org.springframework + spring-web + + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + + + + + + + io.vertx + vertx-web + + + + + io.vertx + vertx-mqtt + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/IotGatewayServerApplication.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/IotGatewayServerApplication.java new file mode 100644 index 000000000..e9c457885 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/IotGatewayServerApplication.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.iot.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class IotGatewayServerApplication { + + public static void main(String[] args) { + SpringApplication.run(IotGatewayServerApplication.class, args); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java new file mode 100644 index 000000000..94dd309dd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.gateway.codec; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; + +/** + * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器 + * + * @author 芋道源码 + */ +public interface IotDeviceMessageCodec { + + /** + * 编码消息 + * + * @param message 消息 + * @return 编码后的消息内容 + */ + byte[] encode(IotDeviceMessage message); + + /** + * 解码消息 + * + * @param bytes 消息内容 + * @return 解码后的消息内容 + */ + IotDeviceMessage decode(byte[] bytes); + + /** + * @return 数据格式(编码器类型) + */ + String type(); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java new file mode 100644 index 000000000..9086480d3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.alink; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 阿里云 Alink {@link IotDeviceMessage} 的编解码器 + * + * @author 芋道源码 + */ +@Component +public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { + + private static final String TYPE = "Alink"; + + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class AlinkMessage { + + public static final String VERSION_1 = "1.0"; + + /** + * 消息 ID,且每个消息 ID 在当前设备具有唯一性 + */ + private String id; + + /** + * 版本号 + */ + private String version; + + /** + * 请求方法 + */ + private String method; + + /** + * 请求参数 + */ + private Object params; + + /** + * 响应结果 + */ + private Object data; + /** + * 响应错误码 + */ + private Integer code; + /** + * 响应提示 + * + * 特殊:这里阿里云是 message,为了保持和项目的 {@link CommonResult#getMsg()} 一致。 + */ + private String msg; + + } + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, + message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg()); + return JsonUtils.toJsonByte(alinkMessage); + } + + @Override + @SuppressWarnings("DataFlowIssue") + public IotDeviceMessage decode(byte[] bytes) { + AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class); + Assert.notNull(alinkMessage, "消息不能为空"); + Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0"); + return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(), + alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java new file mode 100644 index 000000000..e1dae7707 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/package-info.java @@ -0,0 +1,4 @@ +/** + * 提供设备接入的各种数据(请求、响应)的编解码 + */ +package cn.iocoder.yudao.module.iot.gateway.codec; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java new file mode 100644 index 000000000..5bd676ad1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/simple/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO @芋艿:实现一个 alink 的 xml 版本 + */ +package cn.iocoder.yudao.module.iot.gateway.codec.simple; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java new file mode 100644 index 000000000..4f42a8c2f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -0,0 +1,286 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import io.vertx.core.buffer.Buffer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 + *

+ * 二进制协议格式(所有数值使用大端序): + * + *

+ * +--------+--------+--------+---------------------------+--------+--------+
+ * | 魔术字 | 版本号 | 消息类型|         消息长度(4 字节)          |
+ * +--------+--------+--------+---------------------------+--------+--------+
+ * |           消息 ID 长度(2 字节)        |      消息 ID (变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |           方法名长度(2 字节)        |      方法名(变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |                        消息体数据(变长)                              |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * 
+ *

+ * 消息体格式: + * - 请求消息:params 数据(JSON) + * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) + *

+ * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP_BINARY"; + + /** + * 协议魔术字,用于协议识别 + */ + private static final byte MAGIC_NUMBER = (byte) 0x7E; + + /** + * 协议版本号 + */ + private static final byte PROTOCOL_VERSION = (byte) 0x01; + + /** + * 请求消息类型 + */ + private static final byte REQUEST = (byte) 0x01; + + /** + * 响应消息类型 + */ + private static final byte RESPONSE = (byte) 0x02; + + /** + * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息长度) + */ + private static final int HEADER_FIXED_LENGTH = 7; + + /** + * 最小消息长度(头部 + 消息ID长度 + 方法名长度) + */ + private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + Assert.notBlank(message.getMethod(), "消息方法不能为空"); + try { + // 1. 确定消息类型 + byte messageType = determineMessageType(message); + // 2. 构建消息体 + byte[] bodyData = buildMessageBody(message, messageType); + // 3. 构建完整消息 + return buildCompleteMessage(message, messageType, bodyData); + } catch (Exception e) { + log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); + throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); + } + } + + @Override + public IotDeviceMessage decode(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); + try { + Buffer buffer = Buffer.buffer(bytes); + // 解析协议头部和消息内容 + int index = 0; + // 1. 验证魔术字 + byte magic = buffer.getByte(index++); + Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); + + // 2. 验证版本号 + byte version = buffer.getByte(index++); + Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); + + // 3. 读取消息类型 + byte messageType = buffer.getByte(index++); + // 直接验证消息类型,无需抽取方法 + Assert.isTrue(messageType == REQUEST || messageType == RESPONSE, + "无效的消息类型: " + messageType); + + // 4. 读取消息长度 + int messageLength = buffer.getInt(index); + index += 4; + Assert.isTrue(messageLength == buffer.length(), + "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); + + // 5. 读取消息 ID + short messageIdLength = buffer.getShort(index); + index += 2; + String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); + index += messageIdLength; + + // 6. 读取方法名 + short methodLength = buffer.getShort(index); + index += 2; + String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); + index += methodLength; + + // 7. 解析消息体 + return parseMessageBody(buffer, index, messageType, messageId, method); + } catch (Exception e) { + log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); + throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); + } + } + + /** + * 确定消息类型 + * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 + */ + private byte determineMessageType(IotDeviceMessage message) { + // 判断是否为响应消息:有响应码或响应消息时为响应 + if (message.getCode() != null) { + return RESPONSE; + } + // 默认为请求消息 + return REQUEST; + } + + /** + * 构建消息体 + */ + private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { + Buffer bodyBuffer = Buffer.buffer(); + if (messageType == RESPONSE) { + // code + bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); + // msg + String msg = message.getMsg() != null ? message.getMsg() : ""; + byte[] msgBytes = StrUtil.utf8Bytes(msg); + bodyBuffer.appendShort((short) msgBytes.length); + bodyBuffer.appendBytes(msgBytes); + // data + if (message.getData() != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); + } + } else { + // 请求消息只处理 params 参数 + // TODO @haohao:如果为空,是不是得写个长度 0 哈? + if (message.getParams() != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getParams())); + } + } + return bodyBuffer.getBytes(); + } + + /** + * 构建完整消息 + */ + private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { + Buffer buffer = Buffer.buffer(); + // 1. 写入协议头部 + buffer.appendByte(MAGIC_NUMBER); + buffer.appendByte(PROTOCOL_VERSION); + buffer.appendByte(messageType); + // 2. 预留消息长度位置(在 5. 更新消息长度) + int lengthPosition = buffer.length(); + buffer.appendInt(0); + // 3. 写入消息 ID + String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() + : IotDeviceMessageUtils.generateMessageId(); + byte[] messageIdBytes = StrUtil.utf8Bytes(messageId); + buffer.appendShort((short) messageIdBytes.length); + buffer.appendBytes(messageIdBytes); + // 4. 写入方法名 + byte[] methodBytes = StrUtil.utf8Bytes(message.getMethod()); + buffer.appendShort((short) methodBytes.length); + buffer.appendBytes(methodBytes); + // 5. 写入消息体 + buffer.appendBytes(bodyData); + // 6. 更新消息长度 + buffer.setInt(lengthPosition, buffer.length()); + return buffer.getBytes(); + } + + /** + * 解析消息体 + */ + private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, + String messageId, String method) { + if (startIndex >= buffer.length()) { + // 空消息体 + return IotDeviceMessage.of(messageId, method, null, null, null, null); + } + + if (messageType == RESPONSE) { + // 响应消息:解析 code + msg + data + return parseResponseMessage(buffer, startIndex, messageId, method); + } else { + // 请求消息:解析 payload + Object payload = parseJsonData(buffer, startIndex, buffer.length()); + return IotDeviceMessage.of(messageId, method, payload, null, null, null); + } + } + + /** + * 解析响应消息 + */ + private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { + int index = startIndex; + + // 1. 读取响应码 + Integer code = buffer.getInt(index); + index += 4; + + // 2. 读取响应消息 + short msgLength = buffer.getShort(index); + index += 2; + String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; + index += msgLength; + + // 3. 读取响应数据 + Object data = null; + if (index < buffer.length()) { + data = parseJsonData(buffer, index, buffer.length()); + } + + return IotDeviceMessage.of(messageId, method, null, data, code, msg); + } + + /** + * 解析 JSON 数据 + */ + private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { + if (startIndex >= endIndex) { + return null; + } + try { + String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + return JsonUtils.parseObject(jsonStr, Object.class); + } catch (Exception e) { + log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); + return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + } + } + + /** + * 快速检测是否为二进制格式 + * + * @param data 数据 + * @return 是否为二进制格式 + */ + public static boolean isBinaryFormatQuick(byte[] data) { + return data != null && data.length >= 1 && data[0] == MAGIC_NUMBER; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java new file mode 100644 index 000000000..10ffbdf5c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 + * + * 采用纯 JSON 格式传输,格式如下: + * { + * "id": "消息 ID", + * "method": "消息方法", + * "params": {...}, // 请求参数 + * "data": {...}, // 响应结果 + * "code": 200, // 响应错误码 + * "msg": "success", // 响应提示 + * "timestamp": 时间戳 + * } + * + * @author 芋道源码 + */ +@Component +public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP_JSON"; + + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class TcpJsonMessage { + + /** + * 消息 ID,且每个消息 ID 在当前设备具有唯一性 + */ + private String id; + + /** + * 请求方法 + */ + private String method; + + /** + * 请求参数 + */ + private Object params; + + /** + * 响应结果 + */ + private Object data; + + /** + * 响应错误码 + */ + private Integer code; + + /** + * 响应提示 + */ + private String msg; + + /** + * 时间戳 + */ + private Long timestamp; + + } + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( + message.getRequestId(), + message.getMethod(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg(), + System.currentTimeMillis()); + return JsonUtils.toJsonByte(tcpJsonMessage); + } + + @Override + @SuppressWarnings("DataFlowIssue") + public IotDeviceMessage decode(byte[] bytes) { + String jsonStr = StrUtil.utf8Str(bytes).trim(); + TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class); + Assert.notNull(tcpJsonMessage, "消息不能为空"); + Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); + return IotDeviceMessage.of( + tcpJsonMessage.getId(), + tcpJsonMessage.getMethod(), + tcpJsonMessage.getParams(), + tcpJsonMessage.getData(), + tcpJsonMessage.getCode(), + tcpJsonMessage.getMsg()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java new file mode 100644 index 000000000..4b9c3af32 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.iot.gateway.config; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(IotGatewayProperties.class) +@Slf4j +public class IotGatewayConfiguration { + + /** + * IoT 网关 HTTP 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true") + @Slf4j + public static class HttpProtocolConfiguration { + + @Bean + public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) { + return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp()); + } + + @Bean + public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol, + IotMessageBus messageBus) { + return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus); + } + } + + /** + * IoT 网关 EMQX 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true") + @Slf4j + public static class EmqxProtocolConfiguration { + + @Bean(destroyMethod = "close") + public Vertx emqxVertx() { + return Vertx.vertx(); + } + + @Bean + public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties, + Vertx emqxVertx) { + return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); + } + + @Bean + public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties, + Vertx emqxVertx) { + return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx); + } + + @Bean + public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol, + IotMessageBus messageBus) { + return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus); + } + } + + /** + * IoT 网关 TCP 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true") + @Slf4j + public static class TcpProtocolConfiguration { + + @Bean(destroyMethod = "close") + public Vertx tcpVertx() { + return Vertx.vertx(); + } + + @Bean + public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, + IotDeviceService deviceService, + IotDeviceMessageService messageService, + IotTcpConnectionManager connectionManager, + Vertx tcpVertx) { + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), + deviceService, messageService, connectionManager, tcpVertx); + } + + @Bean + public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, + IotDeviceMessageService messageService, + IotDeviceService deviceService, + IotTcpConnectionManager connectionManager, + IotMessageBus messageBus) { + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager, + messageBus); + } + + } + + /** + * IoT 网关 MQTT 协议配置类 + */ + @Configuration + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true") + @Slf4j + public static class MqttProtocolConfiguration { + + @Bean(destroyMethod = "close") + public Vertx mqttVertx() { + return Vertx.vertx(); + } + + @Bean + public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties, + IotDeviceMessageService messageService, + IotMqttConnectionManager connectionManager, + Vertx mqttVertx) { + return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService, + connectionManager, mqttVertx); + } + + @Bean + public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService, + IotMqttConnectionManager connectionManager) { + return new IotMqttDownstreamHandler(messageService, connectionManager); + } + + @Bean + public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol, + IotMqttDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus); + } + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java new file mode 100644 index 000000000..2c2000fd1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -0,0 +1,405 @@ +package cn.iocoder.yudao.module.iot.gateway.config; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.time.Duration; +import java.util.List; + +@ConfigurationProperties(prefix = "yudao.iot.gateway") +@Validated +@Data +public class IotGatewayProperties { + + /** + * 设备 RPC 服务配置 + */ + private RpcProperties rpc; + /** + * Token 配置 + */ + private TokenProperties token; + + /** + * 协议配置 + */ + private ProtocolProperties protocol; + + @Data + public static class RpcProperties { + + /** + * 主程序 API 地址 + */ + @NotEmpty(message = "主程序 API 地址不能为空") + private String url; + /** + * 连接超时时间 + */ + @NotNull(message = "连接超时时间不能为空") + private Duration connectTimeout; + /** + * 读取超时时间 + */ + @NotNull(message = "读取超时时间不能为空") + private Duration readTimeout; + + } + + @Data + public static class TokenProperties { + + /** + * 密钥 + */ + @NotEmpty(message = "密钥不能为空") + private String secret; + /** + * 令牌有效期 + */ + @NotNull(message = "令牌有效期不能为空") + private Duration expiration; + + } + + @Data + public static class ProtocolProperties { + + /** + * HTTP 组件配置 + */ + private HttpProperties http; + + /** + * EMQX 组件配置 + */ + private EmqxProperties emqx; + + /** + * TCP 组件配置 + */ + private TcpProperties tcp; + + /** + * MQTT 组件配置 + */ + private MqttProperties mqtt; + + } + + @Data + public static class HttpProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + /** + * 服务端口 + */ + private Integer serverPort; + + /** + * 是否开启 SSL + */ + @NotNull(message = "是否开启 SSL 不能为空") + private Boolean sslEnabled = false; + + /** + * SSL 证书路径 + */ + private String sslKeyPath; + /** + * SSL 证书路径 + */ + private String sslCertPath; + + } + + @Data + public static class EmqxProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + + /** + * HTTP 服务端口(默认:8090) + */ + private Integer httpPort = 8090; + + /** + * MQTT 服务器地址 + */ + @NotEmpty(message = "MQTT 服务器地址不能为空") + private String mqttHost; + + /** + * MQTT 服务器端口(默认:1883) + */ + @NotNull(message = "MQTT 服务器端口不能为空") + private Integer mqttPort = 1883; + + /** + * MQTT 用户名 + */ + @NotEmpty(message = "MQTT 用户名不能为空") + private String mqttUsername; + + /** + * MQTT 密码 + */ + @NotEmpty(message = "MQTT 密码不能为空") + private String mqttPassword; + + /** + * MQTT 客户端的 SSL 开关 + */ + @NotNull(message = "MQTT 是否开启 SSL 不能为空") + private Boolean mqttSsl = false; + + /** + * MQTT 客户端 ID(如果为空,系统将自动生成) + */ + @NotEmpty(message = "MQTT 客户端 ID 不能为空") + private String mqttClientId; + + /** + * MQTT 订阅的主题 + */ + @NotEmpty(message = "MQTT 主题不能为空") + private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics; + + /** + * 默认 QoS 级别 + *

+ * 0 - 最多一次 + * 1 - 至少一次 + * 2 - 刚好一次 + */ + private Integer mqttQos = 1; + + /** + * 连接超时时间(秒) + */ + private Integer connectTimeoutSeconds = 10; + + /** + * 重连延迟时间(毫秒) + */ + private Long reconnectDelayMs = 5000L; + + /** + * 是否启用 Clean Session (清理会话) + * true: 每次连接都是新会话,Broker 不保留离线消息和订阅关系。 + * 对于网关这类“永远在线”且会主动重新订阅的应用,建议为 true。 + */ + private Boolean cleanSession = true; + + /** + * 心跳间隔(秒) + * 用于保持连接活性,及时发现网络中断。 + */ + private Integer keepAliveIntervalSeconds = 60; + + /** + * 最大未确认消息队列大小 + * 限制已发送但未收到 Broker 确认的 QoS 1/2 消息数量,用于流量控制。 + */ + private Integer maxInflightQueue = 10000; + + /** + * 是否信任所有 SSL 证书 + * 警告:此配置会绕过证书验证,仅建议在开发和测试环境中使用! + * 在生产环境中,应设置为 false,并配置正确的信任库。 + */ + private Boolean trustAll = false; + + /** + * 遗嘱消息配置 (用于网关异常下线时通知其他系统) + */ + private final Will will = new Will(); + + /** + * 高级 SSL/TLS 配置 (用于生产环境) + */ + private final Ssl sslOptions = new Ssl(); + + /** + * 遗嘱消息 (Last Will and Testament) + */ + @Data + public static class Will { + + /** + * 是否启用遗嘱消息 + */ + private boolean enabled = false; + /** + * 遗嘱消息主题 + */ + private String topic; + /** + * 遗嘱消息内容 + */ + private String payload; + /** + * 遗嘱消息 QoS 等级 + */ + private Integer qos = 1; + /** + * 遗嘱消息是否作为保留消息发布 + */ + private boolean retain = true; + + } + + /** + * 高级 SSL/TLS 配置 + */ + @Data + public static class Ssl { + + /** + * 密钥库(KeyStore)路径,例如:classpath:certs/client.jks + * 包含客户端自己的证书和私钥,用于向服务端证明身份(双向认证)。 + */ + private String keyStorePath; + /** + * 密钥库密码 + */ + private String keyStorePassword; + /** + * 信任库(TrustStore)路径,例如:classpath:certs/trust.jks + * 包含服务端信任的 CA 证书,用于验证服务端的身份,防止中间人攻击。 + */ + private String trustStorePath; + /** + * 信任库密码 + */ + private String trustStorePassword; + + } + + } + + @Data + public static class TcpProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + + /** + * 服务器端口 + */ + private Integer port = 8091; + + /** + * 心跳超时时间(毫秒) + */ + private Long keepAliveTimeoutMs = 30000L; + + /** + * 最大连接数 + */ + private Integer maxConnections = 1000; + + /** + * 是否启用SSL + */ + private Boolean sslEnabled = false; + + /** + * SSL证书路径 + */ + private String sslCertPath; + + /** + * SSL私钥路径 + */ + private String sslKeyPath; + + } + + @Data + public static class MqttProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enabled; + + /** + * 服务器端口 + */ + private Integer port = 1883; + + /** + * 最大消息大小(字节) + */ + private Integer maxMessageSize = 8192; + + /** + * 连接超时时间(秒) + */ + private Integer connectTimeoutSeconds = 60; + /** + * 保持连接超时时间(秒) + */ + private Integer keepAliveTimeoutSeconds = 300; + + /** + * 是否启用 SSL + */ + private Boolean sslEnabled = false; + /** + * SSL 配置 + */ + private SslOptions sslOptions = new SslOptions(); + + /** + * SSL 配置选项 + */ + @Data + public static class SslOptions { + + /** + * 密钥证书选项 + */ + private io.vertx.core.net.KeyCertOptions keyCertOptions; + /** + * 信任选项 + */ + private io.vertx.core.net.TrustOptions trustOptions; + /** + * SSL 证书路径 + */ + private String certPath; + /** + * SSL 私钥路径 + */ + private String keyPath; + /** + * 信任存储路径 + */ + private String trustStorePath; + /** + * 信任存储密码 + */ + private String trustStorePassword; + + } + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..90afda224 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.gateway.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * iot gateway 错误码枚举类 + *

+ * iot 系统,使用 1-051-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 设备认证 1-050-001-000 ============ + ErrorCode DEVICE_AUTH_FAIL = new ErrorCode(1_051_001_000, "设备鉴权失败"); // 对应阿里云 20000 + ErrorCode DEVICE_TOKEN_EXPIRED = new ErrorCode(1_051_001_002, "token 失效。需重新调用 auth 进行鉴权,获取token"); // 对应阿里云 20001 + + // ========== 设备信息 1-050-002-000 ============ + ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_051_002_001, "设备({}/{}) 不存在"); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java new file mode 100644 index 000000000..ce10cf76d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 认证事件协议服务 + *

+ * 为 EMQX 提供 HTTP 接口服务,包括: + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxAuthEventProtocol { + + private final IotGatewayProperties.EmqxProperties emqxProperties; + + private final String serverId; + + private final Vertx vertx; + + private HttpServer httpServer; + + public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties, + Vertx vertx) { + this.emqxProperties = emqxProperties; + this.vertx = vertx; + this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); + } + + @PostConstruct + public void start() { + try { + startHttpServer(); + log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e); + throw e; + } + } + + @PreDestroy + public void stop() { + stopHttpServer(); + log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]"); + } + + /** + * 启动 HTTP 服务器 + */ + private void startHttpServer() { + int port = emqxProperties.getHttpPort(); + + // 1. 创建路由 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + + // 2. 创建处理器,传入 serverId + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); + router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); + router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); + // TODO @haohao:/mqtt/acl 需要处理么? + // TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理 + + // 3. 启动 HTTP 服务器 + try { + httpServer = vertx.createHttpServer() + .requestHandler(router) + .listen(port) + .result(); + } catch (Exception e) { + log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e); + throw e; + } + } + + /** + * 停止 HTTP 服务器 + */ + private void stopHttpServer() { + if (httpServer == null) { + return; + } + + try { + httpServer.close().result(); + log.info("[stopHttpServer][HTTP 服务器已停止]"); + } catch (Exception e) { + log.error("[stopHttpServer][HTTP 服务器停止失败]", e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java new file mode 100644 index 000000000..61bf12376 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxDownstreamSubscriber.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber { + + private final IotEmqxDownstreamHandler downstreamHandler; + + private final IotMessageBus messageBus; + + private final IotEmqxUpstreamProtocol protocol; + + public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) { + this.protocol = protocol; + this.messageBus = messageBus; + this.downstreamHandler = new IotEmqxDownstreamHandler(protocol); + } + + @PostConstruct + public void init() { + messageBus.register(this); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + } + + @Override + public String getGroup() { + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); + try { + // 1. 校验 + String method = message.getMethod(); + if (method == null) { + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", + message.getId(), message.getDeviceId()); + return; + } + + // 2. 处理下行消息 + downstreamHandler.handle(message); + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId(), e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java new file mode 100644 index 000000000..a88815874 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java @@ -0,0 +1,365 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.JksOptions; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * IoT 网关 EMQX 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxUpstreamProtocol { + + private final IotGatewayProperties.EmqxProperties emqxProperties; + + private volatile boolean isRunning = false; + + private final Vertx vertx; + + @Getter + private final String serverId; + + private MqttClient mqttClient; + + private IotEmqxUpstreamHandler upstreamHandler; + + public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties, + Vertx vertx) { + this.emqxProperties = emqxProperties; + this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort()); + this.vertx = vertx; + } + + @PostConstruct + public void start() { + if (isRunning) { + return; + } + + try { + // 1. 启动 MQTT 客户端 + startMqttClient(); + + // 2. 标记服务为运行状态 + isRunning = true; + log.info("[start][IoT 网关 EMQX 协议启动成功]"); + } catch (Exception e) { + log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e); + stop(); + + // 异步关闭应用 + Thread shutdownThread = new Thread(() -> { + try { + // 确保日志输出完成,使用更优雅的方式 + log.error("[start][由于 MQTT 连接失败,正在关闭应用]"); + // 等待日志输出完成 + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + log.warn("[start][应用关闭被中断]"); + } + System.exit(1); + }); + shutdownThread.setDaemon(true); + shutdownThread.setName("emergency-shutdown"); + shutdownThread.start(); + + throw e; + } + } + + @PreDestroy + public void stop() { + if (!isRunning) { + return; + } + + // 1. 停止 MQTT 客户端 + stopMqttClient(); + + // 2. 标记服务为停止状态 + isRunning = false; + log.info("[stop][IoT 网关 MQTT 协议服务已停止]"); + } + + /** + * 启动 MQTT 客户端 + */ + private void startMqttClient() { + try { + // 1. 初始化消息处理器 + this.upstreamHandler = new IotEmqxUpstreamHandler(this); + + // 2. 创建 MQTT 客户端 + createMqttClient(); + + // 3. 同步连接 MQTT Broker + connectMqttSync(); + } catch (Exception e) { + log.error("[startMqttClient][MQTT 客户端启动失败]", e); + throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e); + } + } + + /** + * 同步连接 MQTT Broker + */ + private void connectMqttSync() { + String host = emqxProperties.getMqttHost(); + int port = emqxProperties.getMqttPort(); + // 1. 连接 MQTT Broker + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean success = new AtomicBoolean(false); + mqttClient.connect(port, host, connectResult -> { + if (connectResult.succeeded()) { + log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port); + setupMqttHandlers(); + subscribeToTopics(); + success.set(true); + } else { + log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]", + host, port, connectResult.cause()); + } + latch.countDown(); + }); + + // 2. 等待连接结果 + try { + // 应用层超时控制:防止启动过程无限阻塞,与MQTT客户端的网络超时是不同层次的控制 + boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); + if (!awaitResult) { + log.error("[connectMqttSync][等待连接结果超时]"); + throw new RuntimeException("连接 MQTT Broker 超时"); + } + if (!success.get()) { + throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port)); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("[connectMqttSync][等待连接结果被中断]", e); + throw new RuntimeException("连接 MQTT Broker 被中断", e); + } + } + + /** + * 异步连接 MQTT Broker + */ + private void connectMqttAsync() { + String host = emqxProperties.getMqttHost(); + int port = emqxProperties.getMqttPort(); + mqttClient.connect(port, host, connectResult -> { + if (connectResult.succeeded()) { + log.info("[connectMqttAsync][MQTT 客户端重连成功]"); + setupMqttHandlers(); + subscribeToTopics(); + } else { + log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]", + host, port, connectResult.cause()); + log.warn("[connectMqttAsync][重连失败,将再次尝试]"); + reconnectWithDelay(); + } + }); + } + + /** + * 延迟重连 + */ + private void reconnectWithDelay() { + if (!isRunning) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + + long delay = emqxProperties.getReconnectDelayMs(); + log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay); + vertx.setTimer(delay, timerId -> { + if (!isRunning) { + return; + } + if (mqttClient != null && mqttClient.isConnected()) { + return; + } + + log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); + try { + createMqttClient(); + connectMqttAsync(); + } catch (Exception e) { + log.error("[reconnectWithDelay][重连过程中发生异常]", e); + vertx.setTimer(delay, t -> reconnectWithDelay()); + } + }); + } + + /** + * 停止 MQTT 客户端 + */ + private void stopMqttClient() { + if (mqttClient == null) { + return; + } + try { + if (mqttClient.isConnected()) { + // 1. 取消订阅所有主题 + List topicList = emqxProperties.getMqttTopics(); + for (String topic : topicList) { + try { + mqttClient.unsubscribe(topic); + } catch (Exception e) { + log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e); + } + } + + // 2. 断开 MQTT 客户端连接 + try { + CountDownLatch disconnectLatch = new CountDownLatch(1); + mqttClient.disconnect(ar -> disconnectLatch.countDown()); + if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) { + log.warn("[stopMqttClient][断开 MQTT 连接超时]"); + } + } catch (Exception e) { + log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e); + } + } + } catch (Exception e) { + log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e); + } finally { + mqttClient = null; + } + } + + /** + * 创建 MQTT 客户端 + */ + private void createMqttClient() { + // 1.1 创建基础配置 + MqttClientOptions options = (MqttClientOptions) new MqttClientOptions() + .setClientId(emqxProperties.getMqttClientId()) + .setUsername(emqxProperties.getMqttUsername()) + .setPassword(emqxProperties.getMqttPassword()) + .setSsl(emqxProperties.getMqttSsl()) + .setCleanSession(emqxProperties.getCleanSession()) + .setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds()) + .setMaxInflightQueue(emqxProperties.getMaxInflightQueue()) + .setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒 + .setTrustAll(emqxProperties.getTrustAll()); + // 1.2 配置遗嘱消息 + IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill(); + if (will.isEnabled()) { + Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空"); + Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空"); + options.setWillFlag(true) + .setWillTopic(will.getTopic()) + .setWillMessageBytes(Buffer.buffer(will.getPayload())) + .setWillQoS(will.getQos()) + .setWillRetain(will.isRetain()); + } + // 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效) + if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) { + IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions(); + if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) { + options.setTrustStoreOptions(new JksOptions() + .setPath(sslOptions.getTrustStorePath()) + .setPassword(sslOptions.getTrustStorePassword())); + } + if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) { + options.setKeyStoreOptions(new JksOptions() + .setPath(sslOptions.getKeyStorePath()) + .setPassword(sslOptions.getKeyStorePassword())); + } + } + // 1.4 安全警告日志 + if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) { + log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书(trustAll=true),这在生产环境中存在严重安全风险!]"); + } + + // 2. 创建客户端实例 + this.mqttClient = MqttClient.create(vertx, options); + } + + /** + * 设置 MQTT 处理器 + */ + private void setupMqttHandlers() { + // 1. 设置断开重连监听器 + mqttClient.closeHandler(closeEvent -> { + if (!isRunning) { + return; + } + log.warn("[closeHandler][MQTT 连接已断开, 准备重连]"); + reconnectWithDelay(); + }); + + // 2. 设置异常处理器 + mqttClient.exceptionHandler(exception -> + log.error("[exceptionHandler][MQTT 客户端异常]", exception)); + + // 3. 设置消息处理器 + mqttClient.publishHandler(upstreamHandler::handle); + } + + /** + * 订阅设备上行消息主题 + */ + private void subscribeToTopics() { + // 1. 校验 MQTT 客户端是否连接 + List topicList = emqxProperties.getMqttTopics(); + if (mqttClient == null || !mqttClient.isConnected()) { + log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]"); + return; + } + + // 2. 批量订阅所有主题 + Map topics = new HashMap<>(); + int qos = emqxProperties.getMqttQos(); + for (String topic : topicList) { + topics.put(topic, qos); + } + mqttClient.subscribe(topics, subscribeResult -> { + if (subscribeResult.succeeded()) { + log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size()); + } else { + log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]", + topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause()); + } + }); + } + + /** + * 发布消息到 MQTT Broker + * + * @param topic 主题 + * @param payload 消息内容 + */ + public void publishMessage(String topic, byte[] payload) { + if (mqttClient == null || !mqttClient.isConnected()) { + log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]"); + return; + } + MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos()); + mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java new file mode 100644 index 000000000..d6957bd52 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java @@ -0,0 +1,248 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 认证事件处理器 + *

+ * 为 EMQX 提供 HTTP 接口服务,包括: + * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 + * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxAuthEventHandler { + + /** + * HTTP 成功状态码(EMQX 要求固定使用 200) + */ + private static final int SUCCESS_STATUS_CODE = 200; + + /** + * 认证允许结果 + */ + private static final String RESULT_ALLOW = "allow"; + /** + * 认证拒绝结果 + */ + private static final String RESULT_DENY = "deny"; + /** + * 认证忽略结果 + */ + private static final String RESULT_IGNORE = "ignore"; + + /** + * EMQX 事件类型常量 + */ + private static final String EVENT_CLIENT_CONNECTED = "client.connected"; + private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; + + private final String serverId; + + private final IotDeviceMessageService deviceMessageService; + + private final IotDeviceCommonApi deviceApi; + + public IotEmqxAuthEventHandler(String serverId) { + this.serverId = serverId; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + /** + * EMQX 认证接口 + */ + public void handleAuth(RoutingContext context) { + try { + // 1. 参数校验 + JsonObject body = parseRequestBody(context); + if (body == null) { + return; + } + String clientId = body.getString("clientid"); + String username = body.getString("username"); + String password = body.getString("password"); + log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username); + if (StrUtil.hasEmpty(clientId, username, password)) { + log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username); + sendAuthResponse(context, RESULT_DENY); + return; + } + + // 2. 执行认证 + boolean authResult = handleDeviceAuth(clientId, username, password); + log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); + if (authResult) { + sendAuthResponse(context, RESULT_ALLOW); + } else { + sendAuthResponse(context, RESULT_DENY); + } + } catch (Exception e) { + log.error("[handleAuth][设备认证异常]", e); + sendAuthResponse(context, RESULT_IGNORE); + } + } + + /** + * EMQX 统一事件处理接口:根据 EMQX 官方 Webhook 设计,统一处理所有客户端事件 + * 支持的事件类型:client.connected、client.disconnected 等 + */ + public void handleEvent(RoutingContext context) { + JsonObject body = null; + try { + // 1. 解析请求体 + body = parseRequestBody(context); + if (body == null) { + return; + } + String event = body.getString("event"); + String username = body.getString("username"); + log.debug("[handleEvent][收到事件: {} - {}]", event, username); + + // 2. 根据事件类型进行分发处理 + switch (event) { + case EVENT_CLIENT_CONNECTED: + handleClientConnected(body); + break; + case EVENT_CLIENT_DISCONNECTED: + handleClientDisconnected(body); + break; + default: + break; + } + + // EMQX Webhook 只需要 200 状态码,无需响应体 + context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); + } catch (Exception e) { + log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e); + // 即使处理失败,也返回 200 避免EMQX重试 + context.response().setStatusCode(SUCCESS_STATUS_CODE).end(); + } + } + + /** + * 处理客户端连接事件 + */ + private void handleClientConnected(JsonObject body) { + String username = body.getString("username"); + log.info("[handleClientConnected][设备上线: {}]", username); + handleDeviceStateChange(username, true); + } + + /** + * 处理客户端断开连接事件 + */ + private void handleClientDisconnected(JsonObject body) { + String username = body.getString("username"); + String reason = body.getString("reason"); + log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason); + handleDeviceStateChange(username, false); + } + + /** + * 解析请求体 + * + * @param context 路由上下文 + * @return 请求体JSON对象,解析失败时返回null + */ + private JsonObject parseRequestBody(RoutingContext context) { + try { + JsonObject body = context.body().asJsonObject(); + if (body == null) { + log.info("[parseRequestBody][请求体为空]"); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + return body; + } catch (Exception e) { + log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e); + sendAuthResponse(context, RESULT_IGNORE); + return null; + } + } + + /** + * 执行设备认证 + * + * @param clientId 客户端ID + * @param username 用户名 + * @param password 密码 + * @return 认证是否成功 + */ + private boolean handleDeviceAuth(String clientId, String username, String password) { + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password)); + result.checkError(); + return BooleanUtil.isTrue(result.getData()); + } catch (Exception e) { + log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e); + throw e; + } + } + + /** + * 处理设备状态变化 + * + * @param username 用户名 + * @param online 是否在线 true 在线 false 离线 + */ + private void handleDeviceStateChange(String username, boolean online) { + // 1. 解析设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username); + return; + } + + try { + // 2. 构建设备状态消息 + IotDeviceMessage message = online ? IotDeviceMessage.buildStateUpdateOnline() + : IotDeviceMessage.buildStateOffline(); + + // 3. 发送设备状态消息 + deviceMessageService.sendDeviceMessage(message, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, e); + } + } + + /** + * 发送 EMQX 认证响应 + * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应 + * + * @param context 路由上下文 + * @param result 认证结果:allow、deny、ignore + */ + private void sendAuthResponse(RoutingContext context, String result) { + // 构建符合 EMQX 官方规范的响应 + JsonObject response = new JsonObject() + .put("result", result) + .put("is_superuser", false); + + // 可以根据业务需求添加客户端属性 + // response.put("client_attrs", new JsonObject().put("role", "device")); + + // 可以添加认证过期时间(可选) + // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600); + + context.response() + .setStatusCode(SUCCESS_STATUS_CODE) + .putHeader("Content-Type", "application/json; charset=utf-8") + .end(response.encode()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java new file mode 100644 index 000000000..06632b3e8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxDownstreamHandler.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 下行消息处理器 + *

+ * 从消息总线接收到下行消息,然后发布到 MQTT Broker,从而被设备所接收 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxDownstreamHandler { + + private final IotEmqxUpstreamProtocol protocol; + + private final IotDeviceService deviceService; + + private final IotDeviceMessageService deviceMessageService; + + public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) { + this.protocol = protocol; + this.deviceService = SpringUtil.getBean(IotDeviceService.class); + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + /** + * 处理下行消息 + * + * @param message 设备消息 + */ + public void handle(IotDeviceMessage message) { + // 1. 获取设备信息 + IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); + if (deviceInfo == null) { + log.error("[handle][设备信息({})不存在]", message.getDeviceId()); + return; + } + + // 2.1 根据方法构建主题 + String topic = buildTopicByMethod(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + if (StrUtil.isBlank(topic)) { + log.warn("[handle][未知的消息方法: {}]", message.getMethod()); + return; + } + // 2.2 构建载荷 + byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + // 2.3 发布消息 + protocol.publishMessage(topic, payload); + } + + /** + * 根据消息方法和回复状态构建主题 + * + * @param message 设备消息 + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @return 构建的主题,如果方法不支持返回 null + */ + private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) { + // 1. 判断是否为回复消息 + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); + // 2. 根据消息方法类型构建对应的主题 + return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java new file mode 100644 index 000000000..81d8cbb13 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxUpstreamHandler.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 EMQX 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotEmqxUpstreamHandler { + + private final IotDeviceMessageService deviceMessageService; + + private final String serverId; + + public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) { + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + this.serverId = protocol.getServerId(); + } + + /** + * 处理 MQTT 发布消息 + */ + public void handle(MqttPublishMessage mqttMessage) { + log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload()); + String topic = mqttMessage.topicName(); + byte[] payload = mqttMessage.payload().getBytes(); + try { + // 1. 解析主题,一次性获取所有信息 + String[] topicParts = topic.split("/"); + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); + return; + } + + String productKey = topicParts[2]; + String deviceName = topicParts[3]; + + // 3. 解码消息 + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + if (message == null) { + log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); + return; + } + + // 4. 发送消息到队列 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + } catch (Exception e) { + log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java new file mode 100644 index 000000000..585bbdd30 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 HTTP 订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotHttpDownstreamSubscriber implements IotMessageSubscriber { + + private final IotHttpUpstreamProtocol protocol; + + private final IotMessageBus messageBus; + + @PostConstruct + public void init() { + messageBus.register(this); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + } + + @Override + public String getGroup() { + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java new file mode 100644 index 000000000..eda59d13f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 HTTP 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotHttpUpstreamProtocol extends AbstractVerticle { + + private final IotGatewayProperties.HttpProperties httpProperties; + + private HttpServer httpServer; + + @Getter + private final String serverId; + + public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties) { + this.httpProperties = httpProperties; + this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort()); + } + + @Override + @PostConstruct + public void start() { + // 创建路由 + Vertx vertx = Vertx.vertx(); + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + + // 创建处理器,添加路由处理器 + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); + router.post(IotHttpAuthHandler.PATH).handler(authHandler); + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); + router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); + + // 启动 HTTP 服务器 + HttpServerOptions options = new HttpServerOptions() + .setPort(httpProperties.getServerPort()); + if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath()) + .setCertPath(httpProperties.getSslCertPath()); + options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + try { + httpServer = vertx.createHttpServer(options) + .requestHandler(router) + .listen() + .result(); + log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 HTTP 协议启动失败]", e); + throw e; + } + } + + @Override + @PreDestroy + public void stop() { + if (httpServer != null) { + try { + httpServer.close().result(); + log.info("[stop][IoT 网关 HTTP 协议已停止]"); + } catch (Exception e) { + log.error("[stop][IoT 网关 HTTP 协议停止失败]", e); + } + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java new file mode 100644 index 000000000..f5461c2c5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +/** + * IoT 网关 HTTP 协议的处理器抽象基类:提供通用的前置处理(认证)、全局的异常捕获等 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class IotHttpAbstractHandler implements Handler { + + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + + @Override + public final void handle(RoutingContext context) { + try { + // 1. 前置处理 + beforeHandle(context); + + // 2. 执行逻辑 + CommonResult result = handle0(context); + writeResponse(context, result); + } catch (ServiceException e) { + writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); + } catch (Exception e) { + log.error("[handle][path({}) 处理异常]", context.request().path(), e); + writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); + } + } + + protected abstract CommonResult handle0(RoutingContext context); + + private void beforeHandle(RoutingContext context) { + // 如果不需要认证,则不走前置处理 + String path = context.request().path(); + if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { + return; + } + + // 解析参数 + String token = context.request().getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isEmpty(token)) { + throw invalidParamException("token 不能为空"); + } + String productKey = context.pathParam("productKey"); + if (StrUtil.isEmpty(productKey)) { + throw invalidParamException("productKey 不能为空"); + } + String deviceName = context.pathParam("deviceName"); + if (StrUtil.isEmpty(deviceName)) { + throw invalidParamException("deviceName 不能为空"); + } + + // 校验 token + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); + Assert.notNull(deviceInfo, "设备信息不能为空"); + // 校验设备信息是否匹配 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { + throw exception(FORBIDDEN); + } + } + + @SuppressWarnings("deprecation") + public static void writeResponse(RoutingContext context, Object data) { + context.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) + .end(JsonUtils.toJsonString(data)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java new file mode 100644 index 000000000..e6a52cdf0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * IoT 网关 HTTP 协议的【认证】处理器 + * + * 参考 https://help.aliyun.com/zh/iot/user-guide/establish-connections-over-https + * + * @author 芋道源码 + */ +public class IotHttpAuthHandler extends IotHttpAbstractHandler { + + public static final String PATH = "/auth"; + + private final IotHttpUpstreamProtocol protocol; + + private final IotDeviceTokenService deviceTokenService; + + private final IotDeviceCommonApi deviceApi; + + private final IotDeviceMessageService deviceMessageService; + + public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { + this.protocol = protocol; + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + public CommonResult handle0(RoutingContext context) { + // 1. 解析参数 + JsonObject body = context.body().asJsonObject(); + String clientId = body.getString("clientId"); + if (StrUtil.isEmpty(clientId)) { + throw invalidParamException("clientId 不能为空"); + } + String username = body.getString("username"); + if (StrUtil.isEmpty(username)) { + throw invalidParamException("username 不能为空"); + } + String password = body.getString("password"); + if (StrUtil.isEmpty(password)) { + throw invalidParamException("password 不能为空"); + } + + // 2.1 执行认证 + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password)); + result.checkError(); + if (!BooleanUtil.isTrue(result.getData())) { + throw exception(DEVICE_AUTH_FAIL); + } + // 2.2 生成 Token + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username); + Assert.notNull(deviceInfo, "设备信息不能为空"); + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notBlank(token, "生成 token 不能为空位"); + + // 3. 执行上线 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(message, + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId()); + + // 构建响应数据 + return success(MapUtil.of("token", token)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java new file mode 100644 index 000000000..d7d4d52ff --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 HTTP 协议的【上行】处理器 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { + + public static final String PATH = "/topic/sys/:productKey/:deviceName/*"; + + private final IotHttpUpstreamProtocol protocol; + + private final IotDeviceMessageService deviceMessageService; + + public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { + this.protocol = protocol; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + protected CommonResult handle0(RoutingContext context) { + // 1. 解析通用参数 + String productKey = context.pathParam("productKey"); + String deviceName = context.pathParam("deviceName"); + String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); + + // 2.1 解析消息 + byte[] bytes = context.body().buffer().getBytes(); + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, + productKey, deviceName); + Assert.equals(method, message.getMethod(), "method 不匹配"); + // 2.2 发送消息 + deviceMessageService.sendDeviceMessage(message, + productKey, deviceName, protocol.getServerId()); + + // 3. 返回结果 + return CommonResult.success(MapUtil.of("messageId", message.getId())); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java new file mode 100644 index 000000000..3b62368fd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:下行消息订阅器 + *

+ * 负责接收来自消息总线的下行消息,并委托给下行处理器进行业务处理 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttDownstreamSubscriber implements IotMessageSubscriber { + + private final IotMqttUpstreamProtocol upstreamProtocol; + + private final IotMqttDownstreamHandler downstreamHandler; + + private final IotMessageBus messageBus; + + public IotMqttDownstreamSubscriber(IotMqttUpstreamProtocol upstreamProtocol, + IotMqttDownstreamHandler downstreamHandler, + IotMessageBus messageBus) { + this.upstreamProtocol = upstreamProtocol; + this.downstreamHandler = downstreamHandler; + this.messageBus = messageBus; + } + + @PostConstruct + public void subscribe() { + messageBus.register(this); + log.info("[subscribe][MQTT 协议下行消息订阅成功,主题:{}]", getTopic()); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId()); + } + + @Override + public String getGroup() { + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); + } + + @Override + public void onMessage(IotDeviceMessage message) { + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); + try { + // 1. 校验 + String method = message.getMethod(); + if (method == null) { + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", + message.getId(), message.getDeviceId()); + return; + } + + // 2. 委托给下行处理器处理业务逻辑 + boolean success = downstreamHandler.handleDownstreamMessage(message); + if (success) { + log.debug("[onMessage][下行消息处理成功, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); + } else { + log.warn("[onMessage][下行消息处理失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); + } + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId(), e); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java new file mode 100644 index 000000000..fc0b6672c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import io.vertx.mqtt.MqttServer; +import io.vertx.mqtt.MqttServerOptions; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttUpstreamProtocol { + + private final IotGatewayProperties.MqttProperties mqttProperties; + + private final IotDeviceMessageService messageService; + + private final IotMqttConnectionManager connectionManager; + + private final Vertx vertx; + + @Getter + private final String serverId; + + private MqttServer mqttServer; + + public IotMqttUpstreamProtocol(IotGatewayProperties.MqttProperties mqttProperties, + IotDeviceMessageService messageService, + IotMqttConnectionManager connectionManager, + Vertx vertx) { + this.mqttProperties = mqttProperties; + this.messageService = messageService; + this.connectionManager = connectionManager; + this.vertx = vertx; + this.serverId = IotDeviceMessageUtils.generateServerId(mqttProperties.getPort()); + } + + // TODO @haohao:这里的编写,是不是和 tcp 对应的,风格保持一致哈; + @PostConstruct + public void start() { + // 创建服务器选项 + MqttServerOptions options = new MqttServerOptions() + .setPort(mqttProperties.getPort()) + .setMaxMessageSize(mqttProperties.getMaxMessageSize()) + .setTimeoutOnConnect(mqttProperties.getConnectTimeoutSeconds()); + + // 配置 SSL(如果启用) + if (Boolean.TRUE.equals(mqttProperties.getSslEnabled())) { + options.setSsl(true) + .setKeyCertOptions(mqttProperties.getSslOptions().getKeyCertOptions()) + .setTrustOptions(mqttProperties.getSslOptions().getTrustOptions()); + } + + // 创建服务器并设置连接处理器 + mqttServer = MqttServer.create(vertx, options); + mqttServer.endpointHandler(endpoint -> { + IotMqttUpstreamHandler handler = new IotMqttUpstreamHandler(this, messageService, connectionManager); + handler.handle(endpoint); + }); + + // 启动服务器 + try { + mqttServer.listen().result(); + log.info("[start][IoT 网关 MQTT 协议启动成功,端口:{}]", mqttProperties.getPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 MQTT 协议启动失败]", e); + throw e; + } + } + + @PreDestroy + public void stop() { + if (mqttServer != null) { + try { + mqttServer.close().result(); + log.info("[stop][IoT 网关 MQTT 协议已停止]"); + } catch (Exception e) { + log.error("[stop][IoT 网关 MQTT 协议停止失败]", e); + } + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java new file mode 100644 index 000000000..3fd1a3a04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java @@ -0,0 +1,223 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager; + +import cn.hutool.core.util.StrUtil; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.mqtt.MqttEndpoint; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 MQTT 连接管理器 + *

+ * 统一管理 MQTT 连接的认证状态、设备会话和消息发送功能: + * 1. 管理 MQTT 连接的认证状态 + * 2. 管理设备会话和在线状态 + * 3. 管理消息发送到设备 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotMqttConnectionManager { + + /** + * 未知地址常量(当获取端点地址失败时使用) + */ + private static final String UNKNOWN_ADDRESS = "unknown"; + + /** + * 连接信息映射:MqttEndpoint -> 连接信息 + */ + private final Map connectionMap = new ConcurrentHashMap<>(); + + /** + * 设备 ID -> MqttEndpoint 的映射 + */ + private final Map deviceEndpointMap = new ConcurrentHashMap<>(); + + /** + * 安全获取 endpoint 地址 + *

+ * 优先从缓存获取地址,缓存为空时再尝试实时获取 + * + * @param endpoint MQTT 连接端点 + * @return 地址字符串,获取失败时返回 "unknown" + */ + public String getEndpointAddress(MqttEndpoint endpoint) { + String realTimeAddress = UNKNOWN_ADDRESS; + if (endpoint == null) { + return realTimeAddress; + } + + // 1. 优先从缓存获取(避免连接关闭时的异常) + ConnectionInfo connectionInfo = connectionMap.get(endpoint); + if (connectionInfo != null && StrUtil.isNotBlank(connectionInfo.getRemoteAddress())) { + return connectionInfo.getRemoteAddress(); + } + + // 2. 缓存为空时尝试实时获取 + try { + realTimeAddress = endpoint.remoteAddress().toString(); + } catch (Exception ignored) { + // 连接已关闭,忽略异常 + } + + return realTimeAddress; + } + + /** + * 注册设备连接(包含认证信息) + * + * @param endpoint MQTT 连接端点 + * @param deviceId 设备 ID + * @param connectionInfo 连接信息 + */ + public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { + // 如果设备已有其他连接,先清理旧连接 + MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); + if (oldEndpoint != null && oldEndpoint != endpoint) { + log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", + deviceId, getEndpointAddress(oldEndpoint)); + oldEndpoint.close(); + // 清理旧连接的映射 + connectionMap.remove(oldEndpoint); + } + + connectionMap.put(endpoint, connectionInfo); + deviceEndpointMap.put(deviceId, endpoint); + + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); + } + + /** + * 注销设备连接 + * + * @param endpoint MQTT 连接端点 + */ + public void unregisterConnection(MqttEndpoint endpoint) { + ConnectionInfo connectionInfo = connectionMap.remove(endpoint); + if (connectionInfo != null) { + Long deviceId = connectionInfo.getDeviceId(); + deviceEndpointMap.remove(deviceId); + + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, + getEndpointAddress(endpoint)); + } + } + + /** + * 获取连接信息 + */ + public ConnectionInfo getConnectionInfo(MqttEndpoint endpoint) { + return connectionMap.get(endpoint); + } + + /** + * 根据设备 ID 获取连接信息 + * + * @param deviceId 设备 ID + * @return 连接信息 + */ + public IotMqttConnectionManager.ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) { + // 通过设备 ID 获取连接端点 + var endpoint = getDeviceEndpoint(deviceId); + if (endpoint == null) { + return null; + } + + // 获取连接信息 + return getConnectionInfo(endpoint); + } + + /** + * 检查设备是否在线 + */ + public boolean isDeviceOnline(Long deviceId) { + return deviceEndpointMap.containsKey(deviceId); + } + + /** + * 检查设备是否离线 + */ + public boolean isDeviceOffline(Long deviceId) { + return !isDeviceOnline(deviceId); + } + + /** + * 发送消息到设备 + * + * @param deviceId 设备 ID + * @param topic 主题 + * @param payload 消息内容 + * @param qos 服务质量 + * @param retain 是否保留消息 + * @return 是否发送成功 + */ + public boolean sendToDevice(Long deviceId, String topic, byte[] payload, int qos, boolean retain) { + MqttEndpoint endpoint = deviceEndpointMap.get(deviceId); + if (endpoint == null) { + log.warn("[sendToDevice][设备离线,无法发送消息,设备 ID: {},主题: {}]", deviceId, topic); + return false; + } + + try { + endpoint.publish(topic, io.vertx.core.buffer.Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain); + log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败,设备 ID: {},主题: {},错误: {}]", deviceId, topic, e.getMessage()); + return false; + } + } + + /** + * 获取设备连接端点 + */ + public MqttEndpoint getDeviceEndpoint(Long deviceId) { + return deviceEndpointMap.get(deviceId); + } + + /** + * 连接信息 + */ + @Data + public static class ConnectionInfo { + + /** + * 设备 ID + */ + private Long deviceId; + + /** + * 产品 Key + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 客户端 ID + */ + private String clientId; + + /** + * 是否已认证 + */ + private boolean authenticated; + + /** + * 连接地址 + */ + private String remoteAddress; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java new file mode 100644 index 000000000..fabe79466 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/package-info.java @@ -0,0 +1,6 @@ +/** + * MQTT 协议实现包 + *

+ * 提供基于 Vert.x MQTT Server 的 IoT 设备连接和消息处理功能 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java new file mode 100644 index 000000000..c848833f6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 MQTT 协议:下行消息处理器 + *

+ * 专门处理下行消息的业务逻辑,包括: + * 1. 消息编码 + * 2. 主题构建 + * 3. 消息发送 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttDownstreamHandler { + + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttConnectionManager connectionManager; + + public IotMqttDownstreamHandler(IotDeviceMessageService deviceMessageService, + IotMqttConnectionManager connectionManager) { + this.deviceMessageService = deviceMessageService; + this.connectionManager = connectionManager; + } + + /** + * 处理下行消息 + * + * @param message 设备消息 + * @return 是否处理成功 + */ + public boolean handleDownstreamMessage(IotDeviceMessage message) { + try { + // 1. 基础校验 + if (message == null || message.getDeviceId() == null) { + log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]"); + return false; + } + + // 2. 检查设备是否在线 + if (connectionManager.isDeviceOffline(message.getDeviceId())) { + log.warn("[handleDownstreamMessage][设备离线,无法发送消息,设备 ID:{}]", message.getDeviceId()); + return false; + } + + // 3. 获取连接信息 + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId()); + if (connectionInfo == null) { + log.warn("[handleDownstreamMessage][连接信息不存在,设备 ID:{}]", message.getDeviceId()); + return false; + } + + // 4. 编码消息 + byte[] payload = deviceMessageService.encodeDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName()); + if (payload == null || payload.length == 0) { + log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId()); + return false; + } + + // 5. 发送消息到设备 + return sendMessageToDevice(message, connectionInfo, payload); + } catch (Exception e) { + if (message != null) { + log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]", + message.getDeviceId(), e.getMessage(), e); + } + return false; + } + } + + /** + * 发送消息到设备 + * + * @param message 设备消息 + * @param connectionInfo 连接信息 + * @param payload 消息负载 + * @return 是否发送成功 + */ + private boolean sendMessageToDevice(IotDeviceMessage message, + IotMqttConnectionManager.ConnectionInfo connectionInfo, + byte[] payload) { + // 1. 构建主题 + String topic = buildDownstreamTopic(message, connectionInfo); + if (StrUtil.isBlank(topic)) { + log.warn("[sendMessageToDevice][主题构建失败,设备 ID:{},方法:{}]", + message.getDeviceId(), message.getMethod()); + return false; + } + + // 2. 发送消息 + boolean success = connectionManager.sendToDevice(message.getDeviceId(), topic, payload, MqttQoS.AT_LEAST_ONCE.value(), false); + if (success) { + log.debug("[sendMessageToDevice][消息发送成功,设备 ID:{},主题:{},方法:{}]", + message.getDeviceId(), topic, message.getMethod()); + } else { + log.warn("[sendMessageToDevice][消息发送失败,设备 ID:{},主题:{},方法:{}]", + message.getDeviceId(), topic, message.getMethod()); + } + return success; + } + + /** + * 构建下行消息主题 + * + * @param message 设备消息 + * @param connectionInfo 连接信息 + * @return 主题 + */ + private String buildDownstreamTopic(IotDeviceMessage message, + IotMqttConnectionManager.ConnectionInfo connectionInfo) { + String method = message.getMethod(); + if (StrUtil.isBlank(method)) { + return null; + } + + // 使用工具类构建主题,支持回复消息处理 + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); + return IotMqttTopicUtils.buildTopicByMethod(method, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), isReply); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java new file mode 100644 index 000000000..c19053f14 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java @@ -0,0 +1,305 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; + +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.mqtt.MqttEndpoint; +import io.vertx.mqtt.MqttTopicSubscription; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * MQTT 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotMqttUpstreamHandler { + + private final IotDeviceMessageService deviceMessageService; + + private final IotMqttConnectionManager connectionManager; + + private final IotDeviceCommonApi deviceApi; + + private final String serverId; + + public IotMqttUpstreamHandler(IotMqttUpstreamProtocol protocol, + IotDeviceMessageService deviceMessageService, + IotMqttConnectionManager connectionManager) { + this.deviceMessageService = deviceMessageService; + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.connectionManager = connectionManager; + this.serverId = protocol.getServerId(); + } + + /** + * 处理 MQTT 连接 + * + * @param endpoint MQTT 连接端点 + */ + public void handle(MqttEndpoint endpoint) { + String clientId = endpoint.clientIdentifier(); + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null; + + log.debug("[handle][设备连接请求,客户端 ID: {},用户名: {},地址: {}]", + clientId, username, connectionManager.getEndpointAddress(endpoint)); + + // 1. 先进行认证 + if (!authenticateDevice(clientId, username, password, endpoint)) { + log.warn("[handle][设备认证失败,拒绝连接,客户端 ID: {},用户名: {}]", clientId, username); + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + return; + } + + log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); + + // 2. 设置异常和关闭处理器 + endpoint.exceptionHandler(ex -> { + log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); + cleanupConnection(endpoint); + }); + endpoint.closeHandler(v -> { + cleanupConnection(endpoint); + }); + + // 3. 设置消息处理器 + endpoint.publishHandler(message -> { + try { + processMessage(clientId, message.topicName(), message.payload().getBytes()); + + // 根据 QoS 级别发送相应的确认消息 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + // QoS 1: 发送 PUBACK 确认 + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + // QoS 2: 发送 PUBREC 确认 + endpoint.publishReceived(message.messageId()); + } + // QoS 0 无需确认 + + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); + cleanupConnection(endpoint); + endpoint.close(); + } + }); + + // 4. 设置订阅处理器 + endpoint.subscribeHandler(subscribe -> { + // 提取主题名称列表用于日志显示 + List topicNames = subscribe.topicSubscriptions().stream() + .map(MqttTopicSubscription::topicName) + .collect(java.util.stream.Collectors.toList()); + log.debug("[handle][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); + + // 提取 QoS 列表 + List grantedQoSLevels = subscribe.topicSubscriptions().stream() + .map(MqttTopicSubscription::qualityOfService) + .collect(java.util.stream.Collectors.toList()); + endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); + }); + + // 5. 设置取消订阅处理器 + endpoint.unsubscribeHandler(unsubscribe -> { + log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }); + + // 6. 设置 QoS 2消息的 PUBREL 处理器 + endpoint.publishReleaseHandler(endpoint::publishComplete); + + // 7. 设置断开连接处理器 + endpoint.disconnectHandler(v -> { + log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); + cleanupConnection(endpoint); + }); + + // 8. 接受连接 + endpoint.accept(false); + } + + /** + * 处理消息 + * + * @param clientId 客户端 ID + * @param topic 主题 + * @param payload 消息内容 + */ + private void processMessage(String clientId, String topic, byte[] payload) { + // 1. 基础检查 + if (payload == null || payload.length == 0) { + return; + } + + // 2. 解析主题,获取 productKey 和 deviceName + String[] topicParts = topic.split("/"); + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { + log.warn("[processMessage][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); + return; + } + + String productKey = topicParts[2]; + String deviceName = topicParts[3]; + + // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName) + try { + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); + if (message == null) { + log.warn("[processMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); + return; + } + + log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", + productKey, deviceName, message.getMethod()); + + // 4. 处理业务消息(认证已在连接时完成) + handleBusinessRequest(message, productKey, deviceName); + } catch (Exception e) { + log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", + clientId, topic, e.getMessage(), e); + } + } + + /** + * 在 MQTT 连接时进行设备认证 + * + * @param clientId 客户端 ID + * @param username 用户名 + * @param password 密码 + * @param endpoint MQTT 连接端点 + * @return 认证是否成功 + */ + private boolean authenticateDevice(String clientId, String username, String password, MqttEndpoint endpoint) { + try { + // 1. 参数校验 + if (StrUtil.hasEmpty(clientId, username, password)) { + log.warn("[authenticateDevice][认证参数不完整,客户端 ID: {},用户名: {}]", clientId, username); + return false; + } + + // 2. 构建认证参数 + IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 3. 调用设备认证 API + CommonResult authResult = deviceApi.authDevice(authParams); + if (!authResult.isSuccess() || !BooleanUtil.isTrue(authResult.getData())) { + log.warn("[authenticateDevice][设备认证失败,客户端 ID: {},用户名: {},错误: {}]", + clientId, username, authResult.getMsg()); + return false; + } + + // 4. 获取设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); + if (deviceInfo == null) { + log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); + return false; + } + + IotDeviceGetReqDTO getReqDTO = new IotDeviceGetReqDTO() + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()); + + CommonResult deviceResult = deviceApi.getDevice(getReqDTO); + if (!deviceResult.isSuccess() || deviceResult.getData() == null) { + log.warn("[authenticateDevice][获取设备信息失败,客户端 ID: {},用户名: {},错误: {}]", + clientId, username, deviceResult.getMsg()); + return false; + } + + // 5. 注册连接 + IotDeviceRespDTO device = deviceResult.getData(); + registerConnection(endpoint, device, clientId); + + // 6. 发送设备上线消息 + sendOnlineMessage(device); + + return true; + } catch (Exception e) { + log.error("[authenticateDevice][设备认证异常,客户端 ID: {},用户名: {}]", clientId, username, e); + return false; + } + } + + /** + * 处理业务请求 + */ + private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { + // 发送消息到消息总线 + message.setServerId(serverId); + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + } + + /** + * 注册连接 + */ + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, + String clientId) { + + IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setClientId(clientId) + .setAuthenticated(true) + .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); + + connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + log.info("[sendOnlineMessage][设备上线,设备 ID: {},设备名称: {}]", device.getId(), device.getDeviceName()); + } catch (Exception e) { + log.error("[sendOnlineMessage][发送设备上线消息失败,设备 ID: {},错误: {}]", device.getId(), e.getMessage()); + } + } + + /** + * 清理连接 + */ + private void cleanupConnection(MqttEndpoint endpoint) { + try { + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); + if (connectionInfo != null) { + // 发送设备离线消息 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", + connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); + } + + // 注销连接 + connectionManager.unregisterConnection(endpoint); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", + endpoint.clientIdentifier(), e.getMessage()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java new file mode 100644 index 000000000..6eb414ee9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/package-info.java @@ -0,0 +1,4 @@ +/** + * 提供设备接入的各种协议的实现 + */ +package cn.iocoder.yudao.module.iot.gateway.protocol; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java new file mode 100644 index 000000000..e4d46b3af --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { + + private final IotTcpUpstreamProtocol protocol; + + private final IotDeviceMessageService messageService; + + private final IotDeviceService deviceService; + + private final IotTcpConnectionManager connectionManager; + + private final IotMessageBus messageBus; + + private IotTcpDownstreamHandler downstreamHandler; + + @PostConstruct + public void init() { + // 初始化下游处理器 + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager); + + messageBus.register(this); + log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", + protocol.getServerId(), getTopic()); + } + + @Override + public String getTopic() { + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); + } + + @Override + public String getGroup() { + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); + } + + @Override + public void onMessage(IotDeviceMessage message) { + try { + downstreamHandler.handle(message); + } catch (Exception e) { + log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId(), e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java new file mode 100644 index 000000000..791c6cbfc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Vertx; +import io.vertx.core.net.NetServer; +import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.PemKeyCertOptions; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 TCP 协议:接收设备上行消息 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpUpstreamProtocol { + + private final IotGatewayProperties.TcpProperties tcpProperties; + + private final IotDeviceService deviceService; + + private final IotDeviceMessageService messageService; + + private final IotTcpConnectionManager connectionManager; + + private final Vertx vertx; + + @Getter + private final String serverId; + + private NetServer tcpServer; + + public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, + IotDeviceService deviceService, + IotDeviceMessageService messageService, + IotTcpConnectionManager connectionManager, + Vertx vertx) { + this.tcpProperties = tcpProperties; + this.deviceService = deviceService; + this.messageService = messageService; + this.connectionManager = connectionManager; + this.vertx = vertx; + this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); + } + + @PostConstruct + public void start() { + // 创建服务器选项 + NetServerOptions options = new NetServerOptions() + .setPort(tcpProperties.getPort()) + .setTcpKeepAlive(true) + .setTcpNoDelay(true) + .setReuseAddress(true); + // 配置 SSL(如果启用) + if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() + .setKeyPath(tcpProperties.getSslKeyPath()) + .setCertPath(tcpProperties.getSslCertPath()); + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); + } + + // 创建服务器并设置连接处理器 + tcpServer = vertx.createNetServer(options); + tcpServer.connectHandler(socket -> { + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, + connectionManager); + handler.handle(socket); + }); + + // 启动服务器 + try { + tcpServer.listen().result(); + log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 TCP 协议启动失败]", e); + throw e; + } + } + + @PreDestroy + public void stop() { + if (tcpServer != null) { + try { + tcpServer.close().result(); + log.info("[stop][IoT 网关 TCP 协议已停止]"); + } catch (Exception e) { + log.error("[stop][IoT 网关 TCP 协议停止失败]", e); + } + } + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java new file mode 100644 index 000000000..c0d209814 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -0,0 +1,168 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 连接管理器 + *

+ * 统一管理 TCP 连接的认证状态、设备会话和消息发送功能: + * 1. 管理 TCP 连接的认证状态 + * 2. 管理设备会话和在线状态 + * 3. 管理消息发送到设备 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpConnectionManager { + + /** + * 连接信息映射:NetSocket -> 连接信息 + */ + private final Map connectionMap = new ConcurrentHashMap<>(); + + /** + * 设备 ID -> NetSocket 的映射 + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * 注册设备连接(包含认证信息) + * + * @param socket TCP 连接 + * @param deviceId 设备 ID + * @param connectionInfo 连接信息 + */ + public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { + // 如果设备已有其他连接,先清理旧连接 + NetSocket oldSocket = deviceSocketMap.get(deviceId); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", + deviceId, oldSocket.remoteAddress()); + oldSocket.close(); + // 清理旧连接的映射 + connectionMap.remove(oldSocket); + } + + connectionMap.put(socket, connectionInfo); + deviceSocketMap.put(deviceId, socket); + + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + deviceId, socket.remoteAddress(), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); + } + + /** + * 注销设备连接 + * + * @param socket TCP 连接 + */ + public void unregisterConnection(NetSocket socket) { + ConnectionInfo connectionInfo = connectionMap.remove(socket); + if (connectionInfo != null) { + Long deviceId = connectionInfo.getDeviceId(); + deviceSocketMap.remove(deviceId); + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", + deviceId, socket.remoteAddress()); + } + } + + /** + * 检查连接是否已认证 + */ + public boolean isAuthenticated(NetSocket socket) { + ConnectionInfo info = connectionMap.get(socket); + return info != null && info.isAuthenticated(); + } + + /** + * 检查连接是否未认证 + */ + public boolean isNotAuthenticated(NetSocket socket) { + return !isAuthenticated(socket); + } + + /** + * 获取连接信息 + */ + public ConnectionInfo getConnectionInfo(NetSocket socket) { + return connectionMap.get(socket); + } + + /** + * 检查设备是否在线 + */ + public boolean isDeviceOnline(Long deviceId) { + return deviceSocketMap.containsKey(deviceId); + } + + /** + * 检查设备是否离线 + */ + public boolean isDeviceOffline(Long deviceId) { + return !isDeviceOnline(deviceId); + } + + /** + * 发送消息到设备 + */ + public boolean sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); + return false; + } + + try { + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); + log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); + // 发送失败时清理连接 + unregisterConnection(socket); + return false; + } + } + + /** + * 连接信息(包含认证信息) + */ + @Data + public static class ConnectionInfo { + + /** + * 设备 ID + */ + private Long deviceId; + /** + * 产品 Key + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + + /** + * 客户端 ID + */ + private String clientId; + /** + * 消息编解码类型(认证后确定) + */ + private String codecType; + // TODO @haohao:有没可能不要 authenticated 字段,通过 deviceId 或者其他的?进一步简化,想的是哈。 + /** + * 是否已认证 + */ + private boolean authenticated; + + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java new file mode 100644 index 000000000..3ee31d82e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 网关 TCP 下行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotTcpDownstreamHandler { + + private final IotDeviceMessageService deviceMessageService; + + private final IotDeviceService deviceService; + + private final IotTcpConnectionManager connectionManager; + + /** + * 处理下行消息 + */ + public void handle(IotDeviceMessage message) { + try { + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + + // 1.1 获取设备信息 + IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); + if (deviceInfo == null) { + log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId()); + return; + } + // 1.2 检查设备是否在线 + if (connectionManager.isDeviceOffline(message.getDeviceId())) { + log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); + return; + } + + // 2. 根据产品 Key 和设备名称编码消息并发送到设备 + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); + if (success) { + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); + } else { + log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); + } + } catch (Exception e) { + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java new file mode 100644 index 000000000..0aff8f72f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -0,0 +1,408 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.NetSocket; +import lombok.extern.slf4j.Slf4j; + +/** + * TCP 上行消息处理器 + * + * @author 芋道源码 + */ +@Slf4j +public class IotTcpUpstreamHandler implements Handler { + + private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; + private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; + + private static final String AUTH_METHOD = "auth"; + + private final IotDeviceMessageService deviceMessageService; + + private final IotDeviceService deviceService; + + private final IotTcpConnectionManager connectionManager; + + private final IotDeviceCommonApi deviceApi; + + private final String serverId; + + public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, + IotDeviceMessageService deviceMessageService, + IotDeviceService deviceService, + IotTcpConnectionManager connectionManager) { + this.deviceMessageService = deviceMessageService; + this.deviceService = deviceService; + this.connectionManager = connectionManager; + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.serverId = protocol.getServerId(); + } + + @Override + public void handle(NetSocket socket) { + String clientId = IdUtil.simpleUUID(); + log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + + // 设置异常和关闭处理器 + socket.exceptionHandler(ex -> { + log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + cleanupConnection(socket); + }); + socket.closeHandler(v -> { + log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + cleanupConnection(socket); + }); + + // 设置消息处理器 + socket.handler(buffer -> { + try { + processMessage(clientId, buffer, socket); + } catch (Exception e) { + log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", + clientId, socket.remoteAddress(), e.getMessage()); + cleanupConnection(socket); + socket.close(); + } + }); + } + + /** + * 处理消息 + * + * @param clientId 客户端 ID + * @param buffer 消息 + * @param socket 网络连接 + * @throws Exception 消息解码失败时抛出异常 + */ + private void processMessage(String clientId, Buffer buffer, NetSocket socket) throws Exception { + // 1. 基础检查 + if (buffer == null || buffer.length() == 0) { + return; + } + + // 2. 获取消息格式类型 + String codecType = getMessageCodecType(buffer, socket); + + // 3. 解码消息 + IotDeviceMessage message; + try { + message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); + if (message == null) { + throw new Exception("解码后消息为空"); + } + } catch (Exception e) { + // 消息格式错误时抛出异常,由上层处理连接断开 + throw new Exception("消息解码失败: " + e.getMessage(), e); + } + + // 4. 根据消息类型路由处理 + try { + if (AUTH_METHOD.equals(message.getMethod())) { + // 认证请求 + handleAuthenticationRequest(clientId, message, codecType, socket); + } else { + // 业务消息 + handleBusinessRequest(clientId, message, codecType, socket); + } + } catch (Exception e) { + log.error("[processMessage][处理消息失败,客户端 ID: {},消息方法: {}]", + clientId, message.getMethod(), e); + // 发送错误响应,避免客户端一直等待 + try { + sendErrorResponse(socket, message.getRequestId(), "消息处理失败", codecType); + } catch (Exception responseEx) { + log.error("[processMessage][发送错误响应失败,客户端 ID: {}]", clientId, responseEx); + } + } + } + + /** + * 处理认证请求 + * + * @param clientId 客户端 ID + * @param message 消息信息 + * @param codecType 消息编解码类型 + * @param socket 网络连接 + */ + private void handleAuthenticationRequest(String clientId, IotDeviceMessage message, String codecType, + NetSocket socket) { + try { + // 1.1 解析认证参数 + IotDeviceAuthReqDTO authParams = parseAuthParams(message.getParams()); + if (authParams == null) { + log.warn("[handleAuthenticationRequest][认证参数解析失败,客户端 ID: {}]", clientId); + sendErrorResponse(socket, message.getRequestId(), "认证参数不完整", codecType); + return; + } + // 1.2 执行认证 + if (!validateDeviceAuth(authParams)) { + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", + clientId, authParams.getUsername()); + sendErrorResponse(socket, message.getRequestId(), "认证失败", codecType); + return; + } + + // 2.1 解析设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername()); + if (deviceInfo == null) { + sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType); + return; + } + // 2.2 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + sendErrorResponse(socket, message.getRequestId(), "设备不存在", codecType); + return; + } + + // 3.1 注册连接 + registerConnection(socket, device, clientId, codecType); + // 3.2 发送上线消息 + sendOnlineMessage(device); + // 3.3 发送成功响应 + sendSuccessResponse(socket, message.getRequestId(), "认证成功", codecType); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", + device.getId(), device.getDeviceName()); + } catch (Exception e) { + log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); + sendErrorResponse(socket, message.getRequestId(), "认证处理异常", codecType); + } + } + + /** + * 处理业务请求 + * + * @param clientId 客户端 ID + * @param message 消息信息 + * @param codecType 消息编解码类型 + * @param socket 网络连接 + */ + private void handleBusinessRequest(String clientId, IotDeviceMessage message, String codecType, NetSocket socket) { + try { + // 1. 检查认证状态 + if (connectionManager.isNotAuthenticated(socket)) { + log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); + sendErrorResponse(socket, message.getRequestId(), "请先进行认证", codecType); + return; + } + + // 2. 获取认证信息并处理业务消息 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + + // 3. 发送消息到消息总线 + deviceMessageService.sendDeviceMessage(message, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + log.info("[handleBusinessRequest][发送消息到消息总线,客户端 ID: {},消息: {}", + clientId, message.toString()); + } catch (Exception e) { + log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); + } + } + + /** + * 获取消息编解码类型 + * + * @param buffer 消息 + * @param socket 网络连接 + * @return 消息编解码类型 + */ + private String getMessageCodecType(Buffer buffer, NetSocket socket) { + // 1. 如果已认证,优先使用缓存的编解码类型 + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null && connectionInfo.isAuthenticated() && + StrUtil.isNotBlank(connectionInfo.getCodecType())) { + return connectionInfo.getCodecType(); + } + + // 2. 未认证时检测消息格式类型 + return IotTcpBinaryDeviceMessageCodec.isBinaryFormatQuick(buffer.getBytes()) ? CODEC_TYPE_BINARY + : CODEC_TYPE_JSON; + } + + /** + * 注册连接信息 + * + * @param socket 网络连接 + * @param device 设备 + * @param clientId 客户端 ID + * @param codecType 消息编解码类型 + */ + private void registerConnection(NetSocket socket, IotDeviceRespDTO device, + String clientId, String codecType) { + IotTcpConnectionManager.ConnectionInfo connectionInfo = new IotTcpConnectionManager.ConnectionInfo() + .setDeviceId(device.getId()) + .setProductKey(device.getProductKey()) + .setDeviceName(device.getDeviceName()) + .setClientId(clientId) + .setCodecType(codecType) + .setAuthenticated(true); + // 注册连接 + connectionManager.registerConnection(socket, device.getId(), connectionInfo); + } + + /** + * 发送设备上线消息 + * + * @param device 设备信息 + */ + private void sendOnlineMessage(IotDeviceRespDTO device) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, device.getProductKey(), + device.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", device.getDeviceName(), e); + } + } + + /** + * 清理连接 + * + * @param socket 网络连接 + */ + private void cleanupConnection(NetSocket socket) { + try { + // 1. 发送离线消息(如果已认证) + IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(socket); + if (connectionInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), + connectionInfo.getDeviceName(), serverId); + } + + // 2. 注销连接 + connectionManager.unregisterConnection(socket); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败]", e); + } + } + + /** + * 发送响应消息 + * + * @param socket 网络连接 + * @param success 是否成功 + * @param message 消息 + * @param requestId 请求 ID + * @param codecType 消息编解码类型 + */ + private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { + try { + Object responseData = MapUtil.builder() + .put("success", success) + .put("message", message) + .build(); + + int code = success ? 0 : 401; + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, + code, message); + + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); + socket.write(Buffer.buffer(encodedData)); + + } catch (Exception e) { + log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); + } + } + + /** + * 验证设备认证信息 + * + * @param authParams 认证参数 + * @return 是否认证成功 + */ + private boolean validateDeviceAuth(IotDeviceAuthReqDTO authParams) { + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(authParams.getClientId()).setUsername(authParams.getUsername()) + .setPassword(authParams.getPassword())); + result.checkError(); + return BooleanUtil.isTrue(result.getData()); + } catch (Exception e) { + log.error("[validateDeviceAuth][设备认证异常,username: {}]", authParams.getUsername(), e); + return false; + } + } + + /** + * 发送错误响应 + * + * @param socket 网络连接 + * @param requestId 请求 ID + * @param errorMessage 错误消息 + * @param codecType 消息编解码类型 + */ + private void sendErrorResponse(NetSocket socket, String requestId, String errorMessage, String codecType) { + sendResponse(socket, false, errorMessage, requestId, codecType); + } + + /** + * 发送成功响应 + * + * @param socket 网络连接 + * @param requestId 请求 ID + * @param message 消息 + * @param codecType 消息编解码类型 + */ + @SuppressWarnings("SameParameterValue") + private void sendSuccessResponse(NetSocket socket, String requestId, String message, String codecType) { + sendResponse(socket, true, message, requestId, codecType); + } + + /** + * 解析认证参数 + * + * @param params 参数对象(通常为 Map 类型) + * @return 认证参数 DTO,解析失败时返回 null + */ + @SuppressWarnings("unchecked") + private IotDeviceAuthReqDTO parseAuthParams(Object params) { + if (params == null) { + return null; + } + + try { + // 参数默认为 Map 类型,直接转换 + if (params instanceof java.util.Map) { + java.util.Map paramMap = (java.util.Map) params; + return new IotDeviceAuthReqDTO() + .setClientId(MapUtil.getStr(paramMap, "clientId")) + .setUsername(MapUtil.getStr(paramMap, "username")) + .setPassword(MapUtil.getStr(paramMap, "password")); + } + + // 如果已经是目标类型,直接返回 + if (params instanceof IotDeviceAuthReqDTO) { + return (IotDeviceAuthReqDTO) params; + } + + // 其他情况尝试 JSON 转换 + String jsonStr = JsonUtils.toJsonString(params); + return JsonUtils.parseObject(jsonStr, IotDeviceAuthReqDTO.class); + } catch (Exception e) { + log.error("[parseAuthParams][解析认证参数({})失败]", params, e); + return null; + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java new file mode 100644 index 000000000..9aab67236 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.gateway.service.auth; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; + +/** + * IoT 设备 Token Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceTokenService { + + /** + * 创建设备 Token + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 设备 Token + */ + String createToken(String productKey, String deviceName); + + /** + * 验证设备 Token + * + * @param token 设备 Token + * @return 设备信息 + */ + IotDeviceAuthUtils.DeviceInfo verifyToken(String token); + + /** + * 解析用户名 + * + * @param username 用户名 + * @return 设备信息 + */ + IotDeviceAuthUtils.DeviceInfo parseUsername(String username); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java new file mode 100644 index 000000000..79ba4e77e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.gateway.service.auth; + +import cn.hutool.core.lang.Assert; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_TOKEN_EXPIRED; + +/** + * IoT 设备 Token Service 实现类:调用远程的 device http 接口,进行设备 Token 生成、解析等逻辑 + * + * 注意:目前仅 HTTP 协议使用 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { + + @Resource + private IotGatewayProperties gatewayProperties; + + @Override + public String createToken(String productKey, String deviceName) { + Assert.notBlank(productKey, "productKey 不能为空"); + Assert.notBlank(deviceName, "deviceName 不能为空"); + // 构建 JWT payload + Map payload = new HashMap<>(); + payload.put("productKey", productKey); + payload.put("deviceName", deviceName); + LocalDateTime expireTime = LocalDateTimeUtils.addTime(gatewayProperties.getToken().getExpiration()); + payload.put("exp", LocalDateTimeUtils.toEpochSecond(expireTime)); // 过期时间(exp 是 JWT 规范推荐) + + // 生成 JWT Token + return JWTUtil.createToken(payload, gatewayProperties.getToken().getSecret().getBytes()); + } + + @Override + public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) { + Assert.notBlank(token, "token 不能为空"); + // 校验 JWT Token + boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes()); + if (!verify) { + throw exception(DEVICE_TOKEN_EXPIRED); + } + + // 解析 Token + JWT jwt = JWTUtil.parseToken(token); + JSONObject payload = jwt.getPayloads(); + // 检查过期时间 + Long exp = payload.getLong("exp"); + if (exp == null || exp < System.currentTimeMillis() / 1000) { + throw exception(DEVICE_TOKEN_EXPIRED); + } + String productKey = payload.getStr("productKey"); + String deviceName = payload.getStr("deviceName"); + Assert.notBlank(productKey, "productKey 不能为空"); + Assert.notBlank(deviceName, "deviceName 不能为空"); + return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName); + } + + @Override + public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) { + return IotDeviceAuthUtils.parseUsername(username); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceService.java new file mode 100644 index 000000000..c0d4943da --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceService.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.gateway.service.device; + +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; + +/** + * IoT 设备信息 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceService { + + /** + * 根据 productKey 和 deviceName 获取设备信息 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @return 设备信息 + */ + IotDeviceRespDTO getDeviceFromCache(String productKey, String deviceName); + + /** + * 根据 id 获取设备信息 + * + * @param id 设备编号 + * @return 设备信息 + */ + IotDeviceRespDTO getDeviceFromCache(Long id); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceServiceImpl.java new file mode 100644 index 000000000..fee48d10e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceServiceImpl.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.iot.gateway.service.device; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; + +/** + * IoT 设备信息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDeviceServiceImpl implements IotDeviceService { + + private static final Duration CACHE_EXPIRE = Duration.ofMinutes(1); + + /** + * 通过 id 查询设备的缓存 + */ + private final LoadingCache deviceCaches = buildAsyncReloadingCache( + CACHE_EXPIRE, + new CacheLoader<>() { + + @Override + public IotDeviceRespDTO load(Long id) { + CommonResult result = deviceApi.getDevice(new IotDeviceGetReqDTO().setId(id)); + IotDeviceRespDTO device = result.getCheckedData(); + Assert.notNull(device, "设备({}) 不能为空", id); + // 相互缓存 + deviceCaches2.put(new KeyValue<>(device.getProductKey(), device.getDeviceName()), device); + return device; + } + + }); + + /** + * 通过 productKey + deviceName 查询设备的缓存 + */ + private final LoadingCache, IotDeviceRespDTO> deviceCaches2 = buildAsyncReloadingCache( + CACHE_EXPIRE, + new CacheLoader<>() { + + @Override + public IotDeviceRespDTO load(KeyValue kv) { + CommonResult result = deviceApi.getDevice(new IotDeviceGetReqDTO() + .setProductKey(kv.getKey()).setDeviceName(kv.getValue())); + IotDeviceRespDTO device = result.getCheckedData(); + Assert.notNull(device, "设备({}/{}) 不能为空", kv.getKey(), kv.getValue()); + // 相互缓存 + deviceCaches.put(device.getId(), device); + return device; + } + }); + + @Resource + private IotDeviceCommonApi deviceApi; + + @Override + public IotDeviceRespDTO getDeviceFromCache(String productKey, String deviceName) { + return deviceCaches2.getUnchecked(new KeyValue<>(productKey, deviceName)); + } + + @Override + public IotDeviceRespDTO getDeviceFromCache(Long id) { + return deviceCaches.getUnchecked(id); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java new file mode 100644 index 000000000..c86fc0983 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.gateway.service.device.message; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; + +/** + * IoT 设备消息 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceMessageService { + + /** + * 编码消息 + * + * @param message 消息 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 编码后的消息内容 + */ + byte[] encodeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName); + + /** + * 编码消息 + * + * @param message 消息 + * @param codecType 编解码器类型 + * @return 编码后的消息内容 + */ + byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType); + + /** + * 解码消息 + * + * @param bytes 消息内容 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 解码后的消息内容 + */ + IotDeviceMessage decodeDeviceMessage(byte[] bytes, + String productKey, String deviceName); + + /** + * 解码消息 + * + * @param bytes 消息内容 + * @param codecType 编解码器类型 + * @return 解码后的消息内容 + */ + IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + + /** + * 发送消息 + * + * @param message 消息 + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param serverId 设备连接的 serverId + */ + void sendDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName, String serverId); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java new file mode 100644 index 000000000..014da9a5d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -0,0 +1,138 @@ +package cn.iocoder.yudao.module.iot.gateway.service.device.message; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; + +/** + * IoT 设备消息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { + + /** + * 编解码器 + */ + private final Map codes; + + @Resource + private IotDeviceService deviceService; + + @Resource + private IotDeviceMessageProducer deviceMessageProducer; + + public IotDeviceMessageServiceImpl(List codes) { + this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type); + } + + @Override + public byte[] encodeDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName) { + // 1.1 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); + } + // 1.2 获取编解码器 + IotDeviceMessageCodec codec = codes.get(device.getCodecType()); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); + } + + // 2. 编码消息 + return codec.encode(message); + } + + @Override + public byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 编码消息 + return codec.encode(message); + } + + @Override + public IotDeviceMessage decodeDeviceMessage(byte[] bytes, + String productKey, String deviceName) { + // 1.1 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); + } + // 1.2 获取编解码器 + IotDeviceMessageCodec codec = codes.get(device.getCodecType()); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); + } + + // 2. 解码消息 + return codec.decode(bytes); + } + + @Override + public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 解码消息 + return codec.decode(bytes); + } + + @Override + public void sendDeviceMessage(IotDeviceMessage message, + String productKey, String deviceName, String serverId) { + // 1. 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); + } + + // 2. 发送消息 + appendDeviceMessage(message, device, serverId); + deviceMessageProducer.sendDeviceMessage(message); + } + + /** + * 补充消息的后端字段 + * + * @param message 消息 + * @param device 设备信息 + * @param serverId 设备连接的 serverId + */ + private void appendDeviceMessage(IotDeviceMessage message, + IotDeviceRespDTO device, String serverId) { + message.setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now()) + .setDeviceId(device.getId()).setTenantId(device.getTenantId()).setServerId(serverId); + // 特殊:如果设备没有指定 requestId,则使用 messageId + if (StrUtil.isEmpty(message.getRequestId())) { + message.setRequestId(message.getId()); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java new file mode 100644 index 000000000..b32510374 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.iot.gateway.service.device.remote; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDeviceApiImpl implements IotDeviceCommonApi { + + @Resource + private IotGatewayProperties gatewayProperties; + + private RestTemplate restTemplate; + + @PostConstruct + public void init() { + IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc(); + restTemplate = new RestTemplateBuilder() + .rootUri(rpc.getUrl() + "/rpc-api/iot/device") + .readTimeout(rpc.getReadTimeout()) + .connectTimeout(rpc.getConnectTimeout()) + .build(); + } + + @Override + public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { + return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { }); + } + + @Override + public CommonResult getDevice(IotDeviceGetReqDTO getReqDTO) { + return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); + } + + private CommonResult doPost(String url, T body, + ParameterizedTypeReference> responseType) { + try { + // 请求 + HttpEntity requestEntity = new HttpEntity<>(body); + ResponseEntity> response = restTemplate.exchange( + url, HttpMethod.POST, requestEntity, responseType); + // 响应 + CommonResult result = response.getBody(); + Assert.notNull(result, "请求结果不能为空"); + return result; + } catch (Exception e) { + log.error("[doPost][url({}) body({}) 发生异常]", url, body, e); + return CommonResult.error(INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java new file mode 100644 index 000000000..7f72937ef --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.iot.gateway.util; + +import cn.hutool.core.util.StrUtil; + +/** + * IoT 网关 MQTT 主题工具类 + *

+ * 用于统一管理 MQTT 协议中的主题常量,基于 Alink 协议规范 + * + * @author 芋道源码 + */ +public final class IotMqttTopicUtils { + + // ========== 静态常量 ========== + + /** + * 系统主题前缀 + */ + private static final String SYS_TOPIC_PREFIX = "/sys/"; + + /** + * 回复主题后缀 + */ + private static final String REPLY_TOPIC_SUFFIX = "_reply"; + + // ========== MQTT HTTP 接口路径常量 ========== + + /** + * MQTT 认证接口路径 + * 对应 EMQX HTTP 认证插件的认证请求接口 + */ + public static final String MQTT_AUTH_PATH = "/mqtt/auth"; + + /** + * MQTT 统一事件处理接口路径 + * 对应 EMQX Webhook 的统一事件处理接口,支持所有客户端事件 + * 包括:client.connected、client.disconnected、message.publish 等 + */ + public static final String MQTT_EVENT_PATH = "/mqtt/event"; + + // ========== 工具方法 ========== + + /** + * 根据消息方法构建对应的主题 + * + * @param method 消息方法,例如 thing.property.post + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param isReply 是否为回复消息 + * @return 完整的主题路径 + */ + public static String buildTopicByMethod(String method, String productKey, String deviceName, boolean isReply) { + if (StrUtil.isBlank(method)) { + return null; + } + // 1. 将点分隔符转换为斜杠 + String topicSuffix = method.replace('.', '/'); + // 2. 对于回复消息,添加 _reply 后缀 + if (isReply) { + topicSuffix += REPLY_TOPIC_SUFFIX; + } + // 3. 构建完整主题 + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml new file mode 100644 index 000000000..19aae39f0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -0,0 +1,130 @@ +spring: + application: + name: iot-gateway-server + profiles: + active: local # 默认激活本地开发环境 + + # Redis 配置 + data: + redis: + host: 127.0.0.1 # Redis 服务器地址 + port: 6379 # Redis 服务器端口 + database: 0 # Redis 数据库索引 + # password: # Redis 密码,如果有的话 + timeout: 30000ms # 连接超时时间 + +--- #################### 消息队列相关 #################### + +# rocketmq 配置项,对应 RocketMQProperties 配置类 +rocketmq: + name-server: 127.0.0.1:9876 # RocketMQ Namesrv + # Producer 配置项 + producer: + group: ${spring.application.name}_PRODUCER # 生产者分组 + +--- #################### IoT 网关相关配置 #################### + +yudao: + iot: + # 消息总线配置 + message-bus: + type: redis # 消息总线的类型 + + # 网关配置 + gateway: + # 设备 RPC 配置 + rpc: + url: http://127.0.0.1:48091 # 主程序 API 地址 + connect-timeout: 30s + read-timeout: 30s + # 设备 Token 配置 + token: + secret: yudaoIotGatewayTokenSecret123456789 # Token 密钥,至少32位 + expiration: 7d + + # 协议配置 + protocol: + # ==================================== + # 针对引入的 HTTP 组件的配置 + # ==================================== + http: + enabled: true + server-port: 8092 + # ==================================== + # 针对引入的 EMQX 组件的配置 + # ==================================== + emqx: + enabled: false + http-port: 8090 # MQTT HTTP 服务端口 + mqtt-host: 127.0.0.1 # MQTT Broker 地址 + mqtt-port: 1883 # MQTT Broker 端口 + mqtt-username: admin # MQTT 用户名 + mqtt-password: public # MQTT 密码 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID + mqtt-ssl: false # 是否开启 SSL + mqtt-topics: + - "/sys/#" # 系统主题 + clean-session: true # 是否启用 Clean Session (默认: true) + keep-alive-interval-seconds: 60 # 心跳间隔,单位秒 (默认: 60) + max-inflight-queue: 10000 # 最大飞行消息队列,单位:条 + connect-timeout-seconds: 10 # 连接超时,单位:秒 + # 是否信任所有 SSL 证书 (默认: false)。警告:生产环境必须为 false! + # 仅在开发环境或内网测试时,如果使用了自签名证书,可以临时设置为 true + trust-all: true # 在 dev 环境可以设为 true + # 遗嘱消息配置 (用于网关异常下线时通知其他系统) + will: + enabled: true # 生产环境强烈建议开启 + topic: "gateway/status/${yudao.iot.gateway.emqx.mqtt-client-id}" # 遗嘱消息主题 + payload: "offline" # 遗嘱消息负载 + qos: 1 # 遗嘱消息 QoS + retain: true # 遗嘱消息是否保留 + # 高级 SSL/TLS 配置 (当 trust-all: false 且 mqtt-ssl: true 时生效) + ssl-options: + key-store-path: "classpath:certs/client.jks" # 客户端证书库路径 + key-store-password: "your-keystore-password" # 客户端证书库密码 + trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径 + trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码 + # ==================================== + # 针对引入的 TCP 组件的配置 + # ==================================== + tcp: + enabled: false + port: 8091 + keep-alive-timeout-ms: 30000 + max-connections: 1000 + ssl-enabled: false + ssl-cert-path: "classpath:certs/client.jks" + ssl-key-path: "classpath:certs/client.jks" + # ==================================== + # 针对引入的 MQTT 组件的配置 + # ==================================== + mqtt: + enabled: true + port: 1883 + max-message-size: 8192 + connect-timeout-seconds: 60 + keep-alive-timeout-seconds: 300 + ssl-enabled: false + +--- #################### 日志相关配置 #################### + +# 基础日志配置 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + level: + # 应用基础日志级别 + cn.iocoder.yudao.module.iot.gateway: INFO + org.springframework.boot: INFO + # RocketMQ 日志 + org.apache.rocketmq: WARN + # MQTT 客户端日志 + # io.vertx.mqtt: DEBUG + # 开发环境详细日志 + cn.iocoder.yudao.module.iot.gateway.protocol.emqx: DEBUG + cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG + cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG + # 根日志级别 + root: INFO + +debug: false diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md new file mode 100644 index 000000000..d6b2b3fdb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -0,0 +1,193 @@ +# TCP 二进制协议数据包格式说明 + +## 1. 协议概述 + +TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。 + +### 1.1 协议特点 + +- **高效传输**:完全二进制格式,减少数据传输量 +- **版本控制**:内置协议版本号,支持协议升级 +- **类型安全**:明确的消息类型标识 +- **简洁设计**:去除冗余字段,协议更加精简 +- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容 + +## 2. 协议格式 + +### 2.1 整体结构 + +``` ++--------+--------+--------+---------------------------+--------+--------+ +| 魔术字 | 版本号 | 消息类型| 消息长度(4字节) | ++--------+--------+--------+---------------------------+--------+--------+ +| 消息 ID 长度(2字节) | 消息 ID (变长字符串) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 方法名长度(2字节) | 方法名(变长字符串) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 消息体数据(变长) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +``` + +### 2.2 字段详细说明 + +| 字段 | 长度 | 类型 | 说明 | +|------|------|------|------| +| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 | +| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 | +| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 | +| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) | +| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 | +| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) | +| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 | +| 方法名 | 变长 | string | 消息方法名(UTF-8编码) | +| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 | + +**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 + +### 2.3 协议常量定义 + +```java +// 协议标识 +private static final byte MAGIC_NUMBER = (byte) 0x7E; +private static final byte PROTOCOL_VERSION = (byte) 0x01; + +// 消息类型 +private static final byte REQUEST = (byte) 0x01; // 请求消息 +private static final byte RESPONSE = (byte) 0x02; // 响应消息 + +// 协议长度 +private static final int HEADER_FIXED_LENGTH = 7; // 固定头部长度 +private static final int MIN_MESSAGE_LENGTH = 11; // 最小消息长度 +``` + +## 3. 消息类型和格式 + +### 3.1 请求消息 (REQUEST - 0x01) + +请求消息用于设备向服务器发送数据或请求。 + +#### 3.1.1 消息体格式 +``` +消息体 = params 数据(JSON格式) +``` + +#### 3.1.2 示例:设备认证请求 + +**消息内容:** +- 消息 ID: `auth_1704067200000_123` +- 方法名: `auth` +- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}` + +**二进制数据包结构:** +``` +7E // 魔术字 (0x7E) +01 // 版本号 (0x01) +01 // 消息类型 (REQUEST) +00 00 00 89 // 消息长度 (137字节) +00 19 // 消息 ID 长度 (25字节) +61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123" +36 37 32 30 30 30 30 30 5F 31 +32 33 +00 04 // 方法名长度 (4字节) +61 75 74 68 // 方法名: "auth" +7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据 +22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001", +30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName", +6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"} +64 75 63 74 4B 65 79 5F 64 65 +76 69 63 65 4E 61 6D 65 22 2C +22 70 61 73 73 77 6F 72 64 22 +3A 22 64 65 76 69 63 65 5F 70 +61 73 73 77 6F 72 64 22 7D +``` + +#### 3.1.3 示例:属性数据上报 + +**消息内容:** +- 消息 ID: `property_1704067200000_456` +- 方法名: `thing.property.post` +- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}` + +### 3.2 响应消息 (RESPONSE - 0x02) + +响应消息用于服务器向设备回复请求结果。 + +#### 3.2.1 消息体格式 +``` +消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON) +``` + +#### 3.2.2 字段说明 + +| 字段 | 长度 | 类型 | 说明 | +|------|------|------|------| +| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 | +| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 | +| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) | +| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) | + +#### 3.2.3 示例:认证成功响应 + +**消息内容:** +- 消息 ID: `auth_response_1704067200000_123` +- 方法名: `auth` +- 响应码: `0` +- 响应消息: `认证成功` +- 响应数据: `{"success":true,"message":"认证成功"}` + +**二进制数据包结构:** +``` +7E // 魔术字 (0x7E) +01 // 版本号 (0x01) +02 // 消息类型 (RESPONSE) +00 00 00 A4 // 消息长度 (164字节) +00 22 // 消息 ID 长度 (34字节) +61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123" +6E 73 65 5F 31 37 30 34 30 36 +37 32 30 30 30 30 30 5F 31 32 +33 +00 04 // 方法名长度 (4字节) +61 75 74 68 // 方法名: "auth" +00 00 00 00 // 响应码 (0 = 成功) +00 0C // 响应消息长度 (12字节) +E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8) +8A 9F +7B 22 73 75 63 63 65 73 73 22 // JSON响应数据 +3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"} +73 61 67 65 22 3A 22 E8 AE A4 +E8 AF 81 E6 88 90 E5 8A 9F 22 +7D +``` + +## 4. 编解码器标识 + +```java +public static final String TYPE = "TCP_BINARY"; +``` + +## 5. 协议优势 + +- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量 +- **解析高效**:直接二进制操作,减少字符串转换开销 +- **类型安全**:明确的消息类型和字段定义 +- **设计简洁**:去除冗余字段,协议更加精简高效 +- **版本控制**:内置版本号支持协议升级 + +## 6. 与 JSON 协议对比 + +| 特性 | 二进制协议 | JSON协议 | +|------|-------------|--------| +| 数据大小 | 小(节省30-50%) | 大 | +| 解析性能 | 高 | 中等 | +| 网络开销 | 低 | 高 | +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 良好 | 优秀 | + +**推荐场景**: +- ✅ **高频数据传输**:传感器数据实时上报 +- ✅ **带宽受限环境**:移动网络、卫星通信 +- ✅ **性能要求高**:需要低延迟、高吞吐的场景 +- ✅ **设备资源有限**:嵌入式设备、低功耗设备 +- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议 +- ❌ **快速原型开发**:开发效率低 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md new file mode 100644 index 000000000..09ef50cfe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -0,0 +1,191 @@ +# TCP JSON 格式协议说明 + +## 1. 协议概述 + +TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点: + +- **标准化**:使用标准 JSON 格式,易于解析和处理 +- **可读性**:人类可读,便于调试和维护 +- **扩展性**:可以轻松添加新字段,向后兼容 +- **跨平台**:JSON 格式支持所有主流编程语言 +- **安全优化**:移除冗余的 deviceId 字段,提高安全性 + +## 2. 消息格式 + +### 2.1 基础消息结构 + +```json +{ + "id": "消息唯一标识", + "method": "消息方法", + "params": { + // 请求参数 + }, + "data": { + // 响应数据 + }, + "code": 响应码, + "msg": "响应消息", + "timestamp": 时间戳 +} +``` + +**⚠️ 重要说明**: +- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID +- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息 + +### 2.2 字段详细说明 + +| 字段名 | 类型 | 必填 | 用途 | 说明 | +|--------|------|------|------|------| +| id | String | 是 | 所有消息 | 消息唯一标识 | +| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` | +| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 | +| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 | +| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 | +| msg | String | 否 | 响应消息 | 响应提示信息 | +| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 | + +### 2.3 消息分类 + +#### 2.3.1 请求消息(上行) +- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段 +- **方向**:设备 → 服务器 +- **用途**:设备认证、数据上报、状态更新等 + +#### 2.3.2 响应消息(下行) +- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段 +- **方向**:服务器 → 设备 +- **用途**:认证结果、指令响应、错误提示等 + +## 3. 消息示例 + +### 3.1 设备认证 (auth) + +#### 认证请求格式 +**消息方向**:设备 → 服务器 + +```json +{ + "id": "auth_1704067200000_123", + "method": "auth", + "params": { + "clientId": "device_001", + "username": "productKey_deviceName", + "password": "设备密码" + }, + "timestamp": 1704067200000 +} +``` + +**认证参数说明:** + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| clientId | String | 是 | 客户端唯一标识,用于连接管理 | +| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | +| password | String | 是 | 设备密码,在设备管理平台配置 | + +#### 认证响应格式 +**消息方向**:服务器 → 设备 + +**认证成功响应:** +```json +{ + "id": "response_auth_1704067200000_123", + "method": "auth", + "data": { + "success": true, + "message": "认证成功" + }, + "code": 0, + "msg": "认证成功", + "timestamp": 1704067200001 +} +``` + +**认证失败响应:** +```json +{ + "id": "response_auth_1704067200000_123", + "method": "auth", + "data": { + "success": false, + "message": "认证失败:用户名或密码错误" + }, + "code": 401, + "msg": "认证失败", + "timestamp": 1704067200001 +} +``` + +### 3.2 属性数据上报 (thing.property.post) + +**消息方向**:设备 → 服务器 + +**示例:温度传感器数据上报** +```json +{ + "id": "property_1704067200000_456", + "method": "thing.property.post", + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25, + "battery": 85, + "signal_strength": -65 + }, + "timestamp": 1704067200000 +} +``` + +### 3.3 设备状态更新 (thing.state.update) + +**消息方向**:设备 → 服务器 + +**示例:心跳请求** +```json +{ + "id": "heartbeat_1704067200000_321", + "method": "thing.state.update", + "params": { + "state": "online", + "uptime": 86400, + "memory_usage": 65.2, + "cpu_usage": 12.8 + }, + "timestamp": 1704067200000 +} +``` + +## 4. 编解码器标识 + +```java +public static final String TYPE = "TCP_JSON"; +``` + +## 5. 协议优势 + +- **开发效率高**:JSON 格式,开发和调试简单 +- **跨语言支持**:所有主流语言都支持 JSON +- **可读性优秀**:可以直接查看消息内容 +- **扩展性强**:可以轻松添加新字段 +- **安全性高**:移除 deviceId 字段,防止伪造攻击 + +## 6. 与二进制协议对比 + +| 特性 | JSON协议 | 二进制协议 | +|------|----------|------------| +| 开发难度 | 低 | 高 | +| 调试难度 | 低 | 高 | +| 可读性 | 优秀 | 差 | +| 数据大小 | 中等 | 小(节省30-50%) | +| 解析性能 | 中等 | 高 | +| 学习成本 | 低 | 高 | + +**推荐场景**: +- ✅ **开发调试阶段**:调试友好,开发效率高 +- ✅ **快速原型开发**:实现简单,快速迭代 +- ✅ **多语言集成**:广泛的语言支持 +- ❌ **高频数据传输**:建议使用二进制协议 +- ❌ **带宽受限环境**:建议使用二进制协议 \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/pom.xml deleted file mode 100644 index 22a86874b..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/pom.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - yudao-module-iot - cn.iocoder.cloud - ${revision} - - - yudao-module-iot-plugin-common - yudao-module-iot-plugin-http - yudao-module-iot-plugin-mqtt - yudao-module-iot-plugin-emqx - - - 4.0.0 - - yudao-module-iot-plugins - pom - - ${project.artifactId} - - 物联网 插件 模块 - - - \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml deleted file mode 100644 index 9fcc448c2..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - yudao-module-iot-plugins - cn.iocoder.cloud - ${revision} - - 4.0.0 - yudao-module-iot-plugin-common - jar - - ${project.artifactId} - - - 物联网 插件 模块 - 通用功能 - - - - - org.springframework.boot - spring-boot-starter - - - - cn.iocoder.cloud - yudao-module-iot-api - ${revision} - - - - - org.springframework - spring-web - - - - - io.vertx - vertx-web - - - - - org.springframework.boot - spring-boot-starter-validation - true - - - - diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java deleted file mode 100644 index ba7d56fe6..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.config; - -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; -import cn.iocoder.yudao.module.iot.plugin.common.heartbeat.IotPluginInstanceHeartbeatJob; -import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.client.RestTemplate; - -/** - * IoT 插件的通用自动配置类 - * - * @author haohao - */ -@AutoConfiguration -@EnableConfigurationProperties(IotPluginCommonProperties.class) -@EnableScheduling // 开启定时任务,因为 IotPluginInstanceHeartbeatJob 是一个定时任务 -public class IotPluginCommonAutoConfiguration { - - @Bean - public RestTemplate restTemplate(IotPluginCommonProperties properties) { - return new RestTemplateBuilder() - .connectTimeout(properties.getUpstreamConnectTimeout()) - .readTimeout(properties.getUpstreamReadTimeout()) - .build(); - } - - @Bean - public IotDeviceUpstreamApi deviceUpstreamApi(IotPluginCommonProperties properties, - RestTemplate restTemplate) { - return new IotDeviceUpstreamClient(properties, restTemplate); - } - - @Bean(initMethod = "start", destroyMethod = "stop") - public IotDeviceDownstreamServer deviceDownstreamServer(IotPluginCommonProperties properties, - IotDeviceDownstreamHandler deviceDownstreamHandler) { - return new IotDeviceDownstreamServer(properties, deviceDownstreamHandler); - } - - @Bean(initMethod = "init", destroyMethod = "stop") - public IotPluginInstanceHeartbeatJob pluginInstanceHeartbeatJob(IotDeviceUpstreamApi deviceDataApi, - IotDeviceDownstreamServer deviceDownstreamServer, - IotPluginCommonProperties commonProperties) { - return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer, commonProperties); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java deleted file mode 100644 index 03d42c288..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.config; - -import jakarta.validation.constraints.NotEmpty; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import java.time.Duration; - -/** - * IoT 插件的通用配置类 - * - * @author haohao - */ -@ConfigurationProperties(prefix = "yudao.iot.plugin.common") -@Validated -@Data -public class IotPluginCommonProperties { - - /** - * 上行连接超时的默认值 - */ - public static final Duration UPSTREAM_CONNECT_TIMEOUT_DEFAULT = Duration.ofSeconds(30); - /** - * 上行读取超时的默认值 - */ - public static final Duration UPSTREAM_READ_TIMEOUT_DEFAULT = Duration.ofSeconds(30); - - /** - * 下行端口 - 随机 - */ - public static final Integer DOWNSTREAM_PORT_RANDOM = 0; - - /** - * 上行 URL - */ - @NotEmpty(message = "上行 URL 不能为空") - private String upstreamUrl; - /** - * 上行连接超时 - */ - private Duration upstreamConnectTimeout = UPSTREAM_CONNECT_TIMEOUT_DEFAULT; - /** - * 上行读取超时 - */ - private Duration upstreamReadTimeout = UPSTREAM_READ_TIMEOUT_DEFAULT; - - /** - * 下行端口 - */ - private Integer downstreamPort = DOWNSTREAM_PORT_RANDOM; - - /** - * 插件包标识符 - */ - @NotEmpty(message = "插件包标识符不能为空") - private String pluginKey; - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java deleted file mode 100644 index 38aba3df6..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; - -/** - * IoT 设备下行处理器 - * - * 目的:每个 plugin 需要实现,用于处理 server 下行的指令(请求),从而实现从 server => plugin => device 的下行流程 - * - * @author 芋道源码 - */ -public interface IotDeviceDownstreamHandler { - - /** - * 调用设备服务 - * - * @param invokeReqDTO 调用设备服务的请求 - * @return 是否成功 - */ - CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO); - - /** - * 获取设备属性 - * - * @param getReqDTO 获取设备属性的请求 - * @return 是否成功 - */ - CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO); - - /** - * 设置设备属性 - * - * @param setReqDTO 设置设备属性的请求 - * @return 是否成功 - */ - CommonResult setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO); - - /** - * 设置设备配置 - * - * @param setReqDTO 设置设备配置的请求 - * @return 是否成功 - */ - CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO); - - /** - * 升级设备 OTA - * - * @param upgradeReqDTO 升级设备 OTA 的请求 - * @return 是否成功 - */ - CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO); - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java deleted file mode 100644 index 719fdb5c3..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java +++ /dev/null @@ -1,94 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream; - -import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.router.*; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 设备下行服务端,接收来自 server 服务器的请求,转发给 device 设备 - * - * @author 芋道源码 - */ -@Slf4j -public class IotDeviceDownstreamServer { - - private final Vertx vertx; - private final HttpServer server; - private final IotPluginCommonProperties properties; - - public IotDeviceDownstreamServer(IotPluginCommonProperties properties, - IotDeviceDownstreamHandler deviceDownstreamHandler) { - this.properties = properties; - // 创建 Vertx 实例 - this.vertx = Vertx.vertx(); - // 创建 Router 实例 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); // 处理 Body - router.post(IotDeviceServiceInvokeVertxHandler.PATH) - .handler(new IotDeviceServiceInvokeVertxHandler(deviceDownstreamHandler)); - router.post(IotDevicePropertySetVertxHandler.PATH) - .handler(new IotDevicePropertySetVertxHandler(deviceDownstreamHandler)); - router.post(IotDevicePropertyGetVertxHandler.PATH) - .handler(new IotDevicePropertyGetVertxHandler(deviceDownstreamHandler)); - router.post(IotDeviceConfigSetVertxHandler.PATH) - .handler(new IotDeviceConfigSetVertxHandler(deviceDownstreamHandler)); - router.post(IotDeviceOtaUpgradeVertxHandler.PATH) - .handler(new IotDeviceOtaUpgradeVertxHandler(deviceDownstreamHandler)); - // 创建 HttpServer 实例 - this.server = vertx.createHttpServer().requestHandler(router); - } - - /** - * 启动 HTTP 服务器 - */ - public void start() { - log.info("[start][开始启动]"); - server.listen(properties.getDownstreamPort()) - .toCompletionStage() - .toCompletableFuture() - .join(); - log.info("[start][启动完成,端口({})]", this.server.actualPort()); - } - - /** - * 停止所有 - */ - public void stop() { - log.info("[stop][开始关闭]"); - try { - // 关闭 HTTP 服务器 - if (server != null) { - server.close() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - - // 关闭 Vertx 实例 - if (vertx != null) { - vertx.close() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - log.info("[stop][关闭完成]"); - } catch (Exception e) { - log.error("[stop][关闭异常]", e); - throw new RuntimeException(e); - } - } - - /** - * 获得端口 - * - * @return 端口 - */ - public int getPort() { - return this.server.actualPort(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java deleted file mode 100644 index 1693f128d..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceConfigSetReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * IoT 设备配置设置 Vertx Handler - * - * 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotDeviceConfigSetVertxHandler implements Handler { - - // TODO @haohao:是不是可以把 PATH、Method 所有的,抽到一个枚举类里?因为 topic、path、method 相当于不同的几个表达? - public static final String PATH = "/sys/:productKey/:deviceName/thing/service/config/set"; - public static final String METHOD = "thing.service.config.set"; - - private final IotDeviceDownstreamHandler deviceDownstreamHandler; - - @Override - @SuppressWarnings("unchecked") - public void handle(RoutingContext routingContext) { - // 1. 解析参数 - IotDeviceConfigSetReqDTO reqDTO; - try { - String productKey = routingContext.pathParam("productKey"); - String deviceName = routingContext.pathParam("deviceName"); - JsonObject body = routingContext.body().asJsonObject(); - String requestId = body.getString("requestId"); - Map config = (Map) body.getMap().get("config"); - reqDTO = ((IotDeviceConfigSetReqDTO) new IotDeviceConfigSetReqDTO() - .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) - .setConfig(config); - } catch (Exception e) { - log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); - IotStandardResponse errorResponse = IotStandardResponse.error( - null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - return; - } - - // 2. 调用处理器 - try { - CommonResult result = deviceDownstreamHandler.setDeviceConfig(reqDTO); - - // 3. 响应结果 - IotStandardResponse response = result.isSuccess() ? - IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()) - : IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, response); - } catch (Exception e) { - log.error("[handle][请求参数({}) 配置设置异常]", reqDTO, e); - IotStandardResponse errorResponse = IotStandardResponse.error( - reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java deleted file mode 100644 index b417229aa..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceOtaUpgradeReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * IoT 设备 OTA 升级 Vertx Handler - *

- * 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotDeviceOtaUpgradeVertxHandler implements Handler { - - public static final String PATH = "/ota/:productKey/:deviceName/upgrade"; - public static final String METHOD = "ota.device.upgrade"; - - private final IotDeviceDownstreamHandler deviceDownstreamHandler; - - @Override - public void handle(RoutingContext routingContext) { - // 1. 解析参数 - IotDeviceOtaUpgradeReqDTO reqDTO; - try { - String productKey = routingContext.pathParam("productKey"); - String deviceName = routingContext.pathParam("deviceName"); - JsonObject body = routingContext.body().asJsonObject(); - String requestId = body.getString("requestId"); - Long firmwareId = body.getLong("firmwareId"); - String version = body.getString("version"); - String signMethod = body.getString("signMethod"); - String fileSign = body.getString("fileSign"); - Long fileSize = body.getLong("fileSize"); - String fileUrl = body.getString("fileUrl"); - String information = body.getString("information"); - reqDTO = ((IotDeviceOtaUpgradeReqDTO) new IotDeviceOtaUpgradeReqDTO() - .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) - .setFirmwareId(firmwareId).setVersion(version) - .setSignMethod(signMethod).setFileSign(fileSign).setFileSize(fileSize).setFileUrl(fileUrl) - .setInformation(information); - } catch (Exception e) { - log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); - IotStandardResponse errorResponse = IotStandardResponse.error( - null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - return; - } - - // 2. 调用处理器 - try { - CommonResult result = deviceDownstreamHandler.upgradeDeviceOta(reqDTO); - - // 3. 响应结果 - // TODO @haohao:可以考虑 IotStandardResponse.of(requestId, method, CommonResult) - IotStandardResponse response = result.isSuccess() ? - IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()) - :IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, response); - } catch (Exception e) { - log.error("[handle][请求参数({}) OTA 升级异常]", reqDTO, e); - // TODO @haohao:可以考虑 IotStandardResponse.of(requestId, method, ErrorCode) - IotStandardResponse errorResponse = IotStandardResponse.error( - reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java deleted file mode 100644 index 3cb4bc941..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertyGetReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * IoT 设备服务获取 Vertx Handler - * - * 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotDevicePropertyGetVertxHandler implements Handler { - - public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/get"; - public static final String METHOD = "thing.service.property.get"; - - private final IotDeviceDownstreamHandler deviceDownstreamHandler; - - @Override - @SuppressWarnings("unchecked") - public void handle(RoutingContext routingContext) { - // 1. 解析参数 - IotDevicePropertyGetReqDTO reqDTO; - try { - String productKey = routingContext.pathParam("productKey"); - String deviceName = routingContext.pathParam("deviceName"); - JsonObject body = routingContext.body().asJsonObject(); - String requestId = body.getString("requestId"); - List identifiers = (List) body.getMap().get("identifiers"); - reqDTO = ((IotDevicePropertyGetReqDTO) new IotDevicePropertyGetReqDTO() - .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) - .setIdentifiers(identifiers); - } catch (Exception e) { - log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); - IotStandardResponse errorResponse = IotStandardResponse.error( - null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - return; - } - - // 2. 调用处理器 - try { - CommonResult result = deviceDownstreamHandler.getDeviceProperty(reqDTO); - - // 3. 响应结果 - IotStandardResponse response; - if (result.isSuccess()) { - response = IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()); - } else { - response = IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); - } - IotPluginCommonUtils.writeJsonResponse(routingContext, response); - } catch (Exception e) { - log.error("[handle][请求参数({}) 属性获取异常]", reqDTO, e); - IotStandardResponse errorResponse = IotStandardResponse.error( - reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java deleted file mode 100644 index 251be1eb9..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java +++ /dev/null @@ -1,75 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertySetReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * IoT 设置设备属性 Vertx Handler - * - * 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotDevicePropertySetVertxHandler implements Handler { - - public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/set"; - public static final String METHOD = "thing.service.property.set"; - - private final IotDeviceDownstreamHandler deviceDownstreamHandler; - - @Override - @SuppressWarnings("unchecked") - public void handle(RoutingContext routingContext) { - // 1. 解析参数 - IotDevicePropertySetReqDTO reqDTO; - try { - String productKey = routingContext.pathParam("productKey"); - String deviceName = routingContext.pathParam("deviceName"); - JsonObject body = routingContext.body().asJsonObject(); - String requestId = body.getString("requestId"); - Map properties = (Map) body.getMap().get("properties"); - reqDTO = ((IotDevicePropertySetReqDTO) new IotDevicePropertySetReqDTO() - .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) - .setProperties(properties); - } catch (Exception e) { - log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); - IotStandardResponse errorResponse = IotStandardResponse.error( - null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - return; - } - - // 2. 调用处理器 - try { - CommonResult result = deviceDownstreamHandler.setDeviceProperty(reqDTO); - - // 3. 响应结果 - IotStandardResponse response; - if (result.isSuccess()) { - response = IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()); - } else { - response = IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); - } - IotPluginCommonUtils.writeJsonResponse(routingContext, response); - } catch (Exception e) { - log.error("[handle][请求参数({}) 属性设置异常]", reqDTO, e); - IotStandardResponse errorResponse = IotStandardResponse.error( - reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java deleted file mode 100644 index 534823f75..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceServiceInvokeReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * IoT 设备服务调用 Vertx Handler - * - * 芋道源码 - */ -@Slf4j -@RequiredArgsConstructor -public class IotDeviceServiceInvokeVertxHandler implements Handler { - - public static final String PATH = "/sys/:productKey/:deviceName/thing/service/:identifier"; - public static final String METHOD_PREFIX = "thing.service."; - public static final String METHOD_SUFFIX = ""; - - private final IotDeviceDownstreamHandler deviceDownstreamHandler; - - @Override - @SuppressWarnings("unchecked") - public void handle(RoutingContext routingContext) { - // 1. 解析参数 - IotDeviceServiceInvokeReqDTO reqDTO; - try { - String productKey = routingContext.pathParam("productKey"); - String deviceName = routingContext.pathParam("deviceName"); - String identifier = routingContext.pathParam("identifier"); - JsonObject body = routingContext.body().asJsonObject(); - String requestId = body.getString("requestId"); - Map params = (Map) body.getMap().get("params"); - reqDTO = ((IotDeviceServiceInvokeReqDTO) new IotDeviceServiceInvokeReqDTO() - .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) - .setIdentifier(identifier).setParams(params); - } catch (Exception e) { - log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); - String method = METHOD_PREFIX + routingContext.pathParam("identifier") + METHOD_SUFFIX; - IotStandardResponse errorResponse = IotStandardResponse.error( - null, method, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - return; - } - - // 2. 调用处理器 - try { - CommonResult result = deviceDownstreamHandler.invokeDeviceService(reqDTO); - - // 3. 响应结果 - String method = METHOD_PREFIX + reqDTO.getIdentifier() + METHOD_SUFFIX; - IotStandardResponse response; - if (result.isSuccess()) { - response = IotStandardResponse.success(reqDTO.getRequestId(), method, result.getData()); - } else { - response = IotStandardResponse.error(reqDTO.getRequestId(), method, result.getCode(), result.getMsg()); - } - IotPluginCommonUtils.writeJsonResponse(routingContext, response); - } catch (Exception e) { - log.error("[handle][请求参数({}) 服务调用异常]", reqDTO, e); - String method = METHOD_PREFIX + reqDTO.getIdentifier() + METHOD_SUFFIX; - IotStandardResponse errorResponse = IotStandardResponse.error( - reqDTO.getRequestId(), method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java deleted file mode 100644 index f272468c5..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.heartbeat; - -import cn.hutool.system.SystemUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; - -import java.util.concurrent.TimeUnit; - -/** - * IoT 插件实例心跳 Job - * - * 用于定时发送心跳给服务端 - */ -@RequiredArgsConstructor -@Slf4j -public class IotPluginInstanceHeartbeatJob { - - private final IotDeviceUpstreamApi deviceUpstreamApi; - private final IotDeviceDownstreamServer deviceDownstreamServer; - private final IotPluginCommonProperties commonProperties; - - public void init() { - CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true)); - log.info("[init][上线结果:{})]", result); - } - - public void stop() { - CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(false)); - log.info("[stop][下线结果:{})]", result); - } - - @Scheduled(initialDelay = 3, fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3 分钟执行一次 - public void execute() { - CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true)); - log.info("[execute][心跳结果:{})]", result); - } - - private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(Boolean online) { - return new IotPluginInstanceHeartbeatReqDTO() - .setPluginKey(commonProperties.getPluginKey()).setProcessId(IotPluginCommonUtils.getProcessId()) - .setHostIp(SystemUtil.getHostInfo().getAddress()).setDownstreamPort(deviceDownstreamServer.getPort()) - .setOnline(online); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java deleted file mode 100644 index 83b5bb58a..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -// TODO @芋艿:注释 -package cn.iocoder.yudao.module.iot.plugin.common; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java deleted file mode 100644 index 131eb1b9c..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java +++ /dev/null @@ -1,94 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.pojo; - -import lombok.Data; - -// TODO @芋艿:1)后续考虑,要不要叫 IoT 网关之类的 Response;2)包名 pojo -/** - * IoT 标准协议响应实体类 - *

- * 用于统一 MQTT 和 HTTP 的响应格式 - * - * @author haohao - */ -@Data -public class IotStandardResponse { - - /** - * 消息ID - */ - private String id; - - /** - * 状态码 - */ - private Integer code; - - /** - * 响应数据 - */ - private Object data; - - /** - * 响应消息 - */ - private String message; - - /** - * 方法名 - */ - private String method; - - /** - * 协议版本 - */ - private String version; - - /** - * 创建成功响应 - * - * @param id 消息ID - * @param method 方法名 - * @return 成功响应 - */ - public static IotStandardResponse success(String id, String method) { - return success(id, method, null); - } - - /** - * 创建成功响应 - * - * @param id 消息ID - * @param method 方法名 - * @param data 响应数据 - * @return 成功响应 - */ - public static IotStandardResponse success(String id, String method, Object data) { - return new IotStandardResponse() - .setId(id) - .setCode(200) - .setData(data) - .setMessage("success") - .setMethod(method) - .setVersion("1.0"); - } - - /** - * 创建错误响应 - * - * @param id 消息ID - * @param method 方法名 - * @param code 错误码 - * @param message 错误消息 - * @return 错误响应 - */ - public static IotStandardResponse error(String id, String method, Integer code, String message) { - return new IotStandardResponse() - .setId(id) - .setCode(code) - .setData(null) - .setMessage(message) - .setMethod(method) - .setVersion("1.0"); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java deleted file mode 100644 index 1bf4d676c..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java +++ /dev/null @@ -1,91 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.upstream; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.client.RestTemplate; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * 设备数据 Upstream 上行客户端 - * - * 通过 HTTP 调用远程的 IotDeviceUpstreamApi 接口 - * - * @author haohao - */ -@RequiredArgsConstructor -@Slf4j -public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { - - public static final String URL_PREFIX = "/rpc-api/iot/device/upstream"; - - private final IotPluginCommonProperties properties; - - private final RestTemplate restTemplate; - - @Override - public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { - String url = properties.getUpstreamUrl() + URL_PREFIX + "/update-state"; - return doPost(url, updateReqDTO); - } - - @Override - public CommonResult reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { - String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-event"; - return doPost(url, reportReqDTO); - } - - // TODO @芋艿:待实现 - @Override - public CommonResult registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { - return null; - } - - // TODO @芋艿:待实现 - @Override - public CommonResult registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { - return null; - } - - // TODO @芋艿:待实现 - @Override - public CommonResult addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { - return null; - } - - @Override - public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { - String url = properties.getUpstreamUrl() + URL_PREFIX + "/authenticate-emqx-connection"; - return doPost(url, authReqDTO); - } - - @Override - public CommonResult reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { - String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-property"; - return doPost(url, reportReqDTO); - } - - @Override - public CommonResult heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { - String url = properties.getUpstreamUrl() + URL_PREFIX + "/heartbeat-plugin-instance"; - return doPost(url, heartbeatReqDTO); - } - - @SuppressWarnings("unchecked") - private CommonResult doPost(String url, T requestBody) { - try { - CommonResult result = restTemplate.postForObject(url, requestBody, - (Class>) (Class) CommonResult.class); - log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); - return result; - } catch (Exception e) { - log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); - return CommonResult.error(INTERNAL_SERVER_ERROR); - } - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java deleted file mode 100644 index 34c6c0fe2..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java +++ /dev/null @@ -1,76 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.common.util; - -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.system.SystemUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import io.vertx.core.http.HttpHeaders; -import io.vertx.ext.web.RoutingContext; -import org.springframework.http.MediaType; - -/** - * IoT 插件的通用工具类 - * - * @author 芋道源码 - */ -public class IotPluginCommonUtils { - - /** - * 流程实例的进程编号 - */ - private static String processId; - - public static String getProcessId() { - if (StrUtil.isEmpty(processId)) { - initProcessId(); - } - return processId; - } - - private synchronized static void initProcessId() { - processId = String.format("%s@%d@%s", // IP@PID@${uuid} - SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID(), IdUtil.fastSimpleUUID()); - } - - /** - * 将对象转换为JSON字符串后写入HTTP响应 - * - * @param routingContext 路由上下文 - * @param data 数据对象 - */ - @SuppressWarnings("deprecation") - public static void writeJsonResponse(RoutingContext routingContext, Object data) { - routingContext.response() - .setStatusCode(200) - .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(data)); - } - - /** - * 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse) - *

- * 推荐使用此方法,统一MQTT和HTTP的响应格式。使用方式: - * - *

-     * // 成功响应
-     * IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
-     * IotPluginCommonUtils.writeJsonResponse(routingContext, response);
-     *
-     * // 错误响应
-     * IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
-     * IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
-     * 
- * - * @param routingContext 路由上下文 - * @param response IotStandardResponse响应对象 - */ - @SuppressWarnings("deprecation") - public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) { - routingContext.response() - .setStatusCode(200) - .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(response)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index eae9ad882..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonAutoConfiguration \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties deleted file mode 100644 index 565e81eb0..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties +++ /dev/null @@ -1,6 +0,0 @@ -plugin.id=yudao-module-iot-plugin-emqx -plugin.class=cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin -plugin.version=1.0.0 -plugin.provider=yudao -plugin.dependencies= -plugin.description=yudao-module-iot-plugin-emqx-1.0.0 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml deleted file mode 100644 index 7263773f3..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml +++ /dev/null @@ -1,169 +0,0 @@ - - - - yudao-module-iot-plugins - cn.iocoder.cloud - ${revision} - - 4.0.0 - jar - - yudao-module-iot-plugin-emqx - 1.0.0 - - ${project.artifactId} - - - 物联网 插件模块 - emqx 插件 - - - - - emqx-plugin - cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin - ${project.version} - yudao - ${project.artifactId}-${project.version} - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.6 - - - unzip jar file - package - - - - - - - run - - - - - - - maven-assembly-plugin - 2.3 - - - - src/main/assembly/assembly.xml - - - false - - - - make-assembly - package - - attached - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - - ${plugin.id} - ${plugin.class} - ${plugin.version} - ${plugin.provider} - ${plugin.description} - ${plugin.dependencies} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - - - repackage - - - -standalone - - - - - - - - - - - cn.iocoder.cloud - yudao-module-iot-plugin-common - ${revision} - - - - - org.springframework.boot - spring-boot-starter-web - - - - - io.vertx - vertx-web - - - io.vertx - vertx-mqtt - - - \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml deleted file mode 100644 index daec9e431..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml +++ /dev/null @@ -1,31 +0,0 @@ - - plugin - - zip - - false - - - false - runtime - lib - - *:jar:* - - - - - - - target/plugin-classes - classes - - - diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java deleted file mode 100644 index 178038417..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * IoT Emqx 插件的独立运行入口 - */ -@Slf4j -@SpringBootApplication -public class IotEmqxPluginApplication { - - public static void main(String[] args) { - SpringApplication application = new SpringApplication(IotEmqxPluginApplication.class); - application.setWebApplicationType(WebApplicationType.NONE); - application.run(args); - log.info("[main][独立模式启动完成]"); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java deleted file mode 100644 index 275c20eb1..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.config; - -import cn.hutool.extra.spring.SpringUtil; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginWrapper; -import org.pf4j.spring.SpringPlugin; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -/** - * EMQX 插件实现类 - * - * 基于 PF4J 插件框架,实现 EMQX 消息中间件的集成:负责插件的生命周期管理,包括启动、停止和应用上下文的创建 - * - * @author haohao - */ -@Slf4j -public class IotEmqxPlugin extends SpringPlugin { - - public IotEmqxPlugin(PluginWrapper wrapper) { - super(wrapper); - } - - @Override - public void start() { - log.info("[EmqxPlugin][EmqxPlugin 插件启动开始...]"); - try { - log.info("[EmqxPlugin][EmqxPlugin 插件启动成功...]"); - } catch (Exception e) { - log.error("[EmqxPlugin][EmqxPlugin 插件开启动异常...]", e); - } - } - - @Override - public void stop() { - log.info("[EmqxPlugin][EmqxPlugin 插件停止开始...]"); - try { - log.info("[EmqxPlugin][EmqxPlugin 插件停止成功...]"); - } catch (Exception e) { - log.error("[EmqxPlugin][EmqxPlugin 插件停止异常...]", e); - } - } - - @Override - protected ApplicationContext createApplicationContext() { - // 创建插件自己的 ApplicationContext - AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext(); - // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用) - pluginContext.setParent(SpringUtil.getApplicationContext()); - // 继续使用插件自己的 ClassLoader 以加载插件内部的类 - pluginContext.setClassLoader(getWrapper().getPluginClassLoader()); - // 扫描当前插件的自动配置包 - // TODO @芋艿:是不是要配置下包 - pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.emqx.config"); - pluginContext.refresh(); - return pluginContext; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java deleted file mode 100644 index e1d11504c..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.config; - -import cn.hutool.core.util.IdUtil; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl; -import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.IotDeviceUpstreamServer; -import io.vertx.core.Vertx; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.MqttClientOptions; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * IoT 插件 EMQX 的专用自动配置类 - * - * @author haohao - */ -@Slf4j -@Configuration -@EnableConfigurationProperties(IotPluginEmqxProperties.class) -public class IotPluginEmqxAutoConfiguration { - - @Bean - public Vertx vertx() { - return Vertx.vertx(); - } - - @Bean - public MqttClient mqttClient(Vertx vertx, IotPluginEmqxProperties emqxProperties) { - MqttClientOptions options = new MqttClientOptions() - .setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID()) - .setUsername(emqxProperties.getMqttUsername()) - .setPassword(emqxProperties.getMqttPassword()) - .setSsl(emqxProperties.getMqttSsl()); - return MqttClient.create(vertx, options); - } - - @Bean(initMethod = "start", destroyMethod = "stop") - public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, - IotPluginEmqxProperties emqxProperties, - Vertx vertx, - MqttClient mqttClient) { - return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient); - } - - @Bean - public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) { - return new IotDeviceDownstreamHandlerImpl(mqttClient); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java deleted file mode 100644 index 219fe0360..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java +++ /dev/null @@ -1,50 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -/** - * 物联网插件 - EMQX 配置 - * - * @author 芋道源码 - */ -@ConfigurationProperties(prefix = "yudao.iot.plugin.emqx") -@Validated -@Data -public class IotPluginEmqxProperties { - - // TODO @haohao:参数校验,加下,啊哈 - - /** - * 服务主机 - */ - private String mqttHost; - /** - * 服务端口 - */ - private Integer mqttPort; - /** - * 服务用户名 - */ - private String mqttUsername; - /** - * 服务密码 - */ - private String mqttPassword; - /** - * 是否启用 SSL - */ - private Boolean mqttSsl; - - /** - * 订阅的主题列表 - */ - private String[] mqttTopics; - - /** - * 认证端口 - */ - private Integer authPort; - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java deleted file mode 100644 index f5c19224a..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java +++ /dev/null @@ -1,176 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.downstream; - -import cn.hutool.core.util.IdUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttClient; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL; - -/** - * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类 - * - * @author 芋道源码 - */ -@Slf4j -public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { - - private static final String SYS_TOPIC_PREFIX = "/sys/"; - - // TODO @haohao:是不是可以类似 IotDeviceConfigSetVertxHandler 的建议,抽到统一的枚举类 - // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈。;回复 都使用 Alink 格式,方便后续扩展。 - // 设备服务调用 标准 JSON - // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier} - // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply - private static final String SERVICE_TOPIC_PREFIX = "/thing/service/"; - - // 设置设备属性 标准 JSON - // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set - // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply - private static final String PROPERTY_SET_TOPIC = "/thing/service/property/set"; - - private final MqttClient mqttClient; - - /** - * 构造函数 - * - * @param mqttClient MQTT客户端 - */ - public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) { - this.mqttClient = mqttClient; - } - - @Override - public CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO reqDTO) { - log.info("[invokeService][开始调用设备服务][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); - - // 验证参数 - if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null || reqDTO.getIdentifier() == null) { - log.error("[invokeService][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); - return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); - } - - try { - // 构建请求主题 - String topic = buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getIdentifier()); - // 构建请求消息 - String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId(); - JSONObject request = buildServiceRequest(requestId, reqDTO.getIdentifier(), reqDTO.getParams()); - // 发送消息 - publishMessage(topic, request); - - log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic); - return CommonResult.success(true); - } catch (Exception e) { - log.error("[invokeService][调用设备服务异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e); - return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); - } - } - - @Override - public CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) { - return CommonResult.success(true); - } - - @Override - public CommonResult setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) { - // 验证参数 - log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); - if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) { - log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); - return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); - } - - try { - // 构建请求主题 - String topic = buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName()); - // 构建请求消息 - String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId(); - JSONObject request = buildPropertySetRequest(requestId, reqDTO.getProperties()); - // 发送消息 - publishMessage(topic, request); - - log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic); - return CommonResult.success(true); - } catch (Exception e) { - log.error("[setProperty][设置设备属性异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e); - return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); - } - } - - @Override - public CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) { - return CommonResult.success(true); - } - - @Override - public CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) { - return CommonResult.success(true); - } - - /** - * 构建服务调用主题 - */ - private String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) { - return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + SERVICE_TOPIC_PREFIX + serviceIdentifier; - } - - /** - * 构建属性设置主题 - */ - private String buildPropertySetTopic(String productKey, String deviceName) { - return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + PROPERTY_SET_TOPIC; - } - - // TODO @haohao:这个,后面搞个对象,会不会好点哈? - /** - * 构建服务调用请求 - */ - private JSONObject buildServiceRequest(String requestId, String serviceIdentifier, Map params) { - return new JSONObject() - .set("id", requestId) - .set("version", "1.0") - .set("method", "thing.service." + serviceIdentifier) - .set("params", params != null ? params : new JSONObject()); - } - - /** - * 构建属性设置请求 - */ - private JSONObject buildPropertySetRequest(String requestId, Map properties) { - return new JSONObject() - .set("id", requestId) - .set("version", "1.0") - .set("method", "thing.service.property.set") - .set("params", properties); - } - - /** - * 发布 MQTT 消息 - */ - private void publishMessage(String topic, JSONObject payload) { - mqttClient.publish( - topic, - Buffer.buffer(payload.toString()), - MqttQoS.AT_LEAST_ONCE, - false, - false); - log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload); - } - - /** - * 生成请求 ID - */ - private String generateRequestId() { - return IdUtil.fastSimpleUUID(); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java deleted file mode 100644 index 00792ebcf..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java +++ /dev/null @@ -1,236 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.upstream; - -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; -import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; -import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler; -import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceWebhookVertxHandler; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Future; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.mqtt.MqttClient; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -/** - * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 - *

- * 协议:HTTP、MQTT - * - * @author haohao - */ -@Slf4j -public class IotDeviceUpstreamServer { - - /** - * 重连延迟时间(毫秒) - */ - private static final int RECONNECT_DELAY_MS = 5000; - /** - * 连接超时时间(毫秒) - */ - private static final int CONNECTION_TIMEOUT_MS = 10000; - /** - * 默认 QoS 级别 - */ - private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE; - - private final Vertx vertx; - private final HttpServer server; - private final MqttClient client; - private final IotPluginEmqxProperties emqxProperties; - private final IotDeviceMqttMessageHandler mqttMessageHandler; - - /** - * 服务运行状态标志 - */ - private volatile boolean isRunning = false; - - public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties, - IotDeviceUpstreamApi deviceUpstreamApi, - Vertx vertx, - MqttClient client) { - this.vertx = vertx; - this.emqxProperties = emqxProperties; - this.client = client; - - // 创建 Router 实例 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); // 处理 Body - router.post(IotDeviceAuthVertxHandler.PATH) - // TODO @haohao:疑问,mqtt 的认证,需要通过 http 呀? - // 回复:MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式 - .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); - // 添加 Webhook 处理器,用于处理设备连接和断开连接事件 - router.post(IotDeviceWebhookVertxHandler.PATH) - .handler(new IotDeviceWebhookVertxHandler(deviceUpstreamApi)); - // 创建 HttpServer 实例 - this.server = vertx.createHttpServer().requestHandler(router); - this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client); - } - - /** - * 启动 HTTP 服务器、MQTT 客户端 - */ - public void start() { - if (isRunning) { - log.warn("[start][服务已经在运行中,请勿重复启动]"); - return; - } - log.info("[start][开始启动服务]"); - - // TODO @haohao:建议先启动 MQTT Broker,再启动 HTTP Server。类似 jdbc 先连接了,在启动 tomcat 的味道 - // 1. 启动 HTTP 服务器 - CompletableFuture httpFuture = server.listen(emqxProperties.getAuthPort()) - .toCompletionStage() - .toCompletableFuture() - .thenAccept(v -> log.info("[start][HTTP 服务器启动完成,端口: {}]", server.actualPort())); - - // 2. 连接 MQTT Broker - CompletableFuture mqttFuture = connectMqtt() - .toCompletionStage() - .toCompletableFuture() - .thenAccept(v -> { - // 2.1 添加 MQTT 断开重连监听器 - client.closeHandler(closeEvent -> { - log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); - reconnectWithDelay(); - }); - // 2.2 设置 MQTT 消息处理器 - setupMessageHandler(); - }); - - // 3. 等待所有服务启动完成 - CompletableFuture.allOf(httpFuture, mqttFuture) - .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) // TODO @芋艿:JDK8 不兼容 - .whenComplete((result, error) -> { - if (error != null) { - log.error("[start][服务启动失败]", error); - } else { - isRunning = true; - log.info("[start][所有服务启动完成]"); - } - }); - } - - /** - * 设置 MQTT 消息处理器 - */ - private void setupMessageHandler() { - client.publishHandler(mqttMessageHandler::handle); - log.debug("[setupMessageHandler][MQTT 消息处理器设置完成]"); - } - - /** - * 重连 MQTT 客户端 - */ - private void reconnectWithDelay() { - if (!isRunning) { - log.info("[reconnectWithDelay][服务已停止,不再尝试重连]"); - return; - } - - vertx.setTimer(RECONNECT_DELAY_MS, id -> { - log.info("[reconnectWithDelay][开始重新连接 MQTT]"); - connectMqtt(); - }); - } - - /** - * 连接 MQTT Broker 并订阅主题 - * - * @return 连接结果的Future - */ - private Future connectMqtt() { - return client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost()) - .compose(connAck -> { - log.info("[connectMqtt][MQTT客户端连接成功]"); - return subscribeToTopics(); - }) - .recover(error -> { - log.error("[connectMqtt][连接MQTT Broker失败:]", error); - reconnectWithDelay(); - return Future.failedFuture(error); - }); - } - - /** - * 订阅设备上行消息主题 - * - * @return 订阅结果的 Future - */ - private Future subscribeToTopics() { - String[] topics = emqxProperties.getMqttTopics(); - if (ArrayUtil.isEmpty(topics)) { - log.warn("[subscribeToTopics][未配置MQTT主题,跳过订阅]"); - return Future.succeededFuture(); - } - log.info("[subscribeToTopics][开始订阅设备上行消息主题]"); - - Future compositeFuture = Future.succeededFuture(); - for (String topic : topics) { - String trimmedTopic = topic.trim(); - if (trimmedTopic.isEmpty()) { - continue; - } - compositeFuture = compositeFuture.compose(v -> client.subscribe(trimmedTopic, DEFAULT_QOS.value()) - .map(ack -> { - log.info("[subscribeToTopics][成功订阅主题: {}]", trimmedTopic); - return null; - }) - .recover(error -> { - log.error("[subscribeToTopics][订阅主题失败: {}]", trimmedTopic, error); - return Future.succeededFuture(); // 继续订阅其他主题 - })); - } - return compositeFuture; - } - - /** - * 停止所有服务 - */ - public void stop() { - if (!isRunning) { - log.warn("[stop][服务未运行,无需停止]"); - return; - } - log.info("[stop][开始关闭服务]"); - isRunning = false; - - try { - // 关闭 HTTP 服务器 - if (server != null) { - server.close() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - - // 关闭 MQTT 客户端 - if (client != null) { - client.disconnect() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - - // 关闭 Vertx 实例 - if (vertx!= null) { - vertx.close() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - log.info("[stop][关闭完成]"); - } catch (Exception e) { - log.error("[stop][关闭服务异常]", e); - throw new RuntimeException("关闭 IoT 设备上行服务失败", e); - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java deleted file mode 100644 index e9206d5b6..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.util.Collections; - -/** - * IoT EMQX 连接认证的 Vert.x Handler - * - * 参考:EMQX HTTP - * - * 注意:该处理器需要返回特定格式:{"result": "allow"} 或 {"result": "deny"}, - * 以符合 EMQX 认证插件的要求,因此不使用 IotStandardResponse 实体类 - * - * @author haohao - */ -@RequiredArgsConstructor -@Slf4j -public class IotDeviceAuthVertxHandler implements Handler { - - public static final String PATH = "/mqtt/auth"; - - private final IotDeviceUpstreamApi deviceUpstreamApi; - - @Override - public void handle(RoutingContext routingContext) { - try { - // 构建认证请求 DTO - JsonObject json = routingContext.body().asJsonObject(); - String clientId = json.getString("clientid"); - String username = json.getString("username"); - String password = json.getString("password"); - IotDeviceEmqxAuthReqDTO authReqDTO = new IotDeviceEmqxAuthReqDTO() - .setClientId(clientId) - .setUsername(username) - .setPassword(password); - - // 调用认证 API - CommonResult authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO); - if (authResult.getCode() != 0 || !authResult.getData()) { - // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求 - IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny")); - return; - } - - // 响应结果 - // 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求 - IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow")); - } catch (Exception e) { - log.error("[handle][EMQX 认证异常]", e); - // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求 - IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny")); - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java deleted file mode 100644 index 00fa1b96d..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java +++ /dev/null @@ -1,296 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttClient; -import io.vertx.mqtt.messages.MqttPublishMessage; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * IoT 设备 MQTT 消息处理器 - * - * 参考:设备属性、事件、服务 - */ -@Slf4j -public class IotDeviceMqttMessageHandler { - - // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈;回复 都使用 Alink 格式,方便后续扩展。 - // 设备上报属性 标准 JSON - // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post - // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply - - // 设备上报事件 标准 JSON - // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post - // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post_reply - - private static final String SYS_TOPIC_PREFIX = "/sys/"; - private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post"; - private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/"; - private static final String EVENT_POST_TOPIC_SUFFIX = "/post"; - private static final String REPLY_SUFFIX = "_reply"; - private static final String PROPERTY_METHOD = "thing.event.property.post"; - private static final String EVENT_METHOD_PREFIX = "thing.event."; - private static final String EVENT_METHOD_SUFFIX = ".post"; - - private final IotDeviceUpstreamApi deviceUpstreamApi; - private final MqttClient mqttClient; - - public IotDeviceMqttMessageHandler(IotDeviceUpstreamApi deviceUpstreamApi, MqttClient mqttClient) { - this.deviceUpstreamApi = deviceUpstreamApi; - this.mqttClient = mqttClient; - } - - /** - * 处理MQTT消息 - * - * @param message MQTT发布消息 - */ - public void handle(MqttPublishMessage message) { - String topic = message.topicName(); - String payload = message.payload().toString(); - log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload); - - try { - if (StrUtil.isEmpty(payload)) { - log.warn("[messageHandler][消息内容为空][topic: {}]", topic); - return; - } - handleMessage(topic, payload); - } catch (Exception e) { - log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e); - } - } - - /** - * 根据主题类型处理消息 - * - * @param topic 主题 - * @param payload 消息内容 - */ - private void handleMessage(String topic, String payload) { - // 校验前缀 - if (!topic.startsWith(SYS_TOPIC_PREFIX)) { - log.warn("[handleMessage][未知的消息类型][topic: {}]", topic); - return; - } - - // 处理设备属性上报消息 - if (topic.endsWith(PROPERTY_POST_TOPIC)) { - log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic); - handlePropertyPost(topic, payload); - return; - } - - // 处理设备事件上报消息 - if (topic.contains(EVENT_POST_TOPIC_PREFIX) && topic.endsWith(EVENT_POST_TOPIC_SUFFIX)) { - log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic); - handleEventPost(topic, payload); - return; - } - - // 未知消息类型 - log.warn("[handleMessage][未知的消息类型][topic: {}]", topic); - } - - /** - * 处理设备属性上报消息 - * - * @param topic 主题 - * @param payload 消息内容 - */ - private void handlePropertyPost(String topic, String payload) { - try { - // 解析消息内容 - JSONObject jsonObject = JSONUtil.parseObj(payload); - String[] topicParts = parseTopic(topic); - if (topicParts == null) { - return; - } - - // 构建设备属性上报请求对象 - IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts); - - // 调用上游 API 处理设备上报数据 - deviceUpstreamApi.reportDeviceProperty(reportReqDTO); - log.info("[handlePropertyPost][处理设备属性上报成功][topic: {}]", topic); - - // 发送响应消息 - sendResponse(topic, jsonObject, PROPERTY_METHOD, null); - } catch (Exception e) { - log.error("[handlePropertyPost][处理设备属性上报失败][topic: {}][payload: {}]", topic, payload, e); - } - } - - /** - * 处理设备事件上报消息 - * - * @param topic 主题 - * @param payload 消息内容 - */ - private void handleEventPost(String topic, String payload) { - try { - // 解析消息内容 - JSONObject jsonObject = JSONUtil.parseObj(payload); - String[] topicParts = parseTopic(topic); - if (topicParts == null) { - return; - } - - // 构建设备事件上报请求对象 - IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts); - - // 调用上游 API 处理设备上报数据 - deviceUpstreamApi.reportDeviceEvent(reportReqDTO); - log.info("[handleEventPost][处理设备事件上报成功][topic: {}]", topic); - - // 从 topic 中获取事件标识符 - String eventIdentifier = getEventIdentifier(topicParts, topic); - if (eventIdentifier == null) { - return; - } - - // 发送响应消息 - String method = EVENT_METHOD_PREFIX + eventIdentifier + EVENT_METHOD_SUFFIX; - sendResponse(topic, jsonObject, method, null); - } catch (Exception e) { - log.error("[handleEventPost][处理设备事件上报失败][topic: {}][payload: {}]", topic, payload, e); - } - } - - /** - * 解析主题,获取主题各部分 - * - * @param topic 主题 - * @return 主题各部分数组,如果解析失败返回null - */ - private String[] parseTopic(String topic) { - String[] topicParts = topic.split("/"); - if (topicParts.length < 7) { - log.warn("[parseTopic][主题格式不正确][topic: {}]", topic); - return null; - } - return topicParts; - } - - /** - * 从主题部分中获取事件标识符 - * - * @param topicParts 主题各部分 - * @param topic 原始主题,用于日志 - * @return 事件标识符,如果获取失败返回null - */ - private String getEventIdentifier(String[] topicParts, String topic) { - try { - return topicParts[6]; - } catch (ArrayIndexOutOfBoundsException e) { - log.warn("[getEventIdentifier][无法从主题中获取事件标识符][topic: {}][topicParts: {}]", - topic, Arrays.toString(topicParts)); - return null; - } - } - - /** - * 发送响应消息 - * - * @param topic 原始主题 - * @param jsonObject 原始消息JSON对象 - * @param method 响应方法 - * @param customData 自定义数据,可为 null - */ - private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) { - String replyTopic = topic + REPLY_SUFFIX; - - // 响应结果 - IotStandardResponse response = IotStandardResponse.success( - jsonObject.getStr("id"), method, customData); - try { - mqttClient.publish(replyTopic, Buffer.buffer(JsonUtils.toJsonString(response)), - MqttQoS.AT_LEAST_ONCE, false, false); - log.info("[sendResponse][发送响应消息成功][topic: {}]", replyTopic); - } catch (Exception e) { - log.error("[sendResponse][发送响应消息失败][topic: {}][response: {}]", replyTopic, response, e); - } - } - - /** - * 构建设备属性上报请求对象 - * - * @param jsonObject 消息内容 - * @param topicParts 主题部分 - * @return 设备属性上报请求对象 - */ - private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) { - IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO(); - reportReqDTO.setRequestId(jsonObject.getStr("id")); - reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); - reportReqDTO.setReportTime(LocalDateTime.now()); - reportReqDTO.setProductKey(topicParts[2]); - reportReqDTO.setDeviceName(topicParts[3]); - - // 只使用标准JSON格式处理属性数据 - JSONObject params = jsonObject.getJSONObject("params"); - if (params == null) { - log.warn("[buildPropertyReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject); - params = new JSONObject(); - } - - // 将标准格式的params转换为平台需要的properties格式 - Map properties = new HashMap<>(); - for (Map.Entry entry : params.entrySet()) { - String key = entry.getKey(); - Object valueObj = entry.getValue(); - - // 如果是复杂结构(包含value和time) - if (valueObj instanceof JSONObject valueJson) { - properties.put(key, valueJson.getOrDefault("value", valueObj)); - } else { - properties.put(key, valueObj); - } - } - reportReqDTO.setProperties(properties); - - return reportReqDTO; - } - - /** - * 构建设备事件上报请求对象 - * - * @param jsonObject 消息内容 - * @param topicParts 主题部分 - * @return 设备事件上报请求对象 - */ - private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) { - IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO(); - reportReqDTO.setRequestId(jsonObject.getStr("id")); - reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); - reportReqDTO.setReportTime(LocalDateTime.now()); - reportReqDTO.setProductKey(topicParts[2]); - reportReqDTO.setDeviceName(topicParts[3]); - reportReqDTO.setIdentifier(topicParts[6]); - - // 只使用标准JSON格式处理事件参数 - JSONObject params = jsonObject.getJSONObject("params"); - if (params == null) { - log.warn("[buildEventReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject); - params = new JSONObject(); - } - reportReqDTO.setParams(params); - - return reportReqDTO; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java deleted file mode 100644 index 21b49e097..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java +++ /dev/null @@ -1,152 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.util.Collections; - -/** - * IoT EMQX Webhook 事件处理的 Vert.x Handler - * - * 参考:EMQX Webhook - * - * 注意:该处理器需要返回特定格式:{"result": "success"} 或 {"result": "error"}, - * 以符合 EMQX Webhook 插件的要求,因此不使用 IotStandardResponse 实体类。 - * - * @author haohao - */ -@RequiredArgsConstructor -@Slf4j -public class IotDeviceWebhookVertxHandler implements Handler { - - public static final String PATH = "/mqtt/webhook"; - - private final IotDeviceUpstreamApi deviceUpstreamApi; - - @Override - public void handle(RoutingContext routingContext) { - try { - // 解析请求体 - JsonObject json = routingContext.body().asJsonObject(); - String event = json.getString("event"); - String clientId = json.getString("clientid"); - String username = json.getString("username"); - - // 处理不同的事件类型 - switch (event) { - case "client.connected": - handleClientConnected(clientId, username); - break; - case "client.disconnected": - handleClientDisconnected(clientId, username); - break; - default: - log.info("[handle][未处理的 Webhook 事件] event={}, clientId={}, username={}", event, clientId, username); - break; - } - - // 返回成功响应 - // 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求 - IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success")); - } catch (Exception e) { - log.error("[handle][处理 Webhook 事件异常]", e); - // 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求 - IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error")); - } - } - - /** - * 处理客户端连接事件 - * - * @param clientId 客户端ID - * @param username 用户名 - */ - private void handleClientConnected(String clientId, String username) { - // 解析产品标识和设备名称 - if (StrUtil.isEmpty(username) || "undefined".equals(username)) { - log.warn("[handleClientConnected][客户端连接事件,但用户名为空] clientId={}", clientId); - return; - } - String[] parts = parseUsername(username); - if (parts == null) { - return; - } - - // 更新设备状态为在线 - IotDeviceStateUpdateReqDTO updateReqDTO = new IotDeviceStateUpdateReqDTO(); - updateReqDTO.setProductKey(parts[1]); - updateReqDTO.setDeviceName(parts[0]); - updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState()); - updateReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); - updateReqDTO.setReportTime(LocalDateTime.now()); - CommonResult result = deviceUpstreamApi.updateDeviceState(updateReqDTO); - if (result.getCode() != 0 || !result.getData()) { - log.error("[handleClientConnected][更新设备状态为在线失败] clientId={}, username={}, code={}, msg={}", - clientId, username, result.getCode(), result.getMsg()); - } else { - log.info("[handleClientConnected][更新设备状态为在线成功] clientId={}, username={}", clientId, username); - } - } - - /** - * 处理客户端断开连接事件 - * - * @param clientId 客户端ID - * @param username 用户名 - */ - private void handleClientDisconnected(String clientId, String username) { - // 解析产品标识和设备名称 - if (StrUtil.isEmpty(username) || "undefined".equals(username)) { - log.warn("[handleClientDisconnected][客户端断开连接事件,但用户名为空] clientId={}", clientId); - return; - } - String[] parts = parseUsername(username); - if (parts == null) { - return; - } - - // 更新设备状态为离线 - IotDeviceStateUpdateReqDTO offlineReqDTO = new IotDeviceStateUpdateReqDTO(); - offlineReqDTO.setProductKey(parts[1]); - offlineReqDTO.setDeviceName(parts[0]); - offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState()); - offlineReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); - offlineReqDTO.setReportTime(LocalDateTime.now()); - CommonResult offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO); - if (offlineResult.getCode() != 0 || !offlineResult.getData()) { - log.error("[handleClientDisconnected][更新设备状态为离线失败] clientId={}, username={}, code={}, msg={}", - clientId, username, offlineResult.getCode(), offlineResult.getMsg()); - } else { - log.info("[handleClientDisconnected][更新设备状态为离线成功] clientId={}, username={}", clientId, username); - } - } - - /** - * 解析用户名,格式为 deviceName&productKey - * - * @param username 用户名 - * @return 解析结果,[0] 为 deviceName,[1] 为 productKey,解析失败返回 null - */ - private String[] parseUsername(String username) { - if (StrUtil.isEmpty(username)) { - return null; - } - String[] parts = username.split("&"); - if (parts.length != 2) { - log.warn("[parseUsername][用户名格式({})不正确,无法解析产品标识和设备名称]", username); - return null; - } - return parts; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml deleted file mode 100644 index c00621c82..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml +++ /dev/null @@ -1,20 +0,0 @@ -spring: - application: - name: yudao-module-iot-plugin-emqx - -yudao: - iot: - plugin: - common: - upstream-url: http://127.0.0.1:48080 - downstream-port: 8100 - plugin-key: yudao-module-iot-plugin-emqx - emqx: - mqtt-host: 127.0.0.1 - mqtt-port: 1883 - mqtt-ssl: false - mqtt-username: yudao - mqtt-password: 123456 - mqtt-topics: - - "/sys/#" - auth-port: 8101 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties deleted file mode 100644 index 647d55155..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties +++ /dev/null @@ -1,6 +0,0 @@ -plugin.id=yudao-module-iot-plugin-http -plugin.class=cn.iocoder.yudao.module.iot.plugin.http.config.IotHttpVertxPlugin -plugin.version=1.0.0 -plugin.provider=yudao -plugin.dependencies= -plugin.description=yudao-module-iot-plugin-http-1.0.0 \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml deleted file mode 100644 index 75a385db4..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml +++ /dev/null @@ -1,165 +0,0 @@ - - - - yudao-module-iot-plugins - cn.iocoder.cloud - ${revision} - - 4.0.0 - jar - - yudao-module-iot-plugin-http - 1.0.0 - - ${project.artifactId} - - - 物联网 插件模块 - http 插件 - - - - - ${project.artifactId} - cn.iocoder.yudao.module.iot.plugin.http.config.IotHttpVertxPlugin - ${project.version} - yudao - ${project.artifactId}-${project.version} - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.6 - - - unzip jar file - package - - - - - - - run - - - - - - - maven-assembly-plugin - 2.3 - - - - src/main/assembly/assembly.xml - - - false - - - - make-assembly - package - - attached - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - - ${plugin.id} - ${plugin.class} - ${plugin.version} - ${plugin.provider} - ${plugin.description} - ${plugin.dependencies} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - - - repackage - - - -standalone - - - - - - - - - - - cn.iocoder.cloud - yudao-module-iot-plugin-common - ${revision} - - - - - org.springframework.boot - spring-boot-starter-web - - - - - io.vertx - vertx-web - - - \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml deleted file mode 100644 index 9b79e6152..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml +++ /dev/null @@ -1,24 +0,0 @@ - - plugin - - zip - - false - - - false - runtime - lib - - *:jar:* - - - - - - - target/plugin-classes - classes - - - diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java deleted file mode 100644 index a88b34eb3..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * 独立运行入口 - */ -@Slf4j -@SpringBootApplication -public class IotHttpPluginApplication { - - public static void main(String[] args) { - SpringApplication application = new SpringApplication(IotHttpPluginApplication.class); - application.setWebApplicationType(WebApplicationType.NONE); - application.run(args); - log.info("[main][独立模式启动完成]"); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java deleted file mode 100644 index f704c1844..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http.config; - -import cn.hutool.core.lang.Assert; -import cn.hutool.extra.spring.SpringUtil; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginWrapper; -import org.pf4j.spring.SpringPlugin; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -// TODO @芋艿:完善注释 -/** - * 负责插件的启动和停止,与 Vert.x 的生命周期管理 - */ -@Slf4j -public class IotHttpVertxPlugin extends SpringPlugin { - - public IotHttpVertxPlugin(PluginWrapper wrapper) { - super(wrapper); - } - - @Override - public void start() { - log.info("[HttpVertxPlugin][HttpVertxPlugin 插件启动开始...]"); - try { - ApplicationContext pluginContext = getApplicationContext(); - Assert.notNull(pluginContext, "pluginContext 不能为空"); - log.info("[HttpVertxPlugin][HttpVertxPlugin 插件启动成功...]"); - } catch (Exception e) { - log.error("[HttpVertxPlugin][HttpVertxPlugin 插件开启动异常...]", e); - } - } - - @Override - public void stop() { - log.info("[HttpVertxPlugin][HttpVertxPlugin 插件停止开始...]"); - try { - log.info("[HttpVertxPlugin][HttpVertxPlugin 插件停止成功...]"); - } catch (Exception e) { - log.error("[HttpVertxPlugin][HttpVertxPlugin 插件停止异常...]", e); - } - } - - // TODO @芋艿:思考下,未来要不要。。。 - @Override - protected ApplicationContext createApplicationContext() { - // 创建插件自己的 ApplicationContext - AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext(); - // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用) - pluginContext.setParent(SpringUtil.getApplicationContext()); - // 继续使用插件自己的 ClassLoader 以加载插件内部的类 - pluginContext.setClassLoader(getWrapper().getPluginClassLoader()); - // 扫描当前插件的自动配置包 - // TODO @芋艿:后续看看,怎么配置类包 - pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.http.config"); - pluginContext.refresh(); - return pluginContext; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java deleted file mode 100644 index 63e55f58f..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http.config; - -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; -import cn.iocoder.yudao.module.iot.plugin.http.downstream.IotDeviceDownstreamHandlerImpl; -import cn.iocoder.yudao.module.iot.plugin.http.upstream.IotDeviceUpstreamServer; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * IoT 插件 HTTP 的专用自动配置类 - * - * @author haohao - */ -@Configuration -@EnableConfigurationProperties(IotPluginHttpProperties.class) -public class IotPluginHttpAutoConfiguration { - - @Bean(initMethod = "start", destroyMethod = "stop") - public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, - IotPluginHttpProperties properties) { - return new IotDeviceUpstreamServer(properties, deviceUpstreamApi); - } - - @Bean - public IotDeviceDownstreamHandler deviceDownstreamHandler() { - return new IotDeviceDownstreamHandlerImpl(); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java deleted file mode 100644 index 49dca8126..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -@ConfigurationProperties(prefix = "yudao.iot.plugin.http") -@Validated -@Data -public class IotPluginHttpProperties { - - /** - * HTTP 服务端口 - */ - private Integer serverPort; - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java deleted file mode 100644 index 869fe7234..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http.downstream; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; -import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; - -/** - * HTTP 插件的 {@link IotDeviceDownstreamHandler} 实现类 - * - * 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!! - * 类似 MQTT、WebSocket、TCP 插件,是可以实现下行指令的。 - * - * @author 芋道源码 - */ -public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { - - @Override - public CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持调用设备服务"); - } - - @Override - public CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持获取设备属性"); - } - - @Override - public CommonResult setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); - } - - @Override - public CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); - } - - @Override - public CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) { - return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java deleted file mode 100644 index 67129a4d1..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java +++ /dev/null @@ -1,83 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http.upstream; - -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.plugin.http.config.IotPluginHttpProperties; -import cn.iocoder.yudao.module.iot.plugin.http.upstream.router.IotDeviceUpstreamVertxHandler; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import lombok.extern.slf4j.Slf4j; - -/** - * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 - * - * 协议:HTTP - * - * @author haohao - */ -@Slf4j -public class IotDeviceUpstreamServer { - - private final Vertx vertx; - private final HttpServer server; - private final IotPluginHttpProperties properties; - - public IotDeviceUpstreamServer(IotPluginHttpProperties properties, - IotDeviceUpstreamApi deviceUpstreamApi) { - this.properties = properties; - // 创建 Vertx 实例 - this.vertx = Vertx.vertx(); - // 创建 Router 实例 - Router router = Router.router(vertx); - router.route().handler(BodyHandler.create()); // 处理 Body - - // 使用统一的 Handler 处理所有上行请求 - IotDeviceUpstreamVertxHandler upstreamHandler = new IotDeviceUpstreamVertxHandler(deviceUpstreamApi); - router.post(IotDeviceUpstreamVertxHandler.PROPERTY_PATH).handler(upstreamHandler); - router.post(IotDeviceUpstreamVertxHandler.EVENT_PATH).handler(upstreamHandler); - - // 创建 HttpServer 实例 - this.server = vertx.createHttpServer().requestHandler(router); - } - - /** - * 启动 HTTP 服务器 - */ - public void start() { - log.info("[start][开始启动]"); - server.listen(properties.getServerPort()) - .toCompletionStage() - .toCompletableFuture() - .join(); - log.info("[start][启动完成,端口({})]", this.server.actualPort()); - } - - /** - * 停止所有 - */ - public void stop() { - log.info("[stop][开始关闭]"); - try { - // 关闭 HTTP 服务器 - if (server != null) { - server.close() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - - // 关闭 Vertx 实例 - if (vertx != null) { - vertx.close() - .toCompletionStage() - .toCompletableFuture() - .join(); - } - log.info("[stop][关闭完成]"); - } catch (Exception e) { - log.error("[stop][关闭异常]", e); - throw new RuntimeException(e); - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java deleted file mode 100644 index 79d465ea0..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java +++ /dev/null @@ -1,188 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin.http.upstream.router; - -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.ObjUtil; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; -import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; -import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; -import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * IoT 设备上行统一处理的 Vert.x Handler - *

- * 统一处理设备属性上报和事件上报的请求 - * - * @author haohao - */ -@RequiredArgsConstructor -@Slf4j -public class IotDeviceUpstreamVertxHandler implements Handler { - - // TODO @haohao:要不要类似 IotDeviceConfigSetVertxHandler 写的,把这些 PATH、METHOD 之类的抽走 - /** - * 属性上报路径 - */ - public static final String PROPERTY_PATH = "/sys/:productKey/:deviceName/thing/event/property/post"; - /** - * 事件上报路径 - */ - public static final String EVENT_PATH = "/sys/:productKey/:deviceName/thing/event/:identifier/post"; - - private static final String PROPERTY_METHOD = "thing.event.property.post"; - private static final String EVENT_METHOD_PREFIX = "thing.event."; - private static final String EVENT_METHOD_SUFFIX = ".post"; - - private final IotDeviceUpstreamApi deviceUpstreamApi; - - // TODO @haohao:要不要分成多个 Handler?每个只解决一个问题哈。 - @Override - public void handle(RoutingContext routingContext) { - String path = routingContext.request().path(); - String requestId = IdUtil.fastSimpleUUID(); - - try { - // 1. 解析通用参数 - String productKey = routingContext.pathParam("productKey"); - String deviceName = routingContext.pathParam("deviceName"); - JsonObject body = routingContext.body().asJsonObject(); - requestId = ObjUtil.defaultIfBlank(body.getString("id"), requestId); - - // 2. 根据路径模式处理不同类型的请求 - CommonResult result; - String method; - if (path.matches(".*/thing/event/property/post")) { - // 处理属性上报 - IotDevicePropertyReportReqDTO reportReqDTO = parsePropertyReportRequest(productKey, deviceName, requestId, body); - - // 设备上线 - updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); - - // 属性上报 - result = deviceUpstreamApi.reportDeviceProperty(reportReqDTO); - method = PROPERTY_METHOD; - } else if (path.matches(".*/thing/event/.+/post")) { - // 处理事件上报 - String identifier = routingContext.pathParam("identifier"); - IotDeviceEventReportReqDTO reportReqDTO = parseEventReportRequest(productKey, deviceName, identifier, requestId, body); - - // 设备上线 - updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); - - // 事件上报 - result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); - method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX; - } else { - // 不支持的请求路径 - IotStandardResponse errorResponse = IotStandardResponse.error(requestId, "unknown", BAD_REQUEST.getCode(), "不支持的请求路径"); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - return; - } - - // 3. 返回标准响应 - IotStandardResponse response; - if (result.isSuccess()) { - response = IotStandardResponse.success(requestId, method, result.getData()); - } else { - response = IotStandardResponse.error(requestId, method, result.getCode(), result.getMsg()); - } - IotPluginCommonUtils.writeJsonResponse(routingContext, response); - } catch (Exception e) { - log.error("[handle][处理上行请求异常] path={}", path, e); - String method = path.contains("/property/") ? PROPERTY_METHOD - : EVENT_METHOD_PREFIX + (routingContext.pathParams().containsKey("identifier") - ? routingContext.pathParam("identifier") - : "unknown") + EVENT_METHOD_SUFFIX; - IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); - IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - } - - /** - * 更新设备状态 - * - * @param productKey 产品 Key - * @param deviceName 设备名称 - */ - private void updateDeviceState(String productKey, String deviceName) { - deviceUpstreamApi.updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO() - .setRequestId(IdUtil.fastSimpleUUID()).setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) - .setProductKey(productKey).setDeviceName(deviceName)).setState(IotDeviceStateEnum.ONLINE.getState())); - } - - /** - * 解析属性上报请求 - * - * @param productKey 产品 Key - * @param deviceName 设备名称 - * @param requestId 请求 ID - * @param body 请求体 - * @return 属性上报请求 DTO - */ - @SuppressWarnings("unchecked") - private IotDevicePropertyReportReqDTO parsePropertyReportRequest(String productKey, String deviceName, String requestId, JsonObject body) { - // 按照标准 JSON 格式处理属性数据 - Map properties = new HashMap<>(); - Map params = body.getJsonObject("params") != null ? body.getJsonObject("params").getMap() : null; - if (params != null) { - // 将标准格式的 params 转换为平台需要的 properties 格式 - for (Map.Entry entry : params.entrySet()) { - String key = entry.getKey(); - Object valueObj = entry.getValue(); - // 如果是复杂结构(包含 value 和 time) - if (valueObj instanceof Map) { - Map valueMap = (Map) valueObj; - properties.put(key, valueMap.getOrDefault("value", valueObj)); - } else { - properties.put(key, valueObj); - } - } - } - - // 构建属性上报请求 DTO - return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO().setRequestId(requestId) - .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) - .setProductKey(productKey).setDeviceName(deviceName)).setProperties(properties); - } - - /** - * 解析事件上报请求 - * - * @param productKey 产品K ey - * @param deviceName 设备名称 - * @param identifier 事件标识符 - * @param requestId 请求 ID - * @param body 请求体 - * @return 事件上报请求 DTO - */ - private IotDeviceEventReportReqDTO parseEventReportRequest(String productKey, String deviceName, String identifier, String requestId, JsonObject body) { - // 按照标准 JSON 格式处理事件参数 - Map params; - if (body.containsKey("params")) { - params = body.getJsonObject("params").getMap(); - } else { - // 兼容旧格式 - params = new HashMap<>(); - } - - // 构建事件上报请求 DTO - return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId) - .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) - .setProductKey(productKey).setDeviceName(deviceName)).setIdentifier(identifier).setParams(params); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml deleted file mode 100644 index f195628a6..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml +++ /dev/null @@ -1,13 +0,0 @@ -spring: - application: - name: yudao-module-iot-plugin-http - -yudao: - iot: - plugin: - common: - upstream-url: http://127.0.0.1:48080 - downstream-port: 8093 - plugin-key: yudao-module-iot-plugin-http - http: - server-port: 8092 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties deleted file mode 100644 index 939e0f692..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties +++ /dev/null @@ -1,7 +0,0 @@ -plugin.id=mqtt-plugin -plugin.description=Vert.x MQTT plugin -plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin -plugin.version=1.0.0 -plugin.requires= -plugin.provider=ahh -plugin.license=Apache-2.0 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml deleted file mode 100644 index 8dd309234..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml +++ /dev/null @@ -1,155 +0,0 @@ - - - - yudao-module-iot-plugins - cn.iocoder.cloud - ${revision} - - 4.0.0 - jar - - yudao-module-iot-plugin-mqtt - - ${project.artifactId} - - - 物联网 插件模块 - mqtt 插件 - - - - - mqtt-plugin - cn.iocoder.yudao.module.iot.plugin.MqttPlugin - 0.0.1 - ahh - mqtt-plugin-0.0.1 - - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.6 - - - unzip jar file - package - - - - - - - run - - - - - - - maven-assembly-plugin - 2.3 - - - - src/main/assembly/assembly.xml - - - false - - - - make-assembly - package - - attached - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - - ${plugin.id} - ${plugin.class} - ${plugin.version} - ${plugin.provider} - ${plugin.description} - ${plugin.dependencies} - - - - - - - maven-deploy-plugin - - true - - - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - org.pf4j - pf4j-spring - provided - - - - cn.iocoder.cloud - yudao-module-iot-api - ${revision} - - - org.projectlombok - lombok - provided - - - - io.vertx - vertx-mqtt - 4.5.11 - - - \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml deleted file mode 100644 index daec9e431..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml +++ /dev/null @@ -1,31 +0,0 @@ - - plugin - - zip - - false - - - false - runtime - lib - - *:jar:* - - - - - - - target/plugin-classes - classes - - - diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java deleted file mode 100644 index 7883fa8b1..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin; - -import lombok.extern.slf4j.Slf4j; -import org.pf4j.Plugin; -import org.pf4j.PluginWrapper; - -// TODO @芋艿:暂未实现 -@Slf4j -public class MqttPlugin extends Plugin { - - private MqttServerExtension mqttServerExtension; - - public MqttPlugin(PluginWrapper wrapper) { - super(wrapper); - } - - @Override - public void start() { - log.info("MQTT Plugin started."); - mqttServerExtension = new MqttServerExtension(); - mqttServerExtension.startMqttServer(); - } - - @Override - public void stop() { - log.info("MQTT Plugin stopped."); - if (mqttServerExtension != null) { - mqttServerExtension.stopMqttServer().onComplete(ar -> { - if (ar.succeeded()) { - log.info("Stopped MQTT Server successfully"); - } else { - log.error("Failed to stop MQTT Server: {}", ar.cause().getMessage()); - } - }); - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java deleted file mode 100644 index dd0c5da37..000000000 --- a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java +++ /dev/null @@ -1,232 +0,0 @@ -package cn.iocoder.yudao.module.iot.plugin; - -import io.netty.handler.codec.mqtt.MqttProperties; -import io.netty.handler.codec.mqtt.MqttQoS; -import io.vertx.core.Future; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.mqtt.MqttEndpoint; -import io.vertx.mqtt.MqttServer; -import io.vertx.mqtt.MqttServerOptions; -import io.vertx.mqtt.MqttTopicSubscription; -import io.vertx.mqtt.messages.MqttDisconnectMessage; -import io.vertx.mqtt.messages.MqttPublishMessage; -import io.vertx.mqtt.messages.MqttSubscribeMessage; -import io.vertx.mqtt.messages.MqttUnsubscribeMessage; -import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.Extension; - -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; - -// TODO @芋艿:暂未实现 -/** - * 根据官方示例,整合常见 MQTT 功能到 PF4J 的 Extension 类中 - */ -@Slf4j -@Extension -public class MqttServerExtension { - - private Vertx vertx; - private MqttServer mqttServer; - - /** - * 启动 MQTT 服务端 - * 可根据需要决定是否启用 SSL/TLS、WebSocket、多实例部署等 - */ - public void startMqttServer() { - // 初始化 Vert.x - vertx = Vertx.vertx(); - - // ========== 如果需要 SSL/TLS,请参考下面注释,启用注释并替换端口、证书路径等 ========== - // MqttServerOptions options = new MqttServerOptions() - // .setPort(8883) - // .setKeyCertOptions(new PemKeyCertOptions() - // .setKeyPath("./src/test/resources/tls/server-key.pem") - // .setCertPath("./src/test/resources/tls/server-cert.pem")) - // .setSsl(true); - - // ========== 如果需要 WebSocket,请设置 setUseWebSocket(true) ========== - // options.setUseWebSocket(true); - - // ========== 默认不启用 SSL 的示例 ========== - MqttServerOptions options = new MqttServerOptions() - .setPort(1883) - .setHost("0.0.0.0") - .setUseWebSocket(false); // 如果需要 WebSocket,请改为 true - - mqttServer = MqttServer.create(vertx, options); - - // 指定 endpointHandler,处理客户端连接等 - mqttServer.endpointHandler(endpoint -> { - handleClientConnect(endpoint); - handleDisconnect(endpoint); - handleSubscribe(endpoint); - handleUnsubscribe(endpoint); - handlePublish(endpoint); - handlePing(endpoint); - }); - - // 启动监听 - mqttServer.listen(ar -> { - if (ar.succeeded()) { - log.info("MQTT server is listening on port {}", mqttServer.actualPort()); - } else { - log.error("Error on starting the server", ar.cause()); - } - }); - } - - /** - * 优雅关闭 MQTT 服务端 - */ - public Future stopMqttServer() { - if (mqttServer != null) { - return mqttServer.close().onComplete(ar -> { - if (ar.succeeded()) { - log.info("MQTT server closed."); - if (vertx != null) { - vertx.close(); - log.info("Vert.x instance closed."); - } - } else { - log.error("Failed to close MQTT server: {}", ar.cause().getMessage()); - } - }); - } - return Future.succeededFuture(); - } - - // ==================== 以下为官方示例中常见事件的处理封装 ==================== - - /** - * 处理客户端连接 (CONNECT) - */ - private void handleClientConnect(MqttEndpoint endpoint) { - // 打印 CONNECT 的主要信息 - log.info("MQTT client [{}] request to connect, clean session = {}", - endpoint.clientIdentifier(), endpoint.isCleanSession()); - - if (endpoint.auth() != null) { - log.info("[username = {}, password = {}]", endpoint.auth().getUsername(), endpoint.auth().getPassword()); - } - log.info("[properties = {}]", endpoint.connectProperties()); - - if (endpoint.will() != null) { - log.info("[will topic = {}, msg = {}, QoS = {}, isRetain = {}]", - endpoint.will().getWillTopic(), - new String(endpoint.will().getWillMessageBytes()), - endpoint.will().getWillQos(), - endpoint.will().isWillRetain()); - } - - log.info("[keep alive timeout = {}]", endpoint.keepAliveTimeSeconds()); - - // 接受远程客户端的连接 - endpoint.accept(false); - } - - /** - * 处理客户端主动断开 (DISCONNECT) - */ - private void handleDisconnect(MqttEndpoint endpoint) { - endpoint.disconnectMessageHandler((MqttDisconnectMessage disconnectMessage) -> { - log.info("Received disconnect from client [{}], reason code = {}", - endpoint.clientIdentifier(), disconnectMessage.code()); - }); - } - - /** - * 处理客户端订阅 (SUBSCRIBE) - */ - private void handleSubscribe(MqttEndpoint endpoint) { - endpoint.subscribeHandler((MqttSubscribeMessage subscribe) -> { - List reasonCodes = new ArrayList<>(); - for (MqttTopicSubscription s : subscribe.topicSubscriptions()) { - log.info("Subscription for {} with QoS {}", s.topicName(), s.qualityOfService()); - // 将客户端请求的 QoS 转换为返回给客户端的 reason code(可能是错误码或实际 granted QoS) - reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService())); - } - // 回复 SUBACK,MQTT 5.0 时可指定 reasonCodes、properties - endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES); - }); - } - - /** - * 处理客户端取消订阅 (UNSUBSCRIBE) - */ - private void handleUnsubscribe(MqttEndpoint endpoint) { - endpoint.unsubscribeHandler((MqttUnsubscribeMessage unsubscribe) -> { - for (String topic : unsubscribe.topics()) { - log.info("Unsubscription for {}", topic); - } - // 回复 UNSUBACK,MQTT 5.0 时可指定 reasonCodes、properties - endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); - }); - } - - /** - * 处理客户端发布的消息 (PUBLISH) - */ - private void handlePublish(MqttEndpoint endpoint) { - // 接收 PUBLISH 消息 - endpoint.publishHandler((MqttPublishMessage message) -> { - String payload = message.payload().toString(Charset.defaultCharset()); - log.info("Received message [{}] on topic [{}] with QoS [{}]", - payload, message.topicName(), message.qosLevel()); - - // 根据不同 QoS,回复客户端 - if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { - endpoint.publishAcknowledge(message.messageId()); - } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { - endpoint.publishReceived(message.messageId()); - } - }); - - // 如果 QoS = 2,需要处理 PUBREL - endpoint.publishReleaseHandler(messageId -> { - endpoint.publishComplete(messageId); - }); - } - - /** - * 处理客户端 PINGREQ - */ - private void handlePing(MqttEndpoint endpoint) { - endpoint.pingHandler(v -> { - // 这里仅做日志, PINGRESP 已自动发送 - log.info("Ping received from client [{}]", endpoint.clientIdentifier()); - }); - } - - // ==================== 如果需要服务端向客户端发布消息,可用以下示例 ==================== - - /** - * 服务端主动向已连接的某个 endpoint 发布消息的示例 - * 如果使用 MQTT 5.0,可以传递更多消息属性 - */ - public void publishToClient(MqttEndpoint endpoint, String topic, String content) { - endpoint.publish(topic, - Buffer.buffer(content), - MqttQoS.AT_LEAST_ONCE, // QoS 自行选择 - false, - false); - - // 处理 QoS 1 和 QoS 2 的 ACK - endpoint.publishAcknowledgeHandler(messageId -> { - log.info("Received PUBACK from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); - }).publishReceivedHandler(messageId -> { - endpoint.publishRelease(messageId); - }).publishCompletionHandler(messageId -> { - log.info("Received PUBCOMP from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); - }); - } - - // ==================== 如果需要多实例部署,用于多核扩展,可参考以下思路 ==================== - // 例如,在宿主应用或插件中循环启动多个 MqttServerExtension 实例,或使用 Vert.x 的 deployVerticle: - // DeploymentOptions options = new DeploymentOptions().setInstances(10); - // vertx.deployVerticle(() -> new MyMqttVerticle(), options); - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-server/pom.xml similarity index 76% rename from yudao-module-iot/yudao-module-iot-biz/pom.xml rename to yudao-module-iot/yudao-module-iot-server/pom.xml index 35eb94db1..e166c1785 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-server/pom.xml @@ -10,12 +10,12 @@ 4.0.0 jar - yudao-module-iot-biz + yudao-module-iot-server ${project.artifactId} 物联网 模块,主要实现 产品管理、设备管理、协议管理等功能。 - + @@ -36,6 +36,11 @@ yudao-module-iot-api ${revision} + + cn.iocoder.cloud + yudao-module-iot-core + ${revision} + cn.iocoder.cloud @@ -124,13 +129,8 @@ yudao-spring-boot-starter-excel - - - cn.iocoder.cloud - yudao-spring-boot-starter-monitor - - - + + org.apache.rocketmq rocketmq-spring-boot-starter @@ -147,45 +147,16 @@ true - - org.pf4j - pf4j-spring - - - - - org.apache.groovy - groovy-all - 4.0.25 - pom - - - - - org.graalvm.js - js - 24.1.2 - pom - - - org.graalvm.js - js-scriptengine - 24.1.2 - - - - + - - + + + - - - - - - + + + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/IoTServerApplication.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/IoTServerApplication.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/IoTServerApplication.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/IoTServerApplication.java diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java new file mode 100644 index 000000000..eb55b1852 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.api.device; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import org.springframework.context.annotation.Primary; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 设备 API 实现类 + * + * @author haohao + */ +@RestController +@Validated +@Primary // 保证优先匹配,因为 yudao-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入 +public class IoTDeviceApiImpl implements IotDeviceCommonApi { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotProductService productService; + + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") + @PermitAll + public CommonResult authDevice(@RequestBody IotDeviceAuthReqDTO authReqDTO) { + return success(deviceService.authDevice(authReqDTO)); + } + + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/get") // 特殊:方便调用,暂时使用 POST,实际更推荐 GET + @PermitAll + public CommonResult getDevice(@RequestBody IotDeviceGetReqDTO getReqDTO) { + IotDeviceDO device = getReqDTO.getId() != null ? deviceService.getDeviceFromCache(getReqDTO.getId()) + : deviceService.getDeviceFromCache(getReqDTO.getProductKey(), getReqDTO.getDeviceName()); + return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> { + IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId()); + if (product != null) { + deviceDTO.setCodecType(product.getCodecType()); + } + })); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java new file mode 100644 index 000000000..63bca1637 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java @@ -0,0 +1,4 @@ +/** + * iot API 包,定义并实现提供给其它模块的 API + */ +package cn.iocoder.yudao.module.iot.api; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/IotAlertConfigController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/IotAlertConfigController.java new file mode 100644 index 000000000..859f412e6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/IotAlertConfigController.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertConfigService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - IoT 告警配置") +@RestController +@RequestMapping("/iot/alert-config") +@Validated +public class IotAlertConfigController { + + @Resource + private IotAlertConfigService alertConfigService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建告警配置") + @PreAuthorize("@ss.hasPermission('iot:alert-config:create')") + public CommonResult createAlertConfig(@Valid @RequestBody IotAlertConfigSaveReqVO createReqVO) { + return success(alertConfigService.createAlertConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新告警配置") + @PreAuthorize("@ss.hasPermission('iot:alert-config:update')") + public CommonResult updateAlertConfig(@Valid @RequestBody IotAlertConfigSaveReqVO updateReqVO) { + alertConfigService.updateAlertConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除告警配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:alert-config:delete')") + public CommonResult deleteAlertConfig(@RequestParam("id") Long id) { + alertConfigService.deleteAlertConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得告警配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:alert-config:query')") + public CommonResult getAlertConfig(@RequestParam("id") Long id) { + IotAlertConfigDO alertConfig = alertConfigService.getAlertConfig(id); + return success(BeanUtils.toBean(alertConfig, IotAlertConfigRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得告警配置分页") + @PreAuthorize("@ss.hasPermission('iot:alert-config:query')") + public CommonResult> getAlertConfigPage(@Valid IotAlertConfigPageReqVO pageReqVO) { + PageResult pageResult = alertConfigService.getAlertConfigPage(pageReqVO); + + // 转换返回 + Map userMap = adminUserApi.getUserMap( + convertSetByFlatMap(pageResult.getList(), config -> config.getReceiveUserIds().stream())); + return success(BeanUtils.toBean(pageResult, IotAlertConfigRespVO.class, vo -> { + vo.setReceiveUserNames(vo.getReceiveUserIds().stream() + .map(userMap::get) + .filter(Objects::nonNull) + .map(AdminUserRespDTO::getNickname) + .collect(Collectors.toList())); + })); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得告警配置简单列表", description = "只包含被开启的告警配置,主要用于前端的下拉选项") + @PreAuthorize("@ss.hasPermission('iot:alert-config:query')") + public CommonResult> getAlertConfigSimpleList() { + List list = alertConfigService.getAlertConfigListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, config -> // 只返回 id、name 字段 + new IotAlertConfigRespVO().setId(config.getId()).setName(config.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/IotAlertRecordController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/IotAlertRecordController.java new file mode 100644 index 000000000..91f15b989 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/IotAlertRecordController.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordProcessReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static java.util.Collections.singleton; + +@Tag(name = "管理后台 - IoT 告警记录") +@RestController +@RequestMapping("/iot/alert-record") +@Validated +public class IotAlertRecordController { + + @Resource + private IotAlertRecordService alertRecordService; + + @GetMapping("/get") + @Operation(summary = "获得告警记录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:alert-record:query')") + public CommonResult getAlertRecord(@RequestParam("id") Long id) { + IotAlertRecordDO alertRecord = alertRecordService.getAlertRecord(id); + return success(BeanUtils.toBean(alertRecord, IotAlertRecordRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得告警记录分页") + @PreAuthorize("@ss.hasPermission('iot:alert-record:query')") + public CommonResult> getAlertRecordPage(@Valid IotAlertRecordPageReqVO pageReqVO) { + PageResult pageResult = alertRecordService.getAlertRecordPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotAlertRecordRespVO.class)); + } + + @PutMapping("/process") + @Operation(summary = "处理告警记录") + @PreAuthorize("@ss.hasPermission('iot:alert-record:process')") + public CommonResult processAlertRecord(@Valid @RequestBody IotAlertRecordProcessReqVO processReqVO) { + alertRecordService.processAlertRecordList(singleton(processReqVO.getId()), processReqVO.getProcessRemark()); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigPageReqVO.java new file mode 100644 index 000000000..0f9a1e9ce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigPageReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 告警配置分页 Request VO") +@Data +public class IotAlertConfigPageReqVO extends PageParam { + + @Schema(description = "配置名称", example = "赵六") + private String name; + + @Schema(description = "配置状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java new file mode 100644 index 000000000..e68a7b785 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - IoT 告警配置 Response VO") +@Data +public class IotAlertConfigRespVO { + + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3566") + private Long id; + + @Schema(description = "配置名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "配置描述", example = "你猜") + private String description; + + @Schema(description = "告警级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer level; + + @Schema(description = "配置状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "关联的场景联动规则编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + private List sceneRuleIds; + + @Schema(description = "接收的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "100,200") + private List receiveUserIds; + + @Schema(description = "接收的用户名称数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三,李四") + private List receiveUserNames; + + @Schema(description = "接收的类型数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + private List receiveTypes; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java new file mode 100644 index 000000000..694e8bfdf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/config/IotAlertConfigSaveReqVO.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - IoT 告警配置新增/修改 Request VO") +@Data +public class IotAlertConfigSaveReqVO { + + @Schema(description = "配置编号", example = "3566") + private Long id; + + @Schema(description = "配置名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "配置名称不能为空") + private String name; + + @Schema(description = "配置描述", example = "你猜") + private String description; + + @Schema(description = "告警级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "告警级别不能为空") + private Integer level; + + @Schema(description = "配置状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "配置状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "关联的场景联动规则编号数组") + @NotEmpty(message = "关联的场景联动规则编号数组不能为空") + private List sceneRuleIds; + + @Schema(description = "接收的用户编号数组") + @NotEmpty(message = "接收的用户编号数组不能为空") + private List receiveUserIds; + + @Schema(description = "接收的类型数组") + @NotEmpty(message = "接收的类型数组不能为空") + private List receiveTypes; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordPageReqVO.java new file mode 100644 index 000000000..109f24091 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 告警记录分页 Request VO") +@Data +public class IotAlertRecordPageReqVO extends PageParam { + + @Schema(description = "告警配置编号", example = "29320") + private Long configId; + + @Schema(description = "告警级别", example = "1") + private Integer level; + + @Schema(description = "产品编号", example = "2050") + private Long productId; + + @Schema(description = "设备编号", example = "21727") + private String deviceId; + + @Schema(description = "是否处理", example = "true") + private Boolean processStatus; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordProcessReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordProcessReqVO.java new file mode 100644 index 000000000..b64f66c5b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordProcessReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 告警记录处理 Request VO") +@Data +public class IotAlertRecordProcessReqVO { + + @Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "记录编号不能为空") + private Long id; + + @Schema(description = "处理结果(备注)", requiredMode = Schema.RequiredMode.REQUIRED, example = "已处理告警,问题已解决") + private String processRemark; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordRespVO.java new file mode 100644 index 000000000..97ccf6cca --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/alert/vo/recrod/IotAlertRecordRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 告警记录 Response VO") +@Data +public class IotAlertRecordRespVO { + + @Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19904") + private Long id; + + @Schema(description = "告警配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29320") + private Long configId; + + @Schema(description = "告警名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String configName; + + @Schema(description = "告警级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer configLevel; + + @Schema(description = "产品编号", example = "2050") + private Long productId; + + @Schema(description = "设备编号", example = "21727") + private Long deviceId; + + @Schema(description = "触发的设备消息") + private IotDeviceMessage deviceMessage; + + @Schema(description = "是否处理", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Boolean processStatus; + + @Schema(description = "处理结果(备注)", example = "你说的对") + private String processRemark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java similarity index 74% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java index 08fc244b1..f8f78aa63 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java @@ -6,15 +6,13 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; @@ -41,10 +39,6 @@ public class IotDeviceController { @Resource private IotDeviceService deviceService; - @Resource - private IotDeviceUpstreamService deviceUpstreamService; - @Resource - private IotDeviceDownstreamService deviceDownstreamService; @PostMapping("/create") @Operation(summary = "创建设备") @@ -111,7 +105,7 @@ public class IotDeviceController { @PreAuthorize("@ss.hasPermission('iot:device:export')") @ApiAccessLog(operateType = EXPORT) public void exportDeviceExcel(@Valid IotDevicePageReqVO exportReqVO, - HttpServletResponse response) throws IOException { + HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); CommonResult> result = getDevicePage(exportReqVO); // 导出 Excel @@ -129,12 +123,17 @@ public class IotDeviceController { @GetMapping("/simple-list") @Operation(summary = "获取设备的精简信息列表", description = "主要用于前端的下拉选项") - @Parameter(name = "deviceType", description = "设备类型", example = "1") - public CommonResult> getSimpleDeviceList( - @RequestParam(value = "deviceType", required = false) Integer deviceType) { - List list = deviceService.getDeviceListByDeviceType(deviceType); - return success(convertList(list, device -> // 只返回 id、name 字段 - new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName()))); + @Parameters({ + @Parameter(name = "deviceType", description = "设备类型", example = "1"), + @Parameter(name = "productId", description = "产品编号", example = "1024") + }) + public CommonResult> getDeviceSimpleList( + @RequestParam(value = "deviceType", required = false) Integer deviceType, + @RequestParam(value = "productId", required = false) Long productId) { + List list = deviceService.getDeviceListByCondition(deviceType, productId); + return success(convertList(list, device -> // 只返回 id、name、productId 字段 + new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName()) + .setProductId(device.getProductId()).setState(device.getState()))); } @PostMapping("/import") @@ -154,35 +153,28 @@ public class IotDeviceController { // 手动创建导出 demo List list = Arrays.asList( IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110") - .productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(), - IotDeviceImportExcelVO.builder().deviceName("biubiu") - .productKey("YzvHxd4r67sT4s2B").groupNames("").build()); + .productKey("1de24640dfe").groupNames("灰度分组,生产分组") + .locationType(IotLocationTypeEnum.IP.getType()).build(), + IotDeviceImportExcelVO.builder().deviceName("biubiu").productKey("YzvHxd4r67sT4s2B") + .groupNames("").locationType(IotLocationTypeEnum.MANUAL.getType()).build()); // 输出 ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list); } - @PostMapping("/upstream") - @Operation(summary = "设备上行", description = "可用于设备模拟") - @PreAuthorize("@ss.hasPermission('iot:device:upstream')") - public CommonResult upstreamDevice(@Valid @RequestBody IotDeviceUpstreamReqVO upstreamReqVO) { - deviceUpstreamService.upstreamDevice(upstreamReqVO); - return success(true); + @GetMapping("/get-auth-info") + @Operation(summary = "获得设备连接信息") + @PreAuthorize("@ss.hasPermission('iot:device:auth-info')") + public CommonResult getDeviceAuthInfo(@RequestParam("id") Long id) { + return success(deviceService.getDeviceAuthInfo(id)); } - @PostMapping("/downstream") - @Operation(summary = "设备下行", description = "可用于设备模拟") - @PreAuthorize("@ss.hasPermission('iot:device:downstream')") - public CommonResult downstreamDevice(@Valid @RequestBody IotDeviceDownstreamReqVO downstreamReqVO) { - deviceDownstreamService.downstreamDevice(downstreamReqVO); - return success(true); + // TODO @haohao:可以使用 @RequestParam("productKey") String productKey, @RequestParam("deviceNames") List deviceNames 来接收哇? + @GetMapping("/list-by-product-key-and-names") + @Operation(summary = "通过产品标识和设备名称列表获取设备") + @PreAuthorize("@ss.hasPermission('iot:device:query')") + public CommonResult> getDevicesByProductKeyAndNames(@Valid IotDeviceByProductKeyAndNamesReqVO reqVO) { + List devices = deviceService.getDeviceListByProductKeyAndNames(reqVO.getProductKey(), reqVO.getDeviceNames()); + return success(BeanUtils.toBean(devices, IotDeviceRespVO.class)); } - // TODO @haohao:是不是默认详情接口,不返回 secret,然后这个接口,用于统一返回。然后接口名可以更通用一点。 - @GetMapping("/mqtt-connection-params") - @Operation(summary = "获取 MQTT 连接参数") - @PreAuthorize("@ss.hasPermission('iot:device:mqtt-connection-params')") - public CommonResult getMqttConnectionParams(@RequestParam("deviceId") Long deviceId) { - return success(deviceService.getMqttConnectionParams(deviceId)); - } - -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.http b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.http new file mode 100644 index 000000000..93c86e146 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.http @@ -0,0 +1,101 @@ +### 请求 /iot/device/message/send 接口(属性上报)=> 成功 +POST {{baseUrl}}/iot/device/message/send +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "deviceId": 25, + "method": "thing.property.post", + "params": { + "width": 1, + "height": "2", + "oneThree": "3" + } +} + +### 请求 /iot/device/downstream 接口(服务调用)=> 成功 TODO 芋艿:未更新为最新 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "service", + "identifier": "temperature", + "data": { + "xx": "yy" + } +} + +### 请求 /iot/device/downstream 接口(属性设置)=> 成功 TODO 芋艿:未更新为最新 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "property", + "identifier": "set", + "data": { + "xx": "yy" + } +} + +### 请求 /iot/device/downstream 接口(属性获取)=> 成功 TODO 芋艿:未更新为最新 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "property", + "identifier": "get", + "data": ["xx", "yy"] +} + +### 请求 /iot/device/downstream 接口(配置设置)=> 成功 TODO 芋艿:未更新为最新 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "config", + "identifier": "set" +} + +### 请求 /iot/device/downstream 接口(OTA 升级)=> 成功 TODO 芋艿:未更新为最新 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "ota", + "identifier": "upgrade", + "data": { + "firmwareId": 1, + "version": "1.0.0", + "signMethod": "MD5", + "fileSign": "d41d8cd98f00b204e9800998ecf8427e", + "fileSize": 1024, + "fileUrl": "http://example.com/firmware.bin", + "information": "{\"desc\":\"升级到最新版本\"}" + } +} + +### 查询设备消息对分页 - 基础查询(设备编号25) +GET {{baseUrl}}/iot/device/message/pair-page?deviceId=25&pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 查询设备消息对分页 - 按标识符过滤(identifier=eat) +GET {{baseUrl}}/iot/device/message/pair-page?deviceId=25&identifier=eat&pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.java new file mode 100644 index 000000000..8e9d148c9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessageRespPairVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessageRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessageSendReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +@Tag(name = "管理后台 - IoT 设备消息") +@RestController +@RequestMapping("/iot/device/message") +@Validated +public class IotDeviceMessageController { + + @Resource + private IotDeviceMessageService deviceMessageService; + @Resource + private IotDeviceService deviceService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotDeviceMessageMapper deviceMessageMapper; + + @GetMapping("/page") + @Operation(summary = "获得设备消息分页") + @PreAuthorize("@ss.hasPermission('iot:device:message-query')") + public CommonResult> getDeviceMessagePage( + @Valid IotDeviceMessagePageReqVO pageReqVO) { + PageResult pageResult = deviceMessageService.getDeviceMessagePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceMessageRespVO.class)); + } + + @GetMapping("/pair-page") + @Operation(summary = "获得设备消息对分页") + @PreAuthorize("@ss.hasPermission('iot:device:message-query')") + public CommonResult> getDeviceMessagePairPage( + @Valid IotDeviceMessagePageReqVO pageReqVO) { + // 1.1 先按照条件,查询 request 的消息(非 reply) + pageReqVO.setReply(false); + PageResult requestMessagePageResult = deviceMessageService.getDeviceMessagePage(pageReqVO); + if (CollUtil.isEmpty(requestMessagePageResult.getList())) { + return success(PageResult.empty()); + } + // 1.2 接着按照 requestIds,批量查询 reply 消息 + List requestIds = convertList(requestMessagePageResult.getList(), IotDeviceMessageDO::getRequestId); + List replyMessageList = deviceMessageService.getDeviceMessageListByRequestIdsAndReply( + pageReqVO.getDeviceId(), requestIds, true); + Map replyMessages = convertMap(replyMessageList, IotDeviceMessageDO::getRequestId); + + // 2. 组装结果 + List pairMessages = convertList(requestMessagePageResult.getList(), + requestMessage -> { + IotDeviceMessageDO replyMessage = replyMessages.get(requestMessage.getRequestId()); + return new IotDeviceMessageRespPairVO() + .setRequest(BeanUtils.toBean(requestMessage, IotDeviceMessageRespVO.class)) + .setReply(BeanUtils.toBean(replyMessage, IotDeviceMessageRespVO.class)); + }); + return success(new PageResult<>(pairMessages, requestMessagePageResult.getTotal())); + } + + @PostMapping("/send") + @Operation(summary = "发送消息", description = "可用于设备模拟") + @PreAuthorize("@ss.hasPermission('iot:device:message-end')") + public CommonResult sendDeviceMessage(@Valid @RequestBody IotDeviceMessageSendReqVO sendReqVO) { + deviceMessageService.sendDeviceMessage(BeanUtils.toBean(sendReqVO, IotDeviceMessage.class)); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java new file mode 100644 index 000000000..61081ae5a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyDetailRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 设备属性") +@RestController +@RequestMapping("/iot/device/property") +@Validated +public class IotDevicePropertyController { + + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotDeviceService deviceService; + + @GetMapping("/get-latest") + @Operation(summary = "获取设备属性最新属性") + @Parameter(name = "deviceId", description = "设备编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device:property-query')") + public CommonResult> getLatestDeviceProperties( + @RequestParam("deviceId") Long deviceId) { + // 1.1 获取设备信息 + IotDeviceDO device = deviceService.getDevice(deviceId); + Assert.notNull(device, "设备不存在"); + // 1.2 获取设备最新属性 + Map properties = devicePropertyService.getLatestDeviceProperties(deviceId); + // 1.3 根据 productId + type 查询属性类型的物模型 + List thingModels = thingModelService.getThingModelListByProductIdAndType( + device.getProductId(), IotThingModelTypeEnum.PROPERTY.getType()); + + // 2. 基于 thingModels 遍历,拼接 properties + return success(convertList(thingModels, thingModel -> { + ThingModelProperty thingModelProperty = thingModel.getProperty(); + Assert.notNull(thingModelProperty, "属性不能为空"); + IotDevicePropertyDetailRespVO result = new IotDevicePropertyDetailRespVO() + .setName(thingModel.getName()).setDataType(thingModelProperty.getDataType()) + .setDataSpecs(thingModelProperty.getDataSpecs()) + .setDataSpecsList(thingModelProperty.getDataSpecsList()); + result.setIdentifier(thingModel.getIdentifier()); + IotDevicePropertyDO property = properties.get(thingModel.getIdentifier()); + if (property != null) { + result.setValue(property.getValue()) + .setUpdateTime(LocalDateTimeUtil.toEpochMilli(property.getUpdateTime())); + } + return result; + })); + } + + @GetMapping("/history-list") + @Operation(summary = "获取设备属性历史数据列表") + @PreAuthorize("@ss.hasPermission('iot:device:property-query')") + public CommonResult> getHistoryDevicePropertyList( + @Valid IotDevicePropertyHistoryListReqVO listReqVO) { + return success(devicePropertyService.getHistoryDevicePropertyList(listReqVO)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceAuthInfoRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceAuthInfoRespVO.java new file mode 100644 index 000000000..acd65ad80 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceAuthInfoRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备认证信息 Response VO") +@Data +public class IotDeviceAuthInfoRespVO { + + @Schema(description = "客户端 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "product123.device001") + @NotBlank(message = "客户端 ID 不能为空") + private String clientId; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "device001&product123") + @NotBlank(message = "用户名不能为空") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1a2b3c4d5e6f7890abcdef1234567890") + @NotBlank(message = "密码不能为空") + private String password; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceByProductKeyAndNamesReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceByProductKeyAndNamesReqVO.java new file mode 100644 index 000000000..e617cad93 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceByProductKeyAndNamesReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 通过产品标识和设备名称列表获取设备 Request VO") +@Data +public class IotDeviceByProductKeyAndNamesReqVO { + + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1de24640dfe") + @NotBlank(message = "产品标识不能为空") + private String productKey; + + @Schema(description = "设备名称列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "device001,device002") + @NotEmpty(message = "设备名称列表不能为空") + private List deviceNames; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java similarity index 72% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java index ed797677f..55f7a98c6 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java @@ -1,8 +1,11 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -34,4 +37,9 @@ public class IotDeviceImportExcelVO { @ExcelProperty("设备分组") private String groupNames; -} \ No newline at end of file + @ExcelProperty("上报方式(1:IP 定位, 2:设备上报,3:手动定位)") + @NotNull(message = "上报方式不能为空") + @InEnum(IotLocationTypeEnum.class) + private Integer locationType; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java similarity index 94% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java index 686267732..f7d515df9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java similarity index 86% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java index f6f85e494..ecb8f81c4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java @@ -1,12 +1,14 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; -import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; -import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Set; @@ -20,10 +22,6 @@ public class IotDeviceRespVO { @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") private Long id; - @Schema(description = "设备唯一标识符", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty("设备唯一标识符") - private String deviceKey; - @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") @ExcelProperty("设备名称") private String deviceName; @@ -86,6 +84,17 @@ public class IotDeviceRespVO { @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") private String config; + @Schema(description = "定位方式", example = "2") + @ExcelProperty(value = "定位方式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOCATION_TYPE) + private Integer locationType; + + @Schema(description = "设备位置的纬度", example = "45.000000") + private BigDecimal latitude; + + @Schema(description = "设备位置的经度", example = "45.000000") + private BigDecimal longitude; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java similarity index 67% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java index b9ea9b99f..7c8ecadb1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java @@ -1,9 +1,11 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; import lombok.Data; +import java.math.BigDecimal; import java.util.Set; @Schema(description = "管理后台 - IoT 设备新增/修改 Request VO") @@ -13,10 +15,6 @@ public class IotDeviceSaveReqVO { @Schema(description = "设备编号", example = "177") private Long id; - @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.AUTO, example = "177") - @Size(max = 50, message = "设备编号长度不能超过 50 个字符") - private String deviceKey; - @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.AUTO, example = "王五") private String deviceName; @@ -41,4 +39,14 @@ public class IotDeviceSaveReqVO { @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") private String config; + @Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}") + private Integer locationType; + + @Schema(description = "设备位置的纬度", example = "16380") + private BigDecimal latitude; + + @Schema(description = "设备位置的经度", example = "16380") + private BigDecimal longitude; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java similarity index 93% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java index 93b1a1ead..97ac8875b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java @@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessagePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessagePageReqVO.java new file mode 100644 index 000000000..1894dc9d7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessagePageReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.message; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 设备消息分页查询 Request VO") +@Data +public class IotDeviceMessagePageReqVO extends PageParam { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "消息类型", example = "property") + @InEnum(IotDeviceMessageMethodEnum.class) + private String method; + + @Schema(description = "是否上行", example = "true") + private Boolean upstream; + + @Schema(description = "是否回复", example = "true") + private Boolean reply; + + @Schema(description = "标识符", example = "temperature") + private String identifier; + + @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Size(min = 2, max = 2, message = "请选择时间范围") + private LocalDateTime[] times; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespPairVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespPairVO.java new file mode 100644 index 000000000..119dd0277 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespPairVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备消息对 Response VO") +@Data +public class IotDeviceMessageRespPairVO { + + @Schema(description = "请求消息", requiredMode = Schema.RequiredMode.REQUIRED) + private IotDeviceMessageRespVO request; + + @Schema(description = "响应消息") + private IotDeviceMessageRespVO reply; // 通过 requestId 配对 + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java new file mode 100644 index 000000000..e53f5acb6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备消息 Response VO") +@Data +public class IotDeviceMessageRespVO { + + @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "上报时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime reportTime; + + @Schema(description = "记录时间戳", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime ts; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private Long deviceId; + + @Schema(description = "服务编号", example = "server_123") + private String serverId; + + @Schema(description = "是否上行消息", example = "true", examples = "false") + private Boolean upstream; + + @Schema(description = "是否回复消息", example = "false", examples = "true") + private Boolean reply; + + @Schema(description = "标识符", example = "temperature") + private String identifier; + + // ========== codec(编解码)字段 ========== + + @Schema(description = "请求编号", example = "req_123") + private String requestId; + + @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.report") + private String method; + + @Schema(description = "请求参数") + private Object params; + + @Schema(description = "响应结果") + private Object data; + + @Schema(description = "响应错误码", example = "200") + private Integer code; + + @Schema(description = "响应提示", example = "操作成功") + private String msg; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageSendReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageSendReqVO.java new file mode 100644 index 000000000..e93cabbd9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageSendReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.message; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备消息发送 Request VO") // 属性上报、事件上报、状态变更等 +@Data +public class IotDeviceMessageSendReqVO { + + @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") + @NotEmpty(message = "请求方法不能为空") + @InEnum(IotDeviceMessageMethodEnum.class) + private String method; + + @Schema(description = "请求参数") + private Object params; // 例如说:属性上报的 properties、事件上报的 params + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyDetailRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyDetailRespVO.java new file mode 100644 index 000000000..57712691f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyDetailRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.property; + +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - IoT 设备属性详细 Response VO") // 额外增加 来自 ThingModelProperty 的变量 属性 +@Data +public class IotDevicePropertyDetailRespVO extends IotDevicePropertyRespVO { + + @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "int") + private String dataType; + + @Schema(description = "数据定义") + private ThingModelDataSpecs dataSpecs; + + @Schema(description = "数据定义列表") + private List dataSpecsList; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyHistoryListReqVO.java similarity index 72% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyHistoryListReqVO.java index 0de45e4a7..eb737fac1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyHistoryListReqVO.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.property; -import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -12,17 +11,14 @@ import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -@Schema(description = "管理后台 - IoT 设备属性历史分页 Request VO") +@Schema(description = "管理后台 - IoT 设备属性历史列表 Request VO") @Data -public class IotDevicePropertyHistoryPageReqVO extends PageParam { +public class IotDevicePropertyHistoryListReqVO { @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") @NotNull(message = "设备编号不能为空") private Long deviceId; - @Schema(description = "设备 Key", hidden = true) - private String deviceKey; // 非前端传递,后端自己查询设置 - @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "属性标识符不能为空") private String identifier; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyRespVO.java similarity index 55% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyRespVO.java index dd7a0d6ad..841b1f1db 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/property/IotDevicePropertyRespVO.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.property; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,10 +7,10 @@ import lombok.Data; @Data public class IotDevicePropertyRespVO { - @Schema(description = "属性定义", requiredMode = Schema.RequiredMode.REQUIRED) - private ThingModelProperty property; + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED) + private String identifier; - @Schema(description = "最新值", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "属性值", requiredMode = Schema.RequiredMode.REQUIRED) private Object value; @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java index 6cc3918e8..7c3a5b78f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java @@ -3,12 +3,14 @@ package cn.iocoder.yudao.module.iot.controller.admin.ota; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -21,12 +23,14 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - IoT OTA 固件") @RestController -@RequestMapping("/iot/ota-firmware") +@RequestMapping("/iot/ota/firmware") @Validated public class IotOtaFirmwareController { @Resource private IotOtaFirmwareService otaFirmwareService; + @Resource + private IotProductService productService; @PostMapping("/create") @Operation(summary = "创建 OTA 固件") @@ -47,8 +51,16 @@ public class IotOtaFirmwareController { @Operation(summary = "获得 OTA 固件") @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')") public CommonResult getOtaFirmware(@RequestParam("id") Long id) { - IotOtaFirmwareDO otaFirmware = otaFirmwareService.getOtaFirmware(id); - return success(BeanUtils.toBean(otaFirmware, IotOtaFirmwareRespVO.class)); + IotOtaFirmwareDO firmware = otaFirmwareService.getOtaFirmware(id); + if (firmware == null) { + return success(null); + } + return success(BeanUtils.toBean(firmware, IotOtaFirmwareRespVO.class, o -> { + IotProductDO product = productService.getProduct(firmware.getProductId()); + if (product != null) { + o.setProductName(product.getName()); + } + })); } @GetMapping("/page") diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskController.java new file mode 100644 index 000000000..6a2564f5d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskController.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 升级任务") +@RestController +@RequestMapping("/iot/ota/task") +@Validated +public class IotOtaTaskController { + + @Resource + private IotOtaTaskService otaTaskService; + + @PostMapping("/create") + @Operation(summary = "创建 OTA 升级任务") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-task:create')") + public CommonResult createOtaTask(@Valid @RequestBody IotOtaTaskCreateReqVO createReqVO) { + return success(otaTaskService.createOtaTask(createReqVO)); + } + + @PostMapping("/cancel") + @Operation(summary = "取消 OTA 升级任务") + @Parameter(name = "id", description = "升级任务编号", required = true) + @PreAuthorize(value = "@ss.hasPermission('iot:ota-task:cancel')") + public CommonResult cancelOtaTask(@RequestParam("id") Long id) { + otaTaskService.cancelOtaTask(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得 OTA 升级任务分页") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-task:query')") + public CommonResult> getOtaTaskPage(@Valid IotOtaTaskPageReqVO pageReqVO) { + PageResult pageResult = otaTaskService.getOtaTaskPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaTaskRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得 OTA 升级任务") + @Parameter(name = "id", description = "升级任务编号", required = true, example = "1024") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-task:query')") + public CommonResult getOtaTask(@RequestParam("id") Long id) { + IotOtaTaskDO upgradeTask = otaTaskService.getOtaTask(id); + return success(BeanUtils.toBean(upgradeTask, IotOtaTaskRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java new file mode 100644 index 000000000..3c289926f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaTaskRecordController.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - IoT OTA 升级任务记录") +@RestController +@RequestMapping("/iot/ota/task/record") +@Validated +public class IotOtaTaskRecordController { + + @Resource + private IotOtaTaskRecordService otaTaskRecordService; + @Resource + private IotDeviceService deviceService; + @Resource + private IotOtaFirmwareService otaFirmwareService; + + @GetMapping("/get-status-statistics") + @Operation(summary = "获得 OTA 升级记录状态统计") + @Parameters({ + @Parameter(name = "firmwareId", description = "固件编号", example = "1024"), + @Parameter(name = "taskId", description = "升级任务编号", example = "2048") + }) + @PreAuthorize("@ss.hasPermission('iot:ota-task-record:query')") + public CommonResult> getOtaTaskRecordStatusStatistics( + @RequestParam(value = "firmwareId", required = false) Long firmwareId, + @RequestParam(value = "taskId", required = false) Long taskId) { + return success(otaTaskRecordService.getOtaTaskRecordStatusStatistics(firmwareId, taskId)); + } + + @GetMapping("/page") + @Operation(summary = "获得 OTA 升级记录分页") + @PreAuthorize("@ss.hasPermission('iot:ota-task-record:query')") + public CommonResult> getOtaTaskRecordPage( + @Valid IotOtaTaskRecordPageReqVO pageReqVO) { + PageResult pageResult = otaTaskRecordService.getOtaTaskRecordPage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 批量查询固件信息 + Map firmwareMap = otaFirmwareService.getOtaFirmwareMap( + convertSet(pageResult.getList(), IotOtaTaskRecordDO::getFromFirmwareId)); + Map deviceMap = deviceService.getDeviceMap( + convertSet(pageResult.getList(), IotOtaTaskRecordDO::getDeviceId)); + // 转换为响应 VO + return success(BeanUtils.toBean(pageResult, IotOtaTaskRecordRespVO.class, (vo) -> { + MapUtils.findAndThen(firmwareMap, vo.getFromFirmwareId(), firmware -> + vo.setFromFirmwareVersion(firmware.getVersion())); + MapUtils.findAndThen(deviceMap, vo.getDeviceId(), device -> + vo.setDeviceName(device.getDeviceName())); + })); + } + + @GetMapping("/get") + @Operation(summary = "获得 OTA 升级记录") + @PreAuthorize("@ss.hasPermission('iot:ota-task-record:query')") + @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + public CommonResult getOtaTaskRecord(@RequestParam("id") Long id) { + IotOtaTaskRecordDO upgradeRecord = otaTaskRecordService.getOtaTaskRecord(id); + return success(BeanUtils.toBean(upgradeRecord, IotOtaTaskRecordRespVO.class)); + } + + @PutMapping("/cancel") + @Operation(summary = "取消 OTA 升级记录") + @PreAuthorize("@ss.hasPermission('iot:ota-task-record:cancel')") + @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + public CommonResult cancelOtaTaskRecord(@RequestParam("id") Long id) { + otaTaskRecordService.cancelOtaTaskRecord(id); + return success(true); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java new file mode 100644 index 000000000..544cce081 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +@Schema(description = "管理后台 - IoT OTA 固件创建 Request VO") +@Data +public class IotOtaFirmwareCreateReqVO { + + @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能开关固件") + @NotEmpty(message = "固件名称不能为空") + private String name; + + @Schema(description = "固件描述", example = "某品牌型号固件,测试用") + private String description; + + @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0") + @NotEmpty(message = "版本号不能为空") + private String version; + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.zip") + @NotEmpty(message = "固件文件 URL 不能为空") + @URL(message = "固件文件 URL 格式错误") + private String fileUrl; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java similarity index 55% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java index baa741029..589ed00d4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java @@ -3,21 +3,24 @@ package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -@Data @Schema(description = "管理后台 - IoT OTA 固件分页 Request VO") +@Data public class IotOtaFirmwarePageReqVO extends PageParam { - /** - * 固件名称 - */ @Schema(description = "固件名称", example = "智能开关固件") private String name; - /** - * 产品标识 - */ @Schema(description = "产品标识", example = "1024") private String productId; + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java new file mode 100644 index 000000000..d6fdbf726 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT OTA 固件 Response VO") +@Data +public class IotOtaFirmwareRespVO implements VO { + + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "固件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "OTA 固件") + private String name; + + @Schema(description = "固件描述") + private String description; + + @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0.0") + private String version; + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long productId; + + @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能设备") + private String productName; + + @Schema(description = "固件文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/firmware.bin") + private String fileUrl; + + @Schema(description = "固件文件大小", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long fileSize; + + @Schema(description = "固件文件签名算法", example = "MD5") + private String fileDigestAlgorithm; + + @Schema(description = "固件文件签名结果", example = "d41d8cd98f00b204e9800998ecf8427e") + private String fileDigestValue; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java similarity index 56% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java index aa134bcee..57b53bbd3 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java @@ -1,23 +1,18 @@ package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - @Schema(description = "管理后台 - IoT OTA 固件更新 Request VO") @Data public class IotOtaFirmwareUpdateReqVO { - @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @NotNull(message = "固件编号不能为空") private Long id; - // TODO @li:name 是不是可以飞必传哈 - @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") - @NotEmpty(message = "固件名称不能为空") + @Schema(description = "固件名称", example = "智能开关固件") private String name; @Schema(description = "固件描述", example = "某品牌型号固件,测试用") diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskCreateReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskCreateReqVO.java new file mode 100644 index 000000000..65bc07c1b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskCreateReqVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskDeviceScopeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - IoT OTA 升级任务创建 Request VO") +@Data +public class IotOtaTaskCreateReqVO { + + @NotEmpty(message = "任务名称不能为空") + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务") + private String name; + + @Schema(description = "任务描述", example = "升级任务") + private String description; + + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "固件编号不能为空") + private Long firmwareId; + + @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "升级范围不能为空") + @InEnum(value = IotOtaTaskDeviceScopeEnum.class) + private Integer deviceScope; + + @Schema(description = "选中的设备编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + private List deviceIds; + + // TODO @li:如果 deviceScope 等于 2 时,deviceIds 校验非空; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskPageReqVO.java new file mode 100644 index 000000000..4638f1a40 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskPageReqVO.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO") +@Data +public class IotOtaTaskPageReqVO extends PageParam { + + @Schema(description = "任务名称", example = "升级任务") + private String name; + + @Schema(description = "固件编号", example = "1024") + private Long firmwareId; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskRespVO.java new file mode 100644 index 000000000..247f7c658 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/IotOtaTaskRespVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task; + +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT OTA 升级任务 Response VO") +@Data +public class IotOtaTaskRespVO implements VO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "升级任务") + private String name; + + @Schema(description = "任务描述", example = "升级任务") + private String description; + + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long firmwareId; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer status; + + @Schema(description = "升级范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer deviceScope; + + @Schema(description = "设备总共数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer deviceTotalCount; + + @Schema(description = "设备成功数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") + private Integer deviceSuccessCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/record/IotOtaTaskRecordPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/record/IotOtaTaskRecordPageReqVO.java new file mode 100644 index 000000000..00c6fe7f3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/record/IotOtaTaskRecordPageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskRecordStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO") +@Data +public class IotOtaTaskRecordPageReqVO extends PageParam { + + @Schema(description = "升级任务编号", example = "1024") + private Long taskId; + + @Schema(description = "升级记录状态", example = "5") + @InEnum(IotOtaTaskRecordStatusEnum.class) + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/record/IotOtaTaskRecordRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/record/IotOtaTaskRecordRespVO.java new file mode 100644 index 000000000..f7ab1edf5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/task/record/IotOtaTaskRecordRespVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT OTA 升级任务记录 Response VO") +@Data +public class IotOtaTaskRecordRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "固件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long firmwareId; + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long taskId; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long deviceId; + + @Schema(description = "设备名称", example = "智能开关") + private String deviceName; + + @Schema(description = "来源的固件编号", example = "1023") + private Long fromFirmwareId; + + @Schema(description = "来源固件版本", example = "1.0.0") + private String fromFirmwareVersion; + + @Schema(description = "升级状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "升级进度,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer progress; + + @Schema(description = "升级进度描述", example = "正在下载固件...") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java similarity index 84% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java index 2d8c85640..39eec8444 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java @@ -84,6 +84,27 @@ public class IotProductController { @PreAuthorize("@ss.hasPermission('iot:product:query')") public CommonResult getProduct(@RequestParam("id") Long id) { IotProductDO product = productService.getProduct(id); + if (product == null) { + return success(null); + } + // 拼接数据 + IotProductCategoryDO category = categoryService.getProductCategory(product.getCategoryId()); + return success(BeanUtils.toBean(product, IotProductRespVO.class, bean -> { + if (category != null) { + bean.setCategoryName(category.getName()); + } + })); + } + + @GetMapping("/get-by-key") + @Operation(summary = "通过 ProductKey 获得产品") + @Parameter(name = "productKey", description = "产品Key", required = true, example = "abc123") + @PreAuthorize("@ss.hasPermission('iot:product:query')") + public CommonResult getProductByKey(@RequestParam("productKey") String productKey) { + IotProductDO product = productService.getProductByProductKey(productKey); + if (product == null) { + return success(null); + } // 拼接数据 IotProductCategoryDO category = categoryService.getProductCategory(product.getCategoryId()); return success(BeanUtils.toBean(product, IotProductRespVO.class, bean -> { @@ -122,11 +143,11 @@ public class IotProductController { @GetMapping("/simple-list") @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") - public CommonResult> getSimpleProductList() { + public CommonResult> getProductSimpleList() { List list = productService.getProductList(); return success(convertList(list, product -> // 只返回 id、name 字段 new IotProductRespVO().setId(product.getId()).setName(product.getName()) - .setDeviceType(product.getDeviceType()))); + .setDeviceType(product.getDeviceType()).setLocationType(product.getLocationType()))); } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java index 18c69c4ce..0df4a1834 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java @@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; @Schema(description = "管理后台 - IoT 产品分页 Request VO") @Data diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java similarity index 76% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index 5d70dcdf0..bdad139bd 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; -import cn.idev.excel.annotation.ExcelIgnoreUnannotated; -import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -61,27 +61,18 @@ public class IotProductRespVO { @DictFormat(DictTypeConstants.NET_TYPE) private Integer netType; - @Schema(description = "接入网关协议", example = "2") - @ExcelProperty(value = "接入网关协议", converter = DictConvert.class) - @DictFormat(DictTypeConstants.PROTOCOL_TYPE) - private Integer protocolType; + @Schema(description = "定位方式", example = "2") + @ExcelProperty(value = "定位方式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOCATION_TYPE) + private Integer locationType; - @Schema(description = "协议编号(脚本解析 id)", requiredMode = Schema.RequiredMode.REQUIRED, example = "13177") - @ExcelProperty("协议编号(脚本解析 id)") - private Long protocolId; - - @Schema(description = "数据格式") + @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") @ExcelProperty(value = "数据格式", converter = DictConvert.class) - @DictFormat(DictTypeConstants.DATA_FORMAT) - private Integer dataFormat; - - @Schema(description = "数据校验级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty(value = "数据校验级别", converter = DictConvert.class) - @DictFormat(DictTypeConstants.VALIDATE_TYPE) - private Integer validateType; + @DictFormat(DictTypeConstants.CODEC_TYPE) + private String codecType; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java similarity index 72% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 268ab7c6f..5f8cb0053 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.product.*; +import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -43,18 +45,12 @@ public class IotProductSaveReqVO { @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") private Integer netType; - @Schema(description = "接入网关协议", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") - @InEnum(value = IotProtocolTypeEnum.class, message = "接入网关协议必须是 {value}") - private Integer protocolType; + @Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}") + private Integer locationType; - @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") - @InEnum(value = IotDataFormatEnum.class, message = "数据格式必须是 {value}") - @NotNull(message = "数据格式不能为空") - private Integer dataFormat; - - @Schema(description = "数据校验级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") - @InEnum(value = IotValidateTypeEnum.class, message = "数据校验级别必须是 {value}") - @NotNull(message = "数据校验级别不能为空") - private Integer validateType; + @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotEmpty(message = "数据格式不能为空") + private String codecType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataRuleController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataRuleController.java new file mode 100644 index 000000000..f7e64b160 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataRuleController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRuleRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO; +import cn.iocoder.yudao.module.iot.service.rule.data.IotDataRuleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 数据流转规则") +@RestController +@RequestMapping("/iot/data-rule") +@Validated +public class IotDataRuleController { + + @Resource + private IotDataRuleService dataRuleService; + + @PostMapping("/create") + @Operation(summary = "创建数据流转规则") + @PreAuthorize("@ss.hasPermission('iot:data-rule:create')") + public CommonResult createDataRule(@Valid @RequestBody IotDataRuleSaveReqVO createReqVO) { + return success(dataRuleService.createDataRule(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新数据流转规则") + @PreAuthorize("@ss.hasPermission('iot:data-rule:update')") + public CommonResult updateDataRule(@Valid @RequestBody IotDataRuleSaveReqVO updateReqVO) { + dataRuleService.updateDataRule(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除数据流转规则") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:data-rule:delete')") + public CommonResult deleteDataRule(@RequestParam("id") Long id) { + dataRuleService.deleteDataRule(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得数据流转规则") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:data-rule:query')") + public CommonResult getDataRule(@RequestParam("id") Long id) { + IotDataRuleDO dataRule = dataRuleService.getDataRule(id); + return success(BeanUtils.toBean(dataRule, IotDataRuleRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得数据流转规则分页") + @PreAuthorize("@ss.hasPermission('iot:data-rule:query')") + public CommonResult> getDataRulePage(@Valid IotDataRulePageReqVO pageReqVO) { + PageResult pageResult = dataRuleService.getDataRulePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDataRuleRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataSinkController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataSinkController.java new file mode 100644 index 000000000..6e1aae797 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataSinkController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import cn.iocoder.yudao.module.iot.service.rule.data.IotDataSinkService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 数据流转目的") +@RestController +@RequestMapping("/iot/data-sink") +@Validated +public class IotDataSinkController { + + @Resource + private IotDataSinkService dataSinkService; + + @PostMapping("/create") + @Operation(summary = "创建数据目的") + @PreAuthorize("@ss.hasPermission('iot:data-sink:create')") + public CommonResult createDataSink(@Valid @RequestBody IotDataSinkSaveReqVO createReqVO) { + return success(dataSinkService.createDataSink(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新数据目的") + @PreAuthorize("@ss.hasPermission('iot:data-sink:update')") + public CommonResult updateDataSink(@Valid @RequestBody IotDataSinkSaveReqVO updateReqVO) { + dataSinkService.updateDataSink(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除数据目的") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:data-sink:delete')") + public CommonResult deleteDataSink(@RequestParam("id") Long id) { + dataSinkService.deleteDataSink(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得数据目的") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:data-sink:query')") + public CommonResult getDataSink(@RequestParam("id") Long id) { + IotDataSinkDO sink = dataSinkService.getDataSink(id); + return success(BeanUtils.toBean(sink, IotDataSinkRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得数据目的分页") + @PreAuthorize("@ss.hasPermission('iot:data-sink:query')") + public CommonResult> getDataSinkPage(@Valid IotDataSinkPageReqVO pageReqVO) { + PageResult pageResult = dataSinkService.getDataSinkPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDataSinkRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取数据目的的精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getDataSinkSimpleList() { + List list = dataSinkService.getDataSinkListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, sink -> // 只返回 id、name 字段 + new IotDataSinkRespVO().setId(sink.getId()).setName(sink.getName()))); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotSceneRuleController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotSceneRuleController.java new file mode 100644 index 000000000..57d71be82 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotSceneRuleController.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRulePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleUpdateStatusReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 场景联动") +@RestController +@RequestMapping("/iot/scene-rule") +@Validated +public class IotSceneRuleController { + + @Resource + private IotSceneRuleService sceneRuleService; + + @PostMapping("/create") + @Operation(summary = "创建场景联动") + @PreAuthorize("@ss.hasPermission('iot:scene-rule:create')") + public CommonResult createSceneRule(@Valid @RequestBody IotSceneRuleSaveReqVO createReqVO) { + return success(sceneRuleService.createSceneRule(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新场景联动") + @PreAuthorize("@ss.hasPermission('iot:scene-rule:update')") + public CommonResult updateSceneRule(@Valid @RequestBody IotSceneRuleSaveReqVO updateReqVO) { + sceneRuleService.updateSceneRule(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新场景联动状态") + @PreAuthorize("@ss.hasPermission('iot:scene-rule:update')") + public CommonResult updateSceneRuleStatus(@Valid @RequestBody IotSceneRuleUpdateStatusReqVO updateReqVO) { + sceneRuleService.updateSceneRuleStatus(updateReqVO.getId(), updateReqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除场景联动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:scene-rule:delete')") + public CommonResult deleteSceneRule(@RequestParam("id") Long id) { + sceneRuleService.deleteSceneRule(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得场景联动") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:scene-rule:query')") + public CommonResult getSceneRule(@RequestParam("id") Long id) { + IotSceneRuleDO sceneRule = sceneRuleService.getSceneRule(id); + return success(BeanUtils.toBean(sceneRule, IotSceneRuleRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得场景联动分页") + @PreAuthorize("@ss.hasPermission('iot:scene-rule:query')") + public CommonResult> getSceneRulePage(@Valid IotSceneRulePageReqVO pageReqVO) { + PageResult pageResult = sceneRuleService.getSceneRulePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotSceneRuleRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取场景联动的精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSceneRuleSimpleList() { + List list = sceneRuleService.getSceneRuleListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, scene -> // 只返回 id、name 字段 + new IotSceneRuleRespVO().setId(scene.getId()).setName(scene.getName()))); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/package-info.java new file mode 100644 index 000000000..6be90cf32 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRulePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRulePageReqVO.java new file mode 100644 index 000000000..8e21c7992 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRulePageReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 数据流转规则分页 Request VO") +@Data +public class IotDataRulePageReqVO extends PageParam { + + @Schema(description = "数据流转规则名称", example = "芋艿") + private String name; + + @Schema(description = "数据流转规则状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRuleRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRuleRespVO.java new file mode 100644 index 000000000..3427370f7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRuleRespVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - IoT 数据流转规则 Response VO") +@Data +public class IotDataRuleRespVO { + + @Schema(description = "数据流转规则编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8540") + private Long id; + + @Schema(description = "数据流转规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String name; + + @Schema(description = "数据流转规则描述", example = "你猜") + private String description; + + @Schema(description = "数据流转规则状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "数据源配置数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List sourceConfigs; + + @Schema(description = "数据目的编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List sinkIds; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRuleSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRuleSaveReqVO.java new file mode 100644 index 000000000..47748c6eb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/rule/IotDataRuleSaveReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - IoT 数据流转规则新增/修改 Request VO") +@Data +public class IotDataRuleSaveReqVO { + + @Schema(description = "数据流转规则编号", example = "8540") + private Long id; + + @Schema(description = "数据流转规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotEmpty(message = "数据流转规则名称不能为空") + private String name; + + @Schema(description = "数据流转规则描述", example = "你猜") + private String description; + + @Schema(description = "数据流转规则状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据流转规则状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "数据源配置数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "数据源配置数组不能为空") + private List sourceConfigs; + + @Schema(description = "数据目的编号数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "数据目的编号数组不能为空") + private List sinkIds; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkPageReqVO.java similarity index 73% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkPageReqVO.java index e4dc36ef9..06bbecc89 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkPageReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; @@ -11,14 +11,14 @@ import java.time.LocalDateTime; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; -@Schema(description = "管理后台 - IoT 数据桥梁分页 Request VO") +@Schema(description = "管理后台 - IoT 数据流转目的分页 Request VO") @Data -public class IotDataBridgePageReqVO extends PageParam { +public class IotDataSinkPageReqVO extends PageParam { - @Schema(description = "桥梁名称", example = "赵六") + @Schema(description = "数据目的名称", example = "赵六") private String name; - @Schema(description = "桥梁状态", example = "1") + @Schema(description = "数据目的状态", example = "2") @InEnum(CommonStatusEnum.class) private Integer status; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkRespVO.java new file mode 100644 index 000000000..0ced03c22 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotAbstractDataSinkConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 数据流转目的 Response VO") +@Data +public class IotDataSinkRespVO { + + @Schema(description = "数据目的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") + private Long id; + + @Schema(description = "数据目的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "数据目的描述", example = "随便") + private String description; + + @Schema(description = "数据目的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "数据目的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "数据目的配置") + private IotAbstractDataSinkConfig config; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkSaveReqVO.java new file mode 100644 index 000000000..b0e49dedd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/data/sink/IotDataSinkSaveReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotAbstractDataSinkConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 数据流转目的新增/修改 Request VO") +@Data +public class IotDataSinkSaveReqVO { + + @Schema(description = "数据目的编号", example = "18564") + private Long id; + + @Schema(description = "数据目的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "数据目的名称不能为空") + private String name; + + @Schema(description = "数据目的描述", example = "随便") + private String description; + + @Schema(description = "数据目的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据目的状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "数据目的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据目的类型不能为空") + @InEnum(IotDataSinkTypeEnum.class) + private Integer type; + + @Schema(description = "数据目的配置") + @NotNull(message = "数据目的配置不能为空") + private IotAbstractDataSinkConfig config; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRulePageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRulePageReqVO.java new file mode 100644 index 000000000..8345004b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRulePageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 场景联动分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IotSceneRulePageReqVO extends PageParam { + + @Schema(description = "场景名称", example = "赵六") + private String name; + + @Schema(description = "场景描述", example = "你猜") + private String description; + + @Schema(description = "场景状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleRespVO.java new file mode 100644 index 000000000..835ef6293 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleRespVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - IoT 场景联动 Response VO") +@Data +public class IotSceneRuleRespVO { + + @Schema(description = "场景编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15865") + private Long id; + + @Schema(description = "场景名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "场景描述", example = "你猜") + private String description; + + @Schema(description = "场景状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "触发器数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List triggers; + + @Schema(description = "执行器数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List actions; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleSaveReqVO.java new file mode 100644 index 000000000..4a5f1ed9f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleSaveReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - IoT 场景联动新增/修改 Request VO") +@Data +public class IotSceneRuleSaveReqVO { + + @Schema(description = "场景编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15865") + private Long id; + + @Schema(description = "场景名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "场景名称不能为空") + private String name; + + @Schema(description = "场景描述", example = "你猜") + private String description; + + @Schema(description = "场景状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "场景状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "触发器数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "触发器数组不能为空") + private List triggers; + + @Schema(description = "执行器数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "执行器数组不能为空") + private List actions; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleUpdateStatusReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleUpdateStatusReqVO.java new file mode 100644 index 000000000..ea3721fdd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/scene/IotSceneRuleUpdateStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 场景联动更新状态 Request VO") +@Data +public class IotSceneRuleUpdateStatusReqVO { + + @Schema(description = "场景联动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "场景联动编号不能为空") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.http b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.http new file mode 100644 index 000000000..b8cb6b544 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.http @@ -0,0 +1,11 @@ +### 请求 /iot/statistics/get-device-message-summary-by-date 接口(小时) +GET {{baseUrl}}/iot/statistics/get-device-message-summary-by-date?interval=0×[0]=2025-06-13 00:00:00×[1]=2025-06-14 23:59:59 +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} + +### 请求 /iot/statistics/get-device-message-summary-by-date 接口(天) +GET {{baseUrl}}/iot/statistics/get-device-message-summary-by-date?interval=1×[0]=2025-06-13 00:00:00×[1]=2025-06-14 23:59:59 +Content-Type: application/json +tenant-id: {{adminTenantId}} +Authorization: Bearer {{token}} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java index a9c195656..22837c48b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java @@ -1,12 +1,13 @@ package cn.iocoder.yudao.module.iot.controller.admin.statistics; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsReqVO; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; -import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import io.swagger.v3.oas.annotations.Operation; @@ -19,9 +20,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - IoT 数据统计") @RestController @@ -36,24 +38,23 @@ public class IotStatisticsController { @Resource private IotProductService productService; @Resource - private IotDeviceLogService deviceLogService; + private IotDeviceMessageService deviceMessageService; @GetMapping("/get-summary") - @Operation(summary = "获取 IoT 数据统计") - public CommonResult getIotStatisticsSummary(){ + @Operation(summary = "获取全局的数据统计") + public CommonResult getStatisticsSummary(){ IotStatisticsSummaryRespVO respVO = new IotStatisticsSummaryRespVO(); // 1.1 获取总数 respVO.setProductCategoryCount(productCategoryService.getProductCategoryCount(null)); respVO.setProductCount(productService.getProductCount(null)); respVO.setDeviceCount(deviceService.getDeviceCount(null)); - respVO.setDeviceMessageCount(deviceLogService.getDeviceLogCount(null)); + respVO.setDeviceMessageCount(deviceMessageService.getDeviceMessageCount(null)); // 1.2 获取今日新增数量 - // TODO @super:使用 LocalDateTimeUtils.getToday() - LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0); + LocalDateTime todayStart = LocalDateTimeUtils.getToday(); respVO.setProductCategoryTodayCount(productCategoryService.getProductCategoryCount(todayStart)); respVO.setProductTodayCount(productService.getProductCount(todayStart)); respVO.setDeviceTodayCount(deviceService.getDeviceCount(todayStart)); - respVO.setDeviceMessageTodayCount(deviceLogService.getDeviceLogCount(todayStart)); + respVO.setDeviceMessageTodayCount(deviceMessageService.getDeviceMessageCount(todayStart)); // 2. 获取各个品类下设备数量统计 respVO.setProductCategoryDeviceCounts(productCategoryService.getProductCategoryDeviceCountMap()); @@ -66,14 +67,11 @@ public class IotStatisticsController { return success(respVO); } - // TODO @super:要不干掉 IotStatisticsReqVO 参数,直接使用 @RequestParam 接收,简单一些。 - @GetMapping("/get-log-summary") - @Operation(summary = "获取 IoT 设备上下行消息数据统计") - public CommonResult getIotStatisticsDeviceMessageSummary( - @Valid IotStatisticsReqVO reqVO) { - return success(new IotStatisticsDeviceMessageSummaryRespVO() - .setDownstreamCounts(deviceLogService.getDeviceLogUpCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime())) - .setDownstreamCounts((deviceLogService.getDeviceLogDownCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime())))); + @GetMapping("/get-device-message-summary-by-date") + @Operation(summary = "获取设备消息的数据统计") + public CommonResult> getDeviceMessageSummaryByDate( + @Valid IotStatisticsDeviceMessageReqVO reqVO) { + return success(deviceMessageService.getDeviceMessageSummaryByDate(reqVO)); } } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageReqVO.java new file mode 100644 index 000000000..73f83e70c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - IoT 设备消息数量统计 Response VO") +@Data +public class IotStatisticsDeviceMessageReqVO { + + @Schema(description = "时间间隔类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = DateIntervalEnum.class, message = "时间间隔类型,必须是 {value}") + private Integer interval; + + @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Size(min = 2, max = 2, message = "请选择时间范围") + private LocalDateTime[] times; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryByDateRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryByDateRespVO.java new file mode 100644 index 000000000..9c605dd34 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryByDateRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备消息数量统计 Response VO") +@Data +public class IotStatisticsDeviceMessageSummaryByDateRespVO { + + @Schema(description = "时间轴", requiredMode = Schema.RequiredMode.REQUIRED, example = "202401") + private String time; + + @Schema(description = "上行消息数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer upstreamCount; + + @Schema(description = "上行消息数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer downstreamCount; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http similarity index 95% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http index 1e1f72103..4f579c453 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http @@ -174,8 +174,7 @@ GET {{baseUrl}}/iot/product-thing-model/get?id=67 tenant-id: {{adminTenentId}} Authorization: Bearer {{token}} - -### 请求 /iot/product-thing-model/list-by-product-id 接口 => 成功 -GET {{baseUrl}}/iot/product-thing-model/list-by-product-id?productId=1001 +### 请求 /iot/product-thing-model/get-tsl 接口 => 成功 +GET {{baseUrl}}/iot/product-thing-model/get-tsl?productId=1001 tenant-id: {{adminTenentId}} Authorization: Bearer {{token}} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java similarity index 65% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java index 382940fc4..d93c18d47 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java @@ -3,12 +3,13 @@ package cn.iocoder.yudao.module.iot.controller.admin.thingmodel; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import com.google.common.base.Objects; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -21,6 +22,8 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; @Tag(name = "管理后台 - IoT 产品物模型") @RestController @@ -30,6 +33,8 @@ public class IotThingModelController { @Resource private IotThingModelService thingModelService; + @Resource + private IotProductService productService; @PostMapping("/create") @Operation(summary = "创建产品物模型") @@ -64,16 +69,29 @@ public class IotThingModelController { return success(BeanUtils.toBean(thingModel, IotThingModelRespVO.class)); } - @GetMapping("/list-by-product-id") - @Operation(summary = "获得产品物模型") - @Parameter(name = "productId", description = "产品ID", required = true, example = "1024") + @GetMapping("/get-tsl") + @Operation(summary = "获得产品物模型 TSL") + @Parameter(name = "productId", description = "产品 ID", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") - public CommonResult> getThingModelListByProductId(@RequestParam("productId") Long productId) { - List list = thingModelService.getThingModelListByProductId(productId); - return success(BeanUtils.toBean(list, IotThingModelRespVO.class)); + public CommonResult getThingModelTsl(@RequestParam("productId") Long productId) { + // 1. 获得产品 + IotProductDO product = productService.getProduct(productId); + if (product == null) { + return success(null); + } + IotThingModelTSLRespVO tslRespVO = new IotThingModelTSLRespVO() + .setProductId(product.getId()).setProductKey(product.getProductKey()); + // 2. 获得物模型定义 + List thingModels = thingModelService.getThingModelListByProductId(productId); + tslRespVO.setProperties(convertList(filterList(thingModels, item -> + Objects.equal(IotThingModelTypeEnum.PROPERTY.getType(), item.getType())), IotThingModelDO::getProperty)) + .setServices(convertList(filterList(thingModels, item -> + Objects.equal(IotThingModelTypeEnum.SERVICE.getType(), item.getType())), IotThingModelDO::getService)) + .setEvents(convertList(filterList(thingModels, item -> + Objects.equal(IotThingModelTypeEnum.EVENT.getType(), item.getType())), IotThingModelDO::getEvent)); + return success(tslRespVO); } - // TODO @puhui @super:getThingModelListByProductId 和 getThingModelListByProductId 可以融合么? @GetMapping("/list") @Operation(summary = "获得产品物模型列表") @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java index 248fc328c..a7a17dde8 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java @@ -1,10 +1,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; -import cn.idev.excel.annotation.ExcelIgnoreUnannotated; -import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelService; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -12,18 +10,15 @@ import java.time.LocalDateTime; @Schema(description = "管理后台 - IoT 产品物模型 Response VO") @Data -@ExcelIgnoreUnannotated public class IotThingModelRespVO { @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21816") - @ExcelProperty("产品ID") private Long id; @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long productId; @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature_sensor") - @ExcelProperty("产品标识") private String productKey; @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") @@ -48,7 +43,6 @@ public class IotThingModelRespVO { private ThingModelService service; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty("创建时间") private LocalDateTime createTime; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java index 1e8564df4..97404983d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java @@ -1,9 +1,9 @@ package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelTSLRespVO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelTSLRespVO.java new file mode 100644 index 000000000..d3809d881 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelTSLRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; + +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelService; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - IoT 产品物模型 TSL Response VO") +@Data +public class IotThingModelTSLRespVO { + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long productId; + + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature_sensor") + private String productKey; + + @Schema(description = "属性列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List properties; + + @Schema(description = "服务列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List events; + + @Schema(description = "事件列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List services; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/controller/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java similarity index 86% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java index 9577b18f7..7588d8298 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.convert.thingmodel; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import org.mapstruct.Mapper; import org.mapstruct.Mapping; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java similarity index 52% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java index c6a2390ac..69f466bf4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertConfigDO.java @@ -1,14 +1,21 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.rule; +package cn.iocoder.yudao.module.iot.dal.dataobject.alert; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotAlertConfigReceiveTypeEnum; +import cn.iocoder.yudao.framework.mybatis.core.type.IntegerListTypeHandler; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; +import cn.iocoder.yudao.module.iot.enums.alert.IotAlertReceiveTypeEnum; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @@ -17,13 +24,13 @@ import java.util.List; * * @author 芋道源码 */ -@TableName("iot_alert_config") +@TableName(value = "iot_alert_config", autoResultMap = true) @KeySequence("iot_alert_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Builder @NoArgsConstructor @AllArgsConstructor -public class IotAlertConfig extends BaseDO { +public class IotAlertConfigDO extends BaseDO { /** * 配置编号 @@ -41,37 +48,37 @@ public class IotAlertConfig extends BaseDO { /** * 配置状态 * - * TODO 数据字典 + * 字典 {@link DictTypeConstants#ALERT_LEVEL} */ private Integer level; /** * 配置状态 * - * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + * 枚举 {@link CommonStatusEnum} */ private Integer status; /** - * 关联的规则场景编号数组 + * 关联的场景联动规则编号数组 * - * 关联 {@link IotRuleSceneDO#getId()} + * 关联 {@link IotSceneRuleDO#getId()} */ - @TableField(typeHandler = JacksonTypeHandler.class) - private List ruleSceneIds; + @TableField(typeHandler = LongListTypeHandler.class) + private List sceneRuleIds; /** * 接收的用户编号数组 * * 关联 {@link AdminUserRespDTO#getId()} */ - @TableField(typeHandler = JacksonTypeHandler.class) + @TableField(typeHandler = LongListTypeHandler.class) private List receiveUserIds; /** * 接收的类型数组 * - * 枚举 {@link IotAlertConfigReceiveTypeEnum} + * 枚举 {@link IotAlertReceiveTypeEnum} */ - @TableField(typeHandler = JacksonTypeHandler.class) + @TableField(typeHandler = IntegerListTypeHandler.class) private List receiveTypes; } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertRecordDO.java similarity index 53% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertRecordDO.java index 840111078..29b1c7db7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/alert/IotAlertRecordDO.java @@ -1,21 +1,26 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.rule; +package cn.iocoder.yudao.module.iot.dal.dataobject.alert; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** * IoT 告警记录 DO * * @author 芋道源码 */ -@TableName("iot_alert_record") +@TableName(value = "iot_alert_record", autoResultMap = true) @KeySequence("iot_alert_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Builder @@ -26,47 +31,54 @@ public class IotAlertRecordDO extends BaseDO { /** * 记录编号 */ - @TableField + @TableId private Long id; /** * 告警名称 * - * 冗余 {@link IotAlertConfig#getName()} + * 冗余 {@link IotAlertConfigDO#getId()} */ private Long configId; /** * 告警名称 * - * 冗余 {@link IotAlertConfig#getName()} + * 冗余 {@link IotAlertConfigDO#getName()} */ - private String name; + private String configName; + /** + * 告警级别 + * + * 冗余 {@link IotAlertConfigDO#getLevel()} + * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#ALERT_LEVEL} + */ + private Integer configLevel; + /** + * 场景规则编号 + * + * 关联 {@link IotSceneRuleDO#getId()} + */ + private Long sceneRuleId; /** - * 产品标识 + * 产品编号 * - * 关联 {@link IotProductDO#getProductKey()} ()} + * 关联 {@link IotProductDO#getId()} */ - private String productKey; + private Long productId; /** - * 设备名称 + * 设备编号 * - * 冗余 {@link IotDeviceDO#getDeviceName()} + * 关联 {@link IotDeviceDO#getId()} */ - private String deviceName; - - // TODO @芋艿:有没更好的方式 + private Long deviceId; /** * 触发的设备消息 */ @TableField(typeHandler = JacksonTypeHandler.class) private IotDeviceMessage deviceMessage; - // TODO @芋艿:换成枚举,枚举对应 ApiErrorLogProcessStatusEnum /** - * 处理状态 - * - * true - 已处理 - * false - 未处理 + * 是否处理 */ private Boolean processStatus; /** diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java similarity index 79% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index 9633d2feb..97c1fa78c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -2,14 +2,17 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -28,17 +31,16 @@ import java.util.Set; @AllArgsConstructor public class IotDeviceDO extends TenantBaseDO { + /** + * 设备编号 - 全部设备 + */ + public static final Long DEVICE_ID_ALL = 0L; + /** * 设备 ID,主键,自增 */ @TableId private Long id; - /** - * 设备唯一标识符,全局唯一,用于识别设备 - * - * 类似阿里云 QueryDeviceInfo 的 IotInstanceId - */ - private String deviceKey; /** * 设备名称,在产品内唯一,用于标识设备 */ @@ -118,32 +120,24 @@ public class IotDeviceDO extends TenantBaseDO { * * 关联 {@link IotOtaFirmwareDO#getId()} */ - private String firmwareId; + private Long firmwareId; - // TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有 /** - * 设备密钥,用于设备认证,需安全存储 + * 设备密钥,用于设备认证 */ private String deviceSecret; - /** - * MQTT 客户端 ID - */ - private String mqttClientId; - /** - * MQTT 用户名 - */ - private String mqttUsername; - /** - * MQTT 密码 - */ - private String mqttPassword; /** * 认证类型(如一机一密、动态注册) */ // TODO @haohao:是不是要枚举哈 private String authType; - // TODO @芋艿:【待定 002】:1)设备维护的时候,设置位置?类似 tl?;2)设备上传的时候,设置位置,类似 it? + /** + * 定位方式 + *

+ * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum} + */ + private Integer locationType; /** * 设备位置的纬度 */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java index 7865a4424..1da5ca981 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java @@ -4,7 +4,10 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** * IoT 设备分组 DO diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java new file mode 100644 index 000000000..9f1f6a6a0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备消息数据 DO + * + * 目前使用 TDengine 存储 + * + * @author alwayssuper + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceMessageDO { + + /** + * 消息编号 + */ + private String id; + /** + * 上报时间戳 + */ + private Long reportTime; + /** + * 存储时间戳 + */ + private Long ts; + + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + /** + * 租户编号 + */ + private Long tenantId; + + /** + * 服务编号,该消息由哪个 server 发送 + */ + private String serverId; + + /** + * 是否上行消息 + * + * 由 {@link cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils#isUpstreamMessage(IotDeviceMessage)} 计算。 + * 计算并存储的目的:方便计算多少条上行、多少条下行 + */ + private Boolean upstream; + /** + * 是否回复消息 + * + * 由 {@link cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils#isReplyMessage(IotDeviceMessage)} 计算。 + * 计算并存储的目的:方便计算多少条请求、多少条回复 + */ + private Boolean reply; + /** + * 标识符 + * + * 例如说:{@link IotThingModelDO#getIdentifier()} + * 目前,只有事件上报、服务调用才有!!! + */ + private String identifier; + + // ========== codec(编解码)字段 ========== + + /** + * 请求编号 + * + * 由设备生成,对应阿里云 IoT 的 Alink 协议中的 id、华为云 IoTDA 协议的 request_id + */ + private String requestId; + /** + * 请求方法 + * + * 枚举 {@link IotDeviceMessageMethodEnum} + * 例如说:thing.property.report 属性上报 + */ + private String method; + /** + * 请求参数 + * + * 例如说:属性上报的 properties、事件上报的 params + */ + private Object params; + /** + * 响应结果 + */ + private Object data; + /** + * 响应错误码 + */ + private Integer code; + /** + * 响应提示 + */ + private String msg; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java similarity index 67% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java index fa56f6938..1e2672718 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java @@ -1,10 +1,14 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.ota; +import cn.hutool.crypto.digest.DigestAlgorithm; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** * IoT OTA 固件 DO @@ -24,14 +28,14 @@ public class IotOtaFirmwareDO extends BaseDO { /** * 固件编号 */ - @TableField + @TableId private Long id; /** * 固件名称 */ private String name; /** - * 固件版本 + * 固件描述 */ private String description; /** @@ -44,37 +48,25 @@ public class IotOtaFirmwareDO extends BaseDO { * * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} */ - // TODO @li:帮我改成 Long 哈,写错了 - private String productId; - /** - * 产品标识 - * - * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} - */ - private String productKey; + private Long productId; /** - * 签名方式 - * - * 例如说:MD5、SHA256 + * 固件文件 URL */ - private String signMethod; - /** - * 固件文件签名 - */ - private String fileSign; + private String fileUrl; /** * 固件文件大小 */ private Long fileSize; /** - * 固件文件 URL + * 固件文件签名算法 + * + * 枚举 {@link DigestAlgorithm},目前只使用 MD5 */ - private String fileUrl; - + private String fileDigestAlgorithm; /** - * 自定义信息,建议使用 JSON 格式 + * 固件文件签名结果 */ - private String information; + private String fileDigestValue; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaTaskDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaTaskDO.java new file mode 100644 index 000000000..4c9124b89 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaTaskDO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskDeviceScopeEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT OTA 升级任务 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_task", autoResultMap = true) +@KeySequence("iot_ota_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaTaskDO extends BaseDO { + + /** + * 任务编号 + */ + @TableId + private Long id; + /** + * 任务名称 + */ + private String name; + /** + * 任务描述 + */ + private String description; + + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + private Long firmwareId; + + /** + * 任务状态 + *

+ * 关联 {@link IotOtaTaskStatusEnum} + */ + private Integer status; + + /** + * 设备升级范围 + *

+ * 关联 {@link IotOtaTaskDeviceScopeEnum} + */ + private Integer deviceScope; + /** + * 设备总数数量 + */ + private Integer deviceTotalCount; + /** + * 设备成功数量 + */ + private Integer deviceSuccessCount; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaTaskRecordDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaTaskRecordDO.java new file mode 100644 index 000000000..d99a1bb60 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaTaskRecordDO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskRecordStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT OTA 升级任务记录 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_task_record", autoResultMap = true) +@KeySequence("iot_ota_task_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaTaskRecordDO extends BaseDO { + + public static final String DESCRIPTION_CANCEL_BY_TASK = "管理员手动取消升级任务(批量)"; + + public static final String DESCRIPTION_CANCEL_BY_RECORD = "管理员手动取消升级记录(单个)"; + + /** + * 升级记录编号 + */ + @TableId + private Long id; + + /** + * 固件编号 + * + * 关联 {@link IotOtaFirmwareDO#getId()} + */ + private Long firmwareId; + /** + * 任务编号 + * + * 关联 {@link IotOtaTaskDO#getId()} + */ + private Long taskId; + + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + /** + * 来源的固件编号 + * + * 关联 {@link IotDeviceDO#getFirmwareId()} + */ + private Long fromFirmwareId; + + /** + * 升级状态 + * + * 关联 {@link IotOtaTaskRecordStatusEnum} + */ + private Integer status; + /** + * 升级进度,百分比 + */ + private Integer progress; + /** + * 升级进度描述 + * + * 注意,只记录设备最后一次的升级进度描述 + * 如果想看历史记录,可以查看 {@link IotDeviceMessageDO} 设备日志 + */ + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java similarity index 90% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java index 174342afb..91715112b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java @@ -4,7 +4,10 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** * IoT 产品分类 DO diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java similarity index 77% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index 3caebbccb..d8c111a0e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java @@ -4,7 +4,10 @@ import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** * IoT 产品 DO @@ -69,30 +72,19 @@ public class IotProductDO extends TenantBaseDO { * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum} */ private Integer netType; - /** - * 接入网关协议 + * 定位方式 *

- * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotProtocolTypeEnum} + * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum} */ - private Integer protocolType; + private Integer locationType; /** - * 协议编号 + * 数据格式(编解码器类型) *

- * TODO 外键:后续加 + * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} + * + * 目的:用于 gateway-server 解析消息格式 */ - private Long protocolId; - /** - * 数据格式 - *

- * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotDataFormatEnum} - */ - private Integer dataFormat; - /** - * 数据校验级别 - *

- * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotValidateTypeEnum} - */ - private Integer validateType; + private String codecType; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataRuleDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataRuleDO.java new file mode 100644 index 000000000..191df10d0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataRuleDO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * IoT 数据流转规则 DO + * + * 监听 {@link SourceConfig} 数据源,转发到 {@link IotDataSinkDO} 数据目的 + * + * @author 芋道源码 + */ +@TableName(value = "iot_data_rule", autoResultMap = true) +@KeySequence("iot_data_rule_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDataRuleDO extends BaseDO { + + /** + * 数据流转规格编号 + */ + private Long id; + /** + * 数据流转规格名称 + */ + private String name; + /** + * 数据流转规格描述 + */ + private String description; + /** + * 数据流转规格状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 数据源配置数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List sourceConfigs; + /** + * 数据目的编号数组 + * + * 关联 {@link IotDataSinkDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List sinkIds; + + // TODO @芋艿:未来考虑使用 groovy;支持数据处理; + + /** + * 数据源配置 + */ + @Data + public static class SourceConfig { + + /** + * 消息方法 + * + * 枚举 {@link IotDeviceMessageMethodEnum} 中的 upstream 上行部分 + */ + @NotEmpty(message = "消息方法不能为空") + private String method; + + /** + * 产品编号 + * + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + * 特殊:如果为 {@link IotDeviceDO#DEVICE_ID_ALL} 时,则是全部设备 + */ + @NotEmpty(message = "设备编号不能为空") + private Long deviceId; + + /** + * 标识符 + * + * 1. 物模型时,对应:{@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataSinkDO.java similarity index 57% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataSinkDO.java index fed429872..a3cb48e3f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataSinkDO.java @@ -2,66 +2,61 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.rule; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeDirectionEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotAbstractDataSinkConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; /** - * IoT 数据桥梁 DO + * IoT 数据流转目的 DO * * @author 芋道源码 */ -@TableName(value = "iot_data_bridge", autoResultMap = true) +@TableName(value = "iot_data_sink", autoResultMap = true) @KeySequence("iot_data_bridge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Builder @NoArgsConstructor @AllArgsConstructor -public class IotDataBridgeDO extends BaseDO { +public class IotDataSinkDO extends BaseDO { /** - * 桥梁编号 + * 数据流转目的编号 */ @TableId private Long id; /** - * 桥梁名称 + * 数据流转目的名称 */ private String name; /** - * 桥梁描述 + * 数据流转目的描述 */ private String description; /** - * 桥梁状态 + * 数据流转目的状态 * - * 枚举 {@link CommonStatusEnum} + * 枚举 {@link CommonStatusEnum} */ private Integer status; - /** - * 桥梁方向 - * - * 枚举 {@link IotDataBridgeDirectionEnum} - */ - private Integer direction; /** - * 桥梁类型 + * 数据流转目的类型 * - * 枚举 {@link IotDataBridgeTypeEnum} + * 枚举 {@link IotDataSinkTypeEnum} */ private Integer type; - /** - * 桥梁配置 + * 数据流转目的配置 */ @TableField(typeHandler = JacksonTypeHandler.class) - private IotDataBridgeAbstractConfig config; + private IotAbstractDataSinkConfig config; } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotSceneRuleDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotSceneRuleDO.java new file mode 100644 index 000000000..94aa1eb5a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotSceneRuleDO.java @@ -0,0 +1,245 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * IoT 场景联动规则 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_scene_rule", autoResultMap = true) +@KeySequence("iot_scene_rule_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotSceneRuleDO extends TenantBaseDO { + + /** + * 场景联动编号 + */ + @TableId + private Long id; + /** + * 场景联动名称 + */ + private String name; + /** + * 场景联动描述 + */ + private String description; + /** + * 场景联动状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 场景定义配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List triggers; + + /** + * 场景动作配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List actions; + + /** + * 场景定义配置 + */ + @Data + public static class Trigger { + + // ========== 事件部分 ========== + + /** + * 场景事件类型 + * + * 枚举 {@link IotSceneRuleTriggerTypeEnum} + * 1. {@link IotSceneRuleTriggerTypeEnum#DEVICE_STATE_UPDATE} 时,operator 非空,并且 value 为在线状态 + * 2. {@link IotSceneRuleTriggerTypeEnum#DEVICE_PROPERTY_POST} + * {@link IotSceneRuleTriggerTypeEnum#DEVICE_EVENT_POST} 时,identifier、operator 非空,并且 value 为属性值 + * 3. {@link IotSceneRuleTriggerTypeEnum#DEVICE_EVENT_POST} + * {@link IotSceneRuleTriggerTypeEnum#DEVICE_SERVICE_INVOKE} 时,identifier 非空,但是 operator、value 为空 + * 4. {@link IotSceneRuleTriggerTypeEnum#TIMER} 时,conditions 非空,并且设备无关(无需 productId、deviceId 字段) + */ + private Integer type; + + /** + * 产品编号 + * + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + * 特殊:如果为 {@link IotDeviceDO#DEVICE_ID_ALL} 时,则是全部设备 + */ + private Long deviceId; + /** + * 物模型标识符 + * + * 对应:{@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + /** + * 操作符 + * + * 枚举 {@link IotSceneRuleConditionOperatorEnum} + */ + private String operator; + /** + * 参数(属性值、在线状态) + *

+ * 如果有多个值,则使用 "," 分隔,类似 "1,2,3"。 + * 例如说,{@link IotSceneRuleConditionOperatorEnum#IN}、{@link IotSceneRuleConditionOperatorEnum#BETWEEN} + */ + private String value; + + /** + * CRON 表达式 + */ + private String cronExpression; + + // ========== 条件部分 ========== + + /** + * 触发条件分组(状态条件分组)的数组 + *

+ * 第一层 List:分组与分组之间,是“或”的关系 + * 第二层 List:条件与条件之间,是“且”的关系 + */ + private List> conditionGroups; + + } + + /** + * 触发条件(状态条件) + */ + @Data + public static class TriggerCondition { + + /** + * 触发条件类型 + * + * 枚举 {@link IotSceneRuleConditionTypeEnum} + * 1. {@link IotSceneRuleConditionTypeEnum#DEVICE_STATE} 时,operator 非空,并且 value 为在线状态 + * 2. {@link IotSceneRuleConditionTypeEnum#DEVICE_PROPERTY} 时,identifier、operator 非空,并且 value 为属性值 + * 3. {@link IotSceneRuleConditionTypeEnum#CURRENT_TIME} 时,operator 非空(使用 DATE_TIME_ 和 TIME_ 部分),并且 value 非空 + */ + private Integer type; + + /** + * 产品编号 + * + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + /** + * 标识符(属性) + * + * 关联 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + /** + * 操作符 + * + * 枚举 {@link IotSceneRuleConditionOperatorEnum} + */ + private String operator; + /** + * 参数 + * + * 如果有多个值,则使用 "," 分隔,类似 "1,2,3"。 + * 例如说,{@link IotSceneRuleConditionOperatorEnum#IN}、{@link IotSceneRuleConditionOperatorEnum#BETWEEN} + */ + private String param; + + } + + /** + * 场景动作配置 + */ + @Data + public static class Action { + + /** + * 执行类型 + * + * 枚举 {@link IotSceneRuleActionTypeEnum} + * 1. {@link IotSceneRuleActionTypeEnum#DEVICE_PROPERTY_SET} 时,params 非空 + * {@link IotSceneRuleActionTypeEnum#DEVICE_SERVICE_INVOKE} 时,params 非空 + * 2. {@link IotSceneRuleActionTypeEnum#ALERT_TRIGGER} 时,alertConfigId 为空,因为是 {@link IotAlertConfigDO} 里面关联它 + * 3. {@link IotSceneRuleActionTypeEnum#ALERT_RECOVER} 时,alertConfigId 非空 + */ + private Integer type; + + /** + * 产品编号 + * + * 关联 {@link IotProductDO#getId()} + */ + private Long productId; + /** + * 设备编号 + * + * 关联 {@link IotDeviceDO#getId()} + */ + private Long deviceId; + + /** + * 标识符(服务) + *

+ * 关联 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + + /** + * 请求参数 + * + * 一般来说,对应 {@link IotDeviceMessage#getParams()} 请求参数 + */ + private String params; + + /** + * 告警配置编号 + * + * 关联 {@link IotAlertConfigDO#getId()} + */ + private Long alertConfigId; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java new file mode 100644 index 000000000..68a8fd699 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; + +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +/** + * IoT IotDataBridgeConfig 抽象类 + * + * 用于表示数据目的配置数据的通用类型,根据具体的 "type" 字段动态映射到对应的子类 + * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 + * + * @author HUIHUI + */ +@Data +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"), + @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"), + @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"), + @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"), + @JsonSubTypes.Type(value = IotDataSinkRabbitMQConfig.class, name = "31"), + @JsonSubTypes.Type(value = IotDataSinkKafkaConfig.class, name = "32"), +}) +public abstract class IotAbstractDataSinkConfig { + + /** + * 配置类型 + * + * 枚举 {@link IotDataSinkTypeEnum#getType()} + */ + private String type; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkHttpConfig.java similarity index 64% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkHttpConfig.java index eca35c76e..1a702b4ae 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkHttpConfig.java @@ -1,16 +1,16 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; import lombok.Data; import java.util.Map; /** - * IoT HTTP 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * IoT HTTP 配置 {@link IotAbstractDataSinkConfig} 实现类 * * @author HUIHUI */ @Data -public class IotDataBridgeHttpConfig extends IotDataBridgeAbstractConfig { +public class IotDataSinkHttpConfig extends IotAbstractDataSinkConfig { /** * 请求 URL diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkKafkaConfig.java similarity index 63% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkKafkaConfig.java index 1201214d1..1516918df 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkKafkaConfig.java @@ -1,14 +1,14 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; import lombok.Data; /** - * IoT Kafka 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * IoT Kafka 配置 {@link IotAbstractDataSinkConfig} 实现类 * * @author HUIHUI */ @Data -public class IotDataBridgeKafkaMQConfig extends IotDataBridgeAbstractConfig { +public class IotDataSinkKafkaConfig extends IotAbstractDataSinkConfig { /** * Kafka 服务器地址 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkMqttConfig.java similarity index 62% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkMqttConfig.java index 448b21501..ebc0869e1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkMqttConfig.java @@ -1,14 +1,14 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; import lombok.Data; /** - * IoT MQTT 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * IoT MQTT 配置 {@link IotAbstractDataSinkConfig} 实现类 * * @author HUIHUI */ @Data -public class IotDataBridgeMqttConfig extends IotDataBridgeAbstractConfig { +public class IotDataSinkMqttConfig extends IotAbstractDataSinkConfig { /** * MQTT 服务器地址 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRabbitMQConfig.java similarity index 71% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRabbitMQConfig.java index 2c247d1d5..0e9560384 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRabbitMQConfig.java @@ -1,14 +1,14 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; import lombok.Data; /** - * IoT RabbitMQ 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * IoT RabbitMQ 配置 {@link IotAbstractDataSinkConfig} 实现类 * * @author HUIHUI */ @Data -public class IotDataBridgeRabbitMQConfig extends IotDataBridgeAbstractConfig { +public class IotDataSinkRabbitMQConfig extends IotAbstractDataSinkConfig { /** * RabbitMQ 服务器地址 diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRedisConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRedisConfig.java new file mode 100644 index 000000000..07460ac36 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRedisConfig.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRedisDataStructureEnum; +import lombok.Data; + +/** + * IoT Redis 配置 {@link IotAbstractDataSinkConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataSinkRedisConfig extends IotAbstractDataSinkConfig { + + /** + * Redis 服务器地址 + */ + private String host; + /** + * 端口 + */ + private Integer port; + /** + * 密码 + */ + private String password; + /** + * 数据库索引 + */ + private Integer database; + + /** + * Redis 数据结构类型 + *

+ * 枚举 {@link IotRedisDataStructureEnum} + */ + @InEnum(IotRedisDataStructureEnum.class) + private Integer dataStructure; + + /** + * 主题/键名 + *

+ * 对于不同的数据结构: + * - Stream: 流的键名 + * - Hash: Hash 的键名 + * - List: 列表的键名 + * - Set: 集合的键名 + * - ZSet: 有序集合的键名 + * - String: 字符串的键名 + */ + private String topic; + + /** + * Hash 字段名(仅当 dataStructure 为 HASH 时使用) + */ + private String hashField; + + /** + * ZSet 分数字段(仅当 dataStructure 为 ZSET 时使用) + * 指定消息中哪个字段作为分数,如果不指定则使用当前时间戳 + */ + private String scoreField; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRocketMQConfig.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRocketMQConfig.java index e23e3061a..65fd3e053 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkRocketMQConfig.java @@ -1,14 +1,14 @@ -package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; import lombok.Data; /** - * IoT RocketMQ 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * IoT RocketMQ 配置 {@link IotAbstractDataSinkConfig} 实现类 * * @author HUIHUI */ @Data -public class IotDataBridgeRocketMQConfig extends IotDataBridgeAbstractConfig { +public class IotDataSinkRocketMQConfig extends IotAbstractDataSinkConfig { /** * RocketMQ 名称服务器地址 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java index e3b4a6d9a..019b8f659 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelEvent.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelEvent.java index 06cc43809..4d8537001 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelEvent.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceEventTypeEnum; @@ -9,6 +9,7 @@ import lombok.Data; import java.util.List; +// TODO @puhui999:感觉这个,是不是放到 dal 里会好点?(讨论下,先不改哈) /** * IoT 物模型中的事件 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelParam.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelParam.java index 2afad898b..3919542d5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelParam.java @@ -1,7 +1,7 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelParamDirectionEnum; import jakarta.validation.constraints.NotEmpty; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelProperty.java similarity index 91% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelProperty.java index 4b9a05a0e..2fe103a4b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelProperty.java @@ -1,7 +1,7 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum; import jakarta.validation.constraints.NotEmpty; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelService.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelService.java index c98acd824..10476956c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelService.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceCallTypeEnum; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelArrayDataSpecs.java similarity index 58% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelArrayDataSpecs.java index 50011aabf..7107f99f5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelArrayDataSpecs.java @@ -1,6 +1,10 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; @@ -16,18 +20,17 @@ import java.util.List; @JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 public class ThingModelArrayDataSpecs extends ThingModelDataSpecs { - /** - * 数组中的元素个数 - */ + @NotNull(message = "数组元素个数不能为空") private Integer size; - /** - * 数组中的元素的数据类型。可选值:struct、int、float、double 或 text - */ + + @NotEmpty(message = "数组元素的数据类型不能为空") + @Pattern(regexp = "^(struct|int|float|double|text)$", message = "数组元素的数据类型必须为:struct、int、float、double 或 text") private String childDataType; /** * 数据类型(childDataType)为列表型 struct 的数据规范存储在 dataSpecsList 中 * 此时 struct 取值范围为:int、float、double、text、date、enum、bool */ + @Valid private List dataSpecsList; } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java new file mode 100644 index 000000000..8533fcc6f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为布尔型或枚举型的 DataSpec 定义 + * + * 数据类型,取值为 bool 或 enum + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelBoolOrEnumDataSpecs extends ThingModelDataSpecs { + + @NotEmpty(message = "枚举项的名称不能为空") + @Pattern(regexp = "^[\\u4e00-\\u9fa5a-zA-Z0-9][\\u4e00-\\u9fa5a-zA-Z0-9_-]{0,19}$", + message = "枚举项的名称只能包含中文、大小写英文字母、数字、下划线和短划线,必须以中文、英文字母或数字开头,长度不超过 20 个字符") + private String name; + + @NotNull(message = "枚举值不能为空") + private Integer value; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDataSpecs.java similarity index 89% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDataSpecs.java index d9fc12dd9..1643ab2c2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDataSpecs.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -7,8 +7,8 @@ import lombok.Data; /** * IoT ThingModelDataSpecs 抽象类 * - * 用于表示物模型数据的通用类型,根据具体的 "dataType" 字段动态映射到对应的子类。 - * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 + * 用于表示物模型数据的通用类型,根据具体的 "dataType" 字段动态映射到对应的子类 + * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景 * * @author HUIHUI */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java similarity index 56% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java index 62500bc56..18ca982c1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java @@ -1,13 +1,14 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.Max; import lombok.Data; import lombok.EqualsAndHashCode; /** * IoT 物模型数据类型为时间型或文本型的 DataSpec 定义 * - * 数据类型,取值为 date 或 text。 + * 数据类型,取值为 date 或 text * * @author HUIHUI */ @@ -17,12 +18,14 @@ import lombok.EqualsAndHashCode; public class ThingModelDateOrTextDataSpecs extends ThingModelDataSpecs { /** - * 数据长度,单位为字节。取值不能超过 2048。 - * 当 dataType 为 text 时,需传入该参数。 + * 数据长度,单位为字节。取值不能超过 2048 + * + * 当 dataType 为 text 时,需传入该参数 */ + @Max(value = 2048, message = "数据长度不能超过 2048") private Integer length; /** - * 默认值,可选参数,用于存储默认值。 + * 默认值,可选参数,用于存储默认值 */ private String defaultValue; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelNumericDataSpec.java similarity index 55% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelNumericDataSpec.java index 8d0827c01..4433a9b22 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelNumericDataSpec.java @@ -1,13 +1,15 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; /** * IoT 物模型数据类型为数值的 DataSpec 定义 * - * 数据类型,取值为 int、float 或 double。 + * 数据类型,取值为 int、float 或 double * * @author HUIHUI */ @@ -17,34 +19,37 @@ import lombok.EqualsAndHashCode; public class ThingModelNumericDataSpec extends ThingModelDataSpecs { /** - * 最大值,需转为字符串类型。值必须与 dataType 类型一致。 - * 例如,当 dataType 为 int 时,取值为 "200",而不是 200。 + * 最大值,需转为字符串类型。值必须与 dataType 类型一致 */ + @NotEmpty(message = "最大值不能为空") + @Pattern(regexp = "^-?\\d+(\\.\\d+)?$", message = "最大值必须为数值类型") private String max; /** - * 最小值,需转为字符串类型。值必须与 dataType 类型一致。 - * 例如,当 dataType 为 int 时,取值为 "0",而不是 0。 + * 最小值,需转为字符串类型。值必须与 dataType 类型一致 */ + @NotEmpty(message = "最小值不能为空") + @Pattern(regexp = "^-?\\d+(\\.\\d+)?$", message = "最小值必须为数值类型") private String min; /** - * 步长,需转为字符串类型。值必须与 dataType 类型一致。 - * 例如,当 dataType 为 int 时,取值为 "10",而不是 10。 + * 步长,需转为字符串类型。值必须与 dataType 类型一致 */ + @NotEmpty(message = "步长不能为空") + @Pattern(regexp = "^-?\\d+(\\.\\d+)?$", message = "步长必须为数值类型") private String step; /** - * 精度。当 dataType 为 float 或 double 时可选传入。 + * 精度。当 dataType 为 float 或 double 时可选传入 */ private String precise; /** - * 默认值,可传入用于存储的默认值。 + * 默认值,可传入用于存储的默认值 */ private String defaultValue; /** - * 单位的符号。 + * 单位的符号 */ private String unit; /** - * 单位的名称。 + * 单位的名称 */ private String unitName; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelStructDataSpecs.java similarity index 55% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelStructDataSpecs.java index 6d483eeaa..a866a0010 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/dataType/ThingModelStructDataSpecs.java @@ -1,7 +1,11 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType; +import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; @@ -17,35 +21,36 @@ import java.util.List; @JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 public class ThingModelStructDataSpecs extends ThingModelDataSpecs { - /** - * 属性标识符 - */ + @NotEmpty(message = "属性标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "属性标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") private String identifier; - /** - * 属性名称 - */ + + @NotEmpty(message = "属性名称不能为空") private String name; - /** - * 云端可以对该属性进行的操作类型 - * - * 枚举 {@link IotThingModelAccessModeEnum} - */ + + @NotEmpty(message = "操作类型不能为空") + @InEnum(IotThingModelAccessModeEnum.class) private String accessMode; + /** * 是否是标准品类的必选服务 */ private Boolean required; - /** - * struct 数据的数据类型 - */ + + @NotEmpty(message = "数据类型不能为空") + @Pattern(regexp = "^(int|float|double|text|date|enum|bool)$", message = "数据类型必须为:int、float、double、text、date、enum、bool") private String childDataType; + /** * 数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 */ + @Valid private ThingModelDataSpecs dataSpecs; + /** * 数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 */ + @Valid private List dataSpecsList; } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertConfigMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertConfigMapper.java new file mode 100644 index 000000000..c5d7154ff --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertConfigMapper.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.alert; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 告警配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotAlertConfigMapper extends BaseMapperX { + + default PageResult selectPage(IotAlertConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotAlertConfigDO::getName, reqVO.getName()) + .eqIfPresent(IotAlertConfigDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotAlertConfigDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotAlertConfigDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotAlertConfigDO::getStatus, status); + } + + default List selectListBySceneRuleIdAndStatus(Long sceneRuleId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(IotAlertConfigDO::getStatus, status) + .apply(MyBatisUtils.findInSet("scene_rule_id", sceneRuleId))); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertRecordMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertRecordMapper.java new file mode 100644 index 000000000..f23fe60f7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertRecordMapper.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.alert; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * IoT 告警记录 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotAlertRecordMapper extends BaseMapperX { + + default PageResult selectPage(IotAlertRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotAlertRecordDO::getConfigId, reqVO.getConfigId()) + .eqIfPresent(IotAlertRecordDO::getConfigLevel, reqVO.getLevel()) + .eqIfPresent(IotAlertRecordDO::getProductId, reqVO.getProductId()) + .eqIfPresent(IotAlertRecordDO::getDeviceId, reqVO.getDeviceId()) + .eqIfPresent(IotAlertRecordDO::getProcessStatus, reqVO.getProcessStatus()) + .betweenIfPresent(IotAlertRecordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotAlertRecordDO::getId)); + } + + default List selectListBySceneRuleId(Long sceneRuleId, Long deviceId, Boolean processStatus) { + return selectList(new LambdaQueryWrapperX() + .eq(IotAlertRecordDO::getSceneRuleId, sceneRuleId) + .eqIfPresent(IotAlertRecordDO::getDeviceId, deviceId) + .eqIfPresent(IotAlertRecordDO::getProcessStatus, processStatus) + .orderByDesc(IotAlertRecordDO::getId)); + } + + default int updateList(Collection ids, IotAlertRecordDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .in(IotAlertRecordDO::getId, ids)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java similarity index 57% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java index babbf29e7..606cf8f03 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java @@ -6,13 +6,15 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jakarta.annotation.Nullable; import org.apache.ibatis.annotations.Mapper; -import javax.annotation.Nullable; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * IoT 设备 Mapper @@ -50,13 +52,11 @@ public interface IotDeviceMapper extends BaseMapperX { return selectCount(IotDeviceDO::getProductId, productId); } - default IotDeviceDO selectByDeviceKey(String deviceKey) { - return selectOne(new LambdaQueryWrapper() - .apply("LOWER(device_key) = {0}", deviceKey.toLowerCase())); - } - - default List selectListByDeviceType(Integer deviceType) { - return selectList(IotDeviceDO::getDeviceType, deviceType); + default List selectListByCondition(@Nullable Integer deviceType, + @Nullable Long productId) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotDeviceDO::getDeviceType, deviceType) + .eqIfPresent(IotDeviceDO::getProductId, productId)); } default List selectListByState(Integer state) { @@ -77,20 +77,44 @@ public interface IotDeviceMapper extends BaseMapperX { .geIfPresent(IotDeviceDO::getCreateTime, createTime)); } - /** - * 查询指定产品下各状态的设备数量 - * - * @return 设备数量统计列表 - */ - // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! - List> selectDeviceCountMapByProductId(); + default List selectByProductKeyAndDeviceNames(String productKey, Collection deviceNames) { + return selectList(new LambdaQueryWrapperX() + .eq(IotDeviceDO::getProductKey, productKey) + .in(IotDeviceDO::getDeviceName, deviceNames)); + } + + default IotDeviceDO selectBySerialNumber(String serialNumber) { + return selectOne(IotDeviceDO::getSerialNumber, serialNumber); + } + + /** + * 查询指定产品下的设备数量 + * + * @return 产品编号 -> 设备数量的映射 + */ + default Map selectDeviceCountMapByProductId() { + List> result = selectMaps(new QueryWrapper() + .select("product_id AS productId", "COUNT(1) AS deviceCount") + .groupBy("product_id")); + return result.stream().collect(Collectors.toMap( + map -> Long.valueOf(map.get("productId").toString()), + map -> Integer.valueOf(map.get("deviceCount").toString()) + )); + } - // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! /** * 查询各个状态下的设备数量 * - * @return 设备数量统计列表 + * @return 设备状态 -> 设备数量的映射 */ - List> selectDeviceCountGroupByState(); + default Map selectDeviceCountGroupByState() { + List> result = selectMaps(new QueryWrapper() + .select("state", "COUNT(1) AS deviceCount") + .groupBy("state")); + return result.stream().collect(Collectors.toMap( + map -> Integer.valueOf(map.get("state").toString()), + map -> Long.valueOf(map.get("deviceCount").toString()) + )); + } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java similarity index 54% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java index 7adf79349..fea6272d4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java @@ -7,34 +7,19 @@ import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwa import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import org.apache.ibatis.annotations.Mapper; -import java.util.List; - -// TODO @li:参考 IotOtaUpgradeRecordMapper 的写法 @Mapper public interface IotOtaFirmwareMapper extends BaseMapperX { - /** - * 根据产品ID和固件版本号查询固件信息列表。 - * - * @param productId 产品ID,用于筛选固件信息。 - * @param version 固件版本号,用于筛选固件信息。 - * @return 返回符合条件的固件信息列表。 - */ - default List selectByProductIdAndVersion(String productId, String version) { - return selectList(IotOtaFirmwareDO::getProductId, productId, + default IotOtaFirmwareDO selectByProductIdAndVersion(Long productId, String version) { + return selectOne(IotOtaFirmwareDO::getProductId, productId, IotOtaFirmwareDO::getVersion, version); } - /** - * 分页查询固件信息,支持根据名称和产品ID进行筛选,并按创建时间降序排列。 - * - * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件。 - * @return 返回分页查询结果,包含符合条件的固件信息列表。 - */ default PageResult selectPage(IotOtaFirmwarePageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() .likeIfPresent(IotOtaFirmwareDO::getName, pageReqVO.getName()) .eqIfPresent(IotOtaFirmwareDO::getProductId, pageReqVO.getProductId()) + .betweenIfPresent(IotOtaFirmwareDO::getCreateTime, pageReqVO.getCreateTime()) .orderByDesc(IotOtaFirmwareDO::getCreateTime)); } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskMapper.java new file mode 100644 index 000000000..cf7323123 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface IotOtaTaskMapper extends BaseMapperX { + + default IotOtaTaskDO selectByFirmwareIdAndName(Long firmwareId, String name) { + return selectOne(IotOtaTaskDO::getFirmwareId, firmwareId, + IotOtaTaskDO::getName, name); + } + + default PageResult selectPage(IotOtaTaskPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotOtaTaskDO::getFirmwareId, pageReqVO.getFirmwareId()) + .likeIfPresent(IotOtaTaskDO::getName, pageReqVO.getName()) + .orderByDesc(IotOtaTaskDO::getId)); + } + + default int updateByIdAndStatus(Long id, Integer whereStatus, IotOtaTaskDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .eq(IotOtaTaskDO::getId, id) + .eq(IotOtaTaskDO::getStatus, whereStatus)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskRecordMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskRecordMapper.java new file mode 100644 index 000000000..017adc919 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaTaskRecordMapper.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +@Mapper +public interface IotOtaTaskRecordMapper extends BaseMapperX { + + default List selectListByFirmwareIdAndTaskId(Long firmwareId, Long taskId) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotOtaTaskRecordDO::getFirmwareId, firmwareId) + .eqIfPresent(IotOtaTaskRecordDO::getTaskId, taskId) + .select(IotOtaTaskRecordDO::getDeviceId, IotOtaTaskRecordDO::getStatus)); + } + + default PageResult selectPage(IotOtaTaskRecordPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotOtaTaskRecordDO::getTaskId, pageReqVO.getTaskId()) + .eqIfPresent(IotOtaTaskRecordDO::getStatus, pageReqVO.getStatus())); + } + + default List selectListByTaskIdAndStatus(Long taskId, Collection statuses) { + return selectList(new LambdaQueryWrapperX() + .eq(IotOtaTaskRecordDO::getTaskId, taskId) + .in(IotOtaTaskRecordDO::getStatus, statuses)); + } + + default Long selectCountByTaskIdAndStatus(Long taskId, Collection statuses) { + return selectCount(new LambdaQueryWrapperX() + .eq(IotOtaTaskRecordDO::getTaskId, taskId) + .in(IotOtaTaskRecordDO::getStatus, statuses)); + } + + default int updateByIdAndStatus(Long id, Integer status, + IotOtaTaskRecordDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .eq(IotOtaTaskRecordDO::getId, id) + .eq(IotOtaTaskRecordDO::getStatus, status)); + } + + default int updateByIdAndStatus(Long id, Collection whereStatuses, + IotOtaTaskRecordDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .eq(IotOtaTaskRecordDO::getId, id) + .in(IotOtaTaskRecordDO::getStatus, whereStatuses)); + } + + default void updateListByIdAndStatus(Collection ids, Collection whereStatuses, + IotOtaTaskRecordDO updateObj) { + update(updateObj, new LambdaUpdateWrapper() + .in(IotOtaTaskRecordDO::getId, ids) + .in(IotOtaTaskRecordDO::getStatus, whereStatuses)); + } + + default List selectListByDeviceIdAndStatus(Set deviceIds, Set statuses) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(IotOtaTaskRecordDO::getDeviceId, deviceIds) + .inIfPresent(IotOtaTaskRecordDO::getStatus, statuses)); + } + + default List selectListByDeviceIdAndStatus(Long deviceId, Set statuses) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotOtaTaskRecordDO::getDeviceId, deviceId) + .inIfPresent(IotOtaTaskRecordDO::getStatus, statuses)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotOtaTaskRecordDO::getStatus, status); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java index 5ba4a8177..92c927b8a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java @@ -10,7 +10,6 @@ import org.apache.ibatis.annotations.Mapper; import javax.annotation.Nullable; import java.time.LocalDateTime; -import java.util.List; /** * IoT 产品 Mapper diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataRuleMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataRuleMapper.java new file mode 100644 index 000000000..7c0c17d3b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataRuleMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 数据流转规则 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDataRuleMapper extends BaseMapperX { + + default PageResult selectPage(IotDataRulePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotDataRuleDO::getName, reqVO.getName()) + .eqIfPresent(IotDataRuleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotDataRuleDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDataRuleDO::getId)); + } + + default List selectListBySinkId(Long sinkId) { + return selectList(new LambdaQueryWrapperX() + .apply(MyBatisUtils.findInSet("sink_ids", sinkId))); + } + + default List selectListByStatus(Integer status) { + return selectList(IotDataRuleDO::getStatus, status); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataSinkMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataSinkMapper.java new file mode 100644 index 000000000..e65001db8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataSinkMapper.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 数据流转目的 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface IotDataSinkMapper extends BaseMapperX { + + default PageResult selectPage(IotDataSinkPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotDataSinkDO::getName, reqVO.getName()) + .eqIfPresent(IotDataSinkDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotDataSinkDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDataSinkDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotDataSinkDO::getStatus, status); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotSceneRuleMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotSceneRuleMapper.java new file mode 100644 index 000000000..4fd6490d1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotSceneRuleMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRulePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 场景联动 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface IotSceneRuleMapper extends BaseMapperX { + + default PageResult selectPage(IotSceneRulePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotSceneRuleDO::getName, reqVO.getName()) + .likeIfPresent(IotSceneRuleDO::getDescription, reqVO.getDescription()) + .eqIfPresent(IotSceneRuleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotSceneRuleDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotSceneRuleDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotSceneRuleDO::getStatus, status); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java index 082386b4e..64529dfd0 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelP import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; import org.apache.ibatis.annotations.Mapper; -import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; /** @@ -25,8 +25,6 @@ public interface IotThingModelMapper extends BaseMapperX { .likeIfPresent(IotThingModelDO::getName, reqVO.getName()) .eqIfPresent(IotThingModelDO::getType, reqVO.getType()) .eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId()) - // TODO @芋艿:看看要不要加枚举 - .notIn(IotThingModelDO::getIdentifier, "get", "set", "post") .orderByDesc(IotThingModelDO::getId)); } @@ -36,8 +34,6 @@ public interface IotThingModelMapper extends BaseMapperX { .likeIfPresent(IotThingModelDO::getName, reqVO.getName()) .eqIfPresent(IotThingModelDO::getType, reqVO.getType()) .eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId()) - // TODO @芋艿:看看要不要加枚举 - .notIn(IotThingModelDO::getIdentifier, "get", "set", "post") .orderByDesc(IotThingModelDO::getId)); } @@ -50,8 +46,10 @@ public interface IotThingModelMapper extends BaseMapperX { return selectList(IotThingModelDO::getProductId, productId); } - default List selectListByProductKey(String productKey) { - return selectList(IotThingModelDO::getProductKey, productKey); + default List selectListByProductIdAndIdentifiers(Long productId, Collection identifiers) { + return selectList(new LambdaQueryWrapperX() + .eq(IotThingModelDO::getProductId, productId) + .in(IotThingModelDO::getIdentifier, identifiers)); } default List selectListByProductIdAndType(Long productId, Integer type) { @@ -59,30 +57,9 @@ public interface IotThingModelMapper extends BaseMapperX { IotThingModelDO::getType, type); } - default List selectListByProductIdAndIdentifiersAndTypes(Long productId, - List identifiers, - List types) { - return selectList(new LambdaQueryWrapperX() - .eq(IotThingModelDO::getProductId, productId) - .in(IotThingModelDO::getIdentifier, identifiers) - .in(IotThingModelDO::getType, types)); - } - default IotThingModelDO selectByProductIdAndName(Long productId, String name) { return selectOne(IotThingModelDO::getProductId, productId, IotThingModelDO::getName, name); } - // TODO @super:用不到,删除下; - /** - * 统计物模型数量 - * - * @param createTime 创建时间,如果为空,则统计所有物模型数量 - * @return 物模型数量 - */ - default Long selectCountByCreateTime(LocalDateTime createTime) { - return selectCount(new LambdaQueryWrapperX() - .geIfPresent(IotThingModelDO::getCreateTime, createTime)); - } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java new file mode 100644 index 000000000..1187677e5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.dal.redis; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; + +/** + * IoT Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 设备属性的数据缓存,采用 HASH 结构 + *

+ * KEY 格式:device_property:{deviceId} + * HASH KEY:identifier 属性标识 + * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} + */ + String DEVICE_PROPERTY = "iot:device_property:%d"; + + /** + * 设备的最后上报时间,采用 ZSET 结构 + * + * KEY 格式:{deviceId} + * SCORE:上报时间 + */ + String DEVICE_REPORT_TIMES = "iot:device_report_times"; + + /** + * 设备关联的网关 serverId 缓存,采用 HASH 结构 + * + * KEY 格式:device_server_id + * HASH KEY:{deviceId} + * VALUE 数据类型:String serverId + */ + String DEVICE_SERVER_ID = "iot:device_server_id"; + + /** + * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式 1:device_${deviceId} + * KEY 格式 2:device_${productKey}_${deviceName} + * VALUE 数据类型:String(JSON) + */ + String DEVICE = "iot:device"; + + /** + * 产品信息的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式:product_${productId} + * VALUE 数据类型:String(JSON) + */ + String PRODUCT = "iot:product"; + + /** + * 物模型的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式:thing_model_${productId} + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO} 列表 + */ + String THING_MODEL_LIST = "iot:thing_model_list"; + + /** + * 数据流转规则的数据缓存,使用 Spring Cache 操作 + * + * KEY 格式:data_rule_list_${deviceId}_${method}_${identifier} + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO} 列表 + */ + String DATA_RULE_LIST = "iot:data_rule_list"; + + /** + * 数据目的的数据缓存,使用 Spring Cache 操作 + * + * KEY 格式:data_sink_${id} + * VALUE 数据类型:String(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO} + */ + String DATA_SINK = "iot:data_sink"; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java similarity index 80% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java index 0f1196ab6..c8833830f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java @@ -22,8 +22,8 @@ public class DevicePropertyRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; - public Map get(String deviceKey) { - String redisKey = formatKey(deviceKey); + public Map get(Long id) { + String redisKey = formatKey(id); Map entries = stringRedisTemplate.opsForHash().entries(redisKey); if (CollUtil.isEmpty(entries)) { return Collections.emptyMap(); @@ -33,18 +33,18 @@ public class DevicePropertyRedisDAO { entry -> JsonUtils.parseObject((String) entry.getValue(), IotDevicePropertyDO.class)); } - public void putAll(String deviceKey, Map properties) { + public void putAll(Long id, Map properties) { if (CollUtil.isEmpty(properties)) { return; } - String redisKey = formatKey(deviceKey); + String redisKey = formatKey(id); stringRedisTemplate.opsForHash().putAll(redisKey, convertMap(properties.entrySet(), Map.Entry::getKey, entry -> JsonUtils.toJsonString(entry.getValue()))); } - private static String formatKey(String deviceKey) { - return String.format(DEVICE_PROPERTY, deviceKey); + private static String formatKey(Long id) { + return String.format(DEVICE_PROPERTY, id); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java similarity index 58% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java index d84af7543..0b2885583 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.Set; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + /** * 设备的最后上报时间的 Redis DAO * @@ -20,14 +22,15 @@ public class DeviceReportTimeRedisDAO { @Resource private StringRedisTemplate stringRedisTemplate; - public void update(String deviceKey, LocalDateTime reportTime) { - stringRedisTemplate.opsForZSet().add(RedisKeyConstants.DEVICE_REPORT_TIMES, deviceKey, + public void update(Long deviceId, LocalDateTime reportTime) { + stringRedisTemplate.opsForZSet().add(RedisKeyConstants.DEVICE_REPORT_TIMES, String.valueOf(deviceId), LocalDateTimeUtil.toEpochMilli(reportTime)); } - public Set range(LocalDateTime maxReportTime) { - return stringRedisTemplate.opsForZSet().rangeByScore(RedisKeyConstants.DEVICE_REPORT_TIMES, 0, - LocalDateTimeUtil.toEpochMilli(maxReportTime)); + public Set range(LocalDateTime maxReportTime) { + Set values = stringRedisTemplate.opsForZSet().rangeByScore(RedisKeyConstants.DEVICE_REPORT_TIMES, + 0, LocalDateTimeUtil.toEpochMilli(maxReportTime)); + return convertSet(values, Long::parseLong); } } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceServerIdRedisDAO.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceServerIdRedisDAO.java new file mode 100644 index 000000000..cef78f3cf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceServerIdRedisDAO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.dal.redis.device; + +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +/** + * 设备关联的网关 serverId 的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class DeviceServerIdRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public void update(Long deviceId, String serverId) { + stringRedisTemplate.opsForHash().put(RedisKeyConstants.DEVICE_SERVER_ID, + String.valueOf(deviceId), serverId); + } + + public String get(Long deviceId) { + Object value = stringRedisTemplate.opsForHash().get(RedisKeyConstants.DEVICE_SERVER_ID, + String.valueOf(deviceId)); + return value != null ? (String) value : null; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceMessageMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceMessageMapper.java new file mode 100644 index 000000000..b09895fd3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceMessageMapper.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.dal.tdengine; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 设备消息 {@link IotDeviceMessageDO} Mapper 接口 + */ +@Mapper +@TDengineDS +@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 +public interface IotDeviceMessageMapper { + + /** + * 创建设备消息超级表 + */ + void createSTable(); + + /** + * 查询设备消息表是否存在 + * + * @return 存在则返回表名;不存在则返回 null + */ + String showSTable(); + + /** + * 插入设备消息数据 + * + * 如果子表不存在,会自动创建子表 + * + * @param message 设备消息数据 + */ + void insert(IotDeviceMessageDO message); + + /** + * 获得设备消息分页 + * + * @param reqVO 分页查询条件 + * @return 设备消息列表 + */ + IPage selectPage(IPage page, + @Param("reqVO") IotDeviceMessagePageReqVO reqVO); + + /** + * 统计设备消息数量 + * + * @param createTime 创建时间,如果为空,则统计所有消息数量 + * @return 消息数量 + */ + Long selectCountByCreateTime(@Param("createTime") Long createTime); + + /** + * 按照 requestIds 批量查询消息 + * + * @param deviceId 设备编号 + * @param requestIds 请求编号集合 + * @param reply 是否回复消息 + * @return 消息列表 + */ + List selectListByRequestIdsAndReply(@Param("deviceId") Long deviceId, + @Param("requestIds") Collection requestIds, + @Param("reply") Boolean reply); + + /** + * 按照时间范围(小时),统计设备的消息数量 + */ + List> selectDeviceMessageCountGroupByDate(@Param("startTime") Long startTime, + @Param("endTime") Long endTime); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java similarity index 76% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java index 37a72e4b0..a43dcd765 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java @@ -3,13 +3,12 @@ package cn.iocoder.yudao.module.iot.dal.tdengine; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; import com.baomidou.mybatisplus.annotation.InterceptorIgnore; -import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -23,17 +22,17 @@ import java.util.stream.Collectors; @InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 public interface IotDevicePropertyMapper { - List getProductPropertySTableFieldList(@Param("productKey") String productKey); + List getProductPropertySTableFieldList(@Param("productId") Long productId); - void createProductPropertySTable(@Param("productKey") String productKey, + void createProductPropertySTable(@Param("productId") Long productId, @Param("fields") List fields); @SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性 - default void alterProductPropertySTable(String productKey, + default void alterProductPropertySTable(Long productId, List oldFields, List newFields) { oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(), - TDengineTableField.FIELD_TS, "report_time", "device_key")); + TDengineTableField.FIELD_TS, "report_time", "device_id")); List addFields = newFields.stream().filter( // 新增的字段 newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField()))) .collect(Collectors.toList()); @@ -62,29 +61,28 @@ public interface IotDevicePropertyMapper { }); // 执行 - addFields.forEach(field -> alterProductPropertySTableAddField(productKey, field)); - dropFields.forEach(field -> alterProductPropertySTableDropField(productKey, field)); - modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productKey, field)); + addFields.forEach(field -> alterProductPropertySTableAddField(productId, field)); + dropFields.forEach(field -> alterProductPropertySTableDropField(productId, field)); + modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productId, field)); modifyTypeFields.forEach(field -> { - alterProductPropertySTableDropField(productKey, field); - alterProductPropertySTableAddField(productKey, field); + alterProductPropertySTableDropField(productId, field); + alterProductPropertySTableAddField(productId, field); }); } - void alterProductPropertySTableAddField(@Param("productKey") String productKey, + void alterProductPropertySTableAddField(@Param("productId") Long productId, @Param("field") TDengineTableField field); - void alterProductPropertySTableModifyField(@Param("productKey") String productKey, + void alterProductPropertySTableModifyField(@Param("productId") Long productId, @Param("field") TDengineTableField field); - void alterProductPropertySTableDropField(@Param("productKey") String productKey, + void alterProductPropertySTableDropField(@Param("productId") Long productId, @Param("field") TDengineTableField field); void insert(@Param("device") IotDeviceDO device, @Param("properties") Map properties, @Param("reportTime") Long reportTime); - IPage selectPageByHistory(IPage page, - @Param("reqVO") IotDevicePropertyHistoryPageReqVO reqVO); + List selectListByHistory(@Param("reqVO") IotDevicePropertyHistoryListReqVO reqVO); } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/iot/config/YudaoIotProperties.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/iot/config/YudaoIotProperties.java new file mode 100644 index 000000000..07473c029 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/iot/config/YudaoIotProperties.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.framework.iot.config; + +import lombok.Data; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +/** + * 芋道 IoT 全局配置类 + * + * @author 芋道源码 + */ +@Component +@Data +public class YudaoIotProperties { + + /** + * 设备连接超时时间 + */ + private Duration keepAliveTime = Duration.ofMinutes(10); + /** + * 设备连接超时时间的因子 + * + * 因为设备可能会有网络抖动,所以需要乘以一个因子,避免误判 + */ + private double keepAliveFactor = 1.5D; + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/iot/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/iot/package-info.java new file mode 100644 index 000000000..0930a1409 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/iot/package-info.java @@ -0,0 +1,4 @@ +/** + * iot 模块的【全局】拓展封装 + */ +package cn.iocoder.yudao.module.iot.framework.iot; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/config/RpcConfiguration.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/config/RpcConfiguration.java similarity index 52% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/config/RpcConfiguration.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/config/RpcConfiguration.java index d56174474..8f7c5f475 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/config/RpcConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/config/RpcConfiguration.java @@ -1,10 +1,15 @@ package cn.iocoder.yudao.module.iot.framework.rpc.config; +import cn.iocoder.yudao.module.system.api.mail.MailSendApi; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @Configuration(value = "iotRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients(clients = AdminUserApi.class) +@EnableFeignClients(clients = { + AdminUserApi.class, SmsSendApi.class, MailSendApi.class, NotifyMessageSendApi.class +}) public class RpcConfiguration { } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/rpc/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java similarity index 88% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java index 971fc2a41..a301cf407 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java @@ -25,10 +25,10 @@ public class SecurityConfiguration { .requestMatchers("/swagger-ui").permitAll() .requestMatchers("/swagger-ui/**").permitAll(); // Spring Boot Actuator 的安全配置 - registry.requestMatchers("/actuator").permitAll() - .requestMatchers("/actuator/**").permitAll(); + registry.requestMatchers("/actuator").anonymous() + .requestMatchers("/actuator/**").anonymous(); // Druid 监控 - registry.requestMatchers("/druid/**").permitAll(); + registry.requestMatchers("/druid/**").anonymous(); // RPC 服务的安全配置 registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java similarity index 65% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java index 3517e1e58..1de2dcdd3 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.framework.tdengine.config; -import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -17,16 +17,16 @@ import org.springframework.stereotype.Component; @Slf4j public class TDengineTableInitRunner implements ApplicationRunner { - private final IotDeviceLogService deviceLogService; + private final IotDeviceMessageService deviceMessageService; @Override public void run(ApplicationArguments args) { try { - // 初始化设备日志表 - deviceLogService.defineDeviceLog(); + // 初始化设备消息表 + deviceMessageService.defineDeviceMessageStable(); } catch (Exception ex) { - // 初始化失败时打印错误日志并退出系统 - log.error("[run][TDengine初始化设备日志表结构失败,系统无法正常运行,即将退出]", ex); + // 初始化失败时打印错误消息并退出系统 + log.error("[run][TDengine初始化设备消息表结构失败,系统无法正常运行,即将退出]", ex); System.exit(1); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java similarity index 88% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java index e3bbdd204..48c3142ec 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java @@ -23,8 +23,14 @@ public class TDengineTableField { public static final String TYPE_DOUBLE = "DOUBLE"; public static final String TYPE_BOOL = "BOOL"; public static final String TYPE_NCHAR = "NCHAR"; + public static final String TYPE_VARCHAR = "VARCHAR"; public static final String TYPE_TIMESTAMP = "TIMESTAMP"; + /** + * 字段长度 - VARCHAR 默认长度 + */ + public static final int LENGTH_VARCHAR = 1024; + /** * 注释 - TAG 字段 */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/web/config/IotWebConfiguration.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/web/config/IotWebConfiguration.java new file mode 100644 index 000000000..6b3cc6ae5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/web/config/IotWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * iot 模块的 web 组件的 Configuration + * + * @author ahh + */ +@Configuration(proxyBeanMethods = false) +public class IotWebConfiguration { + + /** + * iot 模块的 API 分组 + */ + @Bean + public GroupedOpenApi iotGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("iot"); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/web/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/web/package-info.java new file mode 100644 index 000000000..aafcca274 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * iot 模块的 web 拓展封装 + */ +package cn.iocoder.yudao.module.iot.framework.web; diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java new file mode 100644 index 000000000..f5620c0eb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.iot.job.device; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * IoT 设备离线检查 Job + * + * 检测逻辑:设备最后一条 {@link IotDeviceMessage} 消息超过一定时间,则认为设备离线 + * + * @see 阿里云 IoT —— 设备离线分析 + * @author 芋道源码 + */ +@Component +public class IotDeviceOfflineCheckJob { + + @Resource + private YudaoIotProperties iotProperties; + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotDeviceMessageService deviceMessageService; + + @XxlJob("deviceOfflineCheckJob") + @TenantJob // 多租户 + public String execute(String param) { + // 1.1 获得在线设备列表 + List devices = deviceService.getDeviceListByState(IotDeviceStateEnum.ONLINE.getState()); + if (CollUtil.isEmpty(devices)) { + return JsonUtils.toJsonString(Collections.emptyList()); + } + // 1.2 获取超时的设备集合 + Set timeoutDeviceIds = devicePropertyService.getDeviceIdListByReportTime(getTimeoutTime()); + + // 2. 下线设备 + List offlineDevices = CollUtil.newArrayList(); + for (IotDeviceDO device : devices) { + if (!timeoutDeviceIds.contains(device.getId())) { + continue; + } + offlineDevices.add(new String[]{device.getProductKey(), device.getDeviceName()}); + // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 + deviceMessageService.sendDeviceMessage(IotDeviceMessage.buildStateOffline().setDeviceId(device.getId())); + } + return JsonUtils.toJsonString(offlineDevices); + } + + private LocalDateTime getTimeoutTime() { + return LocalDateTime.now().minus(Duration.ofNanos( + (long) (iotProperties.getKeepAliveTime().toNanos() * iotProperties.getKeepAliveFactor()))); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java new file mode 100644 index 000000000..f632aa0ed --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.job.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskRecordStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * IoT OTA 升级推送 Job:查询待推送的 OTA 升级记录,并推送给设备 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotOtaUpgradeJob { + + @Resource + private IotOtaTaskRecordService otaTaskRecordService; + @Resource + private IotOtaFirmwareService otaFirmwareService; + @Resource + private IotDeviceService deviceService; + + @XxlJob("deviceOfflineCheckJob") + @TenantJob // 多租户 + public String execute(String param) throws Exception { + // 1. 查询待推送的 OTA 升级记录 + List records = otaTaskRecordService.getOtaRecordListByStatus( + IotOtaTaskRecordStatusEnum.PENDING.getStatus()); + if (CollUtil.isEmpty(records)) { + return null; + } + + // TODO 芋艿:可以优化成批量获取 原因是:1. N+1 问题;2. offline 的设备无需查询 + // 2. 遍历推送记录 + int successCount = 0; + int failureCount = 0; + Map otaFirmwares = new HashMap<>(); + for (IotOtaTaskRecordDO record : records) { + try { + // 2.1 设备如果不在线,直接跳过 + IotDeviceDO device = deviceService.getDeviceFromCache(record.getDeviceId()); + // TODO 芋艿:【优化】当前逻辑跳过了离线的设备,但未充分利用 MQTT 的离线消息能力。 + // 1. MQTT 协议本身支持持久化会话(Clean Session=false)和 QoS > 0 的消息,允许 broker 为离线设备缓存消息。 + // 2. 对于 OTA 升级这类非实时性强的任务,即使设备当前离线,也应该可以推送升级指令。设备在下次上线时即可收到。 + // 3. 后续可以考虑:增加一个“允许离线推送”的选项。如果开启,即使设备状态为 OFFLINE,也应尝试推送消息,依赖 MQTT Broker 的能力进行离线缓存。 + if (device == null || IotDeviceStateEnum.isNotOnline(device.getState())) { + continue; + } + // 2.2 获取 OTA 固件信息 + IotOtaFirmwareDO fireware = otaFirmwares.get(record.getFirmwareId()); + if (fireware == null) { + fireware = otaFirmwareService.getOtaFirmware(record.getFirmwareId()); + otaFirmwares.put(record.getFirmwareId(), fireware); + } + // 2.3 推送 OTA 升级任务 + boolean result = otaTaskRecordService.pushOtaTaskRecord(record, fireware, device); + if (result) { + successCount++; + } else { + failureCount++; + } + } catch (Exception e) { + failureCount++; + log.error("[execute][推送 OTA 升级任务({})发生异常]", record.getId(), e); + } + } + return StrUtil.format("升级任务推送成功:{} 条,送失败:{} 条", successCount, failureCount); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotSceneRuleJob.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotSceneRuleJob.java new file mode 100644 index 000000000..9967ccc3b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotSceneRuleJob.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.job.rule; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import java.util.Map; + +/** + * IoT 规则场景 Job,用于执行 {@link IotSceneRuleTriggerTypeEnum#TIMER} 类型的规则场景 + * + * @author 芋道源码 + */ +@Slf4j +public class IotSceneRuleJob extends QuartzJobBean { + + /** + * JobData Key - 规则场景编号 + */ + public static final String JOB_DATA_KEY_RULE_SCENE_ID = "sceneRuleId"; + + @Resource + private IotSceneRuleService sceneRuleService; + + @Override + protected void executeInternal(JobExecutionContext context) { + // 获得规则场景编号 + Long sceneRuleId = context.getMergedJobDataMap().getLong(JOB_DATA_KEY_RULE_SCENE_ID); + + // 执行规则场景 + sceneRuleService.executeSceneRuleByTimer(sceneRuleId); + } + + /** + * 创建 JobData Map + * + * @param sceneRuleId 规则场景编号 + * @return JobData Map + */ + public static Map buildJobDataMap(Long sceneRuleId) { + return MapUtil.of(JOB_DATA_KEY_RULE_SCENE_ID, sceneRuleId); + } + + /** + * 创建 Job 名字 + * + * @param sceneRuleId 规则场景编号 + * @return Job 名字 + */ + public static String buildJobName(Long sceneRuleId) { + return String.format("%s_%d", IotSceneRuleJob.class.getSimpleName(), sceneRuleId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java new file mode 100644 index 000000000..7e039d032 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * 针对 {@link IotDeviceMessage} 的业务处理器:调用 method 对应的逻辑。例如说: + * 1. {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 属性上报时,记录设备属性 + * + * @author alwayssuper + */ +@Component +@Slf4j +public class IotDeviceMessageSubscriber implements IotMessageSubscriber { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotDeviceMessageService deviceMessageService; + + @Resource + private IotMessageBus messageBus; + + @PostConstruct + public void init() { + messageBus.register(this); + } + + @Override + public String getTopic() { + return IotDeviceMessage.MESSAGE_BUS_DEVICE_MESSAGE_TOPIC; + } + + @Override + public String getGroup() { + return "iot_device_message_consumer"; + } + + @Override + public void onMessage(IotDeviceMessage message) { + if (!IotDeviceMessageUtils.isUpstreamMessage(message)) { + log.error("[onMessage][message({}) 非上行消息,不进行处理]", message); + return; + } + + TenantUtils.execute(message.getTenantId(), () -> { + // 1.1 更新设备的最后时间 + IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId()); + devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now()); + // 1.2 更新设备的连接 server + // TODO 芋艿:HTTP 网关的上行消息,不应该更新 serverId,会覆盖掉 MQTT 等长连接的 serverId,导致下行消息无法发送。 + devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId()); + + // 2. 未上线的设备,强制上线 + forceDeviceOnline(message, device); + + // 3. 核心:处理消息 + deviceMessageService.handleUpstreamDeviceMessage(message, device); + }); + } + + private void forceDeviceOnline(IotDeviceMessage message, IotDeviceDO device) { + // 已经在线,无需处理 + if (ObjectUtil.equal(device.getState(), IotDeviceStateEnum.ONLINE.getState())) { + return; + } + // 如果是 STATE 相关的消息,无需处理,不然就重复处理状态了 + if (Objects.equals(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { + return; + } + + // 特殊:设备非在线时,主动标记设备为在线 + // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志、规则引擎等等 + try { + deviceMessageService.sendDeviceMessage(IotDeviceMessage.buildStateUpdateOnline().setDeviceId(device.getId())); + } catch (Exception e) { + // 注意:即使执行失败,也不影响主流程 + log.error("[forceDeviceOnline][message({}) device({}) 强制设备上线失败]", message, device, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java new file mode 100644 index 000000000..843592a27 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.rule; + +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.data.IotDataRuleService; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,处理数据流转 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotDataRuleMessageHandler implements IotMessageSubscriber { + + @Resource + private IotDataRuleService dataRuleService; + + @Resource + private IotMessageBus messageBus; + + @PostConstruct + public void init() { + messageBus.register(this); + } + + @Override + public String getTopic() { + return IotDeviceMessage.MESSAGE_BUS_DEVICE_MESSAGE_TOPIC; + } + + @Override + public String getGroup() { + return "iot_data_rule_consumer"; + } + + @Override + public void onMessage(IotDeviceMessage message) { + TenantUtils.execute(message.getTenantId(), () -> dataRuleService.executeDataRule(message)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java new file mode 100644 index 000000000..c39cefe4a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.rule; + +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleService; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +// TODO @puhui999:后面重构哈 +/** + * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotSceneRuleMessageHandler implements IotMessageSubscriber { + + @Resource + private IotSceneRuleService sceneRuleService; + + @Resource + private IotMessageBus messageBus; + + @PostConstruct + public void init() { + messageBus.register(this); + } + + @Override + public String getTopic() { + return IotDeviceMessage.MESSAGE_BUS_DEVICE_MESSAGE_TOPIC; + } + + @Override + public String getGroup() { + return "iot_rule_consumer"; + } + + @Override + public void onMessage(IotDeviceMessage message) { + if (true) { + return; + } + log.info("[onMessage][消息内容({})]", message); + sceneRuleService.executeSceneRuleByDevice(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java new file mode 100644 index 000000000..a52025050 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的生产者 + */ +package cn.iocoder.yudao.module.iot.mq.producer; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/package-info.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/package-info.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/package-info.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/package-info.java diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigService.java new file mode 100644 index 000000000..d58d42789 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigService.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.service.alert; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 告警配置 Service 接口 + * + * @author 芋道源码 + */ +public interface IotAlertConfigService { + + /** + * 创建告警配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createAlertConfig(@Valid IotAlertConfigSaveReqVO createReqVO); + + /** + * 更新告警配置 + * + * @param updateReqVO 更新信息 + */ + void updateAlertConfig(@Valid IotAlertConfigSaveReqVO updateReqVO); + + /** + * 删除告警配置 + * + * @param id 编号 + */ + void deleteAlertConfig(Long id); + + /** + * 获得告警配置 + * + * @param id 编号 + * @return 告警配置 + */ + IotAlertConfigDO getAlertConfig(Long id); + + /** + * 获得告警配置分页 + * + * @param pageReqVO 分页查询 + * @return 告警配置分页 + */ + PageResult getAlertConfigPage(IotAlertConfigPageReqVO pageReqVO); + + /** + * 获得告警配置列表 + * + * @param status 状态 + * @return 告警配置列表 + */ + List getAlertConfigListByStatus(Integer status); + + /** + * 获得告警配置列表 + * + * @param sceneRuleId 场景流动规则编号 + * @return 告警配置列表 + */ + List getAlertConfigListBySceneRuleIdAndStatus(Long sceneRuleId, Integer status); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java new file mode 100644 index 000000000..aa9378767 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertConfigServiceImpl.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.iot.service.alert; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.config.IotAlertConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.mysql.alert.IotAlertConfigMapper; +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleService; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.ALERT_CONFIG_NOT_EXISTS; + +/** + * IoT 告警配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotAlertConfigServiceImpl implements IotAlertConfigService { + + @Resource + private IotAlertConfigMapper alertConfigMapper; + + @Resource + @Lazy // 延迟,避免循环依赖报错 + private IotSceneRuleService sceneRuleService; + + @Resource + private AdminUserApi adminUserApi; + + @Override + public Long createAlertConfig(IotAlertConfigSaveReqVO createReqVO) { + // 校验关联数据是否存在 + sceneRuleService.validateSceneRuleList(createReqVO.getSceneRuleIds()); + adminUserApi.validateUserList(createReqVO.getReceiveUserIds()); + + // 插入 + IotAlertConfigDO alertConfig = BeanUtils.toBean(createReqVO, IotAlertConfigDO.class); + alertConfigMapper.insert(alertConfig); + return alertConfig.getId(); + } + + @Override + public void updateAlertConfig(IotAlertConfigSaveReqVO updateReqVO) { + // 校验存在 + validateAlertConfigExists(updateReqVO.getId()); + // 校验关联数据是否存在 + sceneRuleService.validateSceneRuleList(updateReqVO.getSceneRuleIds()); + adminUserApi.validateUserList(updateReqVO.getReceiveUserIds()); + + // 更新 + IotAlertConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotAlertConfigDO.class); + alertConfigMapper.updateById(updateObj); + } + + @Override + public void deleteAlertConfig(Long id) { + // 校验存在 + validateAlertConfigExists(id); + // 删除 + alertConfigMapper.deleteById(id); + } + + private void validateAlertConfigExists(Long id) { + if (alertConfigMapper.selectById(id) == null) { + throw exception(ALERT_CONFIG_NOT_EXISTS); + } + } + + @Override + public IotAlertConfigDO getAlertConfig(Long id) { + return alertConfigMapper.selectById(id); + } + + @Override + public PageResult getAlertConfigPage(IotAlertConfigPageReqVO pageReqVO) { + return alertConfigMapper.selectPage(pageReqVO); + } + + @Override + public List getAlertConfigListByStatus(Integer status) { + return alertConfigMapper.selectListByStatus(status); + } + + @Override + public List getAlertConfigListBySceneRuleIdAndStatus(Long sceneRuleId, Integer status) { + return alertConfigMapper.selectListBySceneRuleIdAndStatus(sceneRuleId, status); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java new file mode 100644 index 000000000..68a2da97c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordService.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.iot.service.alert; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordPageReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; +import jakarta.validation.constraints.NotNull; + +import java.util.Collection; +import java.util.List; + +/** + * IoT 告警记录 Service 接口 + * + * @author 芋道源码 + */ +public interface IotAlertRecordService { + + /** + * 获得告警记录 + * + * @param id 编号 + * @return 告警记录 + */ + IotAlertRecordDO getAlertRecord(Long id); + + /** + * 获得告警记录分页 + * + * @param pageReqVO 分页查询 + * @return 告警记录分页 + */ + PageResult getAlertRecordPage(IotAlertRecordPageReqVO pageReqVO); + + /** + * 获得指定场景规则的告警记录列表 + * + * @param sceneRuleId 场景规则编号 + * @param deviceId 设备编号 + * @param processStatus 处理状态,允许空 + * @return 告警记录列表 + */ + List getAlertRecordListBySceneRuleId(@NotNull(message = "场景规则编号不能为空") Long sceneRuleId, + Long deviceId, Boolean processStatus); + + /** + * 处理告警记录 + * + * @param ids 告警记录编号 + * @param remark 处理结果(备注) + */ + void processAlertRecordList(Collection ids, String remark); + + /** + * 创建告警记录(包含场景规则编号) + * + * @param config 告警配置 + * @param sceneRuleId 场景规则编号 + * @param deviceMessage 设备消息,可为空 + * @return 告警记录编号 + */ + Long createAlertRecord(IotAlertConfigDO config, Long sceneRuleId, IotDeviceMessage deviceMessage); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java new file mode 100644 index 000000000..34a673a4b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/alert/IotAlertRecordServiceImpl.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.service.alert; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.alert.vo.recrod.IotAlertRecordPageReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.mysql.alert.IotAlertRecordMapper; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +/** + * IoT 告警记录 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotAlertRecordServiceImpl implements IotAlertRecordService { + + @Resource + private IotAlertRecordMapper alertRecordMapper; + + @Resource + private IotDeviceService deviceService; + + @Override + public IotAlertRecordDO getAlertRecord(Long id) { + return alertRecordMapper.selectById(id); + } + + @Override + public PageResult getAlertRecordPage(IotAlertRecordPageReqVO pageReqVO) { + return alertRecordMapper.selectPage(pageReqVO); + } + + @Override + public List getAlertRecordListBySceneRuleId(Long sceneRuleId, Long deviceId, Boolean processStatus) { + return alertRecordMapper.selectListBySceneRuleId(sceneRuleId, deviceId, processStatus); + } + + @Override + public void processAlertRecordList(Collection ids, String processRemark) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 批量更新告警记录的处理状态 + alertRecordMapper.updateList(ids, IotAlertRecordDO.builder() + .processStatus(true).processRemark(processRemark).build()); + } + + @Override + public Long createAlertRecord(IotAlertConfigDO config, Long sceneRuleId, IotDeviceMessage message) { + // 构建告警记录 + IotAlertRecordDO.IotAlertRecordDOBuilder builder = IotAlertRecordDO.builder() + .configId(config.getId()).configName(config.getName()).configLevel(config.getLevel()) + .sceneRuleId(sceneRuleId).processStatus(false); + if (message != null) { + builder.deviceMessage(message); + // 填充设备信息 + IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); + if (device != null) { + builder.productId(device.getProductId()).deviceId(device.getId()); + } + } + + // 插入记录 + IotAlertRecordDO record = builder.build(); + alertRecordMapper.insert(record); + return record.getId(); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java similarity index 62% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 1dda3f333..6db097d2d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotEmpty; import javax.annotation.Nullable; import java.time.LocalDateTime; @@ -13,6 +13,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + /** * IoT 设备 Service 接口 * @@ -28,18 +30,6 @@ public interface IotDeviceService { */ Long createDevice(@Valid IotDeviceSaveReqVO createReqVO); - /** - * 【设备注册】创建设备 - * - * @param productKey 产品标识 - * @param deviceName 设备名称 - * @param gatewayId 网关设备 ID - * @return 设备 - */ - IotDeviceDO createDevice(@NotEmpty(message = "产品标识不能为空") String productKey, - @NotEmpty(message = "设备名称不能为空") String deviceName, - Long gatewayId); - /** * 更新设备 * @@ -59,6 +49,14 @@ public interface IotDeviceService { updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId)); } + /** + * 更新设备状态 + * + * @param device 设备信息 + * @param state 状态 + */ + void updateDeviceState(IotDeviceDO device, Integer state); + /** * 更新设备状态 * @@ -96,6 +94,14 @@ public interface IotDeviceService { */ IotDeviceDO validateDeviceExists(Long id); + /** + * 【缓存】校验设备是否存在 + * + * @param id 设备 ID + * @return 设备对象 + */ + IotDeviceDO validateDeviceExistsFromCache(Long id); + /** * 获得设备 * @@ -105,12 +111,25 @@ public interface IotDeviceService { IotDeviceDO getDevice(Long id); /** - * 根据设备 key 获得设备 + * 【缓存】获得设备信息 + *

+ * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! * - * @param deviceKey 编号 + * @param id 编号 * @return IoT 设备 */ - IotDeviceDO getDeviceByDeviceKey(String deviceKey); + IotDeviceDO getDeviceFromCache(Long id); + + /** + * 【缓存】根据产品 key 和设备名称,获得设备信息 + *

+ * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! + * + * @param productKey 产品 key + * @param deviceName 设备名称 + * @return 设备信息 + */ + IotDeviceDO getDeviceFromCache(String productKey, String deviceName); /** * 获得设备分页 @@ -121,12 +140,14 @@ public interface IotDeviceService { PageResult getDevicePage(IotDevicePageReqVO pageReqVO); /** - * 基于设备类型,获得设备列表 + * 根据条件,获得设备列表 * * @param deviceType 设备类型 + * @param productId 产品编号 * @return 设备列表 */ - List getDeviceListByDeviceType(@Nullable Integer deviceType); + List getDeviceListByCondition(@Nullable Integer deviceType, + @Nullable Long productId); /** * 获得状态,获得设备列表 @@ -137,21 +158,13 @@ public interface IotDeviceService { List getDeviceListByState(Integer state); /** - * 根据产品ID获取设备列表 + * 根据产品编号,获取设备列表 * - * @param productId 产品ID,用于查询特定产品的设备列表 - * @return 返回与指定产品ID关联的设备列表,列表中的每个元素为IotDeviceDO对象 + * @param productId 产品编号 + * @return 设备列表 */ List getDeviceListByProductId(Long productId); - /** - * 根据设备ID列表获取设备信息列表 - * - * @param deviceIdList 设备ID列表,包含需要查询的设备ID - * @return 返回与设备ID列表对应的设备信息列表,列表中的每个元素为IotDeviceDO对象 - */ - List getDeviceListByIdList(List deviceIdList); - /** * 基于产品编号,获得设备数量 * @@ -168,17 +181,6 @@ public interface IotDeviceService { */ Long getDeviceCountByGroupId(Long groupId); - /** - * 【缓存】根据产品 key 和设备名称,获得设备信息 - *

- * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! - * - * @param productKey 产品 key - * @param deviceName 设备名称 - * @return 设备信息 - */ - IotDeviceDO getDeviceByProductKeyAndDeviceNameFromCache(String productKey, String deviceName); - /** * 导入设备 * @@ -197,12 +199,12 @@ public interface IotDeviceService { Long getDeviceCount(@Nullable LocalDateTime createTime); /** - * 获取 MQTT 连接参数 + * 获得设备认证信息 * - * @param deviceId 设备 ID + * @param id 设备编号 * @return MQTT 连接参数 */ - IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId); + IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id); /** * 获得各个产品下的设备数量 Map @@ -219,4 +221,54 @@ public interface IotDeviceService { */ Map getDeviceCountMapByState(); -} \ No newline at end of file + /** + * 通过产品标识和设备名称列表获取设备列表 + * + * @param productKey 产品标识 + * @param deviceNames 设备名称列表 + * @return 设备列表 + */ + List getDeviceListByProductKeyAndNames(String productKey, List deviceNames); + + /** + * 认证设备 + * + * @param authReqDTO 认证信息 + * @return 是否认证成功 + */ + boolean authDevice(@Valid IotDeviceAuthReqDTO authReqDTO); + + /** + * 校验设备是否存在 + * + * @param ids 设备编号数组 + */ + List validateDeviceListExists(Collection ids); + + /** + * 获得设备列表 + * + * @param ids 设备编号数组 + * @return 设备列表 + */ + List getDeviceList(Collection ids); + + /** + * 获得设备 Map + * + * @param ids 设备编号数组 + * @return 设备 Map + */ + default Map getDeviceMap(Collection ids) { + return convertMap(getDeviceList(ids), IotDeviceDO::getId); + } + + /** + * 更新设备固件版本 + * + * @param deviceId 设备编号 + * @param firmwareId 固件编号 + */ + void updateDeviceFirmware(Long deviceId, Long firmwareId); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index 989f10a09..56f381853 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; @@ -12,21 +12,22 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import cn.iocoder.yudao.module.iot.service.product.IotProductService; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,7 +36,6 @@ import org.springframework.validation.annotation.Validated; import javax.annotation.Nullable; import java.time.LocalDateTime; import java.util.*; -import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @@ -55,6 +55,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { private IotDeviceMapper deviceMapper; @Resource + @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; @Resource @Lazy // 延迟加载,解决循环依赖 @@ -68,10 +69,12 @@ public class IotDeviceServiceImpl implements IotDeviceService { throw exception(PRODUCT_NOT_EXISTS); } // 1.2 统一校验 - validateCreateDeviceParam(product.getProductKey(), createReqVO.getDeviceName(), createReqVO.getDeviceKey(), + validateCreateDeviceParam(product.getProductKey(), createReqVO.getDeviceName(), createReqVO.getGatewayId(), product); // 1.3 校验分组存在 deviceGroupService.validateDeviceGroupExists(createReqVO.getGroupIds()); + // 1.4 校验设备序列号全局唯一 + validateSerialNumberUnique(createReqVO.getSerialNumber(), null); // 2. 插入到数据库 IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); @@ -80,40 +83,14 @@ public class IotDeviceServiceImpl implements IotDeviceService { return device.getId(); } - @Override - public IotDeviceDO createDevice(String productKey, String deviceName, Long gatewayId) { - String deviceKey = generateDeviceKey(); - // 1.1 校验产品是否存在 - IotProductDO product = TenantUtils.executeIgnore(() -> productService.getProductByProductKey(productKey)); - if (product == null) { - throw exception(PRODUCT_NOT_EXISTS); - } - return TenantUtils.execute(product.getTenantId(), () -> { - // 1.2 校验设备名称在同一产品下是否唯一 - validateCreateDeviceParam(productKey, deviceName, deviceKey, gatewayId, product); - - // 2. 插入到数据库 - IotDeviceDO device = new IotDeviceDO().setDeviceName(deviceName).setDeviceKey(deviceKey) - .setGatewayId(gatewayId); - initDevice(device, product); - deviceMapper.insert(device); - return device; - }); - } - - private void validateCreateDeviceParam(String productKey, String deviceName, String deviceKey, + private void validateCreateDeviceParam(String productKey, String deviceName, Long gatewayId, IotProductDO product) { + // 校验设备名称在同一产品下是否唯一 TenantUtils.executeIgnore(() -> { - // 校验设备名称在同一产品下是否唯一 if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) { throw exception(DEVICE_NAME_EXISTS); } - // 校验设备标识是否唯一 - if (deviceMapper.selectByDeviceKey(deviceKey) != null) { - throw exception(DEVICE_KEY_EXISTS); - } }); - // 校验父设备是否为合法网关 if (IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType()) && gatewayId != null) { @@ -121,6 +98,22 @@ public class IotDeviceServiceImpl implements IotDeviceService { } } + /** + * 校验设备序列号全局唯一性 + * + * @param serialNumber 设备序列号 + * @param excludeId 排除的设备编号(用于更新时排除自身) + */ + private void validateSerialNumberUnique(String serialNumber, Long excludeId) { + if (StrUtil.isBlank(serialNumber)) { + return; + } + IotDeviceDO existDevice = deviceMapper.selectBySerialNumber(serialNumber); + if (existDevice != null && ObjUtil.notEqual(existDevice.getId(), excludeId)) { + throw exception(DEVICE_SERIAL_NUMBER_EXISTS); + } + } + private void initDevice(IotDeviceDO device, IotProductDO product) { device.setProductId(product.getId()).setProductKey(product.getProductKey()) .setDeviceType(product.getDeviceType()); @@ -132,7 +125,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @Override public void updateDevice(IotDeviceSaveReqVO updateReqVO) { - updateReqVO.setDeviceKey(null).setDeviceName(null).setProductId(null); // 不允许更新 + updateReqVO.setDeviceName(null).setProductId(null); // 不允许更新 // 1.1 校验存在 IotDeviceDO device = validateDeviceExists(updateReqVO.getId()); // 1.2 校验父设备是否为合法网关 @@ -142,6 +135,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { } // 1.3 校验分组存在 deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); + // 1.4 校验设备序列号全局唯一 + validateSerialNumberUnique(updateReqVO.getSerialNumber(), updateReqVO.getId()); // 2. 更新到数据库 IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); @@ -155,7 +150,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @Transactional(rollbackFor = Exception.class) public void updateDeviceGroup(IotDeviceUpdateGroupReqVO updateReqVO) { // 1.1 校验设备存在 - List devices = deviceMapper.selectBatchIds(updateReqVO.getIds()); + List devices = deviceMapper.selectByIds(updateReqVO.getIds()); if (CollUtil.isEmpty(devices)) { return; } @@ -193,7 +188,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { if (CollUtil.isEmpty(ids)) { return; } - List devices = deviceMapper.selectBatchIds(ids); + List devices = deviceMapper.selectByIds(ids); if (CollUtil.isEmpty(devices)) { return; } @@ -220,6 +215,15 @@ public class IotDeviceServiceImpl implements IotDeviceService { return device; } + @Override + public IotDeviceDO validateDeviceExistsFromCache(Long id) { + IotDeviceDO device = getSelf().getDeviceFromCache(id); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS); + } + return device; + } + /** * 校验网关设备是否存在 * @@ -241,8 +245,17 @@ public class IotDeviceServiceImpl implements IotDeviceService { } @Override - public IotDeviceDO getDeviceByDeviceKey(String deviceKey) { - return deviceMapper.selectByDeviceKey(deviceKey); + @Cacheable(value = RedisKeyConstants.DEVICE, key = "#id", unless = "#result == null") + @TenantIgnore // 忽略租户信息 + public IotDeviceDO getDeviceFromCache(Long id) { + return deviceMapper.selectById(id); + } + + @Override + @Cacheable(value = RedisKeyConstants.DEVICE, key = "#productKey + '_' + #deviceName", unless = "#result == null") + @TenantIgnore // 忽略租户信息,跨租户 productKey + deviceName 是唯一的 + public IotDeviceDO getDeviceFromCache(String productKey, String deviceName) { + return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); } @Override @@ -251,8 +264,8 @@ public class IotDeviceServiceImpl implements IotDeviceService { } @Override - public List getDeviceListByDeviceType(@Nullable Integer deviceType) { - return deviceMapper.selectListByDeviceType(deviceType); + public List getDeviceListByCondition(@Nullable Integer deviceType, @Nullable Long productId) { + return deviceMapper.selectListByCondition(deviceType, productId); } @Override @@ -266,17 +279,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { } @Override - public List getDeviceListByIdList(List deviceIdList) { - return deviceMapper.selectByIds(deviceIdList); - } - - @Override - public void updateDeviceState(Long id, Integer state) { - // 1. 校验存在 - IotDeviceDO device = validateDeviceExists(id); - - // 2. 更新状态和时间 - IotDeviceDO updateObj = new IotDeviceDO().setId(id).setState(state); + public void updateDeviceState(IotDeviceDO device, Integer state) { + // 1. 更新状态和时间 + IotDeviceDO updateObj = new IotDeviceDO().setId(device.getId()).setState(state); if (device.getOnlineTime() == null && Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { updateObj.setActiveTime(LocalDateTime.now()); @@ -288,10 +293,18 @@ public class IotDeviceServiceImpl implements IotDeviceService { } deviceMapper.updateById(updateObj); - // 3. 清空对应缓存 + // 2. 清空对应缓存 deleteDeviceCache(device); } + @Override + public void updateDeviceState(Long id, Integer state) { + // 校验存在 + IotDeviceDO device = validateDeviceExists(id); + // 执行更新 + updateDeviceState(device, state); + } + @Override public Long getDeviceCountByProductId(Long productId) { return deviceMapper.selectCountByProductId(productId); @@ -302,22 +315,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { return deviceMapper.selectCountByGroupId(groupId); } - @Override - @Cacheable(value = RedisKeyConstants.DEVICE, key = "#productKey + '_' + #deviceName", unless = "#result == null") - @TenantIgnore // 忽略租户信息,跨租户 productKey + deviceName 是唯一的 - public IotDeviceDO getDeviceByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) { - return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); - } - - /** - * 生成 deviceKey - * - * @return 生成的 deviceKey - */ - private String generateDeviceKey() { - return RandomUtil.randomString(16); - } - /** * 生成 deviceSecret * @@ -378,8 +375,9 @@ public class IotDeviceServiceImpl implements IotDeviceService { IotDeviceDO existDevice = deviceMapper.selectByDeviceName(importDevice.getDeviceName()); if (existDevice == null) { createDevice(new IotDeviceSaveReqVO() - .setDeviceName(importDevice.getDeviceName()).setDeviceKey(generateDeviceKey()) - .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)); + .setDeviceName(importDevice.getDeviceName()) + .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds) + .setLocationType(importDevice.getLocationType())); respVO.getCreateDeviceNames().add(importDevice.getDeviceName()); return; } @@ -388,7 +386,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { throw exception(DEVICE_KEY_EXISTS); } updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId()) - .setGatewayId(gatewayId).setGroupIds(groupIds)); + .setGatewayId(gatewayId).setGroupIds(groupIds).setLocationType(importDevice.getLocationType())); respVO.getUpdateDeviceNames().add(importDevice.getDeviceName()); } catch (ServiceException ex) { respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); @@ -398,14 +396,12 @@ public class IotDeviceServiceImpl implements IotDeviceService { } @Override - public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { - IotDeviceDO device = validateDeviceExists(deviceId); - MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), - device.getDeviceSecret()); - return new IotDeviceMqttConnectionParamsRespVO() - .setMqttClientId(mqttSignResult.getClientId()) - .setMqttUsername(mqttSignResult.getUsername()) - .setMqttPassword(mqttSignResult.getPassword()); + public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) { + IotDeviceDO device = validateDeviceExists(id); + // 使用 IotDeviceAuthUtils 生成认证信息 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo( + device.getProductKey(), device.getDeviceName(), device.getDeviceSecret()); + return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class); } private void deleteDeviceCache(IotDeviceDO device) { @@ -417,38 +413,94 @@ public class IotDeviceServiceImpl implements IotDeviceService { devices.forEach(this::deleteDeviceCache); } - @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.productKey + '_' + #device.deviceName") + @SuppressWarnings("unused") + @Caching(evict = { + @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.id"), + @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.productKey + '_' + #device.deviceName") + }) public void deleteDeviceCache0(IotDeviceDO device) { } - private IotDeviceServiceImpl getSelf() { - return SpringUtil.getBean(getClass()); - } - @Override public Long getDeviceCount(LocalDateTime createTime) { return deviceMapper.selectCountByCreateTime(createTime); } - // TODO @super:简化 @Override public Map getDeviceCountMapByProductId() { - // 查询结果转换成Map - List> list = deviceMapper.selectDeviceCountMapByProductId(); - return list.stream().collect(Collectors.toMap( - map -> Long.valueOf(map.get("key").toString()), - map -> Integer.valueOf(map.get("value").toString()) - )); + return deviceMapper.selectDeviceCountMapByProductId(); } @Override public Map getDeviceCountMapByState() { - // 查询结果转换成Map - List> list = deviceMapper.selectDeviceCountGroupByState(); - return list.stream().collect(Collectors.toMap( - map -> Integer.valueOf(map.get("key").toString()), - map -> Long.valueOf(map.get("value").toString()) - )); + return deviceMapper.selectDeviceCountGroupByState(); } -} \ No newline at end of file + @Override + public List getDeviceListByProductKeyAndNames(String productKey, List deviceNames) { + if (StrUtil.isBlank(productKey) || CollUtil.isEmpty(deviceNames)) { + return Collections.emptyList(); + } + return deviceMapper.selectByProductKeyAndDeviceNames(productKey, deviceNames); + } + + @Override + public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) { + // 1. 校验设备是否存在 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername()); + if (deviceInfo == null) { + log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername()); + return false; + } + String deviceName = deviceInfo.getDeviceName(); + String productKey = deviceInfo.getProductKey(); + IotDeviceDO device = getSelf().getDeviceFromCache(productKey, deviceName); + if (device == null) { + log.warn("[authDevice][设备({}/{}) 不存在]", productKey, deviceName); + return false; + } + + // 2. 校验密码 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret()); + if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) { + log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); + return false; + } + return true; + } + + @Override + public List validateDeviceListExists(Collection ids) { + List devices = getDeviceList(ids); + if (devices.size() != ids.size()) { + throw exception(DEVICE_NOT_EXISTS); + } + return devices; + } + + @Override + public List getDeviceList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return deviceMapper.selectByIds(ids); + } + + @Override + public void updateDeviceFirmware(Long deviceId, Long firmwareId) { + // 1. 校验设备是否存在 + IotDeviceDO device = validateDeviceExists(deviceId); + + // 2. 更新设备固件版本 + IotDeviceDO updateObj = new IotDeviceDO().setId(deviceId).setFirmwareId(firmwareId); + deviceMapper.updateById(updateObj); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + private IotDeviceServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java new file mode 100644 index 000000000..4a300dfc3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.iot.service.device.message; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 设备消息 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceMessageService { + + /** + * 初始化设备消息的 TDengine 超级表 + * + * 系统启动时,会自动初始化一次 + */ + void defineDeviceMessageStable(); + + /** + * 发送设备消息 + * + * @param message 消息(“codec(编解码)字段” 部分字段) + * @param device 设备 + * @return 设备消息 + */ + IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device); + + /** + * 发送设备消息 + * + * @param message 消息(“codec(编解码)字段” 部分字段) + * @return 设备消息 + */ + IotDeviceMessage sendDeviceMessage(IotDeviceMessage message); + + /** + * 处理设备上行的消息,包括如下步骤: + * + * 1. 处理消息 + * 2. 记录消息 + * 3. 回复消息 + * + * @param message 消息 + * @param device 设备 + */ + void handleUpstreamDeviceMessage(IotDeviceMessage message, IotDeviceDO device); + + /** + * 获得设备消息分页 + * + * @param pageReqVO 分页查询 + * @return 设备消息分页 + */ + PageResult getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO); + + /** + * 获得指定 requestId 的设备消息列表 + * + * @param deviceId 设备编号 + * @param requestIds requestId 列表 + * @param reply 是否回复 + * @return 设备消息列表 + */ + List getDeviceMessageListByRequestIdsAndReply( + @NotNull(message = "设备编号不能为空") Long deviceId, + @NotEmpty(message = "请求编号不能为空") List requestIds, + Boolean reply); + + /** + * 获得设备消息数量 + * + * @param createTime 创建时间,如果为空,则统计所有消息数量 + * @return 消息数量 + */ + Long getDeviceMessageCount(@Nullable LocalDateTime createTime); + + /** + * 获取设备消息的数据统计 + * + * @param reqVO 统计请求 + * @return 设备消息的数据统计 + */ + List getDeviceMessageSummaryByDate( + IotStatisticsDeviceMessageReqVO reqVO); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java new file mode 100644 index 000000000..01d1c45ee --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java @@ -0,0 +1,271 @@ +package cn.iocoder.yudao.module.iot.service.device.message; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.base.Objects; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL; + +/** + * IoT 设备消息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private IotOtaTaskRecordService otaTaskRecordService; + + @Resource + private IotDeviceMessageMapper deviceMessageMapper; + + @Resource + private IotDeviceMessageProducer deviceMessageProducer; + + @Override + public void defineDeviceMessageStable() { + if (StrUtil.isNotEmpty(deviceMessageMapper.showSTable())) { + log.info("[defineDeviceMessageStable][设备消息超级表已存在,创建跳过]"); + return; + } + log.info("[defineDeviceMessageStable][设备消息超级表不存在,创建开始...]"); + deviceMessageMapper.createSTable(); + log.info("[defineDeviceMessageStable][设备消息超级表不存在,创建成功]"); + } + + @Async + void createDeviceLogAsync(IotDeviceMessage message) { + IotDeviceMessageDO messageDO = BeanUtils.toBean(message, IotDeviceMessageDO.class) + .setUpstream(IotDeviceMessageUtils.isUpstreamMessage(message)) + .setReply(IotDeviceMessageUtils.isReplyMessage(message)) + .setIdentifier(IotDeviceMessageUtils.getIdentifier(message)); + if (message.getParams() != null) { + messageDO.setParams(JsonUtils.toJsonString(messageDO.getParams())); + } + if (messageDO.getData() != null) { + messageDO.setData(JsonUtils.toJsonString(messageDO.getData())); + } + deviceMessageMapper.insert(messageDO); + } + + @Override + public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message) { + IotDeviceDO device = deviceService.validateDeviceExists(message.getDeviceId()); + return sendDeviceMessage(message, device); + } + + // TODO @芋艿:针对连接网关的设备,是不是 productKey、deviceName 需要调整下; + @Override + public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { + return sendDeviceMessage(message, device, null); + } + + private IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device, String serverId) { + // 1. 补充信息 + appendDeviceMessage(message, device); + + // 2.1 情况一:发送上行消息 + boolean upstream = IotDeviceMessageUtils.isUpstreamMessage(message); + if (upstream) { + deviceMessageProducer.sendDeviceMessage(message); + return message; + } + + // 2.2 情况二:发送下行消息 + // 如果是下行消息,需要校验 serverId 存在 + // TODO 芋艿:【设计】下行消息需要区分 PUSH 和 PULL 模型 + // 1. PUSH 模型:适用于 MQTT 等长连接协议。通过 serverId 将消息路由到指定网关,实时推送。 + // 2. PULL 模型:适用于 HTTP 等短连接协议。设备无固定 serverId,无法主动推送。 + // 解决方案: + // 当 serverId 不存在时,将下行消息存入“待拉取消息表”(例如 iot_device_pull_message)。 + // 设备端通过定时轮询一个新增的 API(例如 /iot/message/pull)来拉取属于自己的消息。 + if (StrUtil.isEmpty(serverId)) { + serverId = devicePropertyService.getDeviceServerId(device.getId()); + if (StrUtil.isEmpty(serverId)) { + throw exception(DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL); + } + } + deviceMessageProducer.sendDeviceMessageToGateway(serverId, message); + // 特殊:记录消息日志。原因:上行消息,消费时,已经会记录;下行消息,因为消费在 Gateway 端,所以需要在这里记录 + getSelf().createDeviceLogAsync(message); + return message; + } + + /** + * 补充消息的后端字段 + * + * @param message 消息 + * @param device 设备信息 + */ + private void appendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { + message.setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now()) + .setDeviceId(device.getId()).setTenantId(device.getTenantId()); + // 特殊:如果设备没有指定 requestId,则使用 messageId + if (StrUtil.isEmpty(message.getRequestId())) { + message.setRequestId(message.getId()); + } + } + + @Override + public void handleUpstreamDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { + // 1. 处理消息 + Object replyData = null; + ServiceException serviceException = null; + try { + replyData = handleUpstreamDeviceMessage0(message, device); + } catch (ServiceException ex) { + serviceException = ex; + log.warn("[handleUpstreamDeviceMessage][message({}) 业务异常]", message, serviceException); + } catch (Exception ex) { + log.error("[handleUpstreamDeviceMessage][message({}) 发生异常]", message, ex); + throw ex; + } + + // 2. 记录消息 + getSelf().createDeviceLogAsync(message); + + // 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息 + if (IotDeviceMessageUtils.isReplyMessage(message) + || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod()) + || StrUtil.isEmpty(message.getServerId())) { + return; + } + try { + IotDeviceMessage replyMessage = IotDeviceMessage.replyOf(message.getRequestId(), message.getMethod(), replyData, + serviceException != null ? serviceException.getCode() : null, + serviceException != null ? serviceException.getMessage() : null); + sendDeviceMessage(replyMessage, device, message.getServerId()); + } catch (Exception ex) { + log.error("[handleUpstreamDeviceMessage][message({}) 回复消息失败]", message, ex); + } + } + + // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器 + @SuppressWarnings("SameReturnValue") + private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) { + // 设备上下线 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { + String stateStr = IotDeviceMessageUtils.getIdentifier(message); + assert stateStr != null; + Assert.notEmpty(stateStr, "设备状态不能为空"); + deviceService.updateDeviceState(device, Integer.valueOf(stateStr)); + // TODO 芋艿:子设备的关联 + return null; + } + + // 属性上报 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())) { + devicePropertyService.saveDeviceProperty(device, message); + return null; + } + + // OTA 上报升级进度 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) { + otaTaskRecordService.updateOtaRecordProgress(device, message); + return null; + } + + // TODO @芋艿:这里可以按需,添加别的逻辑; + return null; + } + + @Override + public PageResult getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) { + try { + IPage page = deviceMessageMapper.selectPage( + new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } catch (Exception exception) { + if (exception.getMessage().contains("Table does not exist")) { + return PageResult.empty(); + } + throw exception; + } + } + + @Override + public List getDeviceMessageListByRequestIdsAndReply(Long deviceId, + List requestIds, + Boolean reply) { + return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply); + } + + @Override + public Long getDeviceMessageCount(LocalDateTime createTime) { + return deviceMessageMapper.selectCountByCreateTime( + createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + } + + @Override + public List getDeviceMessageSummaryByDate( + IotStatisticsDeviceMessageReqVO reqVO) { + // 1. 按小时统计,获取分项统计数据 + List> countList = deviceMessageMapper.selectDeviceMessageCountGroupByDate( + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); + + // 2. 按照日期间隔,合并数据 + List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], + reqVO.getInterval()); + return convertList(timeRanges, times -> { + Integer upstreamCount = countList.stream() + .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], (Timestamp) vo.get("time"))) + .mapToInt(value -> MapUtil.getInt(value, "upstream_count")).sum(); + Integer downstreamCount = countList.stream() + .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], (Timestamp) vo.get("time"))) + .mapToInt(value -> MapUtil.getInt(value, "downstream_count")).sum(); + return new IotStatisticsDeviceMessageSummaryByDateRespVO() + .setTime(LocalDateTimeUtils.formatDateRange(times[0], times[1], reqVO.getInterval())) + .setUpstreamCount(upstreamCount).setDownstreamCount(downstreamCount); + }); + } + + private IotDeviceMessageServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyService.java new file mode 100644 index 000000000..24c117d65 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyService.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.service.device.property; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * IoT 设备【属性】数据 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDevicePropertyService { + + // ========== 设备属性相关操作 ========== + + /** + * 定义设备属性数据的结构 + * + * @param productId 产品编号 + */ + void defineDevicePropertyData(Long productId); + + /** + * 保存设备数据 + * + * @param device 设备 + * @param message 设备消息 + */ + void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message); + + /** + * 获得设备属性最新数据 + * + * @param deviceId 设备编号 + * @return 设备属性最新数据 + */ + Map getLatestDeviceProperties(Long deviceId); + + /** + * 获得设备属性历史数据 + * + * @param listReqVO 列表请求 + * @return 设备属性历史数据 + */ + List getHistoryDevicePropertyList(@Valid IotDevicePropertyHistoryListReqVO listReqVO); + + // ========== 设备时间相关操作 ========== + + /** + * 获得最后上报时间小于指定时间的设备编号集合 + * + * @param maxReportTime 最大上报时间 + * @return 设备编号集合 + */ + Set getDeviceIdListByReportTime(LocalDateTime maxReportTime); + + /** + * 更新设备上报时间 + * + * @param id 设备编号 + * @param reportTime 上报时间 + */ + void updateDeviceReportTimeAsync(Long id, LocalDateTime reportTime); + + /** + * 更新设备关联的网关服务 serverId + * + * @param id 设备编号 + * @param serverId 网关 serverId + */ + void updateDeviceServerIdAsync(Long id, String serverId); + + /** + * 获得设备关联的网关服务 serverId + * + * @param id 设备编号 + * @return 网关 serverId + */ + String getDeviceServerId(Long id); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java similarity index 61% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java index 77dde64a6..8031c2a11 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java @@ -1,32 +1,31 @@ -package cn.iocoder.yudao.module.iot.service.device.data; +package cn.iocoder.yudao.module.iot.service.device.property; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO; import cn.iocoder.yudao.module.iot.dal.redis.device.DeviceReportTimeRedisDAO; +import cn.iocoder.yudao.module.iot.dal.redis.device.DeviceServerIdRedisDAO; import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -46,30 +45,33 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { /** * 物模型的数据类型,与 TDengine 数据类型的映射关系 + * + * @see TDEngine 数据类型 */ private static final Map TYPE_MAPPING = MapUtil.builder() .put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT) .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT) .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE) - .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? - .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? - .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR) + .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) + .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_VARCHAR) .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP) - .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! - .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! + .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_VARCHAR) + .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_VARCHAR) .build(); - @Resource - private IotDeviceService deviceService; @Resource private IotThingModelService thingModelService; @Resource + @Lazy // 延迟加载,解决循环依赖 private IotProductService productService; @Resource private DevicePropertyRedisDAO deviceDataRedisDAO; @Resource private DeviceReportTimeRedisDAO deviceReportTimeRedisDAO; + @Resource + private DeviceServerIdRedisDAO deviceServerIdRedisDAO; @Resource private IotDevicePropertyMapper devicePropertyMapper; @@ -85,7 +87,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { // 1.2 解析 DB 里的字段 List oldFields = new ArrayList<>(); try { - oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getProductKey())); + oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getId())); } catch (Exception e) { if (!e.getMessage().contains("Table does not exist")) { throw e; @@ -99,11 +101,11 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId); return; } - devicePropertyMapper.createProductPropertySTable(product.getProductKey(), newFields); + devicePropertyMapper.createProductPropertySTable(product.getId(), newFields); return; } // 2.2 情况二:如果是修改的时候,需要更新表 - devicePropertyMapper.alterProductPropertySTable(product.getProductKey(), oldFields, newFields); + devicePropertyMapper.alterProductPropertySTable(product.getId(), oldFields, newFields); } private List buildTableFieldList(List thingModels) { @@ -111,74 +113,68 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { TDengineTableField field = new TDengineTableField( StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写 TYPE_MAPPING.get(thingModel.getProperty().getDataType())); - if (thingModel.getProperty().getDataType().equals(IotDataSpecsDataTypeEnum.TEXT.getDataType())) { + String dataType = thingModel.getProperty().getDataType(); + if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) { field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength()); + } else if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(), + IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { + field.setLength(TDengineTableField.LENGTH_VARCHAR); } return field; }); } @Override - @TenantIgnore - public void saveDeviceProperty(IotDeviceMessage message) { - if (!(message.getData() instanceof Map)) { + public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) { + if (!(message.getParams() instanceof Map)) { log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); return; } - // 1. 获得设备信息 - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(message.getProductKey(), message.getDeviceName()); - if (device == null) { - log.error("[saveDeviceProperty][消息({}) 对应的设备不存在]", message); - return; - } - // 2. 根据物模型,拼接合法的属性 + // 1. 根据物模型,拼接合法的属性 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? - List thingModels = thingModelService.getThingModelListByProductKeyFromCache(device.getProductKey()); + List thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); Map properties = new HashMap<>(); - ((Map) message.getData()).forEach((key, value) -> { - if (CollUtil.findOne(thingModels, thingModel -> thingModel.getIdentifier().equals(key)) == null) { + ((Map) message.getParams()).forEach((key, value) -> { + IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key)); + if (thingModel == null || thingModel.getProperty() == null) { log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); return; } - properties.put((String) key, value); + if (ObjectUtils.equalsAny(thingModel.getProperty().getDataType(), + IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { + // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 + properties.put((String) key, JsonUtils.toJsonString(value)); + } else { + properties.put((String) key, value); + } }); if (CollUtil.isEmpty(properties)) { log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); return; } - // 3.1 保存设备属性【数据】 - devicePropertyMapper.insert(device, properties, - LocalDateTimeUtil.toEpochMilli(message.getReportTime())); + // 2.1 保存设备属性【数据】 + devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime())); - // 3.2 保存设备属性【日志】 - deviceDataRedisDAO.putAll(message.getDeviceKey(), convertMap(properties.entrySet(), Map.Entry::getKey, - entry -> IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build())); + // 2.2 保存设备属性【日志】 + Map properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry -> + IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); + deviceDataRedisDAO.putAll(device.getId(), properties2); } @Override public Map getLatestDeviceProperties(Long deviceId) { - // 获取设备信息 - IotDeviceDO device = deviceService.validateDeviceExists(deviceId); - - // 获得设备属性 - return deviceDataRedisDAO.get(device.getDeviceKey()); + return deviceDataRedisDAO.get(deviceId); } @Override - public PageResult getHistoryDevicePropertyPage(IotDevicePropertyHistoryPageReqVO pageReqVO) { - // 获取设备信息 - IotDeviceDO device = deviceService.validateDeviceExists(pageReqVO.getDeviceId()); - pageReqVO.setDeviceKey(device.getDeviceKey()); - + public List getHistoryDevicePropertyList(IotDevicePropertyHistoryListReqVO listReqVO) { try { - IPage page = devicePropertyMapper.selectPageByHistory( - new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); - return new PageResult<>(page.getRecords(), page.getTotal()); + return devicePropertyMapper.selectListByHistory(listReqVO); } catch (Exception exception) { if (exception.getMessage().contains("Table does not exist")) { - return PageResult.empty(); + return Collections.emptyList(); } throw exception; } @@ -187,14 +183,27 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { // ========== 设备时间相关操作 ========== @Override - public Set getDeviceKeysByReportTime(LocalDateTime maxReportTime) { + public Set getDeviceIdListByReportTime(LocalDateTime maxReportTime) { return deviceReportTimeRedisDAO.range(maxReportTime); } @Override @Async - public void updateDeviceReportTimeAsync(String deviceKey, LocalDateTime reportTime) { - deviceReportTimeRedisDAO.update(deviceKey, reportTime); + public void updateDeviceReportTimeAsync(Long id, LocalDateTime reportTime) { + deviceReportTimeRedisDAO.update(id, reportTime); + } + + @Override + public void updateDeviceServerIdAsync(Long id, String serverId) { + if (StrUtil.isEmpty(serverId)) { + return; + } + deviceServerIdRedisDAO.update(id, serverId); + } + + @Override + public String getDeviceServerId(Long id) { + return deviceServerIdRedisDAO.get(id); } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java new file mode 100644 index 000000000..0ab514e2d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * OTA 固件管理 Service 接口 + * + * @author Shelly Chan + */ +public interface IotOtaFirmwareService { + + /** + * 创建 OTA 固件 + * + * @param saveReqVO 固件信息 + * @return 固件编号 + */ + Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO); + + /** + * 更新 OTA 固件信息 + * + * @param updateReqVO 固件信息 + */ + void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO); + + /** + * 获取 OTA 固件信息 + * + * @param id 固件编号 + * @return 固件信息 + */ + IotOtaFirmwareDO getOtaFirmware(Long id); + + /** + * 根据产品、版本号,获取 OTA 固件信息 + * + * @param productId 产品编号 + * @param version 版本号 + * @return OTA 固件信息 + */ + IotOtaFirmwareDO getOtaFirmwareByProductIdAndVersion(Long productId, String version); + + /** + * 获取 OTA 固件信息列表 + * + * @param ids 固件编号集合 + * @return 固件信息列表 + */ + List getOtaFirmwareList(Collection ids); + + /** + * 获取 OTA 固件信息 Map + * + * @param ids 固件编号集合 + * @return 固件信息 Map + */ + default Map getOtaFirmwareMap(Collection ids) { + return convertMap(getOtaFirmwareList(ids), IotOtaFirmwareDO::getId); + } + + /** + * 分页查询 OTA 固件信息 + * + * @param pageReqVO 分页查询条件 + * @return 分页结果 + */ + PageResult getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO); + + /** + * 验证 OTA 固件是否存在 + * + * @param id 固件编号 + * @return 固件信息 + */ + IotOtaFirmwareDO validateFirmwareExists(Long id); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java similarity index 57% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java index 7c0ddba7c..94dd21398 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java @@ -1,14 +1,14 @@ package cn.iocoder.yudao.module.iot.service.ota; -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; +import cn.hutool.crypto.digest.DigestAlgorithm; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaFirmwareMapper; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; @@ -17,16 +17,23 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.io.ByteArrayInputStream; +import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_NOT_EXISTS; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE; -@Slf4j +/** + * OTA 固件管理 Service 实现类 + * + * @author Shelly Chan + */ @Service @Validated +@Slf4j public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService { @Resource @@ -37,16 +44,22 @@ public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService { @Override public Long createOtaFirmware(IotOtaFirmwareCreateReqVO saveReqVO) { - // 1. 校验固件产品 + 版本号不能重复 - validateProductAndVersionDuplicate(saveReqVO.getProductId(), saveReqVO.getVersion()); + // 1.1 校验固件产品 + 版本号不能重复 + if (otaFirmwareMapper.selectByProductIdAndVersion(saveReqVO.getProductId(), saveReqVO.getVersion()) != null) { + throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE); + } + // 1.2 校验产品存在 + productService.validateProductExists(saveReqVO.getProductId()); - // 2.1.转化数据格式,准备存储到数据库中 + // 2. 构建对象 + 存储 IotOtaFirmwareDO firmware = BeanUtils.toBean(saveReqVO, IotOtaFirmwareDO.class); - // 2.2.查询ProductKey - // TODO @li:productService.getProduct(Convert.toLong(firmware.getProductId())) 放到 1. 后面,先做参考校验。逻辑两段:1)先参数校验;2)构建对象 + 存储 - IotProductDO product = productService.getProduct(Convert.toLong(firmware.getProductId())); - firmware.setProductKey(Objects.requireNonNull(product).getProductKey()); - // TODO @芋艿: 附件、附件签名等属性的计算 + // 2.1 计算文件签名等属性 + try { + calculateFileDigest(firmware); + } catch (Exception e) { + log.error("[createOtaFirmware][url({}) 计算文件签名失败]", firmware.getFileUrl(), e); + throw new RuntimeException("计算文件签名失败: " + e.getMessage()); + } otaFirmwareMapper.insert(firmware); return firmware.getId(); } @@ -66,6 +79,19 @@ public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService { return otaFirmwareMapper.selectById(id); } + @Override + public IotOtaFirmwareDO getOtaFirmwareByProductIdAndVersion(Long productId, String version) { + return otaFirmwareMapper.selectByProductIdAndVersion(productId, version); + } + + @Override + public List getOtaFirmwareList(Collection ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + return otaFirmwareMapper.selectByIds(ids); + } + @Override public PageResult getOtaFirmwarePage(IotOtaFirmwarePageReqVO pageReqVO) { return otaFirmwareMapper.selectPage(pageReqVO); @@ -80,25 +106,21 @@ public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService { return firmware; } - // TODO @li:注释有点冗余 /** - * 验证产品和版本号是否重复 - *

- * 该方法用于确保在系统中不存在具有相同产品ID和版本号的固件条目 - * 它通过调用otaFirmwareMapper的selectByProductIdAndVersion方法来查询数据库中是否存在匹配的产品ID和版本号的固件信息 - * 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在,从而避免数据重复 + * 计算文件签名 * - * @param productId 产品ID,用于数据库查询 - * @param version 版本号,用于数据库查询 - * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,提示固件信息已存在 + * @param firmware 固件对象 */ - private void validateProductAndVersionDuplicate(String productId, String version) { - // 查询数据库中是否存在具有相同产品ID和版本号的固件信息 - List list = otaFirmwareMapper.selectByProductIdAndVersion(productId, version); - // 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在 - if (CollUtil.isNotEmpty(list)) { - throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE); - } + private void calculateFileDigest(IotOtaFirmwareDO firmware) { + String fileUrl = firmware.getFileUrl(); + // 下载文件并计算签名 + byte[] fileBytes = HttpUtil.downloadBytes(fileUrl); + // 设置文件大小 + firmware.setFileSize((long) fileBytes.length); + // 计算 MD5 签名 + firmware.setFileDigestAlgorithm(DigestAlgorithm.MD5.getValue()); + String md5Hex = DigestUtil.digester(firmware.getFileDigestAlgorithm()).digestHex(new ByteArrayInputStream(fileBytes)); + firmware.setFileDigestValue(md5Hex); } } diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordService.java new file mode 100644 index 000000000..be9db71ec --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordService.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; +import jakarta.validation.Valid; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * IoT OTA 升级记录 Service 接口 + */ +public interface IotOtaTaskRecordService { + + /** + * 批量创建 OTA 升级记录 + * + * @param devices 设备列表 + * @param firmwareId 固件编号 + * @param taskId 任务编号 + */ + void createOtaTaskRecordList(List devices, Long firmwareId, Long taskId); + + /** + * 获取 OTA 升级记录的状态统计 + * + * @param firmwareId 固件编号 + * @param taskId 任务编号 + * @return 状态统计 Map,key 为状态码,value 为对应状态的升级记录数量 + */ + Map getOtaTaskRecordStatusStatistics(Long firmwareId, Long taskId); + + /** + * 获取 OTA 升级记录 + * + * @param id 编号 + * @return OTA 升级记录 + */ + IotOtaTaskRecordDO getOtaTaskRecord(Long id); + + /** + * 获取 OTA 升级记录分页 + * + * @param pageReqVO 分页查询 + * @return OTA 升级记录分页 + */ + PageResult getOtaTaskRecordPage(@Valid IotOtaTaskRecordPageReqVO pageReqVO); + + /** + * 根据 OTA 任务编号,取消未结束的升级记录 + * + * @param taskId 升级任务编号 + */ + void cancelTaskRecordListByTaskId(Long taskId); + + /** + * 根据设备编号和记录状态,获取 OTA 升级记录列表 + * + * @param deviceIds 设备编号集合 + * @param statuses 记录状态集合 + * @return OTA 升级记录列表 + */ + List getOtaTaskRecordListByDeviceIdAndStatus(Set deviceIds, Set statuses); + + /** + * 根据记录状态,获取 OTA 升级记录列表 + * + * @param status 升级记录状态 + * @return 升级记录列表 + */ + List getOtaRecordListByStatus(Integer status); + + /** + * 取消 OTA 升级记录 + * + * @param id 记录编号 + */ + void cancelOtaTaskRecord(Long id); + + /** + * 推送 OTA 升级任务记录 + * + * @param record 任务记录 + * @param fireware 固件信息 + * @param device 设备信息 + * @return 是否推送成功 + */ + boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device); + + /** + * 更新 OTA 升级记录进度 + * + * @param device 设备信息 + * @param message 设备消息 + */ + void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java new file mode 100644 index 000000000..eb75b9154 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java @@ -0,0 +1,231 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaTaskRecordMapper; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskRecordStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * OTA 升级任务记录 Service 实现类 + */ +@Service +@Validated +@Slf4j +public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService { + + @Resource + private IotOtaTaskRecordMapper otaTaskRecordMapper; + + @Resource + private IotOtaFirmwareService otaFirmwareService; + @Resource + private IotOtaTaskService otaTaskService; + @Resource + private IotDeviceMessageService deviceMessageService; + @Resource + private IotDeviceService deviceService; + + @Override + public void createOtaTaskRecordList(List devices, Long firmwareId, Long taskId) { + List records = convertList(devices, device -> + IotOtaTaskRecordDO.builder().firmwareId(firmwareId).taskId(taskId) + .deviceId(device.getId()).fromFirmwareId(Convert.toLong(device.getFirmwareId())) + .status(IotOtaTaskRecordStatusEnum.PENDING.getStatus()).progress(0).build()); + otaTaskRecordMapper.insertBatch(records); + } + + @Override + public Map getOtaTaskRecordStatusStatistics(Long firmwareId, Long taskId) { + // 按照 status 枚举,初始化 countMap 为 0 + Map countMap = convertMap(Arrays.asList(IotOtaTaskRecordStatusEnum.values()), + IotOtaTaskRecordStatusEnum::getStatus, iotOtaTaskRecordStatusEnum -> 0L); + + // 查询记录,只返回 id、status 字段 + List records = otaTaskRecordMapper.selectListByFirmwareIdAndTaskId(firmwareId, taskId); + Map> deviceStatusesMap = convertMultiMap(records, + IotOtaTaskRecordDO::getDeviceId, IotOtaTaskRecordDO::getStatus); + // 找到第一个匹配的优先级状态,避免重复计算 + deviceStatusesMap.forEach((deviceId, statuses) -> { + for (Integer priorityStatus : IotOtaTaskRecordStatusEnum.PRIORITY_STATUSES) { + if (statuses.contains(priorityStatus)) { + countMap.put(priorityStatus, countMap.get(priorityStatus) + 1); + return; + } + } + }); + return countMap; + } + + @Override + public IotOtaTaskRecordDO getOtaTaskRecord(Long id) { + return otaTaskRecordMapper.selectById(id); + } + + @Override + public PageResult getOtaTaskRecordPage(IotOtaTaskRecordPageReqVO pageReqVO) { + return otaTaskRecordMapper.selectPage(pageReqVO); + } + + @Override + public void cancelTaskRecordListByTaskId(Long taskId) { + List records = otaTaskRecordMapper.selectListByTaskIdAndStatus( + taskId, IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES); + if (CollUtil.isEmpty(records)) { + return; + } + // 批量更新 + Collection ids = convertSet(records, IotOtaTaskRecordDO::getId); + otaTaskRecordMapper.updateListByIdAndStatus(ids, IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES, + IotOtaTaskRecordDO.builder().status(IotOtaTaskRecordStatusEnum.CANCELED.getStatus()) + .description(IotOtaTaskRecordDO.DESCRIPTION_CANCEL_BY_TASK).build()); + } + + @Override + public List getOtaTaskRecordListByDeviceIdAndStatus(Set deviceIds, Set statuses) { + return otaTaskRecordMapper.selectListByDeviceIdAndStatus(deviceIds, statuses); + } + + @Override + public List getOtaRecordListByStatus(Integer status) { + return otaTaskRecordMapper.selectListByStatus(status); + } + + @Override + public void cancelOtaTaskRecord(Long id) { + // 1. 校验记录是否存在 + IotOtaTaskRecordDO record = validateUpgradeRecordExists(id); + + // 2. 更新记录状态为取消 + int updateCount = otaTaskRecordMapper.updateByIdAndStatus(record.getId(), IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES, + IotOtaTaskRecordDO.builder().id(id).status(IotOtaTaskRecordStatusEnum.CANCELED.getStatus()) + .description(IotOtaTaskRecordDO.DESCRIPTION_CANCEL_BY_RECORD).build()); + if (updateCount == 0) { + throw exception(OTA_TASK_RECORD_CANCEL_FAIL_STATUS_ERROR); + } + + // 3. 检查并更新任务状态 + checkAndUpdateOtaTaskStatus(record.getTaskId()); + } + + @Override + public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) { + try { + // 1. 推送 OTA 任务记录 + IotDeviceMessage message = IotDeviceMessage.buildOtaUpgrade( + fireware.getVersion(), fireware.getFileUrl(), fireware.getFileSize(), + fireware.getFileDigestAlgorithm(), fireware.getFileDigestValue()); + deviceMessageService.sendDeviceMessage(message, device); + + // 2. 更新 OTA 升级记录状态为进行中 + int updateCount = otaTaskRecordMapper.updateByIdAndStatus( + record.getId(), IotOtaTaskRecordStatusEnum.PENDING.getStatus(), + IotOtaTaskRecordDO.builder().status(IotOtaTaskRecordStatusEnum.PUSHED.getStatus()) + .description(StrUtil.format("已推送,设备消息编号({})", message.getId())).build()); + Assert.isTrue(updateCount == 1, "更新设备记录({})状态失败", record.getId()); + return true; + } catch (Exception ex) { + log.error("[pushOtaTaskRecord][推送 OTA 任务记录({}) 失败]", record.getId(), ex); + otaTaskRecordMapper.updateById(IotOtaTaskRecordDO.builder().id(record.getId()) + .description(StrUtil.format("推送失败,错误信息({})", ex.getMessage())).build()); + return false; + } + } + + private IotOtaTaskRecordDO validateUpgradeRecordExists(Long id) { + IotOtaTaskRecordDO upgradeRecord = otaTaskRecordMapper.selectById(id); + if (upgradeRecord == null) { + throw exception(OTA_TASK_RECORD_NOT_EXISTS); + } + return upgradeRecord; + } + + @Override + @Transactional(rollbackFor = Exception.class) + @SuppressWarnings("unchecked") + public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) { + // 1.1 参数解析 + Map params = (Map) message.getParams(); + String version = MapUtil.getStr(params, "version"); + Assert.notBlank(version, "version 不能为空"); + Integer status = MapUtil.getInt(params, "status"); + Assert.notNull(status, "status 不能为空"); + Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确"); + String description = MapUtil.getStr(params, "description"); + Integer progress = MapUtil.getInt(params, "progress"); + Assert.notNull(progress, "progress 不能为空"); + Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间"); + // 1.2 查询 OTA 升级记录 + List records = otaTaskRecordMapper.selectListByDeviceIdAndStatus( + device.getId(), IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES); + if (CollUtil.isEmpty(records)) { + throw exception(OTA_TASK_RECORD_UPDATE_PROGRESS_FAIL_NO_EXISTS); + } + if (records.size() > 1) { + log.warn("[updateOtaRecordProgress][message({}) 对应升级记录过多({})]", message, records); + } + IotOtaTaskRecordDO record = CollUtil.getFirst(records); + // 1.3 查询 OTA 固件 + IotOtaFirmwareDO firmware = otaFirmwareService.getOtaFirmwareByProductIdAndVersion( + device.getProductId(), version); + if (firmware == null) { + throw exception(OTA_FIRMWARE_NOT_EXISTS); + } + + // 2. 更新 OTA 升级记录状态 + int updateCount = otaTaskRecordMapper.updateByIdAndStatus( + record.getId(), IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES, + IotOtaTaskRecordDO.builder().status(status).description(description).progress(progress).build()); + if (updateCount == 0) { + throw exception(OTA_TASK_RECORD_UPDATE_PROGRESS_FAIL_NO_EXISTS); + } + + // 3. 如果升级成功,则更新设备固件版本 + if (IotOtaTaskRecordStatusEnum.SUCCESS.getStatus().equals(status)) { + deviceService.updateDeviceFirmware(device.getId(), firmware.getId()); + } + + // 4. 如果状态是“已结束”(非进行中),则更新任务状态 + if (!IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES.contains(status)) { + checkAndUpdateOtaTaskStatus(record.getTaskId()); + } + } + + /** + * 检查并更新任务状态 + * 如果任务下没有进行中的记录,则将任务状态更新为已结束 + */ + private void checkAndUpdateOtaTaskStatus(Long taskId) { + // 如果还有进行中的记录,直接返回 + Long inProcessCount = otaTaskRecordMapper.selectCountByTaskIdAndStatus( + taskId, IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES); + if (inProcessCount > 0) { + return; + } + + // 没有进行中的记录,将任务状态更新为已结束 + otaTaskService.updateOtaTaskStatusEnd(taskId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskService.java new file mode 100644 index 000000000..ead91e287 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskDO; +import jakarta.validation.Valid; + +/** + * IoT OTA 升级任务 Service 接口 + * + * @author Shelly Chan + */ +public interface IotOtaTaskService { + + /** + * 创建 OTA 升级任务 + * + * @param createReqVO 创建请求对象 + * @return 升级任务编号 + */ + Long createOtaTask(@Valid IotOtaTaskCreateReqVO createReqVO); + + /** + * 取消 OTA 升级任务 + * + * @param id 升级任务编号 + */ + void cancelOtaTask(Long id); + + /** + * 获取 OTA 升级任务 + * + * @param id 升级任务编号 + * @return 升级任务 + */ + IotOtaTaskDO getOtaTask(Long id); + + /** + * 分页查询 OTA 升级任务 + * + * @param pageReqVO 分页查询请求 + * @return 升级任务分页结果 + */ + PageResult getOtaTaskPage(@Valid IotOtaTaskPageReqVO pageReqVO); + + /** + * 更新 OTA 任务状态为已结束 + * + * @param id 任务编号 + */ + void updateOtaTaskStatusEnd(Long id); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskServiceImpl.java new file mode 100644 index 000000000..d6a9b9fda --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskServiceImpl.java @@ -0,0 +1,167 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.IotOtaTaskPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaTaskMapper; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskDeviceScopeEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskRecordStatusEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaTaskStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT OTA 升级任务 Service 实现类 + * + * @author Shelly Chan + */ +@Service +@Validated +@Slf4j +public class IotOtaTaskServiceImpl implements IotOtaTaskService { + + @Resource + private IotOtaTaskMapper otaTaskMapper; + + @Resource + private IotDeviceService deviceService; + @Resource + private IotOtaFirmwareService otaFirmwareService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private IotOtaTaskRecordService otaTaskRecordService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createOtaTask(IotOtaTaskCreateReqVO createReqVO) { + // 1.1 校验固件信息是否存在 + IotOtaFirmwareDO firmware = otaFirmwareService.validateFirmwareExists(createReqVO.getFirmwareId()); + // 1.2 校验同一固件的升级任务名称不重复 + if (otaTaskMapper.selectByFirmwareIdAndName(firmware.getId(), createReqVO.getName()) != null) { + throw exception(OTA_TASK_CREATE_FAIL_NAME_DUPLICATE); + } + // 1.3 校验设备范围信息 + List devices = validateOtaTaskDeviceScope(createReqVO, firmware.getProductId()); + + // 2. 保存升级任务,直接转换 + IotOtaTaskDO task = BeanUtils.toBean(createReqVO, IotOtaTaskDO.class) + .setStatus(IotOtaTaskStatusEnum.IN_PROGRESS.getStatus()) + .setDeviceTotalCount(devices.size()).setDeviceSuccessCount(0); + otaTaskMapper.insert(task); + + // 3. 生成设备升级记录 + otaTaskRecordService.createOtaTaskRecordList(devices, firmware.getId(), task.getId()); + return task.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelOtaTask(Long id) { + // 1.1 校验升级任务是否存在 + IotOtaTaskDO upgradeTask = validateUpgradeTaskExists(id); + // 1.2 校验升级任务是否可以取消 + if (ObjUtil.notEqual(upgradeTask.getStatus(), IotOtaTaskStatusEnum.IN_PROGRESS.getStatus())) { + throw exception(OTA_TASK_CANCEL_FAIL_STATUS_END); + } + + // 2. 更新升级任务状态为已取消 + otaTaskMapper.updateById(IotOtaTaskDO.builder() + .id(id).status(IotOtaTaskStatusEnum.CANCELED.getStatus()) + .build()); + + // 3. 更新升级记录状态为已取消 + otaTaskRecordService.cancelTaskRecordListByTaskId(id); + } + + @Override + public IotOtaTaskDO getOtaTask(Long id) { + return otaTaskMapper.selectById(id); + } + + @Override + public PageResult getOtaTaskPage(IotOtaTaskPageReqVO pageReqVO) { + return otaTaskMapper.selectPage(pageReqVO); + } + + @Override + public void updateOtaTaskStatusEnd(Long taskId) { + int updateCount = otaTaskMapper.updateByIdAndStatus(taskId, IotOtaTaskStatusEnum.IN_PROGRESS.getStatus(), + new IotOtaTaskDO().setStatus(IotOtaTaskStatusEnum.END.getStatus())); + if (updateCount == 0) { + log.warn("[updateOtaTaskStatusEnd][任务({})不存在或状态不是进行中,无法更新]", taskId); + } + } + + private List validateOtaTaskDeviceScope(IotOtaTaskCreateReqVO createReqVO, Long productId) { + // 情况一:选择设备 + if (Objects.equals(createReqVO.getDeviceScope(), IotOtaTaskDeviceScopeEnum.SELECT.getScope())) { + // 1.1 校验设备存在 + List devices = deviceService.validateDeviceListExists(createReqVO.getDeviceIds()); + for (IotDeviceDO device : devices) { + if (ObjUtil.notEqual(device.getProductId(), productId)) { + throw exception(DEVICE_NOT_EXISTS); + } + } + // 1.2 校验设备是否已经是该固件版本 + devices.forEach(device -> { + if (Objects.equals(device.getFirmwareId(), createReqVO.getFirmwareId())) { + throw exception(OTA_TASK_CREATE_FAIL_DEVICE_FIRMWARE_EXISTS, device.getDeviceName()); + } + }); + // 1.3 校验设备是否已经在升级中 + List records = otaTaskRecordService.getOtaTaskRecordListByDeviceIdAndStatus( + convertSet(devices, IotDeviceDO::getId), IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES); + devices.forEach(device -> { + if (CollUtil.contains(records, item -> item.getDeviceId().equals(device.getId()))) { + throw exception(OTA_TASK_CREATE_FAIL_DEVICE_OTA_IN_PROCESS, device.getDeviceName()); + } + }); + return devices; + } + // 情况二:全部设备 + if (Objects.equals(createReqVO.getDeviceScope(), IotOtaTaskDeviceScopeEnum.ALL.getScope())) { + List devices = deviceService.getDeviceListByProductId(productId); + // 2.1.1 移除已经是该固件版本的设备 + devices.removeIf(device -> Objects.equals(device.getFirmwareId(), createReqVO.getFirmwareId())); + // 2.1.2 移除已经在升级中的设备 + List records = otaTaskRecordService.getOtaTaskRecordListByDeviceIdAndStatus( + convertSet(devices, IotDeviceDO::getId), IotOtaTaskRecordStatusEnum.IN_PROCESS_STATUSES); + devices.removeIf(device -> CollUtil.contains(records, + item -> item.getDeviceId().equals(device.getId()))); + // 2.2 校验是否有可升级的设备 + if (CollUtil.isEmpty(devices)) { + throw exception(OTA_TASK_CREATE_FAIL_DEVICE_EMPTY); + } + return devices; + } + throw new IllegalArgumentException("不支持的设备范围:" + createReqVO.getDeviceScope()); + } + + private IotOtaTaskDO validateUpgradeTaskExists(Long id) { + IotOtaTaskDO upgradeTask = otaTaskMapper.selectById(id); + if (Objects.isNull(upgradeTask)) { + throw exception(OTA_TASK_NOT_EXISTS); + } + return upgradeTask; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java similarity index 68% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java index f03186866..3c64caedb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java @@ -17,6 +17,8 @@ import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_CATEGORY_NOT_EXISTS; /** @@ -29,7 +31,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_CATEG public class IotProductCategoryServiceImpl implements IotProductCategoryService { @Resource - private IotProductCategoryMapper productCategoryMapper; + private IotProductCategoryMapper iotProductCategoryMapper; @Resource private IotProductService productService; @@ -40,7 +42,7 @@ public class IotProductCategoryServiceImpl implements IotProductCategoryService public Long createProductCategory(IotProductCategorySaveReqVO createReqVO) { // 插入 IotProductCategoryDO productCategory = BeanUtils.toBean(createReqVO, IotProductCategoryDO.class); - productCategoryMapper.insert(productCategory); + iotProductCategoryMapper.insert(productCategory); // 返回 return productCategory.getId(); } @@ -51,7 +53,7 @@ public class IotProductCategoryServiceImpl implements IotProductCategoryService validateProductCategoryExists(updateReqVO.getId()); // 更新 IotProductCategoryDO updateObj = BeanUtils.toBean(updateReqVO, IotProductCategoryDO.class); - productCategoryMapper.updateById(updateObj); + iotProductCategoryMapper.updateById(updateObj); } @Override @@ -59,18 +61,18 @@ public class IotProductCategoryServiceImpl implements IotProductCategoryService // 校验存在 validateProductCategoryExists(id); // 删除 - productCategoryMapper.deleteById(id); + iotProductCategoryMapper.deleteById(id); } private void validateProductCategoryExists(Long id) { - if (productCategoryMapper.selectById(id) == null) { + if (iotProductCategoryMapper.selectById(id) == null) { throw exception(PRODUCT_CATEGORY_NOT_EXISTS); } } @Override public IotProductCategoryDO getProductCategory(Long id) { - return productCategoryMapper.selectById(id); + return iotProductCategoryMapper.selectById(id); } @Override @@ -78,46 +80,44 @@ public class IotProductCategoryServiceImpl implements IotProductCategoryService if (CollUtil.isEmpty(ids)) { return CollUtil.newArrayList(); } - return productCategoryMapper.selectBatchIds(ids); + return iotProductCategoryMapper.selectByIds(ids); } @Override public PageResult getProductCategoryPage(IotProductCategoryPageReqVO pageReqVO) { - return productCategoryMapper.selectPage(pageReqVO); + return iotProductCategoryMapper.selectPage(pageReqVO); } @Override public List getProductCategoryListByStatus(Integer status) { - return productCategoryMapper.selectListByStatus(status); + return iotProductCategoryMapper.selectListByStatus(status); } @Override public Long getProductCategoryCount(LocalDateTime createTime) { - return productCategoryMapper.selectCountByCreateTime(createTime); + return iotProductCategoryMapper.selectCountByCreateTime(createTime); } @Override public Map getProductCategoryDeviceCountMap() { // 1. 获取所有数据 - List categoryList = productCategoryMapper.selectList(); - List productList = productService.getProductList(); - // TODO @super:不要 list 查询,返回内存,而是查询一个 Map + List categories = iotProductCategoryMapper.selectList(); + List products = productService.getProductList(); Map deviceCountMapByProductId = deviceService.getDeviceCountMapByProductId(); // 2. 统计每个分类下的设备数量 Map categoryDeviceCountMap = new HashMap<>(); - for (IotProductCategoryDO category : categoryList) { - categoryDeviceCountMap.put(category.getName(), 0); - // TODO @super:CollectionUtils.getSumValue(),看看能不能简化下 - // 2.2 找到该分类下的所有产品,累加设备数量 - for (IotProductDO product : productList) { - if (Objects.equals(product.getCategoryId(), category.getId())) { - Integer deviceCount = deviceCountMapByProductId.getOrDefault(product.getId(), 0); - categoryDeviceCountMap.merge(category.getName(), deviceCount, Integer::sum); - } - } + for (IotProductCategoryDO category : categories) { + // 2.1 找到该分类下的所有产品 + List categoryProducts = filterList(products, + product -> Objects.equals(product.getCategoryId(), category.getId())); + // 2.2 累加设备数量 + Integer totalDeviceCount = getSumValue(categoryProducts, + product -> deviceCountMapByProductId.getOrDefault(product.getId(), 0), + Integer::sum, 0); + categoryDeviceCountMap.put(category.getName(), totalDeviceCount); } return categoryDeviceCountMap; } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java similarity index 83% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java index 8497d73aa..70e6afd03 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java @@ -8,6 +8,7 @@ import jakarta.validation.Valid; import javax.annotation.Nullable; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; /** @@ -47,6 +48,16 @@ public interface IotProductService { */ IotProductDO getProduct(Long id); + /** + * 【缓存】获得产品 + *

+ * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! + * + * @param id 编号 + * @return 产品 + */ + IotProductDO getProductFromCache(Long id); + /** * 根据产品 key 获得产品 * @@ -102,5 +113,11 @@ public interface IotProductService { */ Long getProductCount(@Nullable LocalDateTime createTime); + /** + * 批量校验产品存在 + * + * @param ids 产品编号集合 + */ + void validateProductsExist(Collection ids); } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java similarity index 72% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index 4a7263c27..151590ab8 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -1,21 +1,26 @@ package cn.iocoder.yudao.module.iot.service.product; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; -import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import jakarta.annotation.Resource; -import org.springframework.context.annotation.Lazy; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; import java.util.Objects; @@ -35,18 +40,16 @@ public class IotProductServiceImpl implements IotProductService { private IotProductMapper productMapper; @Resource - @Lazy // 延迟加载,解决循环依赖 private IotDevicePropertyService devicePropertyDataService; + @Resource + private IotDeviceService deviceService; @Override public Long createProduct(IotProductSaveReqVO createReqVO) { // 1. 校验 ProductKey - TenantUtils.executeIgnore(() -> { - // 为什么忽略租户?避免多个租户之间,productKey 重复,导致 TDengine 设备属性表重复 - if (productMapper.selectByProductKey(createReqVO.getProductKey()) != null) { - throw exception(PRODUCT_KEY_EXISTS); - } - }); + if (productMapper.selectByProductKey(createReqVO.getProductKey()) != null) { + throw exception(PRODUCT_KEY_EXISTS); + } // 2. 插入 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) @@ -56,23 +59,31 @@ public class IotProductServiceImpl implements IotProductService { } @Override + @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id") public void updateProduct(IotProductSaveReqVO updateReqVO) { updateReqVO.setProductKey(null); // 不更新产品标识 // 1.1 校验存在 IotProductDO iotProductDO = validateProductExists(updateReqVO.getId()); // 1.2 发布状态不可更新 validateProductStatus(iotProductDO); + // 2. 更新 IotProductDO updateObj = BeanUtils.toBean(updateReqVO, IotProductDO.class); productMapper.updateById(updateObj); } @Override + @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#id") public void deleteProduct(Long id) { // 1.1 校验存在 - IotProductDO iotProductDO = validateProductExists(id); + IotProductDO product = validateProductExists(id); // 1.2 发布状态不可删除 - validateProductStatus(iotProductDO); + validateProductStatus(product); + // 1.3 校验是否有设备 + if (deviceService.getDeviceCountByProductId(id) > 0) { + throw exception(PRODUCT_DELETE_FAIL_HAS_DEVICE); + } + // 2. 删除 productMapper.deleteById(id); } @@ -106,6 +117,13 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectById(id); } + @Override + @Cacheable(value = RedisKeyConstants.PRODUCT, key = "#id", unless = "#result == null") + @TenantIgnore // 忽略租户信息 + public IotProductDO getProductFromCache(Long id) { + return productMapper.selectById(id); + } + @Override public IotProductDO getProductByProductKey(String productKey) { return productMapper.selectByProductKey(productKey); @@ -118,6 +136,7 @@ public class IotProductServiceImpl implements IotProductService { @Override @DSTransactional(rollbackFor = Exception.class) + @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#id") public void updateProductStatus(Long id, Integer status) { // 1. 校验存在 validateProductExists(id); @@ -143,5 +162,15 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectCountByCreateTime(createTime); } + @Override + public void validateProductsExist(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + List products = productMapper.selectByIds(ids); + if (products.size() != ids.size()) { + throw exception(PRODUCT_NOT_EXISTS); + } + } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleService.java new file mode 100644 index 000000000..1e0a81330 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleService.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.service.rule.data; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT 数据流转规则 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDataRuleService { + + /** + * 创建数据流转规则 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataRule(@Valid IotDataRuleSaveReqVO createReqVO); + + /** + * 更新数据流转规则 + * + * @param updateReqVO 更新信息 + */ + void updateDataRule(@Valid IotDataRuleSaveReqVO updateReqVO); + + /** + * 删除数据流转规则 + * + * @param id 编号 + */ + void deleteDataRule(Long id); + + /** + * 获得数据流转规则 + * + * @param id 编号 + * @return 数据流转规则 + */ + IotDataRuleDO getDataRule(Long id); + + /** + * 获得数据流转规则分页 + * + * @param pageReqVO 分页查询 + * @return 数据流转规则分页 + */ + PageResult getDataRulePage(IotDataRulePageReqVO pageReqVO); + + /** + * 根据数据目的编号,获得数据流转规则列表 + * + * @param sinkId 数据目的编号 + * @return 是否被使用 + */ + List getDataRuleListBySinkId(Long sinkId); + + /** + * 执行数据流转规则 + * + * @param message 消息 + */ + void executeDataRule(IotDeviceMessage message); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleServiceImpl.java new file mode 100644 index 000000000..8eafcb681 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleServiceImpl.java @@ -0,0 +1,259 @@ +package cn.iocoder.yudao.module.iot.service.rule.data; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataRuleMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.service.rule.data.action.IotDataRuleAction; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_RULE_NOT_EXISTS; + +/** + * IoT 数据流转规则 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDataRuleServiceImpl implements IotDataRuleService { + + @Resource + private IotDataRuleMapper dataRuleMapper; + + @Resource + private IotProductService productService; + @Resource + private IotDeviceService deviceService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotDataSinkService dataSinkService; + + @Resource + private List dataRuleActions; + + @Override + @CacheEvict(value = RedisKeyConstants.DATA_RULE_LIST, allEntries = true) + public Long createDataRule(IotDataRuleSaveReqVO createReqVO) { + // 校验数据源配置和数据目的 + validateDataRuleConfig(createReqVO); + // 新增 + IotDataRuleDO dataRule = BeanUtils.toBean(createReqVO, IotDataRuleDO.class); + dataRuleMapper.insert(dataRule); + return dataRule.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.DATA_RULE_LIST, allEntries = true) + public void updateDataRule(IotDataRuleSaveReqVO updateReqVO) { + // 校验存在 + validateDataRuleExists(updateReqVO.getId()); + // 校验数据源配置和数据目的 + validateDataRuleConfig(updateReqVO); + + // 更新 + IotDataRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotDataRuleDO.class); + dataRuleMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.DATA_RULE_LIST, allEntries = true) + public void deleteDataRule(Long id) { + // 校验存在 + validateDataRuleExists(id); + // 删除 + dataRuleMapper.deleteById(id); + } + + private void validateDataRuleExists(Long id) { + if (dataRuleMapper.selectById(id) == null) { + throw exception(DATA_RULE_NOT_EXISTS); + } + } + + /** + * 校验数据流转规则配置 + * + * @param reqVO 数据流转规则保存请求VO + */ + private void validateDataRuleConfig(IotDataRuleSaveReqVO reqVO) { + // 1. 校验数据源配置 + validateSourceConfigs(reqVO.getSourceConfigs()); + // 2. 校验数据目的 + dataSinkService.validateDataSinksExist(reqVO.getSinkIds()); + } + + /** + * 校验数据源配置 + * + * @param sourceConfigs 数据源配置列表 + */ + private void validateSourceConfigs(List sourceConfigs) { + // 1. 校验产品 + productService.validateProductsExist( + convertSet(sourceConfigs, IotDataRuleDO.SourceConfig::getProductId)); + + // 2. 校验设备 + deviceService.validateDeviceListExists(convertSet(sourceConfigs, IotDataRuleDO.SourceConfig::getDeviceId, + config -> ObjUtil.notEqual(config.getDeviceId(), IotDeviceDO.DEVICE_ID_ALL))); + + // 3. 校验物模型存在 + validateThingModelsExist(sourceConfigs); + } + + /** + * 校验物模型存在 + * + * @param sourceConfigs 数据源配置列表 + */ + private void validateThingModelsExist(List sourceConfigs) { + Map> productIdIdentifiers = new HashMap<>(); + for (IotDataRuleDO.SourceConfig config : sourceConfigs) { + if (StrUtil.isEmpty(config.getIdentifier())) { + continue; + } + productIdIdentifiers.computeIfAbsent(config.getProductId(), + productId -> new HashSet<>()).add(config.getIdentifier()); + } + for (Map.Entry> entry : productIdIdentifiers.entrySet()) { + thingModelService.validateThingModelListExists(entry.getKey(), entry.getValue()); + } + } + + @Override + public IotDataRuleDO getDataRule(Long id) { + return dataRuleMapper.selectById(id); + } + + @Override + public PageResult getDataRulePage(IotDataRulePageReqVO pageReqVO) { + return dataRuleMapper.selectPage(pageReqVO); + } + + @Override + public List getDataRuleListBySinkId(Long sinkId) { + return dataRuleMapper.selectListBySinkId(sinkId); + } + + @Cacheable(value = RedisKeyConstants.DATA_RULE_LIST, + key = "#deviceId + '_' + #method + '_' + (#identifier ?: '')") + public List getDataRuleListByConditionFromCache(Long deviceId, String method, String identifier) { + // 1. 查询所有开启的数据流转规则 + List rules = dataRuleMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 2. 内存里过滤匹配的规则 + List matchedRules = new ArrayList<>(); + for (IotDataRuleDO rule : rules) { + IotDataRuleDO.SourceConfig found = CollUtil.findOne(rule.getSourceConfigs(), + config -> ObjectUtils.equalsAny(config.getDeviceId(), deviceId, IotDeviceDO.DEVICE_ID_ALL) + && Objects.equals(config.getMethod(), method) + && (StrUtil.isEmpty(config.getIdentifier()) || ObjUtil.equal(config.getIdentifier(), identifier))); + if (found != null) { + matchedRules.add(new IotDataRuleDO().setId(rule.getId()).setSinkIds(rule.getSinkIds())); + } + } + return matchedRules; + } + + @Override + public void executeDataRule(IotDeviceMessage message) { + try { + // 1. 获取匹配的数据流转规则 + Long deviceId = message.getDeviceId(); + String method = message.getMethod(); + String identifier = IotDeviceMessageUtils.getIdentifier(message); + List rules = getSelf().getDataRuleListByConditionFromCache(deviceId, method, identifier); + if (CollUtil.isEmpty(rules)) { + log.debug("[executeDataRule][设备({}) 方法({}) 标识符({}) 没有匹配的数据流转规则]", + deviceId, method, identifier); + return; + } + log.info("[executeDataRule][设备({}) 方法({}) 标识符({}) 匹配到 {} 条数据流转规则]", + deviceId, method, identifier, rules.size()); + + // 2. 遍历规则,执行数据流转 + rules.forEach(rule -> executeDataRule(message, rule)); + } catch (Exception e) { + log.error("[executeDataRule][消息({}) 执行数据流转规则异常]", message, e); + } + } + + /** + * 为指定规则的所有数据目的执行数据流转 + * + * @param message 设备消息 + * @param rule 数据流转规则 + */ + private void executeDataRule(IotDeviceMessage message, IotDataRuleDO rule) { + rule.getSinkIds().forEach(sinkId -> { + try { + // 获取数据目的配置 + IotDataSinkDO dataSink = dataSinkService.getDataSinkFromCache(sinkId); + if (dataSink == null) { + log.error("[executeDataRule][规则({}) 对应的数据目的({}) 不存在]", rule.getId(), sinkId); + return; + } + if (CommonStatusEnum.isDisable(dataSink.getStatus())) { + log.info("[executeDataRule][规则({}) 对应的数据目的({}) 状态为禁用]", rule.getId(), sinkId); + return; + } + + // 执行数据桥接操作 + executeDataRuleAction(message, dataSink); + } catch (Exception e) { + log.error("[executeDataRule][规则({}) 数据目的({}) 执行异常]", rule.getId(), sinkId, e); + } + }); + } + + /** + * 执行数据流转操作 + * + * @param message 设备消息 + * @param dataSink 数据目的 + */ + private void executeDataRuleAction(IotDeviceMessage message, IotDataSinkDO dataSink) { + dataRuleActions.forEach(action -> { + if (ObjUtil.notEqual(action.getType(), dataSink.getType())) { + return; + } + try { + action.execute(message, dataSink); + log.info("[executeDataRuleAction][消息({}) 数据目的({}) 执行成功]", message.getId(), dataSink.getId()); + } catch (Exception e) { + log.error("[executeDataRuleAction][消息({}) 数据目的({}) 执行异常]", message.getId(), dataSink.getId(), e); + } + }); + } + + private IotDataRuleServiceImpl getSelf() { + return SpringUtils.getBean(IotDataRuleServiceImpl.class); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkService.java new file mode 100644 index 000000000..d0e2a5282 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkService.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.service.rule.data; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +/** + * IoT 数据流转目的 Service 接口 + * + * @author HUIHUI + */ +public interface IotDataSinkService { + + /** + * 创建数据流转目的 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataSink(@Valid IotDataSinkSaveReqVO createReqVO); + + /** + * 更新数据流转目的 + * + * @param updateReqVO 更新信息 + */ + void updateDataSink(@Valid IotDataSinkSaveReqVO updateReqVO); + + /** + * 删除数据流转目的 + * + * @param id 编号 + */ + void deleteDataSink(Long id); + + /** + * 获得数据流转目的 + * + * @param id 编号 + * @return 数据流转目的 + */ + IotDataSinkDO getDataSink(Long id); + + /** + * 从缓存中获得数据流转目的 + * + * @param id 编号 + * @return 数据流转目的 + */ + IotDataSinkDO getDataSinkFromCache(Long id); + + /** + * 获得数据流转目的分页 + * + * @param pageReqVO 分页查询 + * @return 数据流转目的分页 + */ + PageResult getDataSinkPage(IotDataSinkPageReqVO pageReqVO); + + /** + * 获取数据流转目的列表 + * + * @param status 状态,如果为空,则不进行筛选 + * @return 数据流转目的列表 + */ + List getDataSinkListByStatus(Integer status); + + /** + * 批量校验数据目的存在 + * + * @param ids 数据目的编号集合 + */ + void validateDataSinksExist(Collection ids); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java new file mode 100644 index 000000000..9977afba2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataSinkServiceImpl.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.iot.service.rule.data; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataSinkMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_DELETE_FAIL_USED_BY_RULE; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_NOT_EXISTS; + +/** + * IoT 数据流转目的 Service 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class IotDataSinkServiceImpl implements IotDataSinkService { + + @Resource + private IotDataSinkMapper dataSinkMapper; + + @Resource + @Lazy // 延迟,避免循环依赖报错 + private IotDataRuleService dataRuleService; + + @Override + public Long createDataSink(IotDataSinkSaveReqVO createReqVO) { + IotDataSinkDO dataBridge = BeanUtils.toBean(createReqVO, IotDataSinkDO.class); + dataSinkMapper.insert(dataBridge); + return dataBridge.getId(); + } + + @Override + public void updateDataSink(IotDataSinkSaveReqVO updateReqVO) { + // 校验存在 + validateDataBridgeExists(updateReqVO.getId()); + // 更新 + IotDataSinkDO updateObj = BeanUtils.toBean(updateReqVO, IotDataSinkDO.class); + dataSinkMapper.updateById(updateObj); + } + + @Override + public void deleteDataSink(Long id) { + // 校验存在 + validateDataBridgeExists(id); + // 校验是否被数据流转规则使用 + if (CollUtil.isNotEmpty(dataRuleService.getDataRuleListBySinkId(id))) { + throw exception(DATA_SINK_DELETE_FAIL_USED_BY_RULE); + } + // 删除 + dataSinkMapper.deleteById(id); + } + + private void validateDataBridgeExists(Long id) { + if (dataSinkMapper.selectById(id) == null) { + throw exception(DATA_SINK_NOT_EXISTS); + } + } + + @Override + public IotDataSinkDO getDataSink(Long id) { + return dataSinkMapper.selectById(id); + } + + @Override + @Cacheable(value = RedisKeyConstants.DATA_SINK, key = "#id") + public IotDataSinkDO getDataSinkFromCache(Long id) { + return dataSinkMapper.selectById(id); + } + + @Override + public PageResult getDataSinkPage(IotDataSinkPageReqVO pageReqVO) { + return dataSinkMapper.selectPage(pageReqVO); + } + + @Override + public List getDataSinkListByStatus(Integer status) { + return dataSinkMapper.selectListByStatus(status); + } + + @Override + public void validateDataSinksExist(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + List sinks = dataSinkMapper.selectByIds(ids); + if (sinks.size() != ids.size()) { + throw exception(DATA_SINK_NOT_EXISTS); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleAction.java new file mode 100644 index 000000000..8e6458ba8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleAction.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; + +/** + * IoT 数据流转目的的执行器 action 接口 + * + * @author HUIHUI + */ +public interface IotDataRuleAction { + + /** + * 获取数据流转目的类型 + * + * @return 数据流转目的类型 + */ + Integer getType(); + + /** + * 执行数据流转目的操作 + * + * @param message 设备消息 + * @param dataSink 数据流转目的 + */ + void execute(IotDeviceMessage message, IotDataSinkDO dataSink); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java similarity index 77% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java index e7f84dd6c..431946908 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java @@ -1,8 +1,9 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; +package cn.iocoder.yudao.module.iot.service.rule.data.action; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -17,14 +18,14 @@ import java.time.Duration; // TODO @芋艿:websocket /** - * 带缓存功能的数据桥梁执行器抽象类 + * 可缓存的 {@link IotDataRuleAction} 抽象实现 * * 该类提供了一个通用的缓存机制,用于管理各类数据桥接的生产者(Producer)实例。 * * 主要特点: * - 基于Guava Cache实现高效的生产者实例缓存管理 * - 自动处理生产者的生命周期(创建、获取、关闭) - * - 支持30分钟未访问自动过期清理机制 + * - 支持 30 分钟未访问自动过期清理机制 * - 异常处理与日志记录,便于问题排查 * * 子类需要实现: @@ -36,7 +37,7 @@ import java.time.Duration; * @author HUIHUI */ @Slf4j -public abstract class AbstractCacheableDataBridgeExecute implements IotDataBridgeExecute { +public abstract class IotDataRuleCacheableAction implements IotDataRuleAction { /** * Producer 缓存 @@ -45,10 +46,6 @@ public abstract class AbstractCacheableDataBridgeExecute imple .expireAfterAccess(Duration.ofMinutes(30)) // 30 分钟未访问就提前过期 .removalListener((RemovalListener) notification -> { Producer producer = notification.getValue(); - if (producer == null) { - return; - } - try { closeProducer(producer); log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已关闭]", notification.getKey()); @@ -100,15 +97,21 @@ public abstract class AbstractCacheableDataBridgeExecute imple @Override @SuppressWarnings({"unchecked"}) - public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) { - if (ObjUtil.notEqual(message.getType(), getType())) { - return; - } + public void execute(IotDeviceMessage message, IotDataSinkDO dataSink) { + Assert.isTrue(ObjUtil.equal(dataSink.getType(), getType()), "类型({})不匹配", dataSink.getType()); try { - execute0(message, (Config) dataBridge.getConfig()); + execute(message, (Config) dataSink.getConfig()); } catch (Exception e) { - log.error("[execute][桥梁配置 config({}) 对应的 message({}) 发送异常]", dataBridge.getConfig(), message, e); + log.error("[execute][桥梁配置 config({}) 对应的 message({}) 发送异常]", dataSink.getConfig(), message, e); } } + /** + * 执行数据流转 + * + * @param message 设备消息 + * @param config 配置信息 + */ + protected abstract void execute(IotDeviceMessage message, Config config) throws Exception; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotHttpDataSinkAction.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotHttpDataSinkAction.java index 22b72e055..3f4b8eb02 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotHttpDataSinkAction.java @@ -1,11 +1,13 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; +package cn.iocoder.yudao.module.iot.service.rule.data.action; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeHttpConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkHttpConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; @@ -19,25 +21,27 @@ import java.util.Map; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; /** - * Http 的 {@link IotDataBridgeExecute} 实现类 + * HTTP 的 {@link IotDataRuleAction} 实现类 * * @author HUIHUI */ @Component @Slf4j -public class IotHttpDataBridgeExecute implements IotDataBridgeExecute { +public class IotHttpDataSinkAction implements IotDataRuleAction { @Resource private RestTemplate restTemplate; @Override public Integer getType() { - return IotDataBridgeTypeEnum.HTTP.getType(); + return IotDataSinkTypeEnum.HTTP.getType(); } @Override - @SuppressWarnings({"unchecked", "deprecation"}) - public void execute0(IotDeviceMessage message, IotDataBridgeHttpConfig config) { + @SuppressWarnings("unchecked") + public void execute(IotDeviceMessage message, IotDataSinkDO dataSink) { + IotDataSinkHttpConfig config = (IotDataSinkHttpConfig) dataSink.getConfig(); + Assert.notNull(config, "配置({})不能为空", dataSink.getId()); String url = null; HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase()); HttpEntity requestEntity = null; @@ -70,18 +74,17 @@ public class IotHttpDataBridgeExecute implements IotDataBridgeExecute(JsonUtils.toJsonString(requestBody), headers); } - // 2.1 发送请求 + // 2. 发送请求 responseEntity = restTemplate.exchange(url, method, requestEntity, String.class); - // 2.2 记录日志 if (responseEntity.getStatusCode().is2xxSuccessful()) { - log.info("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求成功({})]", + log.info("[execute][message({}) config({}) url({}) method({}) requestEntity({}) 请求成功({})]", message, config, url, method, requestEntity, responseEntity); } else { - log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求失败({})]", + log.error("[execute][message({}) config({}) url({}) method({}) requestEntity({}) 请求失败({})]", message, config, url, method, requestEntity, responseEntity); } } catch (Exception e) { - log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求异常({})]", + log.error("[execute][message({}) config({}) url({}) method({}) requestEntity({}) 请求异常({})]", message, config, url, method, requestEntity, responseEntity, e); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java similarity index 51% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java index 5674c7d60..94ea1dd49 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotKafkaDataRuleAction.java @@ -1,14 +1,16 @@ -package cn.iocoder.yudao.module.iot.service.rule.action.databridge; +package cn.iocoder.yudao.module.iot.service.rule.data.action; -import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeKafkaMQConfig; -import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; -import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkKafkaConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.kafka.core.DefaultKafkaProducerFactory; import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; import org.springframework.stereotype.Component; import java.time.Duration; @@ -17,36 +19,50 @@ import java.util.Map; import java.util.concurrent.TimeUnit; /** - * Kafka 的 {@link IotDataBridgeExecute} 实现类 + * Kafka 的 {@link IotDataRuleAction} 实现类 * * @author HUIHUI */ @ConditionalOnClass(name = "org.springframework.kafka.core.KafkaTemplate") @Component @Slf4j -public class IotKafkaMQDataBridgeExecute extends - AbstractCacheableDataBridgeExecute> { +public class IotKafkaDataRuleAction extends + IotDataRuleCacheableAction> { - private static final Duration SEND_TIMEOUT = Duration.ofMillis(10000); // 10 秒超时时间 + private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10); @Override public Integer getType() { - return IotDataBridgeTypeEnum.KAFKA.getType(); + return IotDataSinkTypeEnum.KAFKA.getType(); } @Override - public void execute0(IotDeviceMessage message, IotDataBridgeKafkaMQConfig config) throws Exception { - // 1. 获取或创建 KafkaTemplate - KafkaTemplate kafkaTemplate = getProducer(config); + public void execute(IotDeviceMessage message, IotDataSinkKafkaConfig config) throws Exception { + try { + // 1. 获取或创建 KafkaTemplate + KafkaTemplate kafkaTemplate = getProducer(config); - // 2. 发送消息并等待结果 - kafkaTemplate.send(config.getTopic(), message.toString()) - .get(SEND_TIMEOUT.getSeconds(), TimeUnit.SECONDS); // 添加超时等待 - log.info("[execute0][message({}) 发送成功]", message); + // 2. 发送消息并等待结果 + SendResult sendResult = kafkaTemplate.send(config.getTopic(), JsonUtils.toJsonString(message)) + .get(SEND_TIMEOUT.getSeconds(), TimeUnit.SECONDS); + // 3. 处理发送结果 + if (sendResult != null && sendResult.getRecordMetadata() != null) { + log.info("[execute][message({}) config({}) 发送成功,结果: partition={}, offset={}, timestamp={}]", + message, config, + sendResult.getRecordMetadata().partition(), + sendResult.getRecordMetadata().offset(), + sendResult.getRecordMetadata().timestamp()); + } else { + log.warn("[execute][message({}) config({}) 发送结果为空]", message, config); + } + } catch (Exception e) { + log.error("[execute][message({}) config({}) 发送失败]", message, config, e); + throw e; + } } @Override - protected KafkaTemplate initProducer(IotDataBridgeKafkaMQConfig config) { + protected KafkaTemplate initProducer(IotDataSinkKafkaConfig config) { // 1.1 构建生产者配置 Map props = new HashMap<>(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRabbitMQDataRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRabbitMQDataRuleAction.java new file mode 100644 index 000000000..075871a37 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRabbitMQDataRuleAction.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkRabbitMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.stereotype.Component; + +/** + * RabbitMQ 的 {@link IotDataRuleAction} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "com.rabbitmq.client.Channel") +@Component +@Slf4j +public class IotRabbitMQDataRuleAction + extends IotDataRuleCacheableAction { + + @Override + public Integer getType() { + return IotDataSinkTypeEnum.RABBITMQ.getType(); + } + + @Override + public void execute(IotDeviceMessage message, IotDataSinkRabbitMQConfig config) throws Exception { + try { + // 1.1 获取或创建 Channel + Channel channel = getProducer(config); + // 1.2 声明交换机、队列和绑定关系 + channel.exchangeDeclare(config.getExchange(), "direct", true); + channel.queueDeclare(config.getQueue(), true, false, false, null); + channel.queueBind(config.getQueue(), config.getExchange(), config.getRoutingKey()); + + // 2. 发送消息 + channel.basicPublish(config.getExchange(), config.getRoutingKey(), null, + JsonUtils.toJsonByte(message)); + log.info("[execute][message({}) config({}) 发送成功]", message, config); + } catch (Exception e) { + log.error("[execute][message({}) config({}) 发送失败]", message, config, e); + throw e; + } + } + + @Override + @SuppressWarnings("resource") + protected Channel initProducer(IotDataSinkRabbitMQConfig config) throws Exception { + // 1. 创建连接工厂 + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(config.getHost()); + factory.setPort(config.getPort()); + factory.setVirtualHost(config.getVirtualHost()); + factory.setUsername(config.getUsername()); + factory.setPassword(config.getPassword()); + // 2. 创建连接 + Connection connection = factory.newConnection(); + // 3. 创建信道 + return connection.createChannel(); + } + + @Override + protected void closeProducer(Channel channel) throws Exception { + if (channel.isOpen()) { + channel.close(); + } + Connection connection = channel.getConnection(); + if (connection.isOpen()) { + connection.close(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRedisRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRedisRuleAction.java new file mode 100644 index 000000000..904240da8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRedisRuleAction.java @@ -0,0 +1,181 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkRedisConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRedisDataStructureEnum; +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; +import org.redisson.spring.data.connection.RedissonConnectionFactory; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * Redis 的 {@link IotDataRuleAction} 实现类 + * 支持多种 Redis 数据结构:Stream、Hash、List、Set、ZSet、String + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotRedisRuleAction extends + IotDataRuleCacheableAction> { + + @Override + public Integer getType() { + return IotDataSinkTypeEnum.REDIS.getType(); + } + + @Override + public void execute(IotDeviceMessage message, IotDataSinkRedisConfig config) throws Exception { + // 1. 获取 RedisTemplate + RedisTemplate redisTemplate = getProducer(config); + + // 2. 根据数据结构类型执行不同的操作 + String messageJson = JsonUtils.toJsonString(message); + IotRedisDataStructureEnum dataStructure = getDataStructureByType(config.getDataStructure()); + switch (dataStructure) { + case STREAM: + executeStream(redisTemplate, config, messageJson); + break; + case HASH: + executeHash(redisTemplate, config, message, messageJson); + break; + case LIST: + executeList(redisTemplate, config, messageJson); + break; + case SET: + executeSet(redisTemplate, config, messageJson); + break; + case ZSET: + executeZSet(redisTemplate, config, message, messageJson); + break; + case STRING: + executeString(redisTemplate, config, messageJson); + break; + default: + throw new IllegalArgumentException("不支持的 Redis 数据结构类型: " + dataStructure); + } + + log.info("[execute][消息发送成功] dataStructure: {}, config: {}", dataStructure.getName(), config); + } + + /** + * 执行 Stream 操作 + */ + private void executeStream(RedisTemplate redisTemplate, IotDataSinkRedisConfig config, String messageJson) { + ObjectRecord record = StreamRecords.newRecord() + .ofObject(messageJson).withStreamKey(config.getTopic()); + redisTemplate.opsForStream().add(record); + } + + /** + * 执行 Hash 操作 + */ + private void executeHash(RedisTemplate redisTemplate, IotDataSinkRedisConfig config, + IotDeviceMessage message, String messageJson) { + String hashField = StrUtil.isNotBlank(config.getHashField()) ? + config.getHashField() : String.valueOf(message.getDeviceId()); + redisTemplate.opsForHash().put(config.getTopic(), hashField, messageJson); + } + + /** + * 执行 List 操作 + */ + private void executeList(RedisTemplate redisTemplate, IotDataSinkRedisConfig config, String messageJson) { + redisTemplate.opsForList().rightPush(config.getTopic(), messageJson); + } + + /** + * 执行 Set 操作 + */ + private void executeSet(RedisTemplate redisTemplate, IotDataSinkRedisConfig config, String messageJson) { + redisTemplate.opsForSet().add(config.getTopic(), messageJson); + } + + /** + * 执行 ZSet 操作 + */ + private void executeZSet(RedisTemplate redisTemplate, IotDataSinkRedisConfig config, + IotDeviceMessage message, String messageJson) { + double score; + if (StrUtil.isNotBlank(config.getScoreField())) { + // 尝试从消息中获取分数字段 + try { + Map messageMap = JsonUtils.parseObject(messageJson, Map.class); + Object scoreValue = messageMap.get(config.getScoreField()); + score = scoreValue instanceof Number ? ((Number) scoreValue).doubleValue() : System.currentTimeMillis(); + } catch (Exception e) { + score = System.currentTimeMillis(); + } + } else { + // 使用当前时间戳作为分数 + score = System.currentTimeMillis(); + } + redisTemplate.opsForZSet().add(config.getTopic(), messageJson, score); + } + + /** + * 执行 String 操作 + */ + private void executeString(RedisTemplate redisTemplate, IotDataSinkRedisConfig config, String messageJson) { + redisTemplate.opsForValue().set(config.getTopic(), messageJson); + } + + @Override + protected RedisTemplate initProducer(IotDataSinkRedisConfig config) { + // 1.1 创建 Redisson 配置 + Config redissonConfig = new Config(); + SingleServerConfig serverConfig = redissonConfig.useSingleServer() + .setAddress("redis://" + config.getHost() + ":" + config.getPort()) + .setDatabase(config.getDatabase()); + // 1.2 设置密码(如果有) + if (StrUtil.isNotBlank(config.getPassword())) { + serverConfig.setPassword(config.getPassword()); + } + + // 2.1 创建 RedisTemplate 并配置 + RedissonClient redisson = Redisson.create(redissonConfig); + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(new RedissonConnectionFactory(redisson)); + // 2.2 设置序列化器 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + template.setValueSerializer(RedisSerializer.json()); + template.setHashValueSerializer(RedisSerializer.json()); + template.afterPropertiesSet(); + return template; + } + + @Override + protected void closeProducer(RedisTemplate producer) throws Exception { + RedisConnectionFactory factory = producer.getConnectionFactory(); + if (factory != null) { + ((RedissonConnectionFactory) factory).destroy(); + } + } + + /** + * 根据类型值获取数据结构枚举 + */ + private IotRedisDataStructureEnum getDataStructureByType(Integer type) { + for (IotRedisDataStructureEnum dataStructure : IotRedisDataStructureEnum.values()) { + if (dataStructure.getType().equals(type)) { + return dataStructure; + } + } + throw new IllegalArgumentException("不支持的 Redis 数据结构类型: " + type); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRocketMQDataRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRocketMQDataRuleAction.java new file mode 100644 index 000000000..d73205c6d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotRocketMQDataRuleAction.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkRocketMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.Message; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.stereotype.Component; + +/** + * RocketMQ 的 {@link IotDataRuleAction} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "org.apache.rocketmq.client.producer.DefaultMQProducer") +@Component +@Slf4j +public class IotRocketMQDataRuleAction extends + IotDataRuleCacheableAction { + + @Override + public Integer getType() { + return IotDataSinkTypeEnum.ROCKETMQ.getType(); + } + + @Override + public void execute(IotDeviceMessage message, IotDataSinkRocketMQConfig config) throws Exception { + // 1. 获取或创建 Producer + DefaultMQProducer producer = getProducer(config); + + // 2.1 创建消息对象,指定 Topic、Tag 和消息体 + Message msg = new Message(config.getTopic(), config.getTags(), JsonUtils.toJsonByte(message)); + // 2.2 发送同步消息并处理结果 + SendResult sendResult = producer.send(msg); + // 2.3 处理发送结果 + if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { + log.info("[execute][message({}) config({}) 发送成功,结果({})]", message, config, sendResult); + } else { + log.error("[execute][message({}) config({}) 发送失败,结果({})]", message, config, sendResult); + } + } + + @Override + protected DefaultMQProducer initProducer(IotDataSinkRocketMQConfig config) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer(config.getGroup()); + producer.setNamesrvAddr(config.getNameServer()); + producer.start(); + return producer; + } + + @Override + protected void closeProducer(DefaultMQProducer producer) { + producer.shutdown(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleService.java new file mode 100644 index 000000000..bdbc4f39b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleService.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRulePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +/** + * IoT 规则场景规则 Service 接口 + * + * @author 芋道源码 + */ +public interface IotSceneRuleService { + + /** + * 创建场景联动 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSceneRule(@Valid IotSceneRuleSaveReqVO createReqVO); + + /** + * 更新场景联动 + * + * @param updateReqVO 更新信息 + */ + void updateSceneRule(@Valid IotSceneRuleSaveReqVO updateReqVO); + + /** + * 更新场景联动状态 + * + * @param id 场景联动编号 + * @param status 状态 + */ + void updateSceneRuleStatus(Long id, Integer status); + + /** + * 删除场景联动 + * + * @param id 编号 + */ + void deleteSceneRule(Long id); + + /** + * 获得场景联动 + * + * @param id 编号 + * @return 场景联动 + */ + IotSceneRuleDO getSceneRule(Long id); + + /** + * 获得场景联动分页 + * + * @param pageReqVO 分页查询 + * @return 场景联动分页 + */ + PageResult getSceneRulePage(IotSceneRulePageReqVO pageReqVO); + + /** + * 校验规则场景联动规则编号们是否存在。如下情况,视为无效: + * 1. 规则场景联动规则编号不存在 + * + * @param ids 场景联动规则编号数组 + */ + void validateSceneRuleList(Collection ids); + + /** + * 获得指定状态的场景联动列表 + * + * @param status 状态 + * @return 场景联动列表 + */ + List getSceneRuleListByStatus(Integer status); + + /** + * 【缓存】获得指定设备的场景列表 + * + * @param productId 产品 ID + * @param deviceId 设备 ID + * @return 场景列表 + */ + List getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId); + + /** + * 基于 {@link IotSceneRuleTriggerTypeEnum} 场景,执行规则场景 + * 1. {@link IotSceneRuleTriggerTypeEnum#DEVICE_STATE_UPDATE} + * 2. {@link IotSceneRuleTriggerTypeEnum#DEVICE_PROPERTY_POST} + * {@link IotSceneRuleTriggerTypeEnum#DEVICE_EVENT_POST} + * 3. {@link IotSceneRuleTriggerTypeEnum#DEVICE_EVENT_POST} + * {@link IotSceneRuleTriggerTypeEnum#DEVICE_SERVICE_INVOKE} + * @param message 消息 + */ + void executeSceneRuleByDevice(IotDeviceMessage message); + + /** + * 基于 {@link IotSceneRuleTriggerTypeEnum#TIMER} 场景,执行规则场景 + * + * @param id 场景联动规则编号 + */ + void executeSceneRuleByTimer(Long id); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java new file mode 100644 index 000000000..7cbc5b56b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java @@ -0,0 +1,380 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRulePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.RULE_SCENE_NOT_EXISTS; + +/** + * IoT 规则场景 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotSceneRuleServiceImpl implements IotSceneRuleService { + + @Resource + private IotSceneRuleMapper sceneRuleMapper; + + // TODO @puhui999:定时任务,基于它调度; + @Resource(name = "iotSchedulerManager") + private IotSchedulerManager schedulerManager; + @Resource + private IotProductService productService; + @Resource + private IotDeviceService deviceService; + + @Resource + private IotSceneRuleMatcherManager sceneRuleMatcherManager; + @Resource + private List sceneRuleActions; + + @Override + public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) { + IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class); + sceneRuleMapper.insert(sceneRule); + return sceneRule.getId(); + } + + @Override + public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) { + // 校验存在 + validateSceneRuleExists(updateReqVO.getId()); + // 更新 + IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class); + sceneRuleMapper.updateById(updateObj); + } + + @Override + public void updateSceneRuleStatus(Long id, Integer status) { + // 校验存在 + validateSceneRuleExists(id); + // 更新状态 + IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status); + sceneRuleMapper.updateById(updateObj); + } + + @Override + public void deleteSceneRule(Long id) { + // 校验存在 + validateSceneRuleExists(id); + // 删除 + sceneRuleMapper.deleteById(id); + } + + private void validateSceneRuleExists(Long id) { + if (sceneRuleMapper.selectById(id) == null) { + throw exception(RULE_SCENE_NOT_EXISTS); + } + } + + @Override + public IotSceneRuleDO getSceneRule(Long id) { + return sceneRuleMapper.selectById(id); + } + + @Override + public PageResult getSceneRulePage(IotSceneRulePageReqVO pageReqVO) { + return sceneRuleMapper.selectPage(pageReqVO); + } + + @Override + public void validateSceneRuleList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 批量查询存在的规则场景 + List existingScenes = sceneRuleMapper.selectByIds(ids); + if (existingScenes.size() != ids.size()) { + throw exception(RULE_SCENE_NOT_EXISTS); + } + } + + @Override + public List getSceneRuleListByStatus(Integer status) { + return sceneRuleMapper.selectListByStatus(status); + } + + // TODO 芋艿,缓存待实现 @puhui999 + @Override + @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略 + public List getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) { + List list = sceneRuleMapper.selectList(); + // 只返回启用状态的规则场景 + List enabledList = filterList(list, + sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus())); + + // 根据 productKey 和 deviceName 进行匹配 + return filterList(enabledList, sceneRule -> { + if (CollUtil.isEmpty(sceneRule.getTriggers())) { + return false; + } + + for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) { + // 检查触发器是否匹配指定的产品和设备 + try { + // 1. 检查产品是否匹配 + if (trigger.getProductId() == null) { + return false; + } + if (trigger.getDeviceId() == null) { + return false; + } + // 检查是否是全部设备的特殊标识 + if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) { + return true; // 匹配所有设备 + } + // 检查具体设备 ID 是否匹配 + return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId()); + } catch (Exception e) { + log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e); + return false; + } + } + return false; + }); + } + + @Override + public void executeSceneRuleByDevice(IotDeviceMessage message) { + // TODO @芋艿:这里的 tenantId,通过设备获取;@puhui999: + TenantUtils.execute(message.getTenantId(), () -> { + // 1. 获得设备匹配的规则场景 + List sceneRules = getMatchedSceneRuleListByMessage(message); + if (CollUtil.isEmpty(sceneRules)) { + return; + } + + // 2. 执行规则场景 + executeSceneRuleAction(message, sceneRules); + }); + } + + @Override + public void executeSceneRuleByTimer(Long id) { + // 1.1 获得规则场景 + IotSceneRuleDO scene = TenantUtils.executeIgnore(() -> sceneRuleMapper.selectById(id)); + if (scene == null) { + log.error("[executeSceneRuleByTimer][规则场景({}) 不存在]", id); + return; + } + if (CommonStatusEnum.isDisable(scene.getStatus())) { + log.info("[executeSceneRuleByTimer][规则场景({}) 已被禁用]", id); + return; + } + // 1.2 判断是否有定时触发器,避免脏数据 + IotSceneRuleDO.Trigger config = CollUtil.findOne(scene.getTriggers(), + trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType())); + if (config == null) { + log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene); + return; + } + + // 2. 执行规则场景 + TenantUtils.execute(scene.getTenantId(), + () -> executeSceneRuleAction(null, ListUtil.toList(scene))); + } + + /** + * 基于消息,获得匹配的规则场景列表 + * + * @param message 设备消息 + * @return 规则场景列表 + */ + private List getMatchedSceneRuleListByMessage(IotDeviceMessage message) { + // 1. 匹配设备 + // TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999; + // 1.1 通过 deviceId 获取设备信息 + IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); + if (device == null) { + log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId()); + return List.of(); + } + + // 1.2 通过 productId 获取产品信息 + IotProductDO product = productService.getProductFromCache(device.getProductId()); + if (product == null) { + log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId()); + return List.of(); + } + + // 1.3 获取匹配的规则场景 + List sceneRules = getSceneRuleListByProductIdAndDeviceIdFromCache( + product.getId(), device.getId()); + if (CollUtil.isEmpty(sceneRules)) { + return sceneRules; + } + + // 2. 使用重构后的触发器匹配逻辑 + return filterList(sceneRules, sceneRule -> matchSceneRuleTriggers(message, sceneRule)); + } + + /** + * 匹配场景规则的所有触发器 + * + * @param message 设备消息 + * @param sceneRule 场景规则 + * @return 是否匹配 + */ + private boolean matchSceneRuleTriggers(IotDeviceMessage message, IotSceneRuleDO sceneRule) { + if (CollUtil.isEmpty(sceneRule.getTriggers())) { + log.debug("[matchSceneRuleTriggers][规则场景({}) 没有配置触发器]", sceneRule.getId()); + return false; + } + + for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) { + if (matchSingleTrigger(message, trigger, sceneRule)) { + log.info("[matchSceneRuleTriggers][消息({}) 匹配到规则场景编号({}) 的触发器({})]", + message.getRequestId(), sceneRule.getId(), trigger.getType()); + return true; + } + } + return false; + } + + /** + * 匹配单个触发器 + * + * @param message 设备消息 + * @param trigger 触发器 + * @param sceneRule 场景规则(用于日志) + * @return 是否匹配 + */ + private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) { + try { + // 2. 检查触发器的条件分组 + return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule); + } catch (Exception e) { + log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}", + sceneRule.getId(), trigger.getType(), message, e); + return false; + } + } + + /** + * 检查触发器的条件分组是否匹配 + * + * @param message 设备消息 + * @param trigger 触发器 + * @param sceneRule 场景规则(用于日志) + * @return 是否匹配 + */ + private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) { + // 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配) + if (CollUtil.isEmpty(trigger.getConditionGroups())) { + return true; + } + + // 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系 + for (List conditionGroup : trigger.getConditionGroups()) { + if (CollUtil.isEmpty(conditionGroup)) { + continue; + } + + // 检查当前分组中的所有条件是否都匹配(且关系) + boolean allConditionsMatched = true; + for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) { + if (!isTriggerConditionMatched(message, condition, sceneRule, trigger)) { + allConditionsMatched = false; + break; + } + } + + // 如果当前分组的所有条件都匹配,则整个触发器匹配成功 + if (allConditionsMatched) { + return true; + } + } + + // 所有分组都不匹配 + return false; + } + + /** + * 基于消息,判断触发器的子条件是否匹配 + * + * @param message 设备消息 + * @param condition 触发条件 + * @param sceneRule 规则场景(用于日志,无其它作用) + * @param trigger 触发器(用于日志,无其它作用) + * @return 是否匹配 + */ + private boolean isTriggerConditionMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, + IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) { + try { + return sceneRuleMatcherManager.isConditionMatched(message, condition); + } catch (Exception e) { + log.error("[isTriggerConditionMatched][规则场景编号({}) 的触发器({}) 条件匹配异常]", + sceneRule.getId(), trigger, e); + return false; + } + } + + /** + * 执行规则场景的动作 + * + * @param message 设备消息 + * @param sceneRules 规则场景列表 + */ + private void executeSceneRuleAction(IotDeviceMessage message, List sceneRules) { + // 1. 遍历规则场景 + sceneRules.forEach(sceneRule -> { + // 2. 遍历规则场景的动作 + sceneRule.getActions().forEach(actionConfig -> { + // 3.1 获取对应的动作 Action 数组 + List actions = filterList(sceneRuleActions, + action -> action.getType().getType().equals(actionConfig.getType())); + if (CollUtil.isEmpty(actions)) { + return; + } + // 3.2 执行动作 + actions.forEach(action -> { + try { + action.execute(message, sceneRule, actionConfig); + log.info("[executeSceneRuleAction][消息({}) 规则场景编号({}) 的执行动作({}) 成功]", + message, sceneRule.getId(), actionConfig); + } catch (Exception e) { + log.error("[executeSceneRuleAction][消息({}) 规则场景编号({}) 的执行动作({}) 执行异常]", + message, sceneRule.getId(), actionConfig, e); + } + }); + }); + }); + } + + private IotSceneRuleServiceImpl getSelf() { + return SpringUtil.getBean(IotSceneRuleServiceImpl.class); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java new file mode 100644 index 000000000..851f3815f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.action; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertRecordDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertRecordService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +// TODO @puhui999、@芋艿:未测试;需要场景联动开发完 +/** + * IoT 告警恢复的 {@link IotSceneRuleAction} 实现类 + * + * @author 芋道源码 + */ +@Component +public class IotAlertRecoverSceneRuleAction implements IotSceneRuleAction { + + private static final String PROCESS_REMARK = "告警自动回复,基于【{}】场景联动规则"; + + @Resource + private IotAlertRecordService alertRecordService; + + @Override + public void execute(IotDeviceMessage message, + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) throws Exception { + Long deviceId = message != null ? message.getDeviceId() : null; + List alertRecords = alertRecordService.getAlertRecordListBySceneRuleId( + rule.getId(), deviceId, false); + if (CollUtil.isEmpty(alertRecords)) { + return; + } + alertRecordService.processAlertRecordList(convertList(alertRecords, IotAlertRecordDO::getId), + StrUtil.format(PROCESS_REMARK, rule.getName())); + } + + @Override + public IotSceneRuleActionTypeEnum getType() { + return IotSceneRuleActionTypeEnum.ALERT_RECOVER; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java new file mode 100644 index 000000000..28223dbd6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.action; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.alert.IotAlertConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertConfigService; +import cn.iocoder.yudao.module.iot.service.alert.IotAlertRecordService; +import cn.iocoder.yudao.module.system.api.mail.MailSendApi; +import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; +import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; +import java.util.List; + +// TODO @puhui999、@芋艿:未测试;需要场景联动开发完 +/** + * IoT 告警触发的 {@link IotSceneRuleAction} 实现类 + * + * @author 芋道源码 + */ +@Component +public class IotAlertTriggerSceneRuleAction implements IotSceneRuleAction { + + @Resource + private IotAlertConfigService alertConfigService; + @Resource + private IotAlertRecordService alertRecordService; + + @Resource + private SmsSendApi smsSendApi; + @Resource + private MailSendApi mailSendApi; + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + + @Override + public void execute(@Nullable IotDeviceMessage message, + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) throws Exception { + List alertConfigs = alertConfigService.getAlertConfigListBySceneRuleIdAndStatus( + rule.getId(), CommonStatusEnum.ENABLE.getStatus()); + if (CollUtil.isEmpty(alertConfigs)) { + return; + } + alertConfigs.forEach(alertConfig -> { + // 记录告警记录,传递场景规则ID + alertRecordService.createAlertRecord(alertConfig, rule.getId(), message); + // 发送告警消息 + sendAlertMessage(alertConfig, message); + }); + } + + private void sendAlertMessage(IotAlertConfigDO config, IotDeviceMessage deviceMessage) { + // TODO @芋艿:等场景联动开发完,再实现 + // TODO @芋艿:短信 + // TODO @芋艿:邮箱 + // TODO @芋艿:站内信 + } + + @Override + public IotSceneRuleActionTypeEnum getType() { + return IotSceneRuleActionTypeEnum.ALERT_TRIGGER; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java new file mode 100644 index 000000000..b71a92091 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.action; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * IoT 设备控制的 {@link IotSceneRuleAction} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDeviceMessageService deviceMessageService; + + // TODO @puhui999:这里 + @Override + public void execute(IotDeviceMessage message, + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { + //IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl(); + //Assert.notNull(control, "设备控制配置不能为空"); + //// 遍历每个设备,下发消息 + //control.getDeviceNames().forEach(deviceName -> { + // IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName); + // if (device == null) { + // log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig); + // return; + // } + // try { + // // TODO @芋艿:@puhui999:这块可能要改,从 type => method + // IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf( + // control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId())); + // log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage); + // } catch (Exception e) { + // log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e); + // } + //}); + } + + @Override + public IotSceneRuleActionTypeEnum getType() { + return IotSceneRuleActionTypeEnum.DEVICE_PROPERTY_SET; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotSceneRuleAction.java new file mode 100644 index 000000000..c88a37f8c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotSceneRuleAction.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.action; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; + +import javax.annotation.Nullable; + +/** + * IoT 场景联动的执行器接口 + * + * @author 芋道源码 + */ +public interface IotSceneRuleAction { + + /** + * 执行场景联动 + * + * @param message 消息,允许空 + * 1. 空的情况:定时触发 + * 2. 非空的情况:设备触发 + * @param rule 规则 + * @param actionConfig 执行配置(实际对应规则里的哪条执行配置) + */ + void execute(@Nullable IotDeviceMessage message, + IotSceneRuleDO rule, + IotSceneRuleDO.Action actionConfig) throws Exception; + + /** + * 获得类型 + * + * @return 类型 + */ + IotSceneRuleActionTypeEnum getType(); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java new file mode 100644 index 000000000..84795d9fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotSceneRuleConditionMatcher; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher; + +/** + * IoT 场景规则匹配器基础接口 + *

+ * 定义所有匹配器的通用行为,包括优先级、名称和启用状态 + *

+ * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器 + * - {@link IotSceneRuleConditionMatcher} 条件匹配器 + * + * @author HUIHUI + */ +public interface IotSceneRuleMatcher { + + /** + * 获取匹配优先级(数值越小优先级越高) + *

+ * 用于在多个匹配器支持同一类型时确定优先级 + * + * @return 优先级数值 + */ + default int getPriority() { + return 100; + } + + /** + * 是否启用该匹配器 + *

+ * 可用于动态开关某些匹配器 + * + * @return 是否启用 + */ + default boolean isEnabled() { + return true; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java new file mode 100644 index 000000000..7175e37a7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java @@ -0,0 +1,238 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * IoT 场景规则匹配器工具类 + *

+ * 提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用 + *

+ * 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等 + * + * @author HUIHUI + */ +@Slf4j +public final class IotSceneRuleMatcherHelper { + + /** + * 私有构造函数,防止实例化 + */ + private IotSceneRuleMatcherHelper() { + } + + /** + * 评估条件是否匹配 + * + * @param sourceValue 源值(来自消息) + * @param operator 操作符 + * @param paramValue 参数值(来自条件配置) + * @return 是否匹配 + */ + public static boolean evaluateCondition(Object sourceValue, String operator, String paramValue) { + try { + // 1. 校验操作符是否合法 + IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator); + if (operatorEnum == null) { + log.warn("[evaluateCondition][operator({}) 操作符无效]", operator); + return false; + } + + // 2. 构建 Spring 表达式变量 + return evaluateConditionWithOperatorEnum(sourceValue, operatorEnum, paramValue); + } catch (Exception e) { + log.error("[evaluateCondition][sourceValue({}) operator({}) paramValue({}) 条件评估异常]", + sourceValue, operator, paramValue, e); + return false; + } + } + + /** + * 使用操作符枚举评估条件是否匹配 + * + * @param sourceValue 源值(来自消息) + * @param operatorEnum 操作符枚举 + * @param paramValue 参数值(来自条件配置) + * @return 是否匹配 + */ + @SuppressWarnings("DataFlowIssue") + public static boolean evaluateConditionWithOperatorEnum(Object sourceValue, IotSceneRuleConditionOperatorEnum operatorEnum, String paramValue) { + try { + // 1. 构建 Spring 表达式变量 + Map springExpressionVariables = buildSpringExpressionVariables(sourceValue, operatorEnum, paramValue); + + // 2. 计算 Spring 表达式 + return (Boolean) SpringExpressionUtils.parseExpression(operatorEnum.getSpringExpression(), springExpressionVariables); + } catch (Exception e) { + log.error("[evaluateConditionWithOperatorEnum][sourceValue({}) operatorEnum({}) paramValue({}) 条件评估异常]", + sourceValue, operatorEnum, paramValue, e); + return false; + } + } + + /** + * 构建 Spring 表达式变量 + */ + private static Map buildSpringExpressionVariables(Object sourceValue, IotSceneRuleConditionOperatorEnum operatorEnum, String paramValue) { + Map springExpressionVariables = new HashMap<>(); + + // 设置源值 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue); + + // 处理参数值 + if (StrUtil.isNotBlank(paramValue)) { + List parameterValues = StrUtil.splitTrim(paramValue, CharPool.COMMA); + + // 设置原始参数值 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, paramValue); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, parameterValues); + + // 特殊处理:解决数字比较问题 + // Spring 表达式基于 compareTo 方法,对数字的比较存在问题,需要转换为数字类型 + if (isNumericComparisonOperator(operatorEnum) && isNumericComparison(sourceValue, parameterValues)) { + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, + NumberUtil.parseDouble(String.valueOf(sourceValue))); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, + NumberUtil.parseDouble(paramValue)); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, + convertList(parameterValues, NumberUtil::parseDouble)); + } + } + + return springExpressionVariables; + } + + /** + * 判断是否为数字比较操作符 + */ + private static boolean isNumericComparisonOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return ObjectUtils.equalsAny(operatorEnum, + IotSceneRuleConditionOperatorEnum.BETWEEN, + IotSceneRuleConditionOperatorEnum.NOT_BETWEEN, + IotSceneRuleConditionOperatorEnum.GREATER_THAN, + IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS, + IotSceneRuleConditionOperatorEnum.LESS_THAN, + IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS); + } + + /** + * 判断是否为数字比较场景 + */ + private static boolean isNumericComparison(Object sourceValue, List parameterValues) { + return NumberUtil.isNumber(String.valueOf(sourceValue)) && NumberUtils.isAllNumber(parameterValues); + } + + // ========== 【触发器】相关工具方法 ========== + + /** + * 检查基础触发器参数是否有效 + * + * @param trigger 触发器配置 + * @return 是否有效 + */ + public static boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) { + return trigger != null && trigger.getType() != null; + } + + /** + * 检查触发器操作符和值是否有效 + * + * @param trigger 触发器配置 + * @return 是否有效 + */ + public static boolean isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { + return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); + } + + /** + * 记录触发器匹配成功日志 + * + * @param message 设备消息 + * @param trigger 触发器配置 + */ + public static void logTriggerMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + log.debug("[isMatched][message({}) trigger({}) 匹配触发器成功]", message.getRequestId(), trigger.getType()); + } + + /** + * 记录触发器匹配失败日志 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @param reason 失败原因 + */ + public static void logTriggerMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { + log.debug("[isMatched][message({}) trigger({}) reason({}) 匹配触发器失败]", message.getRequestId(), trigger.getType(), reason); + } + + // ========== 【条件】相关工具方法 ========== + + /** + * 检查基础条件参数是否有效 + * + * @param condition 触发条件 + * @return 是否有效 + */ + public static boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) { + return condition != null && condition.getType() != null; + } + + /** + * 检查条件操作符和参数是否有效 + * + * @param condition 触发条件 + * @return 是否有效 + */ + public static boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) { + return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam()); + } + + /** + * 记录条件匹配成功日志 + * + * @param message 设备消息 + * @param condition 触发条件 + */ + public static void logConditionMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + log.debug("[isMatched][message({}) condition({}) 匹配条件成功]", message.getRequestId(), condition.getType()); + } + + /** + * 记录条件匹配失败日志 + * + * @param message 设备消息 + * @param condition 触发条件 + * @param reason 失败原因 + */ + public static void logConditionMatchFailure(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, String reason) { + log.debug("[isMatched][message({}) condition({}) reason({}) 匹配条件失败]", message.getRequestId(), condition.getType(), reason); + } + + // ========== 【通用】工具方法 ========== + + /** + * 检查标识符是否匹配 + * + * @param expectedIdentifier 期望的标识符 + * @param actualIdentifier 实际的标识符 + * @return 是否匹配 + */ + public static boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { + return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java new file mode 100644 index 000000000..3658fc07c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java @@ -0,0 +1,159 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotSceneRuleConditionMatcher; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.Function; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IoT 场景规则匹配器统一管理器 + *

+ * 负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotSceneRuleMatcherManager { + + /** + * 触发器匹配器映射表 + */ + private final Map triggerMatchers; + + /** + * 条件匹配器映射表 + */ + private final Map conditionMatchers; + + public IotSceneRuleMatcherManager(List matchers) { + if (CollUtil.isEmpty(matchers)) { + log.warn("[IotSceneRuleMatcherManager][没有找到任何匹配器]"); + this.triggerMatchers = new HashMap<>(); + this.conditionMatchers = new HashMap<>(); + return; + } + + // 按优先级排序并过滤启用的匹配器 + List allMatchers = matchers.stream() + .filter(IotSceneRuleMatcher::isEnabled) + .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority)) + .toList(); + + // 分离触发器匹配器和条件匹配器 + List triggerMatchers = allMatchers.stream() + .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher) + .map(matcher -> (IotSceneRuleTriggerMatcher) matcher) + .toList(); + List conditionMatchers = allMatchers.stream() + .filter(matcher -> matcher instanceof IotSceneRuleConditionMatcher) + .map(matcher -> (IotSceneRuleConditionMatcher) matcher) + .toList(); + + // 构建触发器匹配器映射表 + this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType, + Function.identity(), + (existing, replacement) -> { + log.warn("[IotSceneRuleMatcherManager][触发器类型({})存在多个匹配器,使用优先级更高的: {}]", + existing.getSupportedTriggerType(), + existing.getPriority() <= replacement.getPriority() ? + existing.getSupportedTriggerType() : replacement.getSupportedTriggerType()); + return existing.getPriority() <= replacement.getPriority() ? existing : replacement; + }, LinkedHashMap::new); + // 构建条件匹配器映射表 + this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType, + Function.identity(), + (existing, replacement) -> { + log.warn("[IotSceneRuleMatcherManager][条件类型({})存在多个匹配器,使用优先级更高的: {}]", + existing.getSupportedConditionType(), + existing.getPriority() <= replacement.getPriority() ? + existing.getSupportedConditionType() : replacement.getSupportedConditionType()); + return existing.getPriority() <= replacement.getPriority() ? existing : replacement; + }, + LinkedHashMap::new); + + // 日志输出初始化信息 + log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]", + allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size()); + this.triggerMatchers.forEach((type, matcher) -> + log.info("[IotSceneRuleMatcherManager][触发器匹配器类型: ({}), 优先级: ({})] ", type, matcher.getPriority())); + this.conditionMatchers.forEach((type, matcher) -> + log.info("[IotSceneRuleMatcherManager][条件匹配器类型: ({}), 优先级: ({})]", type, matcher.getPriority())); + } + + /** + * 检查触发器是否匹配消息(主条件匹配) + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @return 是否匹配 + */ + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + if (message == null || trigger == null || trigger.getType() == null) { + log.debug("[isMatched][message({}) trigger({}) 参数无效]", message, trigger); + return false; + } + IotSceneRuleTriggerTypeEnum triggerType = IotSceneRuleTriggerTypeEnum.typeOf(trigger.getType()); + if (triggerType == null) { + log.warn("[isMatched][triggerType({}) 未知的触发器类型]", trigger.getType()); + return false; + } + IotSceneRuleTriggerMatcher matcher = triggerMatchers.get(triggerType); + if (matcher == null) { + log.warn("[isMatched][triggerType({}) 没有对应的匹配器]", triggerType); + return false; + } + + try { + return matcher.matches(message, trigger); + } catch (Exception e) { + log.error("[isMatched][触发器匹配异常] message: {}, trigger: {}", message, trigger, e); + return false; + } + } + + /** + * 检查子条件是否匹配消息 + * + * @param message 设备消息 + * @param condition 触发条件 + * @return 是否匹配 + */ + public boolean isConditionMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + if (message == null || condition == null || condition.getType() == null) { + log.debug("[isConditionMatched][message({}) condition({}) 参数无效]", message, condition); + return false; + } + + // 根据条件类型查找对应的匹配器 + IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType()); + if (conditionType == null) { + log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType()); + return false; + } + IotSceneRuleConditionMatcher matcher = conditionMatchers.get(conditionType); + if (matcher == null) { + log.warn("[isConditionMatched][conditionType({}) 没有对应的匹配器]", conditionType); + return false; + } + + // 执行匹配逻辑 + try { + return matcher.matches(message, condition); + } catch (Exception e) { + log.error("[isConditionMatched][message({}) condition({}) 条件匹配异常]", message, condition, e); + return false; + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java new file mode 100644 index 000000000..81c8fba59 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java @@ -0,0 +1,229 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * 当前时间条件匹配器 + *

+ * 处理时间相关的子条件匹配逻辑 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { + + /** + * 时间格式化器 - HH:mm:ss + */ + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * 时间格式化器 - HH:mm + */ + private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm"); + + @Override + public IotSceneRuleConditionTypeEnum getSupportedConditionType() { + return IotSceneRuleConditionTypeEnum.CURRENT_TIME; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicConditionValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); + return false; + } + + // 1.2 检查操作符和参数是否有效 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); + return false; + } + + // 1.3 验证操作符是否为支持的时间操作符 + String operator = condition.getOperator(); + IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator); + if (operatorEnum == null) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "无效的操作符: " + operator); + return false; + } + + if (!isTimeOperator(operatorEnum)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); + return false; + } + + // 2.1 执行时间匹配 + boolean matched = executeTimeMatching(operatorEnum, condition.getParam()); + + // 2.2 记录匹配结果 + if (matched) { + IotSceneRuleMatcherHelper.logConditionMatchSuccess(message, condition); + } else { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "时间条件不匹配"); + } + + return matched; + } + + /** + * 执行时间匹配逻辑 + * 直接实现时间条件匹配,不使用 Spring EL 表达式 + */ + private boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + LocalDateTime now = LocalDateTime.now(); + + if (isDateTimeOperator(operatorEnum)) { + // 日期时间匹配(时间戳) + long currentTimestamp = now.toEpochSecond(java.time.ZoneOffset.of("+8")); + return matchDateTime(currentTimestamp, operatorEnum, param); + } else { + // 当日时间匹配(HH:mm:ss) + return matchTime(now.toLocalTime(), operatorEnum, param); + } + } catch (Exception e) { + log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 判断是否为日期时间操作符 + */ + private boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN; + } + + /** + * 判断是否为时间操作符 + */ + private boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN || + isDateTimeOperator(operatorEnum); + } + + /** + * 匹配日期时间(时间戳) + * 直接实现时间戳比较逻辑 + */ + private boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + long targetTimestamp = Long.parseLong(param); + switch (operatorEnum) { + case DATE_TIME_GREATER_THAN: + return currentTimestamp > targetTimestamp; + case DATE_TIME_LESS_THAN: + return currentTimestamp < targetTimestamp; + case DATE_TIME_BETWEEN: + return matchDateTimeBetween(currentTimestamp, param); + default: + log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum); + return false; + } + } catch (Exception e) { + log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 匹配日期时间区间 + */ + private boolean matchDateTimeBetween(long currentTimestamp, String param) { + List timestampRange = StrUtil.splitTrim(param, CharPool.COMMA); + if (timestampRange.size() != 2) { + log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param); + return false; + } + long startTimestamp = Long.parseLong(timestampRange.get(0).trim()); + long endTimestamp = Long.parseLong(timestampRange.get(1).trim()); + return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp; + } + + /** + * 匹配当日时间(HH:mm:ss) + * 直接实现时间比较逻辑 + */ + private boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + LocalTime targetTime = parseTime(param); + switch (operatorEnum) { + case TIME_GREATER_THAN: + return currentTime.isAfter(targetTime); + case TIME_LESS_THAN: + return currentTime.isBefore(targetTime); + case TIME_BETWEEN: + return matchTimeBetween(currentTime, param); + default: + log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum); + return false; + } + } catch (Exception e) { + log.error("[matchTime][][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 匹配时间区间 + */ + private boolean matchTimeBetween(LocalTime currentTime, String param) { + List timeRange = StrUtil.splitTrim(param, CharPool.COMMA); + if (timeRange.size() != 2) { + log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param); + return false; + } + LocalTime startTime = parseTime(timeRange.get(0).trim()); + LocalTime endTime = parseTime(timeRange.get(1).trim()); + return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime); + } + + /** + * 解析时间字符串 + * 支持 HH:mm 和 HH:mm:ss 两种格式 + */ + private LocalTime parseTime(String timeStr) { + Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空"); + + try { + // 尝试不同的时间格式 + if (timeStr.length() == 5) { // HH:mm + return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT); + } else if (timeStr.length() == 8) { // HH:mm:ss + return LocalTime.parse(timeStr, TIME_FORMATTER); + } else { + throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式"); + } + } catch (Exception e) { + log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e); + throw new IllegalArgumentException("时间格式无效: " + timeStr, e); + } + } + + @Override + public int getPriority() { + return 40; // 较低优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java new file mode 100644 index 000000000..4a8a8ab6f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备属性条件匹配器 + *

+ * 处理设备属性相关的子条件匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { + + @Override + public IotSceneRuleConditionTypeEnum getSupportedConditionType() { + return IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicConditionValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); + return false; + } + + // 1.2 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(condition.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "标识符不匹配,期望: " + condition.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 1.3 检查操作符和参数是否有效 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); + return false; + } + + // 2.1. 获取属性值 + Object propertyValue = message.getParams(); + if (propertyValue == null) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(propertyValue, condition.getOperator(), condition.getParam()); + if (matched) { + IotSceneRuleMatcherHelper.logConditionMatchSuccess(message, condition); + } else { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "设备属性条件不匹配"); + } + return matched; + } + + @Override + public int getPriority() { + return 25; // 中等优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java new file mode 100644 index 000000000..d5bb97a53 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备状态条件匹配器 + *

+ * 处理设备状态相关的子条件匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { + + @Override + public IotSceneRuleConditionTypeEnum getSupportedConditionType() { + return IotSceneRuleConditionTypeEnum.DEVICE_STATE; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicConditionValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); + return false; + } + + // 1.2 检查操作符和参数是否有效 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); + return false; + } + + // 2.1 获取设备状态值 + Object stateValue = message.getParams(); + if (stateValue == null) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, condition.getOperator(), condition.getParam()); + if (matched) { + IotSceneRuleMatcherHelper.logConditionMatchSuccess(message, condition); + } else { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "设备状态条件不匹配"); + } + return matched; + } + + @Override + public int getPriority() { + return 30; // 中等优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java new file mode 100644 index 000000000..875e8b156 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; + +/** + * IoT 场景规则条件匹配器接口 + *

+ * 专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等 + *

+ * 条件匹配器负责判断设备消息是否满足场景规则的附加条件, + * 在触发器匹配成功后进行进一步的条件筛选 + * + * @author HUIHUI + */ +public interface IotSceneRuleConditionMatcher extends IotSceneRuleMatcher { + + /** + * 获取支持的条件类型 + * + * @return 条件类型枚举 + */ + IotSceneRuleConditionTypeEnum getSupportedConditionType(); + + /** + * 检查条件是否匹配消息 + *

+ * 判断设备消息是否满足指定的触发条件 + * + * @param message 设备消息 + * @param condition 触发条件 + * @return 是否匹配 + */ + boolean matches(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java new file mode 100644 index 000000000..1ab1bb9d2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备事件上报触发器匹配器 + *

+ * 处理设备事件上报的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.EVENT_POST.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + + IotDeviceMessageMethodEnum.EVENT_POST.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配 + // 但如果配置了操作符和值,则需要进行条件匹配 + if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { + Object eventData = message.getData(); + if (eventData == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中事件数据为空"); + return false; + } + + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(eventData, trigger.getOperator(), trigger.getValue()); + if (!matched) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "事件数据条件不匹配"); + return false; + } + } + + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + return true; + } + + @Override + public int getPriority() { + return 30; // 中等优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java new file mode 100644 index 000000000..6eccdab42 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备属性上报触发器匹配器 + *

+ * 处理设备属性数据上报的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 1.4 检查操作符和值是否有效 + if (!IotSceneRuleMatcherHelper.isTriggerOperatorAndValueValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "操作符或值无效"); + return false; + } + + // 2.1 获取属性值 + Object propertyValue = message.getParams(); + if (propertyValue == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(propertyValue, trigger.getOperator(), trigger.getValue()); + if (matched) { + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + } else { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "属性值条件不匹配"); + } + return matched; + } + + @Override + public int getPriority() { + return 20; // 中等优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java new file mode 100644 index 000000000..e0caba2d3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备服务调用触发器匹配器 + *

+ * 处理设备服务调用的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可 + // 不需要检查操作符和值,因为服务调用本身就是触发条件 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件 + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + return true; + } + + @Override + public int getPriority() { + return 40; // 较低优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java new file mode 100644 index 000000000..edd3c4e90 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备状态更新触发器匹配器 + *

+ * 处理设备上下线状态变更的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + + IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查操作符和值是否有效 + if (!IotSceneRuleMatcherHelper.isTriggerOperatorAndValueValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "操作符或值无效"); + return false; + } + + // 2.1 获取设备状态值 + Object stateValue = message.getParams(); + if (stateValue == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + // TODO @puhui999: 状态匹配重新实现 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); + if (matched) { + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + } else { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "状态值条件不匹配"); + } + return matched; + } + + @Override + public int getPriority() { + return 10; // 高优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java new file mode 100644 index 000000000..89de00a68 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; + +/** + * IoT 场景规则触发器匹配器接口 + *

+ * 专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等 + *

+ * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件, + * 是场景规则执行的第一道门槛 + * + * @author HUIHUI + */ +public interface IotSceneRuleTriggerMatcher extends IotSceneRuleMatcher { + + /** + * 获取支持的触发器类型 + * + * @return 触发器类型枚举 + */ + IotSceneRuleTriggerTypeEnum getSupportedTriggerType(); + + /** + * 检查触发器是否匹配消息 + *

+ * 判断设备消息是否满足指定的触发器条件 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @return 是否匹配 + */ + boolean matches(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger); + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java new file mode 100644 index 000000000..794f8d6ae --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.quartz.CronExpression; +import org.springframework.stereotype.Component; + +/** + * 定时触发器匹配器 + *

+ * 处理定时触发的触发器匹配逻辑 + * 注意:定时触发器不依赖设备消息,主要用于定时任务场景 + * + * @author HUIHUI + */ +@Component +public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.TIMER; + } + + @Override + public boolean matches(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查 CRON 表达式是否存在 + if (StrUtil.isBlank(trigger.getCronExpression())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "定时触发器缺少 CRON 表达式"); + return false; + } + + // 1.3 定时触发器通常不依赖具体的设备消息 + // 它是通过定时任务调度器触发的,这里主要是验证配置的有效性 + if (!CronExpression.isValidExpression(trigger.getCronExpression())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "CRON 表达式格式无效: " + trigger.getCronExpression()); + return false; + } + + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + return true; + } + + @Override + public int getPriority() { + return 50; // 最低优先级,因为定时触发器不依赖消息 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java rename to yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java index 8834772d3..b8c951b94 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java @@ -7,8 +7,9 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelS import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; import jakarta.validation.Valid; -import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; +import java.util.Set; /** * IoT 产品物模型 Service 接口 @@ -55,15 +56,33 @@ public interface IotThingModelService { */ List getThingModelListByProductId(Long productId); + /** + * 获得产品物模型列表 + * + * @param productId 产品编号 + * @param identifiers 功能标识列表 + * @return 产品物模型列表 + */ + List getThingModelListByProductIdAndIdentifiers(Long productId, Collection identifiers); + + /** + * 获得产品物模型列表 + * + * @param productId 产品编号 + * @param type 物模型类型 + * @return 产品物模型列表 + */ + List getThingModelListByProductIdAndType(Long productId, Integer type); + /** * 【缓存】获得产品物模型列表 * * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! * - * @param productKey 产品标识 + * @param productId 产品编号 * @return 产品物模型列表 */ - List getThingModelListByProductKeyFromCache(String productKey); + List getThingModelListByProductIdFromCache(Long productId); /** * 获得产品物模型分页 @@ -81,13 +100,12 @@ public interface IotThingModelService { */ List getThingModelList(IotThingModelListReqVO reqVO); - // TODO @super:用不到,删除下哈。 /** - * 获得物模型数量 + * 批量校验物模型存在 * - * @param createTime 创建时间,如果为空,则统计所有物模型数量 - * @return 物模型数量 + * @param productId 产品编号 + * @param identifiers 标识符集合 */ - Long getThingModelCount(LocalDateTime createTime); + void validateThingModelListExists(Long productId, Set identifiers); } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java new file mode 100644 index 000000000..ca04ecd5f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -0,0 +1,217 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 产品物模型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotThingModelServiceImpl implements IotThingModelService { + + @Resource + private IotThingModelMapper thingModelMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotProductService productService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createThingModel(IotThingModelSaveReqVO createReqVO) { + // 1.1 校验功能标识符在同一产品下是否唯一 + validateIdentifierUnique(null, createReqVO.getProductId(), createReqVO.getIdentifier()); + // 1.2 功能名称在同一产品下是否唯一 + validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); + // 1.3 校验产品状态,发布状态下,不允许新增功能 + validateProductStatus(createReqVO.getProductId()); + + // 2. 插入数据库 + IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(createReqVO); + thingModelMapper.insert(thingModel); + + // 3. 删除缓存 + deleteThingModelListCache(createReqVO.getProductId()); + return thingModel.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateThingModel(IotThingModelSaveReqVO updateReqVO) { + // 1.1 校验功能是否存在 + validateProductThingModelMapperExists(updateReqVO.getId()); + // 1.2 校验功能标识符是否唯一 + validateIdentifierUnique(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); + // 1.3 校验产品状态,发布状态下,不允许操作功能 + validateProductStatus(updateReqVO.getProductId()); + + // 2. 更新数据库 + IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); + thingModelMapper.updateById(thingModel); + + // 3. 删除缓存 + deleteThingModelListCache(updateReqVO.getProductId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteThingModel(Long id) { + // 1.1 校验功能是否存在 + IotThingModelDO thingModel = thingModelMapper.selectById(id); + if (thingModel == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + // 1.2 校验产品状态,发布状态下,不允许操作功能 + validateProductStatus(thingModel.getProductId()); + + // 2. 删除功能 + thingModelMapper.deleteById(id); + + // 3. 删除缓存 + deleteThingModelListCache(thingModel.getProductId()); + } + + @Override + public IotThingModelDO getThingModel(Long id) { + return thingModelMapper.selectById(id); + } + + @Override + public List getThingModelListByProductId(Long productId) { + return thingModelMapper.selectListByProductId(productId); + } + + @Override + public List getThingModelListByProductIdAndIdentifiers(Long productId, Collection identifiers) { + return thingModelMapper.selectListByProductIdAndIdentifiers(productId, identifiers); + } + + @Override + public List getThingModelListByProductIdAndType(Long productId, Integer type) { + return thingModelMapper.selectListByProductIdAndType(productId, type); + } + + @Override + @Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productId") + @TenantIgnore // 忽略租户信息 + public List getThingModelListByProductIdFromCache(Long productId) { + return thingModelMapper.selectListByProductId(productId); + } + + @Override + public PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO) { + return thingModelMapper.selectPage(pageReqVO); + } + + @Override + public List getThingModelList(IotThingModelListReqVO reqVO) { + return thingModelMapper.selectList(reqVO); + } + + @Override + public void validateThingModelListExists(Long productId, Set identifiers) { + if (CollUtil.isEmpty(identifiers)) { + return; + } + List thingModels = thingModelMapper.selectListByProductIdAndIdentifiers( + productId, identifiers); + Set foundIdentifiers = convertSet(thingModels, IotThingModelDO::getIdentifier); + for (String identifier : identifiers) { + if (!foundIdentifiers.contains(identifier)) { + throw exception(THING_MODEL_NOT_EXISTS); + } + } + } + + private void validateProductThingModelMapperExists(Long id) { + if (thingModelMapper.selectById(id) == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + } + + private void validateIdentifierUnique(Long id, Long productId, String identifier) { + // 1. 情况一:创建时校验 + if (id == null) { + // 1.1 系统保留字段,不能用于标识符定义 + if (StrUtil.equalsAny(identifier, "set", "get", "post", "property", "event", "time", "value")) { + throw exception(THING_MODEL_IDENTIFIER_INVALID); + } + // 1.2 校验唯一 + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); + if (thingModel != null) { + throw exception(THING_MODEL_IDENTIFIER_EXISTS); + } + return; + } + + // 2. 情况二:更新时校验 + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); + if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) { + throw exception(THING_MODEL_IDENTIFIER_EXISTS); + } + } + + private void validateProductStatus(Long createReqVO) { + IotProductDO product = productService.validateProductExists(createReqVO); + if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { + throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL); + } + } + + private void validateNameUnique(Long productId, String name) { + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndName(productId, name); + if (thingModel != null) { + throw exception(THING_MODEL_NAME_EXISTS); + } + } + + private void deleteThingModelListCache(Long productId) { + // 保证 Spring AOP 触发 + getSelf().deleteThingModelListCache0(productId); + } + + @CacheEvict(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productId") + @TenantIgnore // 忽略租户信息 + public void deleteThingModelListCache0(Long productId) { + } + + private IotThingModelServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-dev.yaml b/yudao-module-iot/yudao-module-iot-server/src/main/resources/application-dev.yaml similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-dev.yaml rename to yudao-module-iot/yudao-module-iot-server/src/main/resources/application-dev.yaml index 7c7cb0dff..076c0f112 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-dev.yaml +++ b/yudao-module-iot/yudao-module-iot-server/src/main/resources/application-dev.yaml @@ -136,9 +136,4 @@ spring: # 日志文件配置 logging: file: - name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 - ---- #################### iot相关配置 TODO 芋艿【IOT】:再瞅瞅 #################### -pf4j: - # pluginsDir: /tmp/ - pluginsDir: ../plugins \ No newline at end of file + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml b/yudao-module-iot/yudao-module-iot-server/src/main/resources/application-local.yaml similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml rename to yudao-module-iot/yudao-module-iot-server/src/main/resources/application-local.yaml index db74dd066..22e8537c7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml +++ b/yudao-module-iot/yudao-module-iot-server/src/main/resources/application-local.yaml @@ -45,7 +45,7 @@ spring: min-idle: 1 # 最小连接池数量 max-active: 20 # 最大连接池数量 max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 @@ -147,9 +147,4 @@ logging: # 配置自己写的 MyBatis Mapper 打印日志 cn.iocoder.yudao.module.iot.dal.mysql: debug cn.iocoder.yudao.module.iot.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info - org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 - ---- #################### iot相关配置 TODO 芋艿【IOT】:再瞅瞅 #################### -pf4j: - # pluginsDir: /tmp/ - pluginsDir: ../plugins \ No newline at end of file + org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-server/src/main/resources/application.yaml similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/main/resources/application.yaml rename to yudao-module-iot/yudao-module-iot-server/src/main/resources/application.yaml index a3666cf12..8e2036988 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-server/src/main/resources/application.yaml @@ -150,8 +150,8 @@ yudao: ignore-caches: - iot:device - iot:thing_model_list + iot: + message-bus: + type: redis # 消息总线的类型 -debug: false -# 插件配置 TODO 芋艿:【IOT】需要处理下 -pf4j: - pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录 \ No newline at end of file +debug: false \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/logback-spring.xml b/yudao-module-iot/yudao-module-iot-server/src/main/resources/logback-spring.xml similarity index 100% rename from yudao-module-iot/yudao-module-iot-biz/src/main/resources/logback-spring.xml rename to yudao-module-iot/yudao-module-iot-server/src/main/resources/logback-spring.xml diff --git a/yudao-module-iot/yudao-module-iot-server/src/main/resources/mapper/device/IotDeviceMessageMapper.xml b/yudao-module-iot/yudao-module-iot-server/src/main/resources/mapper/device/IotDeviceMessageMapper.xml new file mode 100644 index 000000000..deef23b5c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/main/resources/mapper/device/IotDeviceMessageMapper.xml @@ -0,0 +1,109 @@ + + + + + + CREATE STABLE IF NOT EXISTS device_message ( + ts TIMESTAMP, + id NCHAR(50), + report_time TIMESTAMP, + tenant_id BIGINT, + server_id NCHAR(50), + upstream BOOL, + reply BOOL, + identifier NCHAR(100), + request_id NCHAR(50), + method NCHAR(100), + params NCHAR(2048), + data NCHAR(2048), + code INT, + msg NCHAR(256) + ) TAGS ( + device_id BIGINT + ) + + + + + + INSERT INTO device_message_${deviceId} ( + ts, id, report_time, tenant_id, server_id, + upstream, reply, identifier, request_id, method, + params, data, code, msg + ) + USING device_message + TAGS (#{deviceId}) + VALUES ( + NOW, #{id}, #{reportTime}, #{tenantId}, #{serverId}, + #{upstream}, #{reply}, #{identifier}, #{requestId}, #{method}, + #{params}, #{data}, #{code}, #{msg} + ) + + + + + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml b/yudao-module-iot/yudao-module-iot-server/src/main/resources/mapper/device/IotDevicePropertyMapper.xml similarity index 76% rename from yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml rename to yudao-module-iot/yudao-module-iot-server/src/main/resources/mapper/device/IotDevicePropertyMapper.xml index bdc40e833..fc2d3662f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml +++ b/yudao-module-iot/yudao-module-iot-server/src/main/resources/mapper/device/IotDevicePropertyMapper.xml @@ -5,11 +5,11 @@ - CREATE STABLE product_property_${productKey} ( + CREATE STABLE product_property_${productId} ( ts TIMESTAMP, report_time TIMESTAMP, @@ -20,12 +20,12 @@ ) TAGS ( - device_key NCHAR(50) + device_id BIGINT ) - ALTER STABLE product_property_${productKey} + ALTER STABLE product_property_${productId} ADD COLUMN ${field.field} ${field.type} (${field.length}) @@ -33,7 +33,7 @@ - ALTER STABLE product_property_${productKey} + ALTER STABLE product_property_${productId} MODIFY COLUMN ${field.field} ${field.type} (${field.length}) @@ -41,14 +41,14 @@ - ALTER STABLE product_property_${productKey} + ALTER STABLE product_property_${productId} DROP COLUMN ${field.field} - INSERT INTO device_property_${device.deviceKey} - USING product_property_${device.productKey} - TAGS ('${device.deviceKey}') + INSERT INTO device_property_${device.id} + USING product_property_${device.productId} + TAGS ('${device.id}') (ts, report_time, ${@cn.hutool.core.util.StrUtil@toUnderlineCase(key)} @@ -63,12 +63,13 @@ - SELECT ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} AS `value`, ts AS update_time - FROM device_property_${reqVO.deviceKey} + FROM device_property_${reqVO.deviceId} WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])} AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java new file mode 100644 index 000000000..055ccb01b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.*; +import cn.iocoder.yudao.module.iot.service.rule.data.action.*; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * {@link IotDataRuleAction} 实现类的单元测试 + * + * @author HUIHUI + */ +@Disabled // 默认禁用,需要手动启用测试 +@Slf4j +public class IotDataBridgeExecuteTest extends BaseMockitoUnitTest { + + private IotDeviceMessage message; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private IotHttpDataSinkAction httpDataBridgeExecute; + + @BeforeEach + public void setUp() { + // TODO @芋艿:@puhui999:需要调整下; + // 创建共享的测试消息 + //message = IotDeviceMessage.builder().messageId("TEST-001").reportTime(LocalDateTime.now()) + // .productKey("testProduct").deviceName("testDevice") + // .type("property").identifier("temperature").data("{\"value\": 60}") + // .build(); + } + + @Test + public void testKafkaMQDataBridge() throws Exception { + // 1. 创建执行器实例 + IotKafkaDataRuleAction action = new IotKafkaDataRuleAction(); + + // 2. 创建配置 + IotDataSinkKafkaConfig config = new IotDataSinkKafkaConfig() + .setBootstrapServers("127.0.0.1:9092") + .setTopic("test-topic") + .setSsl(false) + .setUsername(null) + .setPassword(null); + + // 3. 执行测试并验证缓存 + executeAndVerifyCache(action, config, "KafkaMQ"); + } + + @Test + public void testRabbitMQDataBridge() throws Exception { + // 1. 创建执行器实例 + IotRabbitMQDataRuleAction action = new IotRabbitMQDataRuleAction(); + + // 2. 创建配置 + IotDataSinkRabbitMQConfig config = new IotDataSinkRabbitMQConfig() + .setHost("localhost") + .setPort(5672) + .setVirtualHost("/") + .setUsername("admin") + .setPassword("123456") + .setExchange("test-exchange") + .setRoutingKey("test-key") + .setQueue("test-queue"); + + // 3. 执行测试并验证缓存 + executeAndVerifyCache(action, config, "RabbitMQ"); + } + + @Test + public void testRedisDataBridge() throws Exception { + // 1. 创建执行器实例 + IotRedisRuleAction action = new IotRedisRuleAction(); + + // 2. 创建配置 - 测试 Stream 数据结构 + IotDataSinkRedisConfig config = new IotDataSinkRedisConfig(); + config.setHost("127.0.0.1"); + config.setPort(6379); + config.setDatabase(0); + config.setPassword("123456"); + config.setTopic("test-stream"); + config.setDataStructure(1); // Stream 类型 + + // 3. 执行测试并验证缓存 + executeAndVerifyCache(action, config, "Redis"); + } + + @Test + public void testRocketMQDataBridge() throws Exception { + // 1. 创建执行器实例 + IotRocketMQDataRuleAction action = new IotRocketMQDataRuleAction(); + + // 2. 创建配置 + IotDataSinkRocketMQConfig config = new IotDataSinkRocketMQConfig() + .setNameServer("127.0.0.1:9876") + .setGroup("test-group") + .setTopic("test-topic") + .setTags("test-tag"); + + // 3. 执行测试并验证缓存 + executeAndVerifyCache(action, config, "RocketMQ"); + } + + @Test + public void testHttpDataBridge() throws Exception { + // 1. 配置 RestTemplate mock 返回成功响应 + when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(), any(Class.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + // 2. 创建配置 + IotDataSinkHttpConfig config = new IotDataSinkHttpConfig() + .setUrl("https://doc.iocoder.cn/").setMethod(HttpMethod.GET.name()); + + // 3. 执行测试 + log.info("[testHttpDataBridge][执行HTTP数据桥接测试]"); + httpDataBridgeExecute.execute(message, new IotDataSinkDO() + .setType(httpDataBridgeExecute.getType()).setConfig(config)); + } + + /** + * 执行测试并验证缓存的通用方法 + * + * @param action 执行器实例 + * @param config 配置对象 + * @param type MQ 类型 + * @throws Exception 如果执行过程中发生异常 + */ + private void executeAndVerifyCache(IotDataRuleAction action, IotAbstractDataSinkConfig config, String type) + throws Exception { + log.info("[test{}DataBridge][第一次执行,应该会创建新的 producer]", type); + action.execute(message, new IotDataSinkDO().setType(action.getType()).setConfig(config)); + + log.info("[test{}DataBridge][第二次执行,应该会复用缓存的 producer]", type); + action.execute(message, new IotDataSinkDO().setType(action.getType()).setConfig(config)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java new file mode 100644 index 000000000..056794b79 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java @@ -0,0 +1,211 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * {@link IotSceneRuleServiceImpl} 的简化单元测试类 + * 使用 Mockito 进行纯单元测试,不依赖 Spring 容器 + * + * @author 芋道源码 + */ +public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { + + @InjectMocks + private IotSceneRuleServiceImpl sceneRuleService; + + @Mock + private IotSceneRuleMapper sceneRuleMapper; + + @Mock + private List sceneRuleActions; + + @Mock + private IotSchedulerManager schedulerManager; + + @Mock + private IotProductService productService; + + @Mock + private IotDeviceService deviceService; + + @Test + public void testCreateScene_Rule_success() { + // 准备参数 + IotSceneRuleSaveReqVO createReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> { + o.setId(null); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setTriggers(Collections.singletonList(randomPojo(IotSceneRuleDO.Trigger.class))); + o.setActions(Collections.singletonList(randomPojo(IotSceneRuleDO.Action.class))); + }); + + // Mock 行为 + Long expectedId = randomLongId(); + when(sceneRuleMapper.insert(any(IotSceneRuleDO.class))).thenAnswer(invocation -> { + IotSceneRuleDO sceneRule = invocation.getArgument(0); + sceneRule.setId(expectedId); + return 1; + }); + + // 调用 + Long sceneRuleId = sceneRuleService.createSceneRule(createReqVO); + + // 断言 + assertEquals(expectedId, sceneRuleId); + verify(sceneRuleMapper, times(1)).insert(any(IotSceneRuleDO.class)); + } + + @Test + public void testUpdateScene_Rule_success() { + // 准备参数 + Long id = randomLongId(); + IotSceneRuleSaveReqVO updateReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> { + o.setId(id); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setTriggers(Collections.singletonList(randomPojo(IotSceneRuleDO.Trigger.class))); + o.setActions(Collections.singletonList(randomPojo(IotSceneRuleDO.Action.class))); + }); + + // Mock 行为 + IotSceneRuleDO existingSceneRule = randomPojo(IotSceneRuleDO.class, o -> o.setId(id)); + when(sceneRuleMapper.selectById(id)).thenReturn(existingSceneRule); + when(sceneRuleMapper.updateById(any(IotSceneRuleDO.class))).thenReturn(1); + + // 调用 + assertDoesNotThrow(() -> sceneRuleService.updateSceneRule(updateReqVO)); + + // 验证 + verify(sceneRuleMapper, times(1)).selectById(id); + verify(sceneRuleMapper, times(1)).updateById(any(IotSceneRuleDO.class)); + } + + @Test + public void testDeleteSceneRule_success() { + // 准备参数 + Long id = randomLongId(); + + // Mock 行为 + IotSceneRuleDO existingSceneRule = randomPojo(IotSceneRuleDO.class, o -> o.setId(id)); + when(sceneRuleMapper.selectById(id)).thenReturn(existingSceneRule); + when(sceneRuleMapper.deleteById(id)).thenReturn(1); + + // 调用 + assertDoesNotThrow(() -> sceneRuleService.deleteSceneRule(id)); + + // 验证 + verify(sceneRuleMapper, times(1)).selectById(id); + verify(sceneRuleMapper, times(1)).deleteById(id); + } + + @Test + public void testGetSceneRule() { + // 准备参数 + Long id = randomLongId(); + IotSceneRuleDO expectedSceneRule = randomPojo(IotSceneRuleDO.class, o -> o.setId(id)); + + // Mock 行为 + when(sceneRuleMapper.selectById(id)).thenReturn(expectedSceneRule); + + // 调用 + IotSceneRuleDO result = sceneRuleService.getSceneRule(id); + + // 断言 + assertEquals(expectedSceneRule, result); + verify(sceneRuleMapper, times(1)).selectById(id); + } + + @Test + public void testUpdateSceneRuleStatus_success() { + // 准备参数 + Long id = randomLongId(); + Integer status = CommonStatusEnum.DISABLE.getStatus(); + + // Mock 行为 + IotSceneRuleDO existingSceneRule = randomPojo(IotSceneRuleDO.class, o -> { + o.setId(id); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(sceneRuleMapper.selectById(id)).thenReturn(existingSceneRule); + when(sceneRuleMapper.updateById(any(IotSceneRuleDO.class))).thenReturn(1); + + // 调用 + assertDoesNotThrow(() -> sceneRuleService.updateSceneRuleStatus(id, status)); + + // 验证 + verify(sceneRuleMapper, times(1)).selectById(id); + verify(sceneRuleMapper, times(1)).updateById(any(IotSceneRuleDO.class)); + } + + @Test + public void testExecuteSceneRuleByTimer_success() { + // 准备参数 + Long id = randomLongId(); + + // Mock 行为 + IotSceneRuleDO sceneRule = randomPojo(IotSceneRuleDO.class, o -> { + o.setId(id); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(sceneRuleMapper.selectById(id)).thenReturn(sceneRule); + + // 调用 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(id)); + + // 验证 + verify(sceneRuleMapper, times(1)).selectById(id); + } + + @Test + public void testExecuteSceneRuleByTimer_notExists() { + // 准备参数 + Long id = randomLongId(); + + // Mock 行为 + when(sceneRuleMapper.selectById(id)).thenReturn(null); + + // 调用 - 不存在的场景规则应该不会抛异常,只是记录日志 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(id)); + + // 验证 + verify(sceneRuleMapper, times(1)).selectById(id); + } + + @Test + public void testExecuteSceneRuleByTimer_disabled() { + // 准备参数 + Long id = randomLongId(); + + // Mock 行为 + IotSceneRuleDO sceneRule = randomPojo(IotSceneRuleDO.class, o -> { + o.setId(id); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + when(sceneRuleMapper.selectById(id)).thenReturn(sceneRule); + + // 调用 - 禁用的场景规则应该不会执行,只是记录日志 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(id)); + + // 验证 + verify(sceneRuleMapper, times(1)).selectById(id); + } +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java new file mode 100644 index 000000000..4b4bdfd02 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java @@ -0,0 +1,338 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link CurrentTimeConditionMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private CurrentTimeConditionMatcher matcher; + + @Test + public void testGetSupportedConditionType() { + // 调用 + IotSceneRuleConditionTypeEnum result = matcher.getSupportedConditionType(); + + // 断言 + assertEquals(IotSceneRuleConditionTypeEnum.CURRENT_TIME, result); + } + + @Test + public void testGetPriority() { + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(40, result); + } + + @Test + public void testIsEnabled() { + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + // ========== 时间戳条件测试 ========== + + @Test + public void testMatches_DateTimeGreaterThan_success() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + long pastTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN.getOperator(), + String.valueOf(pastTimestamp) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_DateTimeGreaterThan_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN.getOperator(), + String.valueOf(futureTimestamp) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_DateTimeLessThan_success() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN.getOperator(), + String.valueOf(futureTimestamp) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_DateTimeBetween_success() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + long startTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8")); + long endTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN.getOperator(), + startTimestamp + "," + endTimestamp + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_DateTimeBetween_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + long startTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + long endTimestamp = LocalDateTime.now().plusHours(2).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN.getOperator(), + startTimestamp + "," + endTimestamp + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + // ========== 当日时间条件测试 ========== + + @Test + public void testMatches_TimeGreaterThan_earlyMorning() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN.getOperator(), + "06:00:00" // 早上6点 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + // 结果取决于当前时间,如果当前时间大于6点则为true + assertNotNull(result); + } + + @Test + public void testMatches_TimeLessThan_lateNight() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN.getOperator(), + "23:59:59" // 晚上11点59分59秒 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + // 大部分情况下应该为true,除非在午夜前1秒运行测试 + assertNotNull(result); + } + + @Test + public void testMatches_TimeBetween_allDay() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_BETWEEN.getOperator(), + "00:00:00,23:59:59" // 全天 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); // 全天范围应该总是匹配 + } + + @Test + public void testMatches_TimeBetween_workingHours() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_BETWEEN.getOperator(), + "09:00:00,17:00:00" // 工作时间 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + // 结果取决于当前时间是否在工作时间内 + assertNotNull(result); + } + + // ========== 异常情况测试 ========== + + @Test + public void testMatches_nullCondition() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullConditionType() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(null); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidOperator() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.CURRENT_TIME.getType()); + condition.setOperator(randomString()); // 随机无效操作符 + condition.setParam("12:00:00"); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidTimeFormat() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN.getOperator(), + randomString() // 随机无效时间格式 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidTimestampFormat() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN.getOperator(), + randomString() // 随机无效时间戳格式 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidBetweenFormat() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_BETWEEN.getOperator(), + "09:00:00" // 缺少结束时间 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备消息 + */ + private IotDeviceMessage createDeviceMessage() { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + return message; + } + + /** + * 创建日期时间条件 + */ + private IotSceneRuleDO.TriggerCondition createDateTimeCondition(String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.CURRENT_TIME.getType()); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + + /** + * 创建当日时间条件 + */ + private IotSceneRuleDO.TriggerCondition createTimeCondition(String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.CURRENT_TIME.getType()); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java new file mode 100644 index 000000000..c4edf3436 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java @@ -0,0 +1,424 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DevicePropertyConditionMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private DevicePropertyConditionMatcher matcher; + + @Test + public void testGetSupportedConditionType() { + // 调用 + IotSceneRuleConditionTypeEnum result = matcher.getSupportedConditionType(); + + // 断言 + assertEquals(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY, result); + } + + @Test + public void testGetPriority() { + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(20, result); + } + + @Test + public void testIsEnabled() { + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_temperatureEquals_success() { + // 准备参数 + String propertyName = "temperature"; + Double propertyValue = 25.5; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(propertyValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_humidityGreaterThan_success() { + // 准备参数 + String propertyName = "humidity"; + Integer propertyValue = 75; + Integer compareValue = 70; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_pressureLessThan_success() { + // 准备参数 + String propertyName = "pressure"; + Double propertyValue = 1010.5; + Integer compareValue = 1020; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_statusNotEquals_success() { + // 准备参数 + String propertyName = "status"; + String propertyValue = "active"; + String compareValue = "inactive"; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + compareValue + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_propertyMismatch_fail() { + // 准备参数 + String propertyName = "temperature"; + Double propertyValue = 15.0; + Integer compareValue = 20; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_propertyNotFound_fail() { + // 准备参数 + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + randomString(), // 随机不存在的属性名 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "50" + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullCondition_fail() { + // 准备参数 + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullConditionType_fail() { + // 准备参数 + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(null); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingIdentifier_fail() { + // 准备参数 + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier(null); // 缺少标识符 + condition.setOperator(IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator()); + condition.setParam("20"); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingOperator_fail() { + // 准备参数 + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier("temperature"); + condition.setOperator(null); // 缺少操作符 + condition.setParam("20"); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingParam_fail() { + // 准备参数 + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier("temperature"); + condition.setOperator(IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator()); + condition.setParam(null); // 缺少参数 + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullMessage_fail() { + // 准备参数 + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // 调用 + boolean result = matcher.matches(null, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullDeviceProperties_fail() { + // 准备参数 + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(null); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_voltageGreaterThanOrEquals_success() { + // 准备参数 + String propertyName = "voltage"; + Double propertyValue = 12.0; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), + String.valueOf(propertyValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_currentLessThanOrEquals_success() { + // 准备参数 + String propertyName = "current"; + Double propertyValue = 2.5; + Double compareValue = 3.0; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_stringProperty_success() { + // 准备参数 + String propertyName = "mode"; + String propertyValue = "auto"; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + propertyValue + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_booleanProperty_success() { + // 准备参数 + String propertyName = "enabled"; + Boolean propertyValue = true; + Map properties = MapUtil.of(propertyName, propertyValue); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + propertyName, + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(propertyValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_multipleProperties_success() { + // 准备参数 + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .put("humidity", 60) + .put("status", "active") + .put("enabled", true) + .build(); + IotDeviceMessage message = createDeviceMessage(properties); + String targetProperty = "humidity"; + Integer targetValue = 60; + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + targetProperty, + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(targetValue) + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备消息 + */ + private IotDeviceMessage createDeviceMessage(Map properties) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setParams(properties); + return message; + } + + /** + * 创建有效的条件 + */ + private IotSceneRuleDO.TriggerCondition createValidCondition(String identifier, String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier(identifier); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java new file mode 100644 index 000000000..25ea57152 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java @@ -0,0 +1,356 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceStateConditionMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeviceStateConditionMatcher matcher; + + @Test + public void testGetSupportedConditionType() { + // 调用 + IotSceneRuleConditionTypeEnum result = matcher.getSupportedConditionType(); + + // 断言 + assertEquals(IotSceneRuleConditionTypeEnum.DEVICE_STATE, result); + } + + @Test + public void testGetPriority() { + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(30, result); + } + + @Test + public void testIsEnabled() { + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_onlineState_success() { + // 准备参数 + IotDeviceStateEnum deviceState = IotDeviceStateEnum.ONLINE; + IotDeviceMessage message = createDeviceMessage(deviceState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + deviceState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_offlineState_success() { + // 准备参数 + IotDeviceStateEnum deviceState = IotDeviceStateEnum.OFFLINE; + IotDeviceMessage message = createDeviceMessage(deviceState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + deviceState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_inactiveState_success() { + // 准备参数 + IotDeviceStateEnum deviceState = IotDeviceStateEnum.INACTIVE; + IotDeviceMessage message = createDeviceMessage(deviceState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + deviceState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_stateMismatch_fail() { + // 准备参数 + IotDeviceStateEnum actualState = IotDeviceStateEnum.ONLINE; + IotDeviceStateEnum expectedState = IotDeviceStateEnum.OFFLINE; + IotDeviceMessage message = createDeviceMessage(actualState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + expectedState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_notEqualsOperator_success() { + // 准备参数 + IotDeviceStateEnum actualState = IotDeviceStateEnum.ONLINE; + IotDeviceStateEnum compareState = IotDeviceStateEnum.OFFLINE; + IotDeviceMessage message = createDeviceMessage(actualState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + compareState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_greaterThanOperator_success() { + // 准备参数 + IotDeviceStateEnum actualState = IotDeviceStateEnum.OFFLINE; // 状态值为 2 + IotDeviceStateEnum compareState = IotDeviceStateEnum.ONLINE; // 状态值为 1 + IotDeviceMessage message = createDeviceMessage(actualState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + compareState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_lessThanOperator_success() { + // 准备参数 + IotDeviceStateEnum actualState = IotDeviceStateEnum.INACTIVE; // 状态值为 0 + IotDeviceStateEnum compareState = IotDeviceStateEnum.ONLINE; // 状态值为 1 + IotDeviceMessage message = createDeviceMessage(actualState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), + compareState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_nullCondition_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullConditionType_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(null); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingOperator_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(null); + condition.setParam(IotDeviceStateEnum.ONLINE.getState().toString()); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingParam_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + condition.setParam(null); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullMessage_fail() { + // 准备参数 + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(null, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullDeviceState_fail() { + // 准备参数 + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(null); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_greaterThanOrEqualsOperator_success() { + // 准备参数 + IotDeviceStateEnum deviceState = IotDeviceStateEnum.ONLINE; // 状态值为 1 + IotDeviceMessage message = createDeviceMessage(deviceState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), + deviceState.getState().toString() // 比较值也为 1 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_lessThanOrEqualsOperator_success() { + // 准备参数 + IotDeviceStateEnum actualState = IotDeviceStateEnum.ONLINE; // 状态值为 1 + IotDeviceStateEnum compareState = IotDeviceStateEnum.OFFLINE; // 状态值为 2 + IotDeviceMessage message = createDeviceMessage(actualState.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), + compareState.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_invalidOperator_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(randomString()); // 随机无效操作符 + condition.setParam(IotDeviceStateEnum.ONLINE.getState().toString()); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidParamFormat_fail() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + randomString() // 随机无效状态值 + ); + + // 调用 + boolean result = matcher.matches(message, condition); + + // 断言 + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备消息 + */ + private IotDeviceMessage createDeviceMessage(Integer deviceState) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setParams(deviceState); + return message; + } + + /** + * 创建有效的条件 + */ + private IotSceneRuleDO.TriggerCondition createValidCondition(String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java new file mode 100644 index 000000000..1ed8f1c48 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java @@ -0,0 +1,376 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.util.HashMap; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomInt; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceEventPostTriggerMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeviceEventPostTriggerMatcher matcher; + + @Test + public void testGetSupportedTriggerType_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + IotSceneRuleTriggerTypeEnum result = matcher.getSupportedTriggerType(); + + // 断言 + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST, result); + } + + @Test + public void testGetPriority_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(30, result); + } + + @Test + public void testIsEnabled_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_alarmEventSuccess() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .put("message", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_errorEventSuccess() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("code", randomInt()) + .put("description", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_infoEventSuccess() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("status", randomString()) + .put("timestamp", System.currentTimeMillis()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_eventIdentifierMismatch() { + // 准备参数 + String messageIdentifier = randomString(); + String triggerIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", messageIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(triggerIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_wrongMessageMethod() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) + .build(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); // 错误的方法 + message.setParams(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerIdentifier() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType()); + trigger.setIdentifier(null); // 缺少标识符 + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullMessageParams() { + // 准备参数 + String eventIdentifier = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(null); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidMessageParams() { + // 准备参数 + String eventIdentifier = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(randomString()); // 不是 Map 类型 + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingEventIdentifierInParams() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) // 缺少 identifier 字段 + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTrigger() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerType() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + trigger.setIdentifier(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_complexEventValueSuccess() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("type", randomString()) + .put("duration", randomInt()) + .put("components", new String[]{randomString(), randomString()}) + .put("priority", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_emptyEventValueSuccess() { + // 准备参数 + String eventIdentifier = randomString(); + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.ofEntries()) // 空的事件值 + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_caseSensitiveIdentifierMismatch() { + // 准备参数 + String eventIdentifier = randomString().toUpperCase(); // 大写 + String triggerIdentifier = eventIdentifier.toLowerCase(); // 小写 + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", eventIdentifier) + .put("value", MapUtil.builder(new HashMap()) + .put("level", randomString()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(triggerIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + // 根据实际实现,这里可能需要调整期望结果 + // 如果实现是大小写敏感的,则应该为 false + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建事件上报消息 + */ + private IotDeviceMessage createEventPostMessage(Map eventParams) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(eventParams); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String identifier) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType()); + trigger.setIdentifier(identifier); + return trigger; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java new file mode 100644 index 000000000..2bed7fa63 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java @@ -0,0 +1,340 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.util.HashMap; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomDouble; +import static cn.hutool.core.util.RandomUtil.randomInt; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DevicePropertyPostTriggerMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private DevicePropertyPostTriggerMatcher matcher; + + @Test + public void testGetSupportedTriggerType_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + IotSceneRuleTriggerTypeEnum result = matcher.getSupportedTriggerType(); + + // 断言 + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST, result); + } + + @Test + public void testGetPriority_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(20, result); + } + + @Test + public void testIsEnabled_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_numericPropertyGreaterThanSuccess() { + // 准备参数 + String propertyName = randomString(); + Double propertyValue = 25.5; + Integer compareValue = 20; + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, propertyValue) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_integerPropertyEqualsSuccess() { + // 准备参数 + String propertyName = randomString(); + Integer propertyValue = randomInt(); + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, propertyValue) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(propertyValue) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_propertyValueNotMeetCondition() { + // 准备参数 + String propertyName = randomString(); + Double propertyValue = 15.0; + Integer compareValue = 20; + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, propertyValue) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_propertyNotFound() { + // 准备参数 + String existingProperty = randomString(); + String missingProperty = randomString(); + Map properties = MapUtil.builder(new HashMap()) + .put(existingProperty, randomDouble()) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + missingProperty, // 不存在的属性 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(randomInt()) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_wrongMessageMethod() { + // 准备参数 + String propertyName = randomString(); + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, randomDouble()) + .build(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + message.setParams(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(randomInt()) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerIdentifier() { + // 准备参数 + String propertyName = randomString(); + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, randomDouble()) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + trigger.setIdentifier(null); // 缺少标识符 + trigger.setOperator(IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator()); + trigger.setValue(String.valueOf(randomInt())); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullMessageParams() { + // 准备参数 + String propertyName = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(null); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(randomInt()) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidMessageParams() { + // 准备参数 + String propertyName = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(randomString()); // 不是 Map 类型 + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + String.valueOf(randomInt()) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_lessThanOperatorSuccess() { + // 准备参数 + String propertyName = randomString(); + Double propertyValue = 15.0; + Integer compareValue = 20; + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, propertyValue) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), + String.valueOf(compareValue) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_notEqualsOperatorSuccess() { + // 准备参数 + String propertyName = randomString(); + String propertyValue = randomString(); + String compareValue = randomString(); + Map properties = MapUtil.builder(new HashMap()) + .put(propertyName, propertyValue) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + propertyName, + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + compareValue + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_multiplePropertiesTargetPropertySuccess() { + // 准备参数 + String targetProperty = randomString(); + Integer targetValue = randomInt(); + Map properties = MapUtil.builder(new HashMap()) + .put(randomString(), randomDouble()) + .put(targetProperty, targetValue) + .put(randomString(), randomString()) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + targetProperty, + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(targetValue) + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建属性上报消息 + */ + private IotDeviceMessage createPropertyPostMessage(Map properties) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(properties); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String identifier, String operator, String value) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + trigger.setIdentifier(identifier); + trigger.setOperator(operator); + trigger.setValue(value); + return trigger; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java new file mode 100644 index 000000000..a9348456f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java @@ -0,0 +1,398 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import java.util.HashMap; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.*; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeviceServiceInvokeTriggerMatcher matcher; + + @Test + public void testGetSupportedTriggerType_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + IotSceneRuleTriggerTypeEnum result = matcher.getSupportedTriggerType(); + + // 断言 + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE, result); + } + + @Test + public void testGetPriority_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(40, result); + } + + @Test + public void testIsEnabled_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_serviceInvokeSuccess() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_configServiceSuccess() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("interval", randomInt()) + .put("enabled", randomBoolean()) + .put("threshold", randomDouble()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_updateServiceSuccess() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("version", randomString()) + .put("url", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_serviceIdentifierMismatch() { + // 准备参数 + String messageIdentifier = randomString(); + String triggerIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", messageIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(triggerIdentifier); // 不匹配的服务标识符 + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_wrongMessageMethod() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); // 错误的方法 + message.setParams(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerIdentifier() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); + trigger.setIdentifier(null); // 缺少标识符 + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullMessageParams() { + // 准备参数 + String serviceIdentifier = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + message.setParams(null); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_invalidMessageParams() { + // 准备参数 + String serviceIdentifier = randomString(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + message.setParams(randomString()); // 不是 Map 类型 + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_missingServiceIdentifierInParams() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) // 缺少 identifier 字段 + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTrigger() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerType() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + trigger.setIdentifier(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_emptyInputDataSuccess() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.ofEntries()) // 空的输入数据 + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_noInputDataSuccess() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + // 没有 inputData 字段 + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_complexInputDataSuccess() { + // 准备参数 + String serviceIdentifier = randomString(); + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("sensors", new String[]{randomString(), randomString(), randomString()}) + .put("precision", randomDouble()) + .put("duration", randomInt()) + .put("autoSave", randomBoolean()) + .put("config", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .put("level", randomString()) + .build()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(serviceIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_caseSensitiveIdentifierMismatch() { + // 准备参数 + String serviceIdentifier = randomString().toUpperCase(); // 大写 + String triggerIdentifier = serviceIdentifier.toLowerCase(); // 小写 + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", serviceIdentifier) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", randomString()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger(triggerIdentifier); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + // 根据实际实现,这里可能需要调整期望结果 + // 如果实现是大小写敏感的,则应该为 false + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建服务调用消息 + */ + private IotDeviceMessage createServiceInvokeMessage(Map serviceParams) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + message.setParams(serviceParams); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String identifier) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); + trigger.setIdentifier(identifier); + return trigger; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java new file mode 100644 index 000000000..b1e095ea3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java @@ -0,0 +1,262 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceStateUpdateTriggerMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeviceStateUpdateTriggerMatcher matcher; + + @Test + public void testGetSupportedTriggerType_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + IotSceneRuleTriggerTypeEnum result = matcher.getSupportedTriggerType(); + + // 断言 + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE, result); + } + + @Test + public void testGetPriority_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(10, result); + } + + @Test + public void testIsEnabled_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_onlineStateSuccess() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_offlineStateSuccess() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.OFFLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_stateMismatch() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTrigger() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerType() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_wrongMessageMethod() { + // 准备参数 + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerOperator() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator(null); + trigger.setValue(IotDeviceStateEnum.ONLINE.getState().toString()); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerValue() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue(null); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullMessageParams() { + // 准备参数 + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + message.setParams(null); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_greaterThanOperatorSuccess() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + IotDeviceStateEnum.INACTIVE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_notEqualsOperatorSuccess() { + // 准备参数 + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备状态更新消息 + */ + private IotDeviceMessage createStateUpdateMessage(Integer state) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + message.setParams(state); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String operator, String value) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator(operator); + trigger.setValue(value); + return trigger; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java new file mode 100644 index 000000000..52ed5ec3d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java @@ -0,0 +1,276 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link TimerTriggerMatcher} 的单元测试 + * + * @author HUIHUI + */ +public class TimerTriggerMatcherTest extends BaseMockitoUnitTest { + + @InjectMocks + private TimerTriggerMatcher matcher; + + @Test + public void testGetSupportedTriggerType_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + IotSceneRuleTriggerTypeEnum result = matcher.getSupportedTriggerType(); + + // 断言 + assertEquals(IotSceneRuleTriggerTypeEnum.TIMER, result); + } + + @Test + public void testGetPriority_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + int result = matcher.getPriority(); + + // 断言 + assertEquals(50, result); + } + + @Test + public void testIsEnabled_success() { + // 准备参数 + // 无需准备参数 + + // 调用 + boolean result = matcher.isEnabled(); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_validCronExpressionSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 0 12 * * ?"; // 每天中午12点 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_everyMinuteCronSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 * * * * ?"; // 每分钟 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_weekdaysCronSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 0 9 ? * MON-FRI"; // 工作日上午9点 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_invalidCronExpression() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = randomString(); // 随机无效的 cron 表达式 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_emptyCronExpression() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = ""; // 空的 cron 表达式 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullCronExpression() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + trigger.setCronExpression(null); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTrigger() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + + // 调用 + boolean result = matcher.matches(message, null); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_nullTriggerType() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + trigger.setCronExpression("0 0 12 * * ?"); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_complexCronExpressionSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 15 10 ? * 6#3"; // 每月第三个星期五上午10:15 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_incorrectCronFormat() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 0 12 * *"; // 缺少字段的 cron 表达式 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_specificDateCronSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 0 0 1 1 ? 2025"; // 2025年1月1日午夜 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_everySecondCronSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "* * * * * ?"; // 每秒执行 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + @Test + public void testMatches_invalidCharactersCron() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + String cronExpression = "0 0 12 * * @ #"; // 包含无效字符的 cron 表达式 + IotSceneRuleDO.Trigger trigger = createValidTrigger(cronExpression); + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertFalse(result); + } + + @Test + public void testMatches_rangeCronSuccess() { + // 准备参数 + IotDeviceMessage message = createDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 * * MON-FRI"); // 工作日9-17点 + + // 调用 + boolean result = matcher.matches(message, trigger); + + // 断言 + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备消息 + */ + private IotDeviceMessage createDeviceMessage() { + IotDeviceMessage message = new IotDeviceMessage(); + message.setDeviceId(randomLongId()); + return message; + } + + /** + * 创建有效的定时触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String cronExpression) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + trigger.setCronExpression(cronExpression); + return trigger; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/resources/application-unit-test.yaml b/yudao-module-iot/yudao-module-iot-server/src/test/resources/application-unit-test.yaml new file mode 100644 index 000000000..3966a274d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/resources/application-unit-test.yaml @@ -0,0 +1,52 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + +mybatis-plus: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject + +# 日志配置 +logging: + level: + cn.iocoder.yudao.module.iot.service.rule.scene.matcher: DEBUG + cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager: INFO + cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition: DEBUG + cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger: DEBUG + root: WARN + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + base-package: cn.iocoder.yudao + tenant: # 多租户相关配置项 + enable: true + xss: + enable: false + demo: false # 关闭演示模式 diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/resources/logback.xml b/yudao-module-iot/yudao-module-iot-server/src/test/resources/logback.xml new file mode 100644 index 000000000..b68931dc1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/resources/logback.xml @@ -0,0 +1,37 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/resources/sql/clean.sql b/yudao-module-iot/yudao-module-iot-server/src/test/resources/sql/clean.sql new file mode 100644 index 000000000..ae1c5e515 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/resources/sql/clean.sql @@ -0,0 +1,10 @@ +DELETE FROM "iot_scene_rule"; +DELETE FROM "iot_product"; +DELETE FROM "iot_device"; +DELETE FROM "iot_thing_model"; +DELETE FROM "iot_device_data"; +DELETE FROM "iot_alert_config"; +DELETE FROM "iot_alert_record"; +DELETE FROM "iot_ota_firmware"; +DELETE FROM "iot_ota_task"; +DELETE FROM "iot_ota_record"; diff --git a/yudao-module-iot/yudao-module-iot-server/src/test/resources/sql/create_tables.sql b/yudao-module-iot/yudao-module-iot-server/src/test/resources/sql/create_tables.sql new file mode 100644 index 000000000..306c66b5e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-server/src/test/resources/sql/create_tables.sql @@ -0,0 +1,182 @@ +CREATE TABLE IF NOT EXISTS "iot_scene_rule" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL DEFAULT '', + "description" varchar(500) DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "triggers" text, + "actions" text, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT 场景联动规则表'; + +CREATE TABLE IF NOT EXISTS "iot_product" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL DEFAULT '', + "product_key" varchar(100) NOT NULL DEFAULT '', + "protocol_type" tinyint NOT NULL DEFAULT '0', + "category_id" bigint DEFAULT NULL, + "description" varchar(500) DEFAULT NULL, + "data_format" tinyint NOT NULL DEFAULT '0', + "device_type" tinyint NOT NULL DEFAULT '0', + "net_type" tinyint NOT NULL DEFAULT '0', + "validate_type" tinyint NOT NULL DEFAULT '0', + "status" tinyint NOT NULL DEFAULT '0', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT 产品表'; + +CREATE TABLE IF NOT EXISTS "iot_device" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "device_name" varchar(255) NOT NULL DEFAULT '', + "product_id" bigint NOT NULL, + "device_key" varchar(100) NOT NULL DEFAULT '', + "device_secret" varchar(100) NOT NULL DEFAULT '', + "nickname" varchar(255) DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "status_last_update_time" timestamp DEFAULT NULL, + "last_online_time" timestamp DEFAULT NULL, + "last_offline_time" timestamp DEFAULT NULL, + "active_time" timestamp DEFAULT NULL, + "ip" varchar(50) DEFAULT NULL, + "firmware_version" varchar(50) DEFAULT NULL, + "device_type" tinyint NOT NULL DEFAULT '0', + "gateway_id" bigint DEFAULT NULL, + "sub_device_count" int NOT NULL DEFAULT '0', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT 设备表'; + +CREATE TABLE IF NOT EXISTS "iot_thing_model" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "product_id" bigint NOT NULL, + "identifier" varchar(100) NOT NULL DEFAULT '', + "name" varchar(255) NOT NULL DEFAULT '', + "description" varchar(500) DEFAULT NULL, + "type" tinyint NOT NULL DEFAULT '1', + "property" text, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT 物模型表'; + +CREATE TABLE IF NOT EXISTS "iot_device_data" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "device_id" bigint NOT NULL, + "product_id" bigint NOT NULL, + "identifier" varchar(100) NOT NULL DEFAULT '', + "type" tinyint NOT NULL DEFAULT '1', + "data" text, + "ts" bigint NOT NULL DEFAULT '0', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("id") +) COMMENT 'IoT 设备数据表'; + +CREATE TABLE IF NOT EXISTS "iot_alert_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL DEFAULT '', + "product_id" bigint NOT NULL, + "device_id" bigint DEFAULT NULL, + "rule_id" bigint DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT 告警配置表'; + +CREATE TABLE IF NOT EXISTS "iot_alert_record" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "alert_config_id" bigint NOT NULL, + "alert_name" varchar(255) NOT NULL DEFAULT '', + "product_id" bigint NOT NULL, + "device_id" bigint DEFAULT NULL, + "rule_id" bigint DEFAULT NULL, + "alert_data" text, + "alert_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deal_status" tinyint NOT NULL DEFAULT '0', + "deal_time" timestamp DEFAULT NULL, + "deal_user_id" bigint DEFAULT NULL, + "deal_remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT 告警记录表'; + +CREATE TABLE IF NOT EXISTS "iot_ota_firmware" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL DEFAULT '', + "product_id" bigint NOT NULL, + "version" varchar(50) NOT NULL DEFAULT '', + "description" varchar(500) DEFAULT NULL, + "file_url" varchar(500) DEFAULT NULL, + "file_size" bigint NOT NULL DEFAULT '0', + "status" tinyint NOT NULL DEFAULT '0', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT OTA 固件表'; + +CREATE TABLE IF NOT EXISTS "iot_ota_task" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL DEFAULT '', + "firmware_id" bigint NOT NULL, + "product_id" bigint NOT NULL, + "upgrade_type" tinyint NOT NULL DEFAULT '0', + "status" tinyint NOT NULL DEFAULT '0', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT OTA 升级任务表'; + +CREATE TABLE IF NOT EXISTS "iot_ota_record" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "task_id" bigint NOT NULL, + "firmware_id" bigint NOT NULL, + "device_id" bigint NOT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "progress" int NOT NULL DEFAULT '0', + "error_msg" varchar(500) DEFAULT NULL, + "start_time" timestamp DEFAULT NULL, + "end_time" timestamp DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'IoT OTA 升级记录表'; diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index 53e5e0fa2..b7d46d08e 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -111,7 +111,7 @@ - + diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index d4eece606..d69205ac6 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -368,8 +368,8 @@ yudao: kd100: key: pLXUGAwK5305 customer: E77DF18BE109F454A5CD319E44BF5177 + iot: + message-bus: + type: redis # 消息总线的类型 -debug: false -# 插件配置 TODO 芋艿:【IOT】需要处理下 -pf4j: - pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录 \ No newline at end of file +debug: false \ No newline at end of file